
The training loss of a robot policy is a surrogate, not a score. What each curve shape means, why there is often no validation set, and the four shapes that mean stop the run.
What you need to know
- •The loss is an average over independently sampled frames of a surrogate objective. Task success is a property of a whole rollout. The two are only loosely related.
- •The robomimic study (CoRL 2021) measured the gap: the best-validation-loss checkpoint was 50 to 100 percent worse than the best-performing one on the real task.
- •LeRobot had no held-out loss until v0.6.0 (6 July 2026), which added dataset.eval_split and eval_steps. Isaac-GR00T's fine-tune path still ships eval_strategy set to no.
- •Loss magnitude is not comparable across policies or datasets. Diffusion and flow-matching policies score against a randomly drawn noise level, so their curves stay noisy even when training is healthy.
- •Only four shapes justify killing a run: NaN, a flat line at the starting value, a collapse inside the first epoch, and a rise right after a resume. A late flatline is normal.
You start a fine-tune, the loss drops from 1.2 to 0.08 in the first two thousand training steps, flattens, and does nothing for eighteen thousand more. Is it done? Broken? Should you have stopped at step 6000? With no validation curve on the chart, the loss cannot tell you. This article is about what it can.
The context is imitation learning on a small arm: 30 to 200 episodes recorded by teleoperation, a policy fine-tuned on a rented GPU for a few hours, then run back on the hardware. That is the loop the training guides here describe, and where loss curves get misread most. Everything below is checked against LeRobot v0.6.1 (3 August 2026) and Isaac-GR00T at commit 51d4c89 (20 August 2026).

