Finetuning Training Reference
This is the precise reference for what happens between "Pay & start training" and "Your endpoint is live". The guide covers the journey; this page covers the mechanics.
The one rule that decides everything
You never pick a training objective. The shape of your records picks it.
Every file is validated before payment, the validator detects the record
shape, and that shape maps to exactly one objective. One shape per file,
declared by the first record; mixing shapes is a fatal
mixed_output_objectives error.
chronicle — the output field selects among four objectives:
| Output shape | Objective | Loss | The tuned model returns |
|---|---|---|---|
"output": { "series": ... } | Quantile forecasting | Pinball on the canonical grid (0.1 … 0.9) | A quantile fan: median values + full quantiles |
"output": { "text": ... } | Text generation | Cross-entropy | Generated text |
"output": { "series": ..., "text": ... } | Both, jointly | Pinball + cross-entropy | Fan and text, split by special tokens |
"output": { "label": "..." } | Classification | Selectable (mse/mae/huber) | The predicted label |
"output": { "value": 1.5 } (or array) | Regression | Selectable (mse/mae/huber) | The predicted value |
Time-series output always trains the quantile objective — the same
quantile-fan contract as the production forecast model — and text output
always trains cross-entropy; the loss knob applies only to
classification/regression. A record may not mix target families
(label next to series is a fatal invalid_target).
:::info You split the series; we don't
For forecasting records, you decide where history ends and the target
begins: input.series is the observed history window, and output.series
is the future values the model should learn to predict from it. The
platform never auto-splits a single series into input and target — a record
whose output.series merely repeats the tail of input.series teaches the
model nothing and will lose to the frozen base on the held-out eval. Build
one record per forecast window (sliding windows over a long series are
fine), and keep the target length consistent with the horizon you plan to
request at serving time.
:::
inertialai-embed — contrastive, or classification/regression:
| Record shape | Objective | The tuned model is |
|---|---|---|
{ "anchor": ..., "positive": ..., "negatives": [...] } | Contrastive (retrieval alignment) | A retrieval embedding model |
{ "input": { text + series } } (input only) | Contrastive (the record's two modalities are the pair) | A domain-adapted embedding model |
{ "input": ..., "output": { "label": "..." } } | Classification | A classifier via embeddings (nearest-centroid) |
{ "input": ..., "output": { "value": ... } } | Regression | A regressor via embeddings (k-NN) |
Special tokens: how joint outputs stay parsable
Multi-modal outputs are serialized with reserved delimiter tokens:
<|ts|>14.2,13.9,15.1<|/ts|><|txt|>demand rising after the promo<|/txt|>
Training targets and served responses both round-trip through this codec, so
a joint series+text output always splits deterministically back into its
typed parts — the API response you receive has already been decoded into
output.series and output.text; you never parse the stream yourself.
How each modality combination trains
Every input (and anchor/positive/negative) is a multi-modal object with
text, series, or both. Features are extracted per modality and
concatenated, so the objective is the same regardless of the combination —
what changes is which signal the model can learn from:
| Input modalities | Chronicle (any objective) | Embed (any objective) |
|---|---|---|
series only | Pure autoregressive: the lag window of the input series drives the target (quantile fan, text, label, or value) | Embedding geometry driven by temporal/statistical structure |
series + text | Context-conditioned: text features condition the target (e.g. "promo launched" shifts the forecast fan) | Joint embedding of both modalities |
text only | Text-conditioned targets — mechanically supported, but weak without any series history; prefer including a series | Embedding geometry driven by text alone (labeled/pairs modes) |
Special cases:
- Unsupervised mode requires both
textandseriesin every record — the objective is the correspondence between the two, so a record with one modality has nothing to align (fatal error). - Multivariate series (
valuesas arrays + achannelslist) are accepted everywhere a series is; forecasting reads the primary (first) channel, embeddings patch every channel, and the channel count must be consistent across the whole file. - Chronicle text generation is real next-token training on the Chronicle checkpoint: the target text trains cross-entropy over the model's full vocabulary (prompt positions masked out), and serving decodes free-form text from the tuned model.
The optimizer, precisely
Every model family trains through one centralized trainer: a standard mini-batch gradient loop over the trainable parameters (the LoRA adapters, a task head, or — for full finetunes — every weight):
- Each epoch iterates shuffled mini-batches (
batch_size), computes the objective's masked loss, backpropagates, clips gradients (grad_clip), and steps the chosenoptimizer(adamwdefault,adam,sgdwith momentum) under the chosenscheduler(cosinedefault,constant). - The loss follows the objective: masked quantile (pinball) for series
targets — padded steps never contribute, exactly like the base models'
pretraining objective — cross-entropy for text, contrastive/prototype
losses for embeddings, and the selectable
lossknob (msedefault,mae,huber) for classification/regression heads. The stored training curves measure the loss per epoch on the real iterates. - Everything is deterministic: the same data,
seed, and knobs reproduce the same weights, curves, and eval.
Knobs and ranges (preset defaults in parentheses):
| Knob | Range | Default |
|---|---|---|
epochs | 1 – 200 | preset: 5 / 20 / 60 |
learning_rate | 1e-5 – 1.0 | preset: 3e-3 / 1e-3 / 3e-4 |
optimizer | adamw, adam, sgd | adamw |
batch_size | 1 – 1024 | 32 |
weight_decay | 0 – 1 | 0.01 |
scheduler | cosine, constant | cosine |
grad_clip | 0 – 100 | 1.0 |
lora_rank | 1 – 64 | 8 |
holdout_fraction | 0.05 – 0.5 | 0.1 |
loss | mse, mae, huber | mse |
early_stopping / early_stop_patience | bool / 1 – 50 | false / 5 |
seed | ≥ 0 | 13 |
chronicle_stage (chronicle only) | 1, 2 | 2 |
With early_stopping: true, training halts once the validation loss
stops improving (5 epochs of patience) and keeps the best iterate rather
than the last one. The training curves record whether it triggered and which
epoch was kept (early_stopped, best_epoch) — useful with high epoch
counts, where the tail of the budget would otherwise train past the optimum.
LoRA vs full finetuning
Every job also declares a training method (training_method on the job,
method= in the SDK):
lora(default) — trains low-rank adapters (ranklora_rank, default 8) on the base model's attention/MLP weights (Chronicle), the router's calibration heads (forecast), or a rank-limited residual head (embed). 75% off the full-finetune rate, and the right choice for almost all datasets.full— updates every weight with no rank constraint, at the model's listed rate (the pricing page has the numbers — LoRA is 75% off it). Choose it when a LoRA run plateaus below the quality you need.
Both methods train on dedicated GPUs; the quote always states the method it was priced under.
Train vs validation data
Two ways to define the validation set — never both:
- Random split (default):
holdout_fractionof your records is held out with a deterministic seed. The held-out records never train. - Explicit validation file: upload a second file with the job. It must be the same format and the same record shape/mode as the training file (mismatch is fatal). Then all training records train, and your validation file alone drives the loss curves and the eval. Validation records are never billed.
The validation set does double duty: it is the val series of the training curves and the held-out set of the before/after eval — the tuned adapter versus the frozen base on records neither has seen. The eval metric follows the objective: mean pinball loss (quantile series), token-F1 (text), their average (both), accuracy (classification), NMAE (regression), retrieval recall@1 (pairs), cross-modal recall@1 (unsupervised). If the tuned model doesn't beat the base, the fee auto-refunds.
Inference: predicting vs generating
Chronicle endpoints — POST /v1/endpoints/{id}/predict. One endpoint,
one contract; the response mirrors the objective the model was trained on:
{
"input": { "text": "optional context", "series": { "timestamps": [...], "values": [...], "freq": "1h" } },
"options": { "horizon": 24, "return_text": true }
}
- Quantile objective (trained on
output.series):output.seriescarriesvalues(the median path continuing your input's timestamp grid) plus the full fan —quantiles(steps × 9) andquantile_levels(0.1 … 0.9), monotonic per step. This is generative time-series output.horizonmay be 1 – 1000; beyond the trained horizon the frozen base extends the median and the trained quantile offsets extend the fan, so keephorizon≤ your training target length for fully-tuned output. - Text objective (trained on
output.text):output.textis generated from the cross-entropy head;output.seriesis null. - Both: series fan and text in one response; internally the joint
output round-trips the special-token codec, so the split is exact.
options.return_text: falsesuppresses the text block. - Classification / regression:
output.label/output.valuecarry the prediction; series and horizon don't apply. (On quantile-objective models,return_text: truestill yields a derived narration of the forecast.) - The same input modality rules as training apply — inputs the validator would reject return HTTP 422.
Embed endpoints — POST /v1/endpoints/{id}/embed. Always returns a
768-dim unit vector plus usage. What you do with it follows the training
objective:
- Classification finetune: classify a new record by nearest class centroid (or any downstream classifier) — the objective made same-label inputs cluster.
- Regression finetune: predict by k-NN over your training embeddings — the objective aligned the geometry with your numeric target.
- Contrastive finetune (pairs / unsupervised): cosine similarity reflects your pairing — store vectors in any vector DB and query across modalities.
Data constraints (training and validation files)
Validation runs on the entire file before you can pay; any fatal error blocks the job, warnings don't. The same checks apply to the optional validation file.
Container
- JSONL (one JSON object per line), or a ZIP whose entries are
.jsonlshards. Anything else is fatal.
Size limits (hitting one is a clear validation error, never a silent truncation)
- 2 GiB per uploaded file. Larger datasets: shard into multiple JSONL files inside one ZIP.
- A ZIP may contain up to 512 shards and decompress to at most 8 GiB in total; a single JSONL line may be at most 16 MiB.
- Retained datasets count against a 10 GiB per-account storage allowance (free; deleting old jobs frees it — contact support to raise it).
Every series, everywhere
timestampsandvaluesequal length; timestamps ISO-8601 and strictly increasing; values numeric (nullallowed as missing, non-numeric fatal).freqoptional; if present and inconsistent with actual spacing → warning.- Multivariate:
valuesrows all the same width,channels(if given) matching that width, and one channel count for the whole file. - Input-series length bounds: 8 – 10,000 steps (chronicle), 4 – 10,000 (embed). Output series are exempt from the minimum (a 1-step target is normal).
Per record shape
- Chronicle:
inputandoutputrequired;inputneedstextand/orseries;outputneeds exactly one target family —seriesand/ortext, or a non-empty stringlabel, or a numericvalue(number or array of numbers). Mixing families in one record is fatal (invalid_target). - Embed labeled:
inputplusoutput.label(string, classification) oroutput.value(numeric, regression). - Pairs:
anchorandpositiverequired (each a text/series object);negativesoptional array of the same. - Unsupervised:
inputonly, with bothtextandseries. - One shape/objective per file (fatal
mixed_output_objectivesotherwise); empty files are fatal.
Warnings (non-blocking, reported with counts)
- Series shorter than 8 steps · >20 % missing (null) values · one label
covering >90 % of records · duplicate records ·
freq/spacing mismatch.
Validation file extras
- Same format (jsonl/zip) and same record shape/mode as the training file — a labeled training file with a chronicle-shaped validation file is fatal.
- Not billed; the flat training fee is unaffected by a validation file.
At inference
- Same modality rules as training; violations return 422 with the
validator-style message. Every response carries a
usageblock.