The AY-Robots public dataset directory, listing LeRobot-format datasets recorded on SO-100 class arms
LeRobotDataset FormatGR00TData PipelineFine-Tuning

LeRobot Dataset v2.1 vs v3.0: What Changed, How to Convert

AY-Robots ResearchAugust 23, 202618 min read

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.0v2.1v3.0 (main)
info.jsonpresentpresentpresent
stats.jsonpresentabsentpresent
episodes.jsonlpresentpresentabsent
episodes_stats.jsonlabsentpresentabsent
tasks.jsonlpresentpresentabsent
tasks.parquetabsentabsentpresent
episodes/chunk-000/file-000.parquetabsentabsentpresent
bash
# 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'])"
codebase_version in meta/info.json is the authoritative answer. Directory shape is a hint, this is proof.
The cheapest conversion is the one you do not run

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

text
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
The camera key and the chunk directory swapped places in the video path. That alone breaks every glob written against v2.1.
  • 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.pyValue at v0.6.1What it controls
DEFAULT_CHUNK_SIZE1000Maximum number of files in one chunk- directory
DEFAULT_DATA_FILE_SIZE_IN_MB100Roll over to a new data/ Parquet file above this size
DEFAULT_VIDEO_FILE_SIZE_IN_MB200Roll over to a new videos/ MP4 file above this size
DEFAULT_DATA_PATHdata/chunk-{chunk_index:03d}/file-{file_index:03d}.parquetData shard template written into info.json
DEFAULT_VIDEO_PATHvideos/{video_key}/chunk-{chunk_index:03d}/file-{file_index:03d}.mp4Video shard template written into info.json
DEFAULT_TASKS_PATHmeta/tasks.parquetTask table location
DEFAULT_EPISODES_PATHmeta/episodes/chunk-{chunk_index:03d}/file-{file_index:03d}.parquetEpisode metadata and per-episode stats
Upstream docs and upstream code disagree in two places

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.

PolicyVendorDataset formatGPU tierMin episodesInference per action step
GR00T N1.7NVIDIALeRobot v2.0 or v2.1A100 80 GB or H100 80 GB50152 ms
GR00T N1.5NVIDIALeRobot v2.0 or v2.1A100 80 GB or H100 80 GB50165 ms
Pi0.5Physical IntelligenceLeRobot v3.0A100 80 GB or H100 80 GB50485 ms
SmolVLAHugging FaceLeRobot v3.0RTX 4090 or any 24 GB card30245 ms
ACTStanford (ALOHA)LeRobot v3.0RTX 4090 or any 24 GB card5020 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.

python
# 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"
)
Four of the five metadata files this loader opens do not exist in a v3.0 dataset. It never looks at meta/episodes/ or meta/tasks.parquet.

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.

The AY-Robots recording tutorial page, showing how episodes are captured from a teleoperation session into a LeRobot dataset
The recording tutorial. Whatever format your client writes, the number that matters later is codebase_version in meta/info.json.

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.

  1. 1
    Install lerobot with the dataset extra

    Conversion needs jsonlines, pyarrow and the datasets stack, which sit behind the dataset extra rather than the base install.

    bash
    python -m venv .venv && source .venv/bin/activate
    pip install "lerobot[dataset]>=0.4.0"
    lerobot-info   # prints python, torch and ffmpeg versions
  2. 2
    Confirm 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.

    bash
    python -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
  3. 3
    Convert 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.

    bash
    python -m lerobot.scripts.convert_dataset_v21_to_v30 \
      --repo-id you/so100_pickplace \
      --root /data/so100_pickplace \
      --push-to-hub=false
  4. 4
    Check 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.

    bash
    ls -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
  5. 5
    Tune the shard size only if you have a reason

    Larger shards mean fewer files and faster init, smaller shards mean cheaper partial downloads.

    bash
    python -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
The default that eats a day: --push-to-hub is true

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.

bash
# 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=false
Two hops, two environments. There is no single v2.0 to v3.0 path in current lerobot.

Converting 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.

  1. 1
    Set 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.

    bash
    git 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
  2. 2
    Run 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.

    bash
    python 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
  3. 3
    Check 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.

    bash
    ls -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
  4. 4
    Add 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": {} }
    }
  5. 5
    Regenerate 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.

    bash
    cd ../..   # 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
Three traps in the downward path

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 AY-Robots download page for the desktop client that records LeRobot-format datasets from a teleoperation session
The desktop client records episodes and camera streams from a teleop session. Conversion between format versions still happens with the upstream scripts on your machine.

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.jsonGR00T meta/stats.json
Produced byaggregate_stats() over the per-episode statscalculate_dataset_statistics() over the data Parquet files
Fields per featuremin, max, mean, std, count, plus q01/q10/q50/q90/q99 if the per-episode stats carry themmean, std, min, max, q01, q99, always
Quantilesfrom a 5000-bin running histogram, and only forwarded if every episode has them, so a tree converted up from v2.1 has noneq01 and q99, exact, via numpy
Image and video featuresincluded, per channel, RGB divided by 255 into [0, 1]; depth keeps its stored unitsexcluded, only features whose dtype contains "float"
Rows used, numeric featuresevery frame of every episodeevery row of every data/*/*.parquet, in one DataFrame
Rows used, image featuressampled: 100 up to about 500 frames, 177 at 1000, 594 at 5000, 1000 at 10000, cap 10000not applicable
Cache invalidationnone, 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.

bash
# 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.py
Two toolchains, two definitions of correct. Do not expect the numbers to agree to the last digit on identical data.
The fingerprint only hashes dtype and shape

GR00T 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.

Converting, versus the two alternatives
Reasons to convert
  • 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.
Reasons not to
  • 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.

  1. 1
    Record

    Use lerobot-record against your leader and follower arms. Current lerobot writes v3.0.

    bash
    lerobot-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"
  2. 2
    Keep the v3.0 copy for lerobot policies

    SmolVLA, ACT and Pi0.5 train off this tree directly. No conversion step at all.

    bash
    lerobot-train --dataset.repo_id=you/so100_pickplace \
      --policy.type=smolvla --steps=20000
  3. 3
    Branch a v2.1 copy for GR00T

    Convert down into a separate parent directory so the v3.0 original stays untouched.

    bash
    cp -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
  4. 4
    Add modality.json and rebuild stats

    The two steps the converter does not do for you.

    bash
    cp 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
What this costs you in wall clock

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.

Pre-flight checklist

  1. Read codebase_version out of meta/info.json. Do not infer the version from directory shape.
  2. List meta/ and compare against the file table above. A valid version number with a missing file is the common surprise.
  3. If the dataset came from the Hub, look for a revision tag in the version you need before running any converter.
  4. Decide the direction from the trainer: GR00T N1.7 and GR00T N1.5 need v2.0 or v2.1, everything else needs v3.0.
  5. Copy the dataset first. Both scripts convert in place and rename the original out from under you.
  6. Pass --push-to-hub=false unless you specifically intend to rewrite the Hub repo.
  7. Write meta/modality.json for any GR00T-bound dataset and check the index ranges against your arm's degrees of freedom.
  8. 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.
The AY-Robots glossary entry for the LeRobot dataset format, describing episodes, camera streams and joint states
The glossary entry for the format itself, which is the shortest possible version of everything above.

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 catalogue

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started