What the training loss actually measures
A behaviour-cloning trainer samples a batch of frames from your LeRobot dataset, asks the model to predict the demonstrated action chunk, and reduces the error. The important word is samples. The loss is an expectation over independently drawn frames. A rollout is not: the observation at step t+1 depends on the action taken at step t. Ross, Gordon and Bagnell put it plainly in the DAgger paper: sequential prediction problems where future observations depend on previous predictions violate the i.i.d. assumptions supervised learning rests on.
That mismatch is the whole story. Your loss is computed on states a human produced. Your policy runs on states it produced itself. The ACT paper describes the same failure from the practical side: errors in the policy can compound over time, and human demonstrations can be non-stationary. A loss curve is blind to both, because every frame it sees came from the human.
| Policy | What the reported loss is | Why the number looks the way it does |
|---|---|---|
| ACT | L1 on the action chunk plus kl_weight times the VAE KL term, kl_weight default 10.0 | The KL term can dominate early. loss_dict exposes l1_loss and kld_loss separately, and only l1_loss tracks action accuracy. |
| Diffusion Policy | MSE against the noise (or the clean trajectory) at a timestep drawn with torch.randint over num_train_timesteps | A new noise level per sample every step. Noisy by construction, not by instability. |
| SmolVLA, Pi0.5, GR00T | Flow-matching MSE against the velocity field u_t = noise - actions, time drawn from Beta(1.5, 1.0) | Every batch lands elsewhere on the noise schedule, so variance stays high forever. |
| All of them | Computed on normalised actions | Change the normalisation statistics and the regression targets change with them, so the same dataset reports a different loss scale after a stats recompute. |
A GR00T flow-matching loss of 0.04 and an ACT loss of 0.04 have nothing to do with each other, and neither has an absolute meaning. Never compare loss values across policies, across datasets, or across runs with different normalisation statistics. Only the shape within one run is informative. For cross-model numbers that are genuinely comparable, use benchmark results such as those in the model arena.
Why there is usually no validation curve
For most of LeRobot's life there was no held-out loss at all. That changed recently and it is worth dating precisely, because half the tutorials on the internet still describe the old behaviour. Pull request 3824, "Add inline offline validation with train/eval split", was merged on 25 June 2026 and shipped in v0.6.0 on 6 July 2026. It added dataset.eval_split, eval_steps and max_eval_samples. LeRobot v0.5.1, released 7 April 2026, has none of them.
| Trainer | Version checked | Held-out loss available? | Default logging cadence |
|---|---|---|---|
| lerobot-train | v0.5.1 (7 Apr 2026) | No | log_freq = 200 steps |
| lerobot-train | v0.6.0 and later | Yes, via dataset.eval_split plus eval_steps, both off by default | log_freq = 200 steps |
| Isaac-GR00T launch_finetune.py | main, 20 Aug 2026 | Trainer supports it, but training_config.py ships eval_strategy = "no" | logging_steps = 10 |
| openpi (Pi0 upstream) | issue 544, opened 25 Jun 2025 | Requested, not merged at time of writing | n/a |
When you do turn it on, read the split rule before you trust the curve. The docstring of make_train_eval_datasets in LeRobot's dataset factory says: the last ceil(n_episodes * eval_split) episodes per task are held out. That is chronological, not random. Recording sessions drift, so your held-out episodes are the ones where the lighting had changed, your wrist had tired and the object had migrated two centimetres left. A rising eval loss there may be measuring your session, not your model.
# LeRobot v0.6.x: hold out the last 10 percent of episodes per task
lerobot-train \
--dataset.repo_id=${HF_USER}/so100_pick_place \
--dataset.eval_split=0.1 \
--eval_steps=1000 \
--max_eval_samples=512 \
--policy.type=act \
--steps=100000 \
--save_freq=5000 \
--log_freq=100 \
--output_dir=outputs/train/act_so100 \
--job_name=act_so100 \
--wandb.enable=trueLeRobot's MetricsTracker prints step, smpl (samples), ep (episodes) and epch (epochs) before any metric. That epch field is the most under-read number in the log. A loss that hits its floor at epch:0.4 means something very different from one that hits it at epch:25.
step:2K smpl:64K ep:2K epch:2.13 loss:0.083 grdn:1.204 lr:1.0e-04 updt_s:0.412 data_s:0.003 smp/s:78 mem_gb:41.20
step:2K smpl:65K ep:2K epch:2.16 loss:0.081 grdn:0.998 lr:1.0e-04 updt_s:0.409 data_s:0.002 smp/s:78 mem_gb:41.20
step 1000: eval_loss=0.0912The number that decides your run is not on the chart
The strongest evidence is still the robomimic study, What Matters in Learning from Offline Human Demonstrations for Robot Manipulation, Mandlekar et al., CoRL 2021. They list it as challenge C4, "Train Objective is not Eval Objective", and their phrasing is blunt: policies are trained with surrogate losses, which makes it hard to know which checkpoints are good without trying each one directly on the robot.
Lesson 3 of the robomimic study reports that the best validation policy is 50 to 100 percent worse than the best performing policy. Not a few points. Roughly half as good. If you pick your checkpoint by validation loss and never run the alternatives on the arm, that is what you leave on the table.
This is also the mechanism behind the most common support question we see, which has its own page at loss falls, policy does nothing. The loss went where it should and the arm still sits there. Nothing is broken; the objective never promised what you wanted. The related pattern, a policy that works in the recorded setup and nowhere else, is under policy only works in one setup.

