The AY-Robots training matrix: five trainable policies as rows and four supported robot arms as columns, each cell a guide for that combination
NormalisationLeRobotIsaac-GR00TDataset statisticsVLA trainingDebugging

Normalisation Statistics and Why They Break Training Runs

AY-Robots ResearchAugust 23, 202622 min read

Dataset statistics define the map between robot units and the model input space. Change the action representation without regenerating them and the loss still falls while the arm does nothing.

A vision-language-action policy never sees your robot's joint angles. It sees numbers that have been shifted and scaled into roughly [-1, 1], trained on those, and predicts in that space. The shift and the scale come from a small metadata file: the dataset statistics. Get that file wrong and nothing crashes. The loss curve looks fine, the checkpoint saves, and the arm does something dull and confidently wrong.

This page is about that file. What is in it, where each trainer keeps it, why it has to be regenerated when you change how actions are represented, and the specific errors that follow when you do not. The numbers below are computed from the cube_to_bowl_5 demo dataset that ships inside NVIDIA's Isaac-GR00T repository, so you can reproduce every one of them. Versions checked on 2026-08-23: lerobot 0.6.2 on main, Isaac-GR00T main (gr00t 0.1.0), openpi main.

What you need to know

  • Dataset statistics are per-dimension mean, std, min, max and quantiles over your action and state columns. They define the affine map between robot units and the model's input space.
  • Absolute actions and relative actions are different numbers in the same column. On the GR00T demo dataset, shoulder_lift has a standard deviation of 57.8 degrees absolute and 3.6 degrees relative. The stats file does not record which one it holds.
  • LeRobot writes either one into the same meta/stats.json action entry. Recomputing with --operation.relative_action true replaces that entry with relative numbers, and nothing in the file records which representation it now holds.
  • GR00T keeps them apart in meta/stats.json and meta/relative_stats.json, and fingerprints them. But the fingerprint for stats.json hashes only dtype and shape, so adding episodes does not invalidate the cache.
  • Relative offsets scored with absolute statistics land in under a quarter of the usable range, or clip to a rail outright when the joint's absolute range excludes zero. Absolute targets scored with relative statistics saturate at the rails, because the relative ranges are several times too narrow. Neither raises an exception.
  • Regenerating is cheap: minutes of CPU, no GPU. An A100-class fine-tune on AY-Robots costs 4 to 12 USD. Check the file first.

What is actually in a stats file

A LeRobot dataset stores raw sensor and motor values. Joint angles in degrees, gripper aperture in whatever unit your calibration produced, image tensors in 0 to 255. A neural network trained directly on that mix would spend its capacity learning that elbow_flex lives around 50 and wrist_roll around -1.6. So every trainer normalises first, and the parameters of that normalisation are computed once over the whole dataset and written to a metadata file.

In LeRobot this file is meta/stats.json. It holds one entry per feature key, and each entry holds a fixed set of per-dimension arrays. LeRobot's default quantile list is [0.01, 0.10, 0.50, 0.90, 0.99], so a fully populated entry carries mean, std, min, max, count and the five quantiles. GR00T computes a narrower set: mean, std, min, max, q01, q99.

StatisticComputed overUsed by which normalisation modeFails when
mean, stdevery frame in every episodeMEAN_STD (ACT, SmolVLA)a dimension barely moves, so std approaches zero and the normalised values explode
min, maxevery frameMIN_MAX, and GR00T when use_percentiles is falseone bad episode with a servo at the hard stop stretches the range for the whole dataset
q01, q991st and 99th percentileQUANTILES (Pi0.5), GR00T fine-tuning defaultthe dataset is too small for the tail to be meaningful
q10, q9010th and 90th percentileQUANTILE10 in LeRobotrarely used, missing from most older datasets
countnumber of frames aggregatedinternal, for merging per-episode statsnot a failure mode, but it is how per-episode stats are weighted
json
{
  "action": {
    "mean": [  2.01, -58.36,  52.07,  84.16,  -1.61,   9.50],
    "std":  [ 10.54,  57.81,  64.36,  10.12,   2.42,  15.96],
    "min":  [-29.98, -100.0, -97.84,  25.30, -11.13,   0.41],
    "max":  [ 12.48,  59.79,  99.61, 100.00,   3.92,  63.69],
    "q01":  [-28.77, -99.39, -91.99,  46.93, -11.08,   0.41],
    "q99":  [ 11.08,  56.50,  97.64, 100.00,   2.77,  63.11]
  }
}
The action entry from meta/stats.json in Isaac-GR00T's cube_to_bowl_5 demo dataset. Six values per array, one per action dimension: five arm joints then the gripper.

