
LeRobot v3.0 packs many episodes per file and moved metadata to Parquet. Which trainer accepts which version, both conversion directions, and what happens to your stats.
Two LeRobot dataset formats are in active use, and the difference is not cosmetic. A v2.1 LeRobot dataset is one Parquet file and one MP4 per episode. A v3.0 dataset packs many episodes into shared files and rebuilds the episode boundaries out of metadata. Feed the wrong one to the wrong trainer and you get a stack trace, a FileNotFoundError on a file that has not existed since 2025, or, quietest of the three, a run that trains against normalization statistics that no longer describe the data.
This is the practical version: which files each version has, which trainer accepts which, the commands for both conversion directions, and what happens to meta/stats.json on the way. Everything below was checked against lerobot v0.6.1 (released 3 August 2026) and the Isaac-GR00T main branch as read on 23 August 2026. Where a path or a default has already changed once, that is called out.
What you need to know
- •v3.0 is not backward compatible with v2.1. LeRobot raises BackwardCompatibilityError on a major version mismatch and prints the conversion command in the error text.
- •The meta folder identifies the version: v2.1 has episodes.jsonl and episodes_stats.jsonl, v3.0 has meta/episodes/chunk-000/file-000.parquet and meta/tasks.parquet.
- •Converting up is one command, python -m lerobot.scripts.convert_dataset_v21_to_v30. Its --push-to-hub flag defaults to true and deletes the old per-episode files from your Hub repo.
- •Converting down for GR00T is NVIDIA's scripts/lerobot_conversion/convert_v3_to_v2.py. It needs ffmpeg and it does not write meta/modality.json for you.
- •The two scripts disagree about --root. LeRobot wants the dataset folder itself, NVIDIA's appends the repo id to whatever you pass.
- •Nothing recomputes statistics unless you ask. LeRobot carries them across verbatim, GR00T discards a LeRobot stats.json and recomputes because it wants q01 and q99.
- •On AY-Robots: GR00T N1.7 and N1.5 take LeRobot v2.0 or v2.1, while Pi0.5, SmolVLA and ACT take v3.0.
Three versions, and the files that tell them apart
You can identify a LeRobot dataset without opening a single Parquet file. Look at meta/. Every version rearranged that folder, and every rearrangement removed something the previous one relied on. The table below is not from the docs. It is the HTTP status of each file on the public reference dataset lerobot/pusht, fetched at three Hub revisions on 23 August 2026.
| File under meta/ | v2.0 | v2.1 | v3.0 (main) |
|---|---|---|---|
| info.json | present | present | present |
| stats.json | present | absent | present |
| episodes.jsonl | present | present | absent |
| episodes_stats.jsonl | absent | present | absent |
| tasks.jsonl | present | present | absent |
| tasks.parquet | absent | absent | present |
| episodes/chunk-000/file-000.parquet | absent | absent | present |
# Which format is this dataset in? Ask, do not guess.
curl -s https://huggingface.co/datasets/lerobot/pusht/raw/main/meta/info.json \
| python -c "import json,sys; print(json.load(sys.stdin)['codebase_version'])"
# v3.0
# The same repo still serves the older layout as a git revision:
curl -s https://huggingface.co/datasets/lerobot/pusht/raw/v2.1/meta/info.json \
| python -c "import json,sys; print(json.load(sys.stdin)['codebase_version'])"
# v2.1
# Locally, for a dataset you recorded yourself:
python -c "import json; print(json.load(open('meta/info.json'))['codebase_version'])"A LeRobot dataset repo on the Hub keeps every format it has ever been published in, as a git tag. lerobot/pusht still serves complete v2.0 and v2.1 trees next to the v3.0 files on main. Before converting, check whether the version you need is already on a tag: snapshot_download(repo_id, repo_type="dataset", revision="v2.1"). LeRobot's own converter does exactly this when it pulls the source. One you recorded yourself has no tags to fall back on.
The stats.json that vanished and came back
The row worth staring at is stats.json. v2.0 had one global statistics file. v2.1 deleted it in favour of per-episode statistics in episodes_stats.jsonl, so episodes could be added or filtered without invalidating the constants for the whole dataset. v3.0 kept the per-episode statistics, moved them into the episodes Parquet as columns prefixed stats/, and brought the aggregated file back.
That zigzag causes most conversion confusion, because a strict v2.1 dataset has no global stats file at all. Any consumer that opens meta/stats.json and asserts on it, which is exactly what the GR00T loader does, fails on a clean v2.1 dataset even though the version number is the one it asked for. The format version and the file set are not the same question.
What v3.0 actually changed
v2.1 v3.0
---- ----
data/chunk-000/episode_000000.parquet data/chunk-000/file-000.parquet
data/chunk-000/episode_000001.parquet (many episodes per file)
...
videos/chunk-000/observation.image/ videos/observation.image/chunk-000/
episode_000000.mp4 file-000.mp4
meta/info.json meta/info.json
meta/episodes.jsonl meta/episodes/chunk-000/file-000.parquet
meta/episodes_stats.jsonl (folded into the same parquet)
meta/tasks.jsonl meta/tasks.parquet
(no stats.json) meta/stats.json- One episode per file became many episodes per file. Episode boundaries are byte and frame offsets in metadata, not filenames.
- Metadata moved from JSON Lines to Parquet: meta/episodes/ is chunked Parquet, meta/tasks.parquet replaces meta/tasks.jsonl.
- info.json lost total_chunks and total_videos, and gained data_files_size_in_mb and video_files_size_in_mb.
- Streaming arrived. StreamingLeRobotDataset iterates a Hub repo without downloading it, and lerobot-train accepts --dataset.streaming=true.
| Constant in lerobot/datasets/utils.py | Value at v0.6.1 | What it controls |
|---|---|---|
| DEFAULT_CHUNK_SIZE | 1000 | Maximum number of files in one chunk- directory |
| DEFAULT_DATA_FILE_SIZE_IN_MB | 100 | Roll over to a new data/ Parquet file above this size |
| DEFAULT_VIDEO_FILE_SIZE_IN_MB | 200 | Roll over to a new videos/ MP4 file above this size |
| DEFAULT_DATA_PATH | data/chunk-{chunk_index:03d}/file-{file_index:03d}.parquet | Data shard template written into info.json |
| DEFAULT_VIDEO_PATH | videos/{video_key}/chunk-{chunk_index:03d}/file-{file_index:03d}.mp4 | Video shard template written into info.json |
| DEFAULT_TASKS_PATH | meta/tasks.parquet | Task table location |
| DEFAULT_EPISODES_PATH | meta/episodes/chunk-{chunk_index:03d}/file-{file_index:03d}.parquet | Episode metadata and per-episode stats |
As of 23 August 2026 the Hugging Face v3.0 format page still lists meta/tasks.jsonl as a v3.0 file. The code says DEFAULT_TASKS_PATH = "meta/tasks.parquet", and a real v3.0 dataset serves tasks.parquet and 404s on tasks.jsonl. Separately, the converter's --video-file-size-in-mb help string says "Defaults to 100 for data and 500 for videos", while the constant it falls back to is 200. The fossil of the old default is visible in lerobot/pusht, whose info.json still reads "video_files_size_in_mb": 500. Read the constants, not the help text.
Which trainer accepts which version
Here the format question stops being academic. The five trainable policies on AY-Robots split down the middle, and the split is a property of each vendor's data loader rather than of the platform.
| Policy | Vendor | Dataset format | GPU tier | Min episodes | Inference per action step |
|---|---|---|---|---|---|
| GR00T N1.7 | NVIDIA | LeRobot v2.0 or v2.1 | A100 80 GB or H100 80 GB | 50 | 152 ms |
| GR00T N1.5 | NVIDIA | LeRobot v2.0 or v2.1 | A100 80 GB or H100 80 GB | 50 | 165 ms |
| Pi0.5 | Physical Intelligence | LeRobot v3.0 | A100 80 GB or H100 80 GB | 50 | 485 ms |
| SmolVLA | Hugging Face | LeRobot v3.0 | RTX 4090 or any 24 GB card | 30 | 245 ms |
| ACT | Stanford (ALOHA) | LeRobot v3.0 | RTX 4090 or any 24 GB card | 50 | 20 ms |
The v3.0 side is simply lerobot itself. Any lerobot from v0.4.0 (23 October 2025) onward sets CODEBASE_VERSION to "v3.0" and runs every dataset through check_version_compatibility, which raises on a major mismatch. Pi0.5, SmolVLA and ACT inherit the requirement from the framework. GR00T has its own loader, and it is worth seeing why a v3.0 tree cannot satisfy it.
# Isaac-GR00T: gr00t/data/dataset/lerobot_episode_loader.py
LEROBOT_INFO_FILENAME = "info.json"
LEROBOT_EPISODES_FILENAME = "episodes.jsonl"
LEROBOT_TASKS_FILENAME = "tasks.jsonl"
LEROBOT_MODALITY_FILENAME = "modality.json"
LEROBOT_STATS_FILE_NAME = "stats.json"
LEROBOT_RELATIVE_STATS_FILE_NAME = "relative_stats.json"
# ... later, in the metadata loader:
stats_path = meta_dir / LEROBOT_STATS_FILE_NAME
assert stats_path.exists(), (
f"{stats_path} does not exist for {self.dataset_path}, please use gr00t/data/stats.py to generate it"
)So this is not a version check you can override with a flag. The loader opens meta/episodes.jsonl with a bare open() call, and a v3.0 tree has no such file. That is why a v3.0 dataset must be converted down before fine-tuning a GR00T model, and why there is a dedicated failure page at dataset rejected because it is v3.0.