Curve shapes and what they actually mean
The right-hand column is the part people get wrong: most bad-looking curves do not justify killing a run, and one good-looking curve does.
| Shape | Most likely cause | Kill the run? |
|---|---|---|
| NaN or inf, a step or two after a grad_norm spike | Numerical blow-up: fp16, corrupt or all-black frames, or statistics computed over a broken episode. | Yes, immediately. It never recovers. |
| Dead flat at the step-0 value for 500+ steps | Nothing is being optimised: LR resolved to zero, wrong parameter group trainable, or image keys that do not match the dataset. | Yes. Fix the config, do not wait. |
| Near zero before one full epoch (read epch) | Far too few unique frames for the model size. Memorising, not learning. | Yes. Record more episodes. |
| Very noisy, trend downward | Normal for diffusion and flow-matching policies. | No. Smooth the chart, not the model. |
| grad_norm pinned at the clip value nearly every step | LR too high for this batch size. Pi0.5 and GR00T clip at 1.0, SmolVLA at 10. | Not urgent, but restart lower rather than hoping. |
| Flattens after warmup and stays flat | Usually convergence of the surrogate objective. | No. The LeRobot multi-task DiT docs say to train on, up to 100k steps, even when the loss flatlines. |
| Jumps upward right after a resume | Optimizer or scheduler state was not restored; the LR restarted at its peak. | Yes. Redo the resume, or you keep a worse checkpoint than you had. |
| Sawtooth locked to epoch boundaries | Dataloader ordering, or a subset of episodes much harder than the rest. | No, but go find those episodes. |
| eval_loss rises while train loss falls | Overfitting, or held-out episodes that are simply the last ones you recorded. | No. Work out which one first. |
A lot of curve shape is schedule shape rather than model behaviour. ACT returns None from get_scheduler_preset, so it trains at a constant 1e-5 and its loss creeps down for a very long time. SmolVLA and Pi0.5 use a cosine decay with warmup that auto-scales down when your step count is below scheduler_decay_steps, default 30000. GR00T N1.7 uses lr_scheduler_type "cosine" with warmup_ratio 0.05. In all three the last few thousand steps flatten because the learning rate is nearing its floor, not because the model stopped improving.
You shorten a run, the loss looks worse than the reference curve, and you conclude the model is undertrained. The real cause is that the schedule never decayed. The LeRobot hardware guide says it directly: shorten the LR schedule with --policy.scheduler_decay_steps roughly equal to --steps, or the LR stays near its peak. The cosine-with-warmup config auto-scales for you; not every scheduler does, and no external trainer will. Look for the Auto-scaling LR scheduler line before you blame the data.
What to do instead: an evaluation ladder
Since the loss will not select your checkpoint, something else has to. The practical answer is a ladder, cheapest first, where each rung filters candidates for the next.
- 1Rung 1: sanity checks on the training log
A liveness signal, not a quality one. Confirm the run is learning and the epoch count is sane, in the first ten minutes rather than at the end.
bash# is the run alive, and how many passes over the data has it done? grep -E 'epch:|eval_loss' outputs/train/act_so100/train.log | tail -20 - 2Rung 2: held-out loss, if your trainer has it
On LeRobot v0.6.0 or newer. Treat a rising eval_loss as a prompt to investigate, not a stopping rule, and remember the split is chronological.
bashlerobot-train \ --dataset.repo_id=${HF_USER}/so100_pick_place \ --dataset.eval_split=0.1 \ --eval_steps=500 \ --max_eval_samples=256 \ --policy.type=smolvla \ --policy.pretrained_path=lerobot/smolvla_base \ --steps=20000 \ --policy.scheduler_decay_steps=20000 \ --save_freq=2000 - 3Rung 3: open-loop evaluation against recorded trajectories
Feed a held-out episode's recorded observations to the checkpoint and compare the predicted chunk against what the human did. Not success, but it catches gross failures before you burn arm time.
bashuv run python gr00t/eval/open_loop_eval.py \ --dataset-path /data/so100_pick_place \ --embodiment-tag NEW_EMBODIMENT \ --model-path /checkpoints/step-8000 \ --traj-ids 0 1 2 \ --execution-horizon 16 \ --denoising-steps 4 - 4Rung 4: rollouts on the arm
The only signal that answers your question. Run each surviving checkpoint the same number of times from the same start states and count completions. Ten trials each is a usable minimum.
bash# same start pose, same object placement, count successes out of 10 for ck in 6000 8000 10000 12000; do echo "checkpoint $ck" # start the policy server on this checkpoint, then run 10 scripted attempts done
Rung 3 is the least known, so here are its details. The script prints "Unnormalized Action MSE across single traj" and "Average MSE across all trajs", plus MAE variants, and writes a plot of ground truth against predicted actions to /tmp/open_loop_eval/traj_{traj_id}.jpeg. --save-plot-path overrides that with the literal filename you give it, for every trajectory, so with several --traj-ids the plots overwrite each other; leave it off. Defaults: execution_horizon 16, denoising_steps 4, steps 200, traj_ids [0]. The plot beats the scalar. You see at a glance whether the gripper channel is flat, whether a joint sits at a limit, and whether the prediction lags.
Open-loop evaluation feeds the model the human's observations, so it never leaves the demonstration distribution. It answers "does this checkpoint reproduce the demonstration frame by frame", stronger than the training loss but weaker than "does it complete the task". Use it to reject checkpoints, never to crown one. A checkpoint that looks fine open-loop and stalls on the arm is the case behind policy freezes mid-motion.
One thing to check before comparing checkpoints: make sure they still exist. Isaac-GR00T's FinetuneConfig defaults to save_steps 1000 with save_total_limit 5, so an unattended run keeps the last five and silently deletes everything earlier. Raise it at launch time, not after the run.
The launch_finetune.py tyro CLI exposes no seed flag, so a GR00T run cannot be reproduced exactly. The README also notes 5 to 6 percent variance between runs from non-deterministic image augmentations. LeRobot defaults seed to 1000. When two GR00T runs differ by a few percent in success rate, that may be the noise floor rather than your change.
Two ways to get a checkpoint you can trust
Everything above runs on your own machine: install LeRobot, clone Isaac-GR00T if you want GR00T, rent or own a GPU.
- 1Install the trainer
v0.6.x if you want eval_split. Anything older rejects the flag.
bashpip install "lerobot[smolvla]==0.6.1" lerobot-train --help | head -40 - 2Train with held-out evaluation and frequent checkpoints
Low enough save_freq for 5 to 10 candidates, LR schedule matched to the step count.
bashlerobot-train \ --dataset.repo_id=${HF_USER}/so100_pick_place \ --dataset.eval_split=0.1 --eval_steps=1000 \ --policy.type=act --steps=60000 \ --save_freq=6000 --log_freq=100 \ --wandb.enable=true - 3Screen the candidates open-loop
Rank by open-loop error on held-out episodes, then take the top three forward.
bashuv run python gr00t/eval/open_loop_eval.py \ --dataset-path /data/so100_pick_place \ --embodiment-tag NEW_EMBODIMENT \ --model-path /checkpoints/step-24000 --traj-ids 0 1 2 - 4Roll out on the arm and count
Fixed start state, fixed trial count, written down. The only number worth keeping.
bash# your own harness; the metric is successes / attempts
You own GPU procurement, driver and CUDA versions, and dataset conversion: a v3.0 dataset crashes the GR00T loader and has to go down to v2.1. See dataset rejected as v3 and out of memory during training.
The platform runs the same upstream trainers behind a form. The training form picks model, dataset and hyperparameters; the backend rents a GPU on a spot market sized by required VRAM, runs the trainer, and writes checkpoints to object storage. That removes the plumbing, not the evaluation problem.
| Policy | Batch size | Learning rate | Max steps | Extra knobs in the form |
|---|---|---|---|---|
| GR00T N1.7 | 32 | 1e-4 | 20000 | saveSteps |
| GR00T N1.5 | 1 | 1e-5 | 2000 | saveSteps |
| Pi0.5 | 1 | 5e-5 | 30000 | seed, logFreq |
| SmolVLA | 2 | 1e-4 | 20000 | seed, logFreq |
| ACT | 8 | 1e-5 | 100000 | chunkSize (100), nActionSteps (100), seed, logFreq |
saveSteps on the GR00T rows is what matters here: it gives you a ladder of checkpoints instead of one final artefact. For inference, /api/inference/pod auto-provisions a GPU pod serving the policy, with an idle watchdog that destroys the pod after an idle period so nothing bills quietly.
It does not give you a validation curve that predicts task success, because none exists, and it does not run the arm for you. Nor can it fix the latency limit: the control loop is 20 to 485 ms per action step depending on the model, and public-internet round trips on top turn a working policy into a hesitant one. Remote inference is viable for slow pick-and-place, not fast reactive motion. See the training docs.