Two things are worth noticing in those numbers. The fourth dimension, wrist_flex, has a q01 of 46.93 against a min of 25.30: the joint spends 99 percent of its frames above roughly 47 degrees and drops far below that in the remaining 1 percent. And min for shoulder_lift is exactly -100.0 while q01 is -99.39, which is the signature of a joint that touched a software limit in at least one episode. That gap between min and q01 is why GR00T defaults to percentiles rather than raw extremes.

Where each trainer keeps its statistics

Three ecosystems, three conventions. If you train more than one policy family on the same data, you will end up with all three on disk.

StackFileWritten byContains
LeRobot (v3.0 datasets)meta/stats.jsondataset recording, or lerobot-edit-dataset with recompute_statsone entry per feature key, including observation.state and action
Isaac-GR00Tmeta/stats.jsongr00t/data/stats.py, and automatically at the start of a fine-tuneabsolute stats for every float column in info.json, plus a reserved __fingerprints__ key inside the same file
Isaac-GR00Tmeta/relative_stats.jsonthe same script, only for action keys marked RELATIVEper-horizon-step stats, shape (chunk length, dims), plus its own fingerprints
openpiassets///norm_stats.jsonscripts/compute_norm_stats.pymean, std, q01, q99 for the keys state and actions
Any trained checkpointstatistics.json (GR00T) or normalizer buffers in the state dict (LeRobot)save_pretrainedthe stats the model was actually trained with, frozen
Statistics travel with the checkpoint

This is the part people miss. GR00T's save_pretrained writes a statistics.json next to the model weights, and reads it back in from_pretrained. LeRobot serialises the normaliser's tensors into the saved processor state dict. So a checkpoint is not just weights, it is weights plus the exact affine map it was trained under. Serving a checkpoint against a freshly regenerated stats file is a real way to break a working policy, and the fix is to leave the checkpoint's own statistics alone.

Normalisation modes, and which policy uses which

LeRobot's NormalizationMode enum has five members: MIN_MAX, MEAN_STD, IDENTITY, QUANTILES and QUANTILE10. Each policy config declares a mapping from feature type to mode, and those defaults differ in ways that matter for which statistics your dataset needs to carry.

PolicyVISUALSTATEACTIONStatistics the dataset must carry
ACTMEAN_STDMEAN_STDMEAN_STDmean and std
SmolVLAIDENTITYMEAN_STDMEAN_STDmean and std
Pi0.5IDENTITYQUANTILESQUANTILESq01 and q99
GR00T N1.7handled inside the model processormin/max or q01/q99min/max or q01/q99q01 and q99 by default, plus relative_stats.json
GR00T N1.5handled inside the model processormin/max or q01/q99min/max or q01/q99same as N1.7

The practical consequence: a dataset recorded before quantile statistics existed will train ACT and SmolVLA without complaint and refuse Pi0.5 outright, with the message QUANTILES normalization mode requires q01 and q99 stats, please update the dataset with the correct stats using the `augment_dataset_quantile_stats.py` script. That is one of the few places in this whole area where you get a clear error instead of a bad policy.

text
normalised = 2 * (x - min) / (max - min) - 1
raw        = (normalised + 1) / 2 * (max - min) + min

# GR00T then applies, with clip_outliers=True by default:
normalised = clip(normalised, -1.0, 1.0)
The MIN_MAX map, identical in LeRobot's normalize_processor.py and GR00T's normalize_values_minmax. QUANTILES is the same formula with q01 and q99 substituted for min and max.
The AY-Robots training matrix on /train: five policies as rows, four supported robot arms as columns, each cell linking to a per-combination guide
Each cell of the matrix on /train is a guide for one policy and arm pairing. The dataset format column is the one that decides which statistics your data has to carry.