Converting up: v2.1 to v3.0
The upward converter ships inside lerobot. It is not a console entry point, so you invoke it as a module. Mind the path: it was lerobot.datasets.v30.convert_dataset_v21_to_v30 through v0.5.0 and moved to lerobot.scripts.convert_dataset_v21_to_v30 in v0.5.1. The Hugging Face announcement post still prints the old path; the error message lerobot raises today prints the new one. Only the second spelling works on anything current.
- 1Install lerobot with the dataset extra
Conversion needs jsonlines, pyarrow and the datasets stack, which sit behind the dataset extra rather than the base install.
bashpython -m venv .venv && source .venv/bin/activate pip install "lerobot[dataset]>=0.4.0" lerobot-info # prints python, torch and ffmpeg versions - 2Confirm the source really is v2.1
The converter calls validate_local_dataset_version and refuses anything else. A v2.0 dataset fails here, not halfway through.
bashpython -c "import json; print(json.load(open('/data/so100_pickplace/meta/info.json'))['codebase_version'])" ls /data/so100_pickplace/meta/ # expect: info.json episodes.jsonl episodes_stats.jsonl tasks.jsonl - 3Convert locally, with the Hub explicitly disabled
For this script --root is the exact dataset folder containing meta/, data/ and videos/. Setting --push-to-hub=false is the important part, and it is not the default.
bashpython -m lerobot.scripts.convert_dataset_v21_to_v30 \ --repo-id you/so100_pickplace \ --root /data/so100_pickplace \ --push-to-hub=false - 4Check what it left behind
The original tree is moved to a sibling folder with an _old suffix and the converted tree takes the original path. Your v2.1 copy is not deleted, but it is not where you left it.
bashls -d /data/so100_pickplace* # /data/so100_pickplace <- now v3.0 # /data/so100_pickplace_old <- the original v2.1 tree find /data/so100_pickplace/meta -type f | sort - 5Tune the shard size only if you have a reason
Larger shards mean fewer files and faster init, smaller shards mean cheaper partial downloads.
bashpython -m lerobot.scripts.convert_dataset_v21_to_v30 \ --repo-id you/so100_pickplace \ --root /data/so100_pickplace \ --data-file-size-in-mb 100 \ --video-file-size-in-mb 200 \ --push-to-hub=false
Omit --push-to-hub and the converter finishes against your Hub repo in this order: it deletes the v3.0 tag, calls delete_files with the patterns data/chunk*/episode_*, meta/*.jsonl and videos/chunk*, recreates the tag, then pushes. The v2.1 files disappear from main in one command. The v2.1 git tag still points at the old commit, so the data is recoverable, but only if that tag existed. Convert with --push-to-hub=false, verify locally, push as a separate deliberate step.
If your dataset is still v2.0
The upward converter reads meta/episodes_stats.jsonl, which a v2.0 dataset does not have. Worse, v2.0 gets no helpful error anywhere in the chain: BackwardCompatibilityError only formats a message for version 2.1 and raises NotImplementedError for everything else, so a v2.0 tree handed to lerobot fails with a bare pointer at the Discord. The obvious fix, running the v2.0 to v2.1 converter first, has a catch: that script was removed from lerobot after v0.3.3. It is still fetchable at that tag, so the workaround is a throwaway environment pinned to the old release for one hop.
# Hop 1: v2.0 -> v2.1, in a pinned throwaway env
python -m venv .venv-021 && source .venv-021/bin/activate
pip install "lerobot==0.3.3"
python -m lerobot.datasets.v21.convert_dataset_v20_to_v21 --repo-id you/so100_pickplace
deactivate
# Hop 2: v2.1 -> v3.0, in a current env
source .venv/bin/activate
python -m lerobot.scripts.convert_dataset_v21_to_v30 \
--repo-id you/so100_pickplace --root /data/so100_pickplace --push-to-hub=falseConverting down: v3.0 to v2.1 for GR00T
The downward converter is not in lerobot. NVIDIA ships it inside Isaac-GR00T at scripts/lerobot_conversion/convert_v3_to_v2.py, with its own pyproject.toml, because it needs a lerobot install while the repo root installs the gr00t package instead. It is purely local: there is no --push-to-hub flag, which is a mercy after the previous section.
- 1Set up the conversion subproject
Run this from inside scripts/lerobot_conversion, not from the repo root. Installing from the root gets you gr00t and not the converter's dependencies.
bashgit clone https://github.com/NVIDIA/Isaac-GR00T.git cd Isaac-GR00T/scripts/lerobot_conversion uv venv && source .venv/bin/activate uv pip install -e . --verbose ffmpeg -version # hard requirement, the script shells out to it - 2Run the converter
Careful with --root. This script does root = Path(root) / repo_id, so it wants the parent directory, the opposite of lerobot's converter.
bashpython convert_v3_to_v2.py \ --repo-id you/so100_pickplace \ --root /data # Omit --root entirely and it downloads a fresh snapshot # from the Hub into $HF_LEROBOT_HOME/you/so100_pickplace - 3Check the swap it performed
Like the upward script it converts in place. The original v3.0 tree moves to a sibling with a _v3.0 suffix and the reconstructed v2.1 tree takes the original path.
bashls -d /data/you/so100_pickplace* # .../so100_pickplace <- now v2.1 # .../so100_pickplace_v3.0 <- the original ls /data/you/so100_pickplace/meta/ # info.json episodes.jsonl episodes_stats.jsonl tasks.jsonl stats.json - 4Add meta/modality.json yourself
The converter never writes it. GR00T needs it to know which slice of the state and action vectors is which joint, and which camera key maps to which model input. Start from demo_data/cube_to_bowl_5/meta/modality.json in the repo.
json{ "state": { "single_arm": {"start": 0, "end": 5}, "gripper": {"start": 5, "end": 6} }, "action": { "single_arm": {"start": 0, "end": 5}, "gripper": {"start": 5, "end": 6} }, "video": { "webcam": {"original_key": "observation.images.front"} }, "annotation": { "human.task_description": {} } } - 5Regenerate the statistics GR00T actually reads
A tyro CLI, so run --help once to confirm how your checkout spells the embodiment tag enum. A six-axis arm that is not a built-in embodiment also needs a modality config module.
bashcd ../.. # back to the Isaac-GR00T root python -m gr00t.data.stats --help python -m gr00t.data.stats \ --dataset-path /data/you/so100_pickplace \ --embodiment-tag new_embodiment \ --modality-config-path examples/SO100/so100_config.py
1. --root means the opposite thing. For lerobot's converter --root is the exact dataset folder containing meta/, data/ and videos/. NVIDIA's script does Path(root) / repo_id. Get it wrong and it silently downloads a second copy from the Hub. 2. Video splitting is a stream copy. It calls ffmpeg with -ss before -i and -c copy, so cuts snap to the nearest preceding keyframe and an episode can pick up a few frames of its predecessor. 3. Per-segment limits. A five minute ffmpeg timeout per segment, and a hard refusal above 3600 seconds. And before you go looking for a folder that is not there: the docstring says the backup suffix is _v30, while the code writes _v3.0.

The statistics, and what actually survives
Normalization statistics are the part of a dataset that fails quietly. A wrong path throws. A wrong mean shifts every input the policy sees by a constant and hands you a run that converges to something plausible and useless.
| LeRobot v3.0 meta/stats.json | GR00T meta/stats.json | |
|---|---|---|
| Produced by | aggregate_stats() over the per-episode stats | calculate_dataset_statistics() over the data Parquet files |
| Fields per feature | min, max, mean, std, count, plus q01/q10/q50/q90/q99 if the per-episode stats carry them | mean, std, min, max, q01, q99, always |
| Quantiles | from a 5000-bin running histogram, and only forwarded if every episode has them, so a tree converted up from v2.1 has none | q01 and q99, exact, via numpy |
| Image and video features | included, per channel, RGB divided by 255 into [0, 1]; depth keeps its stored units | excluded, only features whose dtype contains "float" |
| Rows used, numeric features | every frame of every episode | every row of every data/*/*.parquet, in one DataFrame |
| Rows used, image features | sampled: 100 up to about 500 frames, 177 at 1000, 594 at 5000, 1000 at 10000, cap 10000 | not applicable |
| Cache invalidation | none, recomputing is explicit | __fingerprints__, a hash of each feature's dtype and shape from info.json |
Read across and the answer falls out. The upward converter recomputes nothing: it folds your existing episodes_stats.jsonl into the stats/ columns of meta/episodes/ and re-aggregates it into a fresh meta/stats.json. Statistics that were already wrong stay wrong in a new file format. The reference dataset shows the consequence: every feature in lerobot/pusht's v3.0 meta/stats.json carries exactly count, max, mean, min and std, not one quantile, because the v2.1 stats it was built from had none to forward. Going the other way, NVIDIA's script unflattens those columns back into episodes_stats.jsonl and copies meta/stats.json byte for byte, and GR00T then throws that copy away: any feature missing one of its six required fields, mean, std, min, max, q01 and q99, is stale and gets recomputed.
# LeRobot side: recompute from scratch over all episodes.
# Without --operation.overwrite it writes to <repo_id>_recomputed_stats.
lerobot-edit-dataset \
--repo_id you/so100_pickplace \
--new_repo_id you/so100_pickplace \
--operation.type recompute_stats \
--operation.overwrite true
# GR00T side: force a full recompute by deleting the cache first.
rm -f /data/you/so100_pickplace/meta/stats.json \
/data/you/so100_pickplace/meta/relative_stats.json
python -m gr00t.data.stats \
--dataset-path /data/you/so100_pickplace \
--embodiment-tag new_embodiment \
--modality-config-path examples/SO100/so100_config.pyGR00T caches stats.json and invalidates an entry when a feature's dtype or shape in info.json changes. The code comment is blunt about why: without it, stats were reused whenever every feature name was still present, giving "silently wrong normalization at training/eval time". The converse still bites. Reorder your joints, swap two camera keys, or recalibrate so the same six float32 columns span a different range, and the fingerprint is unchanged. The cache stays fresh and you train on last week's constants. Delete meta/stats.json whenever the arm or the recording setup changed. From the outside this looks like loss falls but the policy does nothing.
- It is the only option for a dataset you recorded yourself, since there is no upstream revision tag to fall back on.
- Both directions are scripted and local, and neither re-encodes video, so image quality is untouched.
- One recording session can feed both camps: convert down for GR00T N1.7, keep the v3.0 copy for SmolVLA or ACT.
- Conversion is cheap next to recording. A run on the 24 GB tier costs about 1 to 3 USD, re-recording 50 episodes costs an afternoon.
- Two full copies on disk, plus the _old or _v3.0 backup neither script cleans up.
- The downward path needs ffmpeg and splits on keyframes, so episode boundaries can move by a few frames.
- Neither script writes meta/modality.json, so a GR00T-bound dataset always needs a hand-written file.
- Version numbers do not describe the file set, so a dataset can be a valid v2.1 and still lack what a loader opens.
- Statistics are carried, not verified. Nothing tells you whether they were right to begin with.
Do it yourself, or do it on AY-Robots
The full manual loop for one SO-100 dataset that has to train both a GR00T model and a SmolVLA model. Everything runs on your machine except the training itself.
- 1Record
Use lerobot-record against your leader and follower arms. Current lerobot writes v3.0.
bashlerobot-record \ --robot.type=so100_follower --robot.port=/dev/ttyACM0 \ --teleop.type=so100_leader --teleop.port=/dev/ttyACM1 \ --dataset.repo_id=you/so100_pickplace \ --dataset.num_episodes=50 \ --dataset.single_task="Put the red cube in the bowl" - 2Keep the v3.0 copy for lerobot policies
SmolVLA, ACT and Pi0.5 train off this tree directly. No conversion step at all.
bashlerobot-train --dataset.repo_id=you/so100_pickplace \ --policy.type=smolvla --steps=20000 - 3Branch a v2.1 copy for GR00T
Convert down into a separate parent directory so the v3.0 original stays untouched.
bashcp -r ~/.cache/huggingface/lerobot/you /data/groot_copy cd Isaac-GR00T/scripts/lerobot_conversion python convert_v3_to_v2.py --repo-id you/so100_pickplace --root /data/groot_copy - 4Add modality.json and rebuild stats
The two steps the converter does not do for you.
bashcp modality_so100.json /data/groot_copy/you/so100_pickplace/meta/modality.json cd ../.. && python -m gr00t.data.stats \ --dataset-path /data/groot_copy/you/so100_pickplace \ --embodiment-tag new_embodiment \ --modality-config-path examples/SO100/so100_config.py
The conversions themselves are minutes for a 50-episode SO-100 dataset. The time sink is everything around them: two Python environments with incompatible lerobot pins, a hand-written modality.json, and a first stats regeneration that quietly loads every Parquet file into one pandas DataFrame.
The platform side of this problem is narrower than the manual side. What it removes is the guessing: every training guide and every model page prints the dataset format its trainer expects, so you find out before you rent a GPU rather than four minutes into a run.
- The Find your combination matrix pairs each of the five models with each of the four supported arms, and every cell is a guide with the format requirement in its spec strip.
- Each policy page states the requirement directly, for example GR00T N1.7 as v2.0 or v2.1 against SmolVLA as v3.0.
- Datasets can come from the desktop client, from a Hugging Face repo id, or from your own machine, so a converted tree is a first-class input.
- /fix/dataset-rejected-v3 covers this exact rejection, next to the rest of the failure catalogue.
- The training form rents a GPU by required VRAM and writes checkpoints to object storage, so a failed run costs the minutes it ran for.
- The same operations are exposed to the CLI and to AI agents through the MCP server, which is the practical way to script a convert-then-train loop.
It does not convert dataset formats for you. The two scripts in this article are upstream tooling and they run on your machine against your files. If you came looking for a one-click convert button, the honest answer is that this article is the path and there is no shortcut in it. The platform's contribution is telling you which direction you need before you pay for a GPU.
Cost is worth stating plainly, because it changes the calculus on convert versus re-record. A run on the A100 or H100 tier takes 3 to 6 hours at 1.20 to 2.00 USD per hour, roughly 4 to 12 USD. A run on the RTX 4090 tier 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, and the recording side is covered in collecting high-quality VLA training data.
Pre-flight checklist
- Read codebase_version out of meta/info.json. Do not infer the version from directory shape.
- List meta/ and compare against the file table above. A valid version number with a missing file is the common surprise.
- If the dataset came from the Hub, look for a revision tag in the version you need before running any converter.
- Decide the direction from the trainer: GR00T N1.7 and GR00T N1.5 need v2.0 or v2.1, everything else needs v3.0.
- Copy the dataset first. Both scripts convert in place and rename the original out from under you.
- Pass --push-to-hub=false unless you specifically intend to rewrite the Hub repo.
- Write meta/modality.json for any GR00T-bound dataset and check the index ranges against your arm's degrees of freedom.
- Delete meta/stats.json and regenerate whenever calibration, joint order or camera keys changed, then watch the first few hundred training steps before walking away.

