The AY-Robots training matrix: five policies as rows, four robot arms as columns, each cell linking to the training guide for that combination
fine-tuningtrainingvlalerobothyperparameters

How Long Should a Fine-Tuning Run Be? Steps vs Episodes

AY-Robots ResearchAugust 23, 202618 min read

A step is not a unit of progress. How to turn published step counts for GR00T, SmolVLA, ACT and Pi0.5 into passes over your own dataset, and when to stop the run.

Every training form has a field called max steps, and almost nobody changes it. That number decides what a run costs, how long you wait, and whether the policy that comes out has memorised your demonstrations or learned the task. On its own it tells you nothing. A training step is one gradient update over one batch, so 20000 steps at batch 32 and 20000 steps at batch 1 are two completely different runs on the same screen.

This article converts the published fine-tuning step counts for GR00T, SmolVLA, ACT and Pi0.5 into a unit that transfers between them, works out how run length should scale with the number of episodes you recorded, and gives a stopping rule that does not need a validation loss. Upstream numbers were checked against the main branches in August 2026.

What you need to know

  • A step is one gradient update over one batch. Step counts are not comparable across models until you multiply by batch size.
  • Passes over the data (samples divided by frames) is the unit that transfers. LeRobot computes it and logs it as epochs on every line.
  • Epoch means one sample per episode in the original ACT repo and one sample per frame in LeRobot, a factor of several hundred.
  • On a 50-episode SO-100 dataset the trainer defaults here range from about 1 pass (Pi0.5) to about 27 (ACT). Same form, very different runs.
  • The ACT authors report success rate improving after the loss plateaus. The robomimic study found the lowest-validation-loss checkpoint is often a bad policy.
  • Stop by saving checkpoints often and evaluating several on the arm, not by watching the loss flatten.

A step is not a unit of progress

One step consumes one batch. The number of training samples a run has seen is therefore steps multiplied by batch size, multiplied again by gradient accumulation where the trainer actually implements it. Divide that by the number of frames in your LeRobot dataset and you get the number of passes over the data. That is the number worth comparing: it stays meaningful when batch size changes by a factor of 32 between two models on the same page.

LeRobot does this arithmetic for you and nobody looks at it. The metrics tracker keeps a running epoch count, and the training script prints the three inputs at startup, so the ratio is on screen before the first gradient lands.

python
# src/lerobot/utils/logging_utils.py - MetricsTracker
self.samples  = self.steps * self._batch_size * self._dp_world_size
self.episodes = self.samples / self._avg_samples_per_ep
self.epochs   = self.samples / self._num_frames

# src/lerobot/scripts/lerobot_train.py - printed once at startup
logging.info(f"{cfg.steps=} ({format_big_number(cfg.steps)})")
logging.info(f"{dataset.num_frames=} ({format_big_number(dataset.num_frames)})")
logging.info(f"{dataset.num_episodes=}")
LeRobot main, August 2026. The epoch figure in your log line is samples divided by frames, nothing more.
The only formula in this article

samples = steps x batch_size x gradient_accumulation
passes over the data = samples / num_frames
Every table below is that arithmetic applied to a published recipe. Where a trainer ignores the accumulation value it was given, the multiplier is 1 whatever the form says.

The word epoch means two different things

First, one trap. In the original ACT repository from the ALOHA paper, the dataset returns one item per episode and picks a random start timestep inside it. In LeRobot it returns one item per frame. Both call a full sweep an epoch, and the two differ by roughly the number of frames in an episode.

python
# tonyzhaozh/act - utils.py
class EpisodicDataset(torch.utils.data.Dataset):
    def __len__(self):
        return len(self.episode_ids)     # one item per EPISODE
    def __getitem__(self, index):
        ...
        start_ts = np.random.choice(episode_len)   # random window inside it

# huggingface/lerobot - datasets/lerobot_dataset.py
class LeRobotDataset(torch.utils.data.Dataset):
    def __len__(self):
        """Return the number of frames in the selected episodes."""
        return self.num_frames           # one item per FRAME
Same word, different denominator. This is the single most common reason a step count copied from a paper does the wrong thing.
CodebaseOne epoch isOn 50 episodes of 600 framesOptimizer steps at batch 8
tonyzhaozh/actone random window per episode50 samplesabout 6
huggingface/lerobotone sample per frame30000 samples3750

So the ACT README command with --num_epochs 2000 is 100000 samples, about 12500 optimizer steps at batch 8, and about 3.3 passes over a 30000-frame dataset. Their advice for real hardware, at least 5000 epochs, is about 8 passes. LeRobot's own default for ACT is 100000 steps at batch 8: 800000 samples, about 27 passes. Same architecture, same task size, an eightfold difference, both shipped as sensible.