Using the loss curve as your primary signal
- Free: already logged, at log_freq 200 in LeRobot and logging_steps 10 in GR00T.
- Catches hard failures within minutes: NaN, dead-flat curves, instant collapse.
- grad_norm beside it is a real diagnostic for learning rate and clipping problems.
- The epoch counter tells you whether you are undertraining or memorising.
- Comparable across two runs on one dataset with identical normalisation, enough to A/B one hyperparameter.
- Does not rank checkpoints by task success. robomimic measured a 50 to 100 percent gap.
- Held-out loss on a chronological split partly measures session drift.
- Noisy for diffusion and flow-matching policies, so small differences are unreadable without smoothing.
- Not comparable across policies, datasets, or runs with different normalisation statistics.
- Flat does not mean converged; the LeRobot docs advise training past a flatline to 100k steps.
What it costs to be wrong
People over-read loss curves because the alternative feels expensive. On the spot market this platform uses, an A100 80 GB or H100 run for GR00T N1.7, GR00T N1.5 or Pi0.5 takes 3 to 6 hours at 1.20 to 2.00 USD per hour, roughly 4 to 12 USD per run. A 24 GB card for SmolVLA or ACT takes 2 to 5 hours at 0.30 to 0.60 USD per hour, roughly 1 to 3 USD. Full numbers on the pricing page.
Against that, an afternoon spent staring at a flat line, shipping the final checkpoint because it had the lowest loss, and concluding the model does not work is far more expensive. Retraining is cheap. Evaluating properly is what costs, and it costs arm time rather than GPU time.