If you are still choosing between the two camps rather than converting between them, the head-to-head at GR00T N1.7 against Pi0.5 puts the numbers side by side, and the SO-100 setup guide covers everything upstream of the dataset. The full format description lives in the dataset docs.
Can I train a GR00T model directly on a LeRobot v3.0 dataset?▾
No. GR00T's LeRobotEpisodeLoader opens meta/episodes.jsonl, meta/tasks.jsonl, meta/modality.json and meta/stats.json. A v3.0 dataset has none of the first three, and it fails on a plain open() call rather than a version check, so there is no override flag. Convert down with NVIDIA's scripts/lerobot_conversion/convert_v3_to_v2.py, then add modality.json and regenerate stats.
Does converting between versions re-encode my video?▾
No. The upward converter concatenates existing MP4 files into larger shards. The downward converter splits them again with ffmpeg using -c copy, a stream copy. Neither touches the pixels. The downward split does snap to keyframes, so an episode boundary can move by up to one group of pictures.
Will conversion change my normalization statistics?▾
The upward converter recomputes nothing: it reads your existing per-episode stats, folds them into meta/episodes/ as stats/ columns and re-aggregates them into meta/stats.json. The downward converter copies meta/stats.json byte for byte. GR00T then discards that copy and recomputes, because it requires q01 and q99 that a converted-up LeRobot stats.json does not carry. Neither direction validates whether the original numbers were correct.
My dataset is v2.0. Why does the v2.1 to v3.0 converter fail?▾
It reads meta/episodes_stats.jsonl, which only exists from v2.1 onward. You need the v2.0 to v2.1 hop first, and that script was removed from lerobot after v0.3.3. Install lerobot 0.3.3 in a throwaway virtual environment, run lerobot.datasets.v21.convert_dataset_v20_to_v21, then switch back for the second hop.
Do I have to convert at all, or can I download an older revision?▾
For public Hub datasets, often you can. A LeRobot dataset repo keeps each published format as a git tag, so lerobot/pusht still serves complete v2.0 and v2.1 trees. Pass revision="v2.1" to snapshot_download. This does not help for a dataset you recorded yourself, and a strict v2.1 tree has no meta/stats.json, so a GR00T run still needs one generated.
Which version should I record in today?▾
Record in v3.0, because that is what current lerobot writes and what SmolVLA, ACT and Pi0.5 consume with no conversion step. Treat the v2.1 copy as a derived artifact you regenerate when you want to fine-tune a GR00T model, the way you would treat a build output rather than a source file.
Dataset rejected, loss falls, policy does nothing
Most format mistakes surface as a stack trace on the wrong file or a run that trains on stale normalization constants. The failure catalogue has a page per symptom, including the v3.0 rejection and the silent stats cache.
Open the failure catalogueSources
- LeRobotDataset v3.0 format documentation
- lerobot: convert_dataset_v21_to_v30.py
- lerobot: dataset path templates and default chunk sizes
- lerobot: compute_stats.py, sampling heuristic and aggregate_stats
- lerobot-edit-dataset: recompute_stats and other dataset operations
- LeRobotDataset v3.0: bringing large-scale datasets to lerobot (16 September 2025)
- lerobot v0.4.0, the first stable release with v3.0 datasets
- lerobot v0.6.1 release (3 August 2026)
- Isaac-GR00T: convert_v3_to_v2.py
- lerobot v0.5.1, the release where the converter moved to lerobot.scripts
- Isaac-GR00T: lerobot_episode_loader.py, the loader that opens episodes.jsonl and asserts on stats.json
- Isaac-GR00T: converting from LeRobot v3 to v2
- Isaac-GR00T data preparation and the modality.json schema
- Isaac-GR00T: stats.py, statistics generation and fingerprint cache
- lerobot/pusht, the reference dataset used for the version file matrix
Sources
- LeRobotDataset v3.0 format documentation
- lerobot: convert_dataset_v21_to_v30.py
- lerobot: dataset path templates and default chunk sizes
- lerobot: compute_stats.py, sampling heuristic and aggregate_stats
- lerobot-edit-dataset: recompute_stats and other dataset operations
- LeRobotDataset v3.0: bringing large-scale datasets to lerobot (16 September 2025)
- lerobot v0.4.0, the first stable release with v3.0 datasets
- lerobot v0.6.1 release (3 August 2026)
- Isaac-GR00T: convert_v3_to_v2.py
- lerobot v0.5.1, the release where the converter moved to lerobot.scripts
- Isaac-GR00T: lerobot_episode_loader.py, the loader that opens episodes.jsonl and asserts on stats.json
- Isaac-GR00T: converting from LeRobot v3 to v2
- Isaac-GR00T data preparation and the modality.json schema
- Isaac-GR00T: stats.py, statistics generation and fingerprint cache
- lerobot/pusht, the reference dataset used for the version file matrix
Ready for high-quality robotics data?
AY-Robots connects your robots to skilled operators worldwide.
Get Started