What the upstream projects actually publish

Here is every published recipe for the five policies this platform trains, converted to samples and then to passes over one hypothetical dataset: 50 episodes on an SO-100 at 30 fps, 20 seconds each, so 30000 frames. That is the yardstick below; your own frame count will differ and you should use it instead.

RecipeWhat it specifiesSamplesPasses over 30000 frames
ACT README, simulation example--num_epochs 2000, batch 8100000about 3.3
ACT README, real-world adviceat least 5000 epochsat least 250000about 8.3
LeRobot TrainPipelineConfig defaultsteps 100000, batch 8800000about 27
SmolVLA docs, fine-tune example--steps=20000 --batch_size=641280000about 43
SmolVLA model card example--steps=100000 --batch_size=4400000about 13
openpi TrainConfig defaultnum_train_steps 30000, batch 32960000about 32
openpi pi05_droid_finetunenum_train_steps 20000, batch 32640000about 21
Isaac-GR00T README example--max-steps 2000 --global-batch-size 3264000about 2.1
Isaac-GR00T FinetuneConfig defaultmax_steps 10000, global_batch_size 64640000about 21

The spread is 2 to 43 passes and every row is a shipped default or a documented recommendation. There is no consensus number, and the projects say so themselves. The Isaac-GR00T training tips say to maximize batch size for your hardware and train for a few thousand steps, and that 2000-step example runs against a five-episode demo dataset of 4148 frames, so it is about 15 passes over that data rather than 2 over yours. The SmolVLA documentation says 20k steps takes roughly 4 hours on a single A100 and that you should tune the number to your use case. The SmolVLA paper fine-tunes for 200000 steps on real-world tasks, then adds that in practice far fewer steps sacrifice no significant performance.

One thing the spread does explain: models that start from a pretrained vision-language-action model checkpoint already know how to look at a scene and produce an action chunk, so the fine-tune only attaches your embodiment and task. ACT starts from nothing, with no base model at all, and learns the whole mapping from your 30000 frames, which is why its published run lengths sit at the high end. The policy comparison page shows which of the five have a vendor base checkpoint.

The AY-Robots policies page showing a comparison table of GR00T N1.7, GR00T N1.5, Pi0.5, SmolVLA and ACT with parameter counts, GPU tier, inference latency and minimum episodes
The five trainable policies side by side. Minimum episodes is a floor for the dataset, not a target for the run length.

What this platform sends by default

The training backend sends a fixed set of hyperparameters per policy unless you override them. Run the same arithmetic on those defaults and the picture is uncomfortable.

Policymax stepsbatch sizegradient accumulationsamplespasses over 30000 frames
GR00T N1.720000321, applied640000about 21
GR00T N1.52000116, applied32000about 1.1
Pi0.530000116, not applied30000about 1.0
SmolVLA2000028, not applied40000about 1.3
ACT10000081800000about 27
The accumulation value for Pi0.5 and SmolVLA never reaches the trainer

The form carries a gradient accumulation of 16 for Pi0.5 and 8 for SmolVLA, and neither is applied: lerobot 0.5.1 has no such flag. Effective batch stays at 1 and 2. If you sized a step count assuming accumulation was real, the run sees sixteen or eight times fewer samples than planned. Raise max steps to compensate, or raise the batch size if the card has the memory. If it does not, see out of memory during training.

Read the table row by row. GR00T N1.7 at batch 32 and ACT at batch 8 both get tens of passes, which is a regime where a small dataset can be memorised. Pi0.5 and GR00T N1.5 get roughly one pass, which for a pretrained VLA is not absurd but sits at the floor. The step counts match the upstream recipes; the batch sizes were chosen to fit memory, and nobody rescaled the steps afterwards. The default is a starting point, not a recommendation.

The numbered step list on the AY-Robots GR00T N1.7 on SO-100 training guide, showing the run itself broken into ordered steps
The GR00T N1.7 on SO-100 guide. The defaults it sends are the ones in the table above, and max steps is the number you should be changing.

Scaling run length with dataset size