- Minimum episodes before a run is worth attempting: 50 for GR00T N1.7, GR00T N1.5, Pi0.5 and ACT, 30 for SmolVLA. Below that the loss still falls and the policy still does not work.
- GR00T and Pi0.5 are cloud-only here. SmolVLA and ACT also run locally, the sensible choices while you iterate on evaluation method.
- ACT has no base model, so there is no zero-shot baseline to compare a checkpoint against.
A checkpoint protocol you can actually follow
The rules that survive contact with a real arm are short. Recording is covered in record your first dataset and running in run your first policy. What follows is only the part about deciding when a run is done.
| When | What to look at | Decision |
|---|---|---|
| First 200 steps | Is a loss printed at all, and are loss and grad_norm finite | Kill and fix, or continue |
| First 2000 steps | epch, grad_norm against the clip value, the Auto-scaling LR scheduler line | Adjust LR or data, or continue |
| Mid-run | Trend only, smoothed. Ignore step-to-step noise | Almost always continue |
| Every save_freq | That a checkpoint was written and not deleted by save_total_limit | Raise the limit if needed |
| End of run | Open-loop error on held-out episodes, last 4 to 6 checkpoints | Shortlist 3 |
| After the run | Success count on the arm, fixed start state, 10 trials each | Ship one |
To run that shortlist without owning hardware, the live arm streams a physical SO-100 with no signup, and the try page lists the three ways to start. The same operations are exposed to a terminal through the CLI and to agents through the MCP server. Public datasets are in the dataset directory.
My loss flatlined at step 5000 of a 20000 step run. Should I stop?▾
Almost certainly not. A late flatline usually means the surrogate objective has converged and, for cosine-scheduled policies, that the learning rate is near its floor. The LeRobot multi-task DiT documentation advises training on, up to 100k steps, even when the loss flatlines. Check the epch field first: a flatline at 0.5 epochs is suspicious, one at 20 epochs is expected. Stop only for NaN, a dead-flat line at the starting value, a collapse inside the first epoch, or a jump after a resume.
Can I compare loss values between GR00T N1.7 and SmolVLA?▾
No. They optimise different objectives on differently normalised targets. ACT reports an L1 term plus 10 times a KL term; GR00T, SmolVLA and Pi0.5 report a flow-matching MSE against a velocity field at a randomly drawn time. Even two runs of one policy are incomparable if the normalisation statistics were recomputed between them, because the loss is measured against normalised targets. For cross-model numbers use benchmark results, such as the model arena's 85 models and 332 results with every value linked to its source.
Does LeRobot support a validation set now?▾
Yes, since v0.6.0, released 6 July 2026. Set dataset.eval_split to a fraction and eval_steps to a cadence; eval_steps without eval_split raises a ValueError, and max_eval_samples caps the pass. Two limits: the split holds out the last ceil(n_episodes * eval_split) episodes per task, so it is chronological rather than random, and in v0.6.1 it is unsupported with repo_type set to bucket. LeRobot v0.5.1 and earlier have no such option.
Does a lower validation loss mean a better policy?▾
Only weakly. The robomimic study (Mandlekar et al., CoRL 2021) measured the best-validation-loss checkpoint as 50 to 100 percent worse than the best performing one on the real task. Use validation loss to reject broken checkpoints and spot overfitting, then decide between survivors on the arm. No offline metric substitutes for a rollout.
Why is my flow-matching loss so much noisier than my ACT loss?▾
Because a different noise level is drawn for every sample on every step. SmolVLA, Pi0.5 and GR00T sample the flow-matching time from Beta(1.5, 1.0) and regress against the velocity field u_t = noise - actions; Diffusion Policy draws its timestep with torch.randint. That variance is the noise schedule being sampled, not instability. Smooth over a few hundred steps before reading a trend.
How many checkpoints should I keep to compare?▾
Five to ten across the second half of the run. Set save_freq in LeRobot, or saveSteps in the platform form for the GR00T rows. Watch Isaac-GR00T's FinetuneConfig, which defaults save_steps to 1000 and save_total_limit to 5: a long unattended run silently keeps only the last five.
Train a policy and keep enough checkpoints to compare
Pick a model and an arm; the guide gives the defaults the trainer actually sends, the GPU tier, the dataset format and the checkpoint cadence. A 24 GB run costs about 1 to 3 USD.
Open the training guidesRelated reading
On how much data has to exist before a loss curve means anything, BC-Z and what scale really means is the read, and how to collect high quality VLA training data covers recording. For the hardware end, start with the SO-100 complete guide and the SO-100 robot page. Fine-tuning, action chunking and flow matching have short glossary entries if any of those terms were new.
Sources
- Mandlekar et al., What Matters in Learning from Offline Human Demonstrations for Robot Manipulation (robomimic), CoRL 2021
- robomimic study page: challenge C4, Train Objective is not Eval Objective, and Lesson 3
- Ross, Gordon, Bagnell, A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning (DAgger), 2011
- Zhao et al., Learning Fine-Grained Bimanual Manipulation with Low-Cost Hardware (ACT / ALOHA), 2023
- Chi et al., Diffusion Policy: Visuomotor Policy Learning via Action Diffusion, 2023
- lerobot_train.py: MetricsTracker keys, log_freq handling and the eval_loss code path
- LeRobot PR 3824, Add inline offline validation with train/eval split, merged 25 June 2026
- LeRobot v0.6.0 release notes, 6 July 2026
- LeRobot make_train_eval_datasets: the chronological per-task eval split rule
- LeRobot CosineDecayWithWarmupSchedulerConfig and its auto-scaling behaviour
- LeRobot ACT policy: loss is l1_loss plus kl_weight times the KL term
- LeRobot hardware guide: shorten the LR schedule when you shorten training
- LeRobot multi-task DiT docs: train for longer even when the loss flatlines
- Isaac-GR00T open_loop_eval.py: arguments, MSE and MAE output, plot path
- Isaac-GR00T TrainingConfig: logging_steps 10, eval_strategy no, cosine schedule, save_steps 1000, save_total_limit 5
Sources
- Mandlekar et al., What Matters in Learning from Offline Human Demonstrations for Robot Manipulation (robomimic), CoRL 2021
- robomimic study page: challenge C4, Train Objective is not Eval Objective, and Lesson 3
- Ross, Gordon, Bagnell, A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning (DAgger), 2011
- Zhao et al., Learning Fine-Grained Bimanual Manipulation with Low-Cost Hardware (ACT / ALOHA), 2023
- Chi et al., Diffusion Policy: Visuomotor Policy Learning via Action Diffusion, 2023
- lerobot_train.py: MetricsTracker keys, log_freq handling and the eval_loss code path
- LeRobot PR 3824, Add inline offline validation with train/eval split, merged 25 June 2026
- LeRobot v0.6.0 release notes, 6 July 2026
- LeRobot make_train_eval_datasets: the chronological per-task eval split rule
- LeRobot CosineDecayWithWarmupSchedulerConfig and its auto-scaling behaviour
- LeRobot ACT policy: loss is l1_loss plus kl_weight times the KL term
- LeRobot hardware guide: shorten the LR schedule when you shorten training
- LeRobot multi-task DiT docs: train for longer even when the loss flatlines
- Isaac-GR00T open_loop_eval.py: arguments, MSE and MAE output, plot path
- Isaac-GR00T TrainingConfig: logging_steps 10, eval_strategy no, cosine schedule, save_steps 1000, save_total_limit 5
Ready for high-quality robotics data?
AY-Robots connects your robots to skilled operators worldwide.
Get Started