Your data must be in LeRobot v2 format. If you're converting from v3, run scripts/lerobot_conversion/convert_v3_to_v2.py first. For the full format specification (parquet layout, episode structure, video conventions), see the Data Preparation Guide.
Each dataset needs a modality.json that maps raw parquet columns and index ranges to named keys (e.g., eef_pose, gripper). If your dataset stores delta commands or joint angles in the action column rather than absolute EEF poses, see the Copy-EEF pattern, which sources action data from observation.state at future timesteps.
Your embodiment must have a registered config. Built-in Open-H embodiments are auto-registered on import, so no extra setup is needed. For new embodiments, follow Adding Your Own Embodiment.
GR00T-H training relies on normalization statistics stored alongside each dataset. Three files live under meta/, each serving a distinct purpose.
Flat statistics computed directly from raw parquet data.
- Contains: mean, std, min, max, q01, q99 per state and action key
- Shape:
(dim,)... one value per dimension, no temporal component - Used for: state normalization (min-max or mean-std scaling)
- Generated by:
gr00t/data/stats.py
Per-timestep statistics computed after REL_XYZ_ROT6D action conversion. This is a GR00T-H addition for action normalization across multi-step horizons.
- Contains: mean, std, q02, q98 per action key, per timestep in the action chunk
- Shape:
(horizon, dim)... separate statistics for every step in the chunk - Used for: action normalization with
normalization_type="temporal_meanstd" - Generated by:
launch_finetune.py --calculate-norm-stats
REL_XYZ_ROT6D computes cumulative displacements from the current EEF pose to each future timestep in the action chunk. The magnitude of those displacements grows with the prediction horizon:
- At step 1 (33ms into the future at 30Hz), displacement is small.
- At step 50 (1.7s into the future), cumulative displacement is much larger.
Flat normalization (a single mean and std across all steps) applies the same scaling to both, which can under-normalize early steps and over-normalize late ones. Temporal stats address this by providing each timestep with its own mean and std, calibrated to the actual displacement magnitude at that horizon. The released GR00T-H checkpoint was trained with these per-timestep statistics.
Here's what temporal_stats.json looks like in practice:
{
"action": {
"eef_pose": {
"mean": [[0.001, 0.002, ...], [0.003, 0.005, ...], ...],
"std": [[0.01, 0.01, ...], [0.02, 0.03, ...], ...],
"q02": [[-0.02, -0.02, ...], [-0.05, -0.06, ...], ...],
"q98": [[0.02, 0.02, ...], [0.05, 0.06, ...], ...]
}
}
}Each inner list has length equal to the action dimension (e.g., 9 for a single-arm EEF: 3 xyz + 6 rot6d). The outer list has horizon entries (e.g., 50 for a 50-step action chunk).
Generated alongside stats.json. Contains per-timestep stats for RELATIVE (non-EEF) actions, with the same (horizon, dim) shape concept as temporal_stats.json but computed differently. You generally don't need to worry about this file. It's produced automatically during stats generation.
Run the preparation script from the repository root:
bash open_h/prepare_datasets.sh \
--embodiment-tag <EMBODIMENT_TAG> \
--modality-json <path/to/modality.json> \
/path/to/dataset_a /path/to/dataset_b ...The script processes each dataset through three steps.
cp <modality.json> <dataset_path>/meta/modality.jsonPlaces the column-to-key mapping where the data loader expects it. If the meta/ directory doesn't exist, the script creates it.
uv run python gr00t/data/stats.py \
--dataset-path <dataset_path> \
--embodiment-tag <EMBODIMENT_TAG>Reads all parquet files, computes per-column statistics, and writes meta/stats.json (plus meta/relative_stats.json).
uv run python gr00t/experiment/launch_finetune.py \
--base-model-path nvidia/GR00T-H-N1.7 \
--dataset-path <dataset_path> \
--embodiment-tag <EMBODIMENT_TAG> \
--calculate-norm-statsThis step loads the modality config for the embodiment (auto-registered, no --modality-config-path needed), iterates through episodes, performs the actual REL_XYZ_ROT6D conversion for each action chunk, computes per-timestep percentile statistics on the converted actions, and writes meta/temporal_stats.json.
bash open_h/prepare_datasets.sh \
--embodiment-tag TUD_TUNDRA_UR5E \
--modality-json open_h/embodiments/tud_tundra_ur5e/modality_grasping_retraction.json \
/path/to/Surgical/TUD/260131_TUNDRA_dataset/grasping_retractionAfter running, verify the output:
ls /path/to/Surgical/TUD/260131_TUNDRA_dataset/grasping_retraction/meta/
# Expected: episodes.jsonl info.json modality.json stats.json tasks.jsonl temporal_stats.jsonWhen training on multiple datasets under the same embodiment tag (e.g., twelve dVRK datasets under jhu_imerse_dvrk), the training pipeline automatically merges their statistics. Each dataset keeps its own temporal_stats.json. At training time, gr00t/data/percentile_merge.py merges them, weighted by the mix ratios specified in the training config.
| Problem | Cause | Fix |
|---|---|---|
AssertionError: Embodiment tag already registered |
Passed --modality-config-path for a built-in tag |
Remove --modality-config-path; the config is auto-registered |
IndexError: boolean index did not match indexed array along dimension 0 |
temporal_stats.json shape doesn't match current delta_indices |
Regenerate temporal stats with prepare_datasets.sh |
FileNotFoundError: meta/temporal_stats.json |
Stats not generated before training | Run prepare_datasets.sh before training |
KeyError: '<tag>' not in MODALITY_CONFIGS |
Embodiment tag misspelled or new embodiment not registered | Check spelling against EmbodimentTag enum; for new embodiments, ensure the config calls register_modality_config() |