The rule follows from the formula: hold passes roughly constant and let steps scale with frames. Double the dataset at a fixed step count and you halve how often the model sees each frame. Double the batch instead and you double it. Neither is a decision you want to make by accident.

  1. 1
    Count frames, not episodes

    A 50-episode dataset can be 20000 or 60000 frames depending on task length and whether reset phases were trimmed. Ask the dataset, do not estimate.

    python
    from lerobot.datasets.lerobot_dataset import LeRobotDataset
    
    ds = LeRobotDataset("your-user/so100-pick-place")
    print("episodes:", ds.num_episodes)
    print("frames:  ", ds.num_frames)
    print("avg frames/episode:", ds.num_frames / ds.num_episodes)
  2. 2
    Pick a target number of passes

    From the published recipes: 10 to 30 passes for a pretrained VLA, 20 to 40 for ACT from scratch. Start at 20. That bracket comes from other people's shipped defaults, not from a law.

  3. 3
    Convert to steps

    steps = passes x frames / (batch x accumulation). Use the effective batch, which for Pi0.5 and SmolVLA here means ignoring the accumulation value entirely.

    bash
    FRAMES=30000
    BATCH=8
    PASSES=20
    
    echo $(( FRAMES * PASSES / BATCH ))    # 75000 steps
  4. 4
    Set the checkpoint interval to give yourself choices

    Divide the step count by 8 to 10. The LeRobot default of save_freq 20000 against a 20000-step run leaves one checkpoint, which makes the stopping question unanswerable.

    bash
    # 75000 steps, a checkpoint roughly every 7500
    --steps=75000 --save_freq=7500
  5. 5
    Launch and watch the epoch counter, not the loss

    The first log line tells you whether the arithmetic was right. If it ends at epochs 0.3, the run was too short regardless of what the loss did.

    bash
    lerobot-train \
      --policy.path=lerobot/smolvla_base \
      --dataset.repo_id=your-user/so100-pick-place \
      --batch_size=8 \
      --steps=75000 \
      --save_freq=7500 \
      --output_dir=outputs/train/smolvla_pickplace \
      --job_name=smolvla_pickplace \
      --policy.device=cuda \
      --wandb.enable=true
Episodes at 20 s, 30 fpsFramesSteps for 20 passes at batch 2at batch 8at batch 32
30180001800004500011250
50300003000007500018750
1006000060000015000037500
200120000120000030000075000
Shortening a run changes the learning rate schedule, and each trainer handles it differently

LeRobot's CosineDecayWithWarmupSchedulerConfig, used by Pi0.5 and SmolVLA, auto-scales. The class itself takes num_warmup_steps and num_decay_steps as required arguments; the values come from the policy config, where scheduler_warmup_steps is 1000 and scheduler_decay_steps is 30000. Below that decay length both are rescaled by the ratio, so --steps=3000 gives 100 warmup steps and a 3000-step decay. GR00T uses warmup_ratio (default 0.05), proportional to max steps by construction. ACT has no scheduler: get_scheduler_preset() returns None, so the learning rate stays at 1e-5 throughout and the final checkpoint has no special status.

What overfitting looks like on a small task

With 30 to 50 episodes of one task there is not much to learn and a 3 billion parameter model can memorise most of it. Imitation learning gives no warning, because the loss is measured against the demonstrations you already have. Here is what it looks like from the outside.

  • The training loss keeps falling and the robot gets worse. Nothing in the trainer output contradicts it.
  • The arm runs the demonstrated trajectory regardless of where the object is. Move the cube 10 cm and it still goes to the mean of the training positions.
  • The gripper closes on time rather than on sight, because timing was easier to memorise than the visual cue.
  • Success from the trained start pose stays high while a new pose collapses, the pattern behind policy only works in one setup.
  • The motion is confident and wrong, which is harder to debug than hesitant and wrong. The opposite failure is loss falls but the policy does nothing.

The counterweight is that stopping early is also wrong. The ACT README opens by saying that if the policy is jerky or pauses mid-episode you should just train for longer, because success rate and smoothness can improve well after the loss plateaus. The robomimic study goes further: the checkpoint with the lowest validation loss was frequently far worse than the best one, in one case 2.7 percent success against 80.7 percent on the same task, and success rate kept climbing while validation loss increased substantially. Loss is a poor selector in both directions.

Training past the point where the loss flattens
What you gain
  • The ACT authors recommend it for real hardware: at least 5000 epochs, or 3 to 4 times the length after the loss has plateaued.
  • More checkpoints inside the useful range, so one bad checkpoint does not end the run.
  • On the RTX 4090 tier a longer run is cheap: 2 to 5 hours at 0.30 to 0.60 USD per hour, about 1 to 3 USD.
  • Past scheduler_decay_steps the Pi0.5 and SmolVLA cosine schedule is clamped, so every step beyond 30000 runs at the 2.5e-6 floor rather than decaying further. Cheap fine-tuning at a low learning rate, not a wasted tail.