Absolute, relative and delta are different numbers in the same column

An action chunk can express the same motion in three ways. LeRobot's own documentation follows the terminology from the Universal Manipulation Interface paper: absolute, relative and delta. Absolute is the default everywhere. Relative subtracts the current state once, so every step in the chunk is an offset from the same reference. Delta subtracts the previous action, so errors accumulate along the chunk, which is why UMI argues against it.

RepresentationDefinitionTypical magnitude on an SO-100Who uses it
Absoluteaction[t] is the target joint positiontens of degrees, offset far from zeroLeRobot default, ACT, SmolVLA out of the box
Relativeaction[t] - state[0], one reference per chunksingle-digit standard deviation at the head of the chunk, centred near zeroopenpi and the pi family, GR00T for non-gripper joints
Deltaaction[t] - action[t-1]fractions of a degreediscouraged; error accumulates over the chunk

The GR00T example config for the SO-100 makes the mixture explicit and it is worth reading closely, because it is the exact shape that trips people up: the arm joints are relative, the gripper is absolute, in the same action vector.

python
"action": ModalityConfig(
    delta_indices=list(range(0, 16)),   # predict 16 future steps
    modality_keys=["single_arm", "gripper"],
    action_configs=[
        # single_arm: RELATIVE = delta from current state
        ActionConfig(rep=ActionRepresentation.RELATIVE,
                     type=ActionType.NON_EEF,
                     format=ActionFormat.DEFAULT),
        # gripper: ABSOLUTE = target position
        ActionConfig(rep=ActionRepresentation.ABSOLUTE,
                     type=ActionType.NON_EEF,
                     format=ActionFormat.DEFAULT),
    ],
),
From examples/SO100/so100_config.py in Isaac-GR00T. The action horizon is delta_indices=list(range(0, 16)), so relative statistics are computed per horizon step: an array of shape (16, 5).

The failure: same file, different meaning

Here is the whole problem in one sentence. A stats file records what the numbers were, not what the numbers meant. Nothing in meta/stats.json says whether the action column it summarises was absolute or relative. So when you flip the representation and reuse the old file, the normaliser applies a perfectly valid affine map derived from the wrong distribution.

The table below is computed from the shipped GR00T demo dataset. Column two is the absolute action range from meta/stats.json. Column three is the relative range for the first step of the chunk from meta/relative_stats.json. Column four is what happens when relative action values are normalised with the absolute statistics, which is exactly what you get if you switch on relative actions and skip the regeneration step.

JointAbsolute q01 to q99 (deg)Relative q01 to q99, first step (deg)Relative values scored with absolute statsShare of [-1, 1] actually used
shoulder_pan-28.8 to 11.1-4.2 to 4.70.23 to 0.6822 percent
shoulder_lift-99.4 to 56.5-14.4 to 11.70.09 to 0.4217 percent
elbow_flex-92.0 to 97.6-14.2 to 16.1-0.18 to 0.1416 percent
wrist_flex46.9 to 100.0-7.3 to 11.3-3.04 to -2.340 percent, both ends clip to -1
wrist_roll-11.1 to 2.8-1.4 to 1.40.40 to 0.8020 percent

Read the wrist_flex row again. In the absolute dataset the q01 to q99 band for that joint is 47 to 100 degrees. Relative offsets around zero are therefore far below the recorded minimum, the normalised value comes out at -3.04 to -2.34, and GR00T's clip_outliers=True default squashes every single one to exactly -1. The training target for that joint is a constant. The model dutifully learns to output a constant, the loss for that dimension drops to near zero, and the wrist never moves. That is the textbook shape of loss falls but the policy does nothing.

The day-eating trap: relative stats land under the absolute key

LeRobot's recompute_stats operation, when called with relative_action=true, computes chunk-based relative statistics and then assigns them straight onto the action key: new_stats[ACTION] = relative_action_stats. The resulting meta/stats.json looks like any other stats file and nothing in it records the change. Two things follow. First, watch where the output goes: lerobot-edit-dataset writes to <repo_id>_recomputed_stats unless you pass --new_repo_id, and it refuses to modify the input dataset unless you also pass --operation.overwrite true. Train the original repo id after a recompute and you have simply trained the old statistics again. Second, if you train the recomputed dataset without --policy.use_relative_actions=true, absolute joint targets get normalised against relative ranges several times too narrow, everything saturates at the rails, and you get a bang-bang policy. Keep one dataset directory per representation.

The reverse direction is quieter but not harmless. Even in the rows above where nothing clips, the real signal occupies 16 to 22 percent of the representable range. Since the same wrong map is applied in both directions at inference, min-max normalisation does round-trip cleanly, so the policy is not immediately broken. What breaks is learning: the differences that matter are compressed into a narrow band, while the flow-matching or diffusion head is being asked to resolve them against noise defined over the full range.

text
joint            absolute std   relative std (step 0)   ratio
shoulder_pan          10.54            1.13             9.3x
shoulder_lift         57.81            3.61            16.0x
elbow_flex            64.36            5.19            12.4x
wrist_flex            10.12            3.00             3.4x
wrist_roll             2.42            0.40             6.0x
The same column, summarised two ways. Standard deviations in degrees, from stats.json and relative_stats.json of cube_to_bowl_5.

Regenerating statistics: the actual commands

All three stacks give you a way to do this from a terminal, and none of them needs a GPU. Regeneration is a CPU pass over the parquet files. Do it before you rent anything, not after; see what a run costs on the pricing page for why the ordering matters.

  1. 1
    LeRobot: recompute absolute statistics

    The plain form. Use this after you add, delete or trim episodes, because LeRobot has no content fingerprint and will happily keep serving the old file. Note where the result lands: without --new_repo_id this writes a new dataset called your_user/your_dataset_recomputed_stats, and editing the input dataset in place additionally requires --operation.overwrite true.

    bash
    lerobot-edit-dataset \
        --repo_id your_user/your_dataset \
        --operation.type recompute_stats
  2. 2
    LeRobot: add quantiles to an older dataset

    Required before Pi0.5 will train, because its normalisation mapping is QUANTILES for both state and action. Add --overwrite to replace quantiles that are already present, --skip-images to leave video features alone.

    bash
    python src/lerobot/scripts/augment_dataset_quantile_stats.py \
        --repo-id your_user/your_dataset \
        --skip-images
  3. 3
    LeRobot: relative action statistics for the pi family

    chunk_size should match your policy chunk size. relative_exclude_joints keeps listed dimensions in absolute space; it defaults to ['gripper'] and you almost always want that, because a binary open/close signal gains nothing from being expressed as an offset. The same output rule applies here: the recomputed dataset is written to your_user/your_dataset_recomputed_stats unless you say otherwise, so point the training run at that repo id and not at the original.

    bash
    lerobot-edit-dataset \
        --repo_id your_user/your_dataset \
        --operation.type recompute_stats \
        --operation.relative_action true \
        --operation.chunk_size 50 \
        --operation.relative_exclude_joints "['gripper']"
  4. 4
    LeRobot: train with the matching flag

    This flag and the previous command are a pair. Setting one without the other is the failure this whole article is about. use_relative_actions defaults to false.

    bash
    lerobot-train \
        --dataset.repo_id=your_user/your_dataset \
        --policy.type=pi05 \
        --policy.use_relative_actions=true \
        --policy.relative_exclude_joints='["gripper"]'
  5. 5
    Isaac-GR00T: generate both stats files

    A tyro CLI. It writes meta/stats.json and, for every action key marked RELATIVE in your modality config, meta/relative_stats.json. The --modality-config-path argument is required for NEW_EMBODIMENT, which is what a custom SO-100 setup uses.

    bash
    python gr00t/data/stats.py \
        --dataset-path ./my_so100_dataset \
        --embodiment-tag NEW_EMBODIMENT \
        --modality-config-path examples/SO100/so100_config.py
  6. 6
    openpi: compute norm stats for a config

    openpi computes statistics after running the data transforms, so if your config includes DeltaActions the resulting norm_stats.json is already in the relative space. This is the cleanest of the three designs, because the representation cannot drift away from the statistics.

    bash
    uv run scripts/compute_norm_stats.py --config-name pi05_libero
GR00T v2 only

Isaac-GR00T's loader still expects a LeRobot v2 dataset. A v3.0 dataset has to be converted down first with scripts/lerobot_conversion/convert_v3_to_v2.py, and the GR00T-specific meta/modality.json has to be copied into meta/ afterwards. If your dataset was rejected before you ever got to the statistics, start at the v3 rejection page.

What GR00T does for you, and what it still does not

GR00T is the only one of the three that tries to protect you. Its dataset factory calls generate_stats and generate_rel_stats on rank zero at the start of every fine-tune, so you rarely have to run the script by hand. And it fingerprints the cached entries so that a configuration change invalidates them.

Change you makeInvalidates stats.json?Invalidates relative_stats.json?Consequence if not caught
Add or remove an action dimensionyes, shape is in the hashno, the relative fingerprint covers the modality config onlystats.json is recomputed, relative_stats.json stays stale until deleted
Change dtype of a columnyes, dtype is in the hashno, dtype is not in the relative fingerprintstats.json is recomputed, relative_stats.json stays stale until deleted
Change delta_indices, the chunk lengthnoyesrelative stats recomputed for the new horizon
Flip a key from ABSOLUTE to RELATIVEnoyes, rep is in the hasha relative entry appears for that key
Record 50 more episodes with a wider joint rangenonosilently trains on statistics from the old subset
Recalibrate the arm, shifting every joint offsetnonosilently trains on statistics from before the recalibration
The fingerprint is schema-blind, not content-aware

GR00T's cache key for meta/stats.json is a sha256 over exactly three things: the feature name, its dtype and its shape, all read from info.json. Nothing about the data itself is hashed. Doubling your dataset, re-recording every episode, or recalibrating the arm leaves all three unchanged, so the cached entry is judged fresh and reused. Delete meta/stats.json and meta/relative_stats.json by hand whenever the contents of the dataset change, not just its schema.

There is a second GR00T trap worth naming, because it produces no error at all. When the fine-tuning pipeline merges your dataset's statistics into the processor, it calls set_statistics with override=False by default. If the base checkpoint already carries statistics for the embodiment tag you selected, your dataset's merged statistics are discarded and only a log warning records it. Using NEW_EMBODIMENT for a custom SO-100 avoids this, because the base model has no statistics under that tag.

text
ValueError: Relative action statistics required for embodiment 'new_embodiment'
            but 'relative_action' not found in statistics

ValueError: Relative action statistics required for key 'single_arm'
            in embodiment 'new_embodiment' but not found

WARNING  Statistics for embodiment 'oxe_droid_relative_eef_relative_joint'
         already present; new stats DISCARDED (override=False). If the new
         data differs from the existing distribution this will cause silent
         normalization mismatch
Two GR00T errors you can actually get, and the warning you probably cannot see in a scrolling log.
The numbered step list from the AY-Robots guide for training GR00T N1.7 on an SO-100, showing dataset preparation before the GPU run
The GR00T N1.7 on SO-100 guide, step by step. Dataset preparation sits ahead of the GPU rental for exactly the reason this article describes.

Two ways to get this right

You control the dataset directory, so you control the statistics. The cost is that you have to be disciplined about it, every time, on every machine that touches the data.

  1. Clone the trainer you intend to use and pin the version. The flag names in this article are from lerobot 0.6.2 and Isaac-GR00T main as of 2026-08-23, and older tags differ.
  2. Record the dataset, then run the recompute for your target representation before anything else.
  3. Keep the absolute and the relative dataset as two repo ids rather than recomputing over one, and pass --operation.overwrite true only when you really mean to lose the absolute stats.
  4. Print the action mean and std and sanity-check them against the joint ranges you expect from your arm.
  5. Train, then read the first hundred steps of the loss. A loss that starts near zero on some dimensions is a clipping symptom, not a good sign.
  6. Keep the checkpoint's own statistics.json with the weights. Do not regenerate statistics for a checkpoint you already trained.
bash
# write the relative-stats copy to its own repo id
lerobot-edit-dataset --repo_id me/task \
  --new_repo_id me/task_rel \
  --operation.type recompute_stats \
  --operation.relative_action true \
  --operation.chunk_size 50

# train the dataset whose stats you just recomputed
lerobot-train --dataset.repo_id=me/task_rel --policy.type=pi05 \
  --policy.use_relative_actions=true
The minimum discipline, as two commands.

This path is free, fully reproducible, and it is what you want if you are modifying the trainer or comparing representations as an experiment. It is also the path where a forgotten step costs you a rented GPU and a day.

Symptoms and what they actually mean

Normalisation bugs do not announce themselves. They arrive disguised as model problems, which is why people spend a day tuning learning rates first. This table maps what you see to what to check. Isaac-GR00T's own fine-tuning guide lists the same class of symptom, attributing an exploding or NaN loss to action and state normalisation rather than to a model bug.

What you seeMost likely causeFirst thing to check
Loss drops fast then flattens near zero, policy barely movesactions clipped to a rail by a too-narrow rangeprint the fraction of normalised targets at exactly -1 or +1
One joint is frozen, the others workthat dimension's range does not contain its values in the new representationcompare per-dimension min/max against actual data for that joint
Loss is NaN from the first stepa std or a q01 to q99 span close to zero somewherelook for a dimension whose range is a rounding error, usually a sensor that barely moved
Training looks fine, the arm overshoots on the real robotcheckpoint statistics and serving statistics disagreediff the checkpoint's statistics.json against the dataset's stats.json
Policy works on the training episodes, fails on anything newnot a stats problem, a data problemread the generalisation notes instead, linked below the table
Diverging loss with huge normalised valuesa rarely used dimension with a tiny q99 minus q01openpi documents this exact case and suggests adjusting the stats by hand

The fifth row is the one worth being careful about, because it is the case where statistics are not to blame. Generalisation failure looks superficially similar and has nothing to do with normalisation; the page on policies that only work in one setup covers that separately. The full list of failure modes and their fixes is at /fix.

A five-line check before you rent a GPU

You do not need any of the training stacks installed to look at a stats file. It is JSON. The check below takes seconds and catches most of what this article describes.

python
import json, numpy as np

s = json.load(open("meta/stats.json"))["action"]
mean, std = np.array(s["mean"]), np.array(s["std"])
lo, hi = np.array(s["q01"]), np.array(s["q99"])

print("mean :", np.round(mean, 2))
print("std  :", np.round(std, 2))
print("span :", np.round(hi - lo, 2))
print("degenerate dims:", np.where(hi - lo < 1e-6)[0])
print("looks relative:", bool(np.all(np.abs(mean) < 5) and np.all(std < 20)))
Run this against meta/stats.json before submitting a training job. If the action mean sits close to zero and the std is single-digit, you are holding relative statistics.
Switching to relative actions
Advantages
  • Targets are centred near zero, so a fixed [-1, 1] output range is used efficiently across the whole workspace.
  • The policy learns motion rather than absolute position, which transfers better when the arm is repositioned or recalibrated.
  • Every step in the chunk references the same state, so there is no error accumulation across the chunk, unlike delta encoding.
  • It composes cleanly with real-time chunking, because the reference state is captured once per inference call.
  • It is a supported path rather than a fork: LeRobot ships the processor steps for the pi family, and GR00T's example SO-100 config already uses relative for the arm joints.
Trade-offs
  • It doubles your statistics bookkeeping: two files or two representations of the same column, and no marker in the file saying which you have.
  • Relative statistics depend on the chunk length, so changing the horizon means recomputing them.
  • A gripper is usually better left absolute, so your action vector is now mixed and the exclude list has to be right.
  • It makes the policy depend on an accurate current state at inference; a stale or noisy state reading now biases the whole chunk.
  • ACT and SmolVLA in LeRobot do not expose the relative flags that the pi family does, so the option is not available for every policy.