What it costs
  • Cost and wall clock scale linearly. On the A100 80 GB or H100 tier a run is 3 to 6 hours at 1.20 to 2.00 USD per hour, about 4 to 12 USD, and doubling steps doubles that.
  • On a 30 to 50 episode dataset most of the extra passes memorise positions rather than learn the task.
  • GR00T keeps only save_total_limit checkpoints, default 5, so a long run with frequent saves quietly deletes the early ones.
  • Every extra checkpoint has to be evaluated on the arm. Human time, not GPU time, is what a long run really costs.

When to stop

There is no free stopping signal here. LeRobot does support a held-out loss: eval_steps computes loss on held-out episodes every N steps, but it defaults to 0 and errors unless dataset.eval_split is non-zero. The curve is informative, but the robomimic result means you should not select a checkpoint by it. The GR00T fine-tune path computes no validation loss at all.

So the stopping rule is behavioural. Save often, evaluate several checkpoints on the real arm, and stop when two consecutive ones fail to beat the best you have. More work than watching a curve, and the only thing that measures what you care about.

  1. 1
    Turn on the held-out loss, for information only

    A 10 percent split costs five episodes out of fifty. Worth it above 100 episodes, arguable below.

    bash
    --dataset.eval_split=0.1 --eval_steps=1000
  2. 2
    Save 8 to 10 checkpoints across the run

    Set save_freq to steps divided by 10. On GR00T set save_steps the same way and raise save_total_limit above the checkpoint count you expect, or the early ones are deleted.

    bash
    # lerobot: 75000 steps
    --save_freq=7500
    
    # Isaac-GR00T FinetuneConfig fields (defaults 1000 and 5)
    # save_steps, save_total_limit
  3. 3
    Evaluate at least three checkpoints on the arm

    The last one, one at about 60 percent of the run, one at about 30 percent. Ten to twenty rollouts each from varied start positions, not the demonstrated one.

    bash
    lerobot-rollout \
      --strategy.type=base \
      --policy.path=outputs/train/smolvla_pickplace/checkpoints/045000/pretrained_model \
      --robot.type=so100_follower \
      --robot.port=/dev/ttyACM1 \
      --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
      --task="Put the lego brick in the box" \
      --duration=60
  4. 4
    Decide from the rollouts

    If the last checkpoint is best, the run was probably too short: extend it rather than shipping the edge of the curve. If a middle checkpoint wins, you have found the length for this dataset.

  5. 5
    Write the number down with the dataset size

    The transferable quantity is passes, not steps. Record steps, batch size and frame count together, or the number is useless on the next dataset.

Two defaults that destroy the evidence you need

LeRobot's save_freq defaults to 20000, so a 20000-step run produces exactly one checkpoint and there is nothing to compare. Isaac-GR00T's save_total_limit defaults to 5 with save_steps at 1000, so a 20000-step run writes 20 checkpoints and keeps the last 5. Both are reasonable for disk usage and hostile to checkpoint selection. Change them before the run, not after.

Doing it by hand, or on this platform

Everything above is public. Clone LeRobot, count your frames, compute the step count, run it on a card you own or rent. The work is the arithmetic and the evaluation, not the tooling.

bash
git clone https://github.com/huggingface/lerobot.git
cd lerobot && pip install -e ".[smolvla]"

# 1. what is actually in the dataset
python -c "
from lerobot.datasets.lerobot_dataset import LeRobotDataset
d = LeRobotDataset('your-user/so100-pick-place')
print(d.num_episodes, d.num_frames)"

# 2. 30000 frames, 20 passes, batch 8 -> 75000 steps
lerobot-train \
  --policy.path=lerobot/smolvla_base \
  --dataset.repo_id=your-user/so100-pick-place \
  --batch_size=8 --steps=75000 --save_freq=7500 \
  --dataset.eval_split=0.1 --eval_steps=1000 \
  --output_dir=outputs/train/smolvla_pickplace \
  --job_name=smolvla_pickplace --policy.device=cuda

For GR00T the entry point differs and the flags are a tyro CLI, not draccus. Copy the README example and replace max steps with your own arithmetic.

bash
CUDA_VISIBLE_DEVICES=0 uv run python \
    gr00t/experiment/launch_finetune.py \
    --base-model-path nvidia/GR00T-N1.7-3B \
    --dataset-path /data/so100_pick_place \
    --embodiment-tag NEW_EMBODIMENT \
    --modality-config-path examples/SO100/so100_config.py \
    --num-gpus 1 \
    --output-dir /tmp/run1 \
    --max-steps 18750 \
    --global-batch-size 32 \
    --dataloader-num-workers 4
GR00T runs are not reproducible

The GR00T fine-tune entry point exposes no seed, so two identical commands do not produce identical weights. The repository notes 5 to 6 percent variance between runs from non-deterministic image augmentation. LeRobot does have a seed, default 1000. If you are comparing two run lengths on GR00T, that variance sits inside your measurement.

What a longer run does not fix

Run length is one of the smaller levers, and the one people reach for first because it is a single number in a form. These are the things no step count touches.

  • Bad data. If the wrist camera pointed at the ceiling for twelve episodes, more passes learn that harder. See collecting high-quality VLA training data.
  • Too few episodes. The floor is 30 for SmolVLA and 50 for the other four, and a floor is not a target. The SmolVLA documentation reports a 25-episode version of their pick-and-place dataset was not enough and performed badly.
  • Inference latency. The control loop is 20 ms per action step for ACT, 152 ms for GR00T N1.7, 165 ms for GR00T N1.5, 245 ms for SmolVLA and 485 ms for Pi0.5. Training longer shortens none of them.
  • Distance between the policy and the servos. Public-internet round trips on top of a 152 ms step turn a working policy into a hesitant one. Remote inference is viable for slow pick-and-place, not fast reactive motion.
  • A task the demonstrations never showed. Generalisation comes from variation in the data, not from gradient steps over variation you never recorded.
The AY-Robots cost table showing which GPU card each policy needs, typical run time and price per run, and how many episodes are needed before a policy is useful
Run cost by tier. Because cost is close to linear in steps, the step count is also the budget decision.

One thing to take away: stop quoting step counts without the batch size next to them. Record steps, batch, effective batch and frame count for every run and the next dataset becomes arithmetic instead of a guess. The train your first policy walkthrough and the training documentation assume the defaults; this article is about the point where you stop assuming them. Still choosing a model? The GR00T N1.7 against SmolVLA comparison and the overview of vision-language-action models are the better starting points, and the model arena carries benchmark numbers for 85 of them.

Set the step count with the arithmetic in front of you

Every training guide names the GPU tier, dataset format and the defaults actually sent for that model and arm, so you can see what you are overriding before you launch.

Open the training guides

Frequently asked questions

How many steps should I train for on 50 episodes?

Compute it rather than copy it. Take the frame count from the dataset, target about 20 passes, divide by the effective batch size. For a 30000-frame dataset that is 300000 steps at batch 2, 75000 at batch 8 and 18750 at batch 32. Then evaluate three checkpoints on the arm.

Is more steps always better?

No, but stopping early is the more common mistake. The ACT authors recommend at least 5000 epochs on real-world data, or 3 to 4 times the length after the loss plateaus, because success and smoothness keep improving past that point. Against that, on a 30 to 50 episode dataset the extra passes increasingly memorise demonstrated positions, and cost scales linearly.

Why does my SmolVLA run only see one pass over my data?

Because the default batch size here is 2 and the gradient accumulation value of 8 is not applied, since lerobot 0.5.1 has no such flag. 20000 steps at an effective batch of 2 is 40000 samples, about 1.3 passes over a 30000-frame dataset. The upstream example uses batch 64 for the same 20000 steps, 32 times more data. Raise max steps, or the batch size if the card has memory.

Can I stop a run early and still use the checkpoint?

Yes, if a checkpoint was written, which is why save_freq should be about a tenth of the step count rather than the LeRobot default of 20000. One caveat: for Pi0.5 and SmolVLA the cosine schedule is sized to the step count you declared, so stopping halfway ends at a mid-slope learning rate. ACT has no scheduler, so an early checkpoint there is on equal footing with the last.

Do I need a validation split to decide when to stop?

It helps you see the curve but should not pick the checkpoint. LeRobot supports it through eval_steps together with dataset.eval_split, both off by default. The robomimic study found that selecting by lowest validation loss produced far worse policies than the best available checkpoint, in one case 2.7 percent success against 80.7 percent. Rollouts on the arm are the only measurement of the thing you want.

How much does doubling the run length cost?

Roughly double, since GPU time is close to linear in steps. On the A100 80 GB or H100 tier a run is 3 to 6 hours at 1.20 to 2.00 USD per hour, about 4 to 12 USD. On the RTX 4090 tier it is 2 to 5 hours at 0.30 to 0.60 USD per hour, about 1 to 3 USD, so there the arithmetic favours running long and picking a checkpoint.

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started