Where none of this helps

Correct statistics are necessary and nowhere near sufficient. If your demonstrations are inconsistent, if the camera moved between recording sessions, or if you have 20 episodes when the policy needs 50, a perfect stats file changes nothing. Normalisation bugs are worth ruling out early precisely because they are cheap to rule out, not because they are the most common cause of a bad policy.

It is also worth being clear about what the platform does and does not do for you here. AY-Robots runs the trainers and rents the GPUs; it does not expose a normalisation-mode selector, and it cannot tell you whether your action column is absolute or relative any more than the file itself can. And on the serving side there is a hard physical limit that no amount of preprocessing fixes: the control loop is 20 to 485 ms per action step depending on the model, so inference latency over the public internet turns a working policy into a hesitant one. Remote inference is viable for slow pick-and-place and not for fast reactive motion.

The AY-Robots cost table showing which GPU each policy needs, typical run time and price, and how many episodes are needed before a policy is useful
What a run costs by policy and card. Regenerating statistics is a CPU job that takes minutes; the run it protects is the expensive part.

Something already broken?

The failure-mode pages cover the specific symptoms: a loss that falls while the policy does nothing, a dataset rejected for being v3.0, a policy that freezes mid-motion. Each one names the check to run first.

Open the fix index

Related reading

If you are earlier in the pipeline, collecting high-quality VLA training data covers what to record in the first place, and the full SO-100 guide covers build, teleoperation and training end to end. For the model side, the VLA overview explains what the action head is doing with those normalised numbers. If you are still choosing, GR00T N1.7 against Pi0.5 compares them directly, and the arena has 85 models with 332 benchmark results, each value linked to its source.

Do I have to regenerate statistics after adding episodes to a dataset?

Yes, and no tool will do it for you automatically based on content. LeRobot has no content fingerprint at all. GR00T fingerprints its cache, but the hash covers only the feature name, dtype and shape from info.json, so more episodes with the same schema look identical to it and the stale cache is reused. Delete meta/stats.json and meta/relative_stats.json by hand, or rerun the recompute, whenever the data changes.

How do I tell whether a stats file holds absolute or relative action statistics?

There is no field that says so, which is the root of the problem. The heuristic that works: relative statistics have a mean close to zero and a small standard deviation, while absolute joint statistics have means offset by tens of degrees. On the GR00T demo dataset, the absolute mean for wrist_flex is 84.16 degrees and the relative mean is 1.54. If your action mean is near zero and your std is single-digit degrees, you are holding relative statistics.

Why does Pi0.5 refuse to train on my dataset when ACT trains fine?

Pi0.5's normalization mapping is QUANTILES for both state and action, so it needs q01 and q99. ACT and SmolVLA use MEAN_STD and only need mean and std. Datasets created before quantile statistics were added to LeRobot carry the second set and not the first. Run augment_dataset_quantile_stats.py with your repo id and the error goes away.

Should I regenerate statistics for a checkpoint I already trained?

No. The statistics a model was trained with are part of the model. GR00T writes them to statistics.json next to the weights, LeRobot serialises them into the saved processor state dict. Serving a trained checkpoint against a freshly computed stats file applies a different affine map at inference than at training, which is a good way to break a policy that was working.

Does GR00T compute the statistics itself, or do I have to run the script?

The dataset factory calls generate_stats and generate_rel_stats on rank zero at the start of a fine-tune, so on a clean dataset you usually do not need to run gr00t/data/stats.py by hand. Running it yourself is still worth doing, because it surfaces configuration errors before a GPU is rented, and because it is the only way to regenerate a cache that the schema-only fingerprint considers fresh.

Is relative always better than absolute for an SO-100?

Not universally, and the shipped configs reflect that. Isaac-GR00T's SO-100 example uses relative for the five arm joints and absolute for the gripper, and LeRobot's relative_exclude_joints defaults to ['gripper'] for the same reason: a binary open and close signal gains nothing from being expressed as an offset. The arm joints benefit; the gripper does not.

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started