The AY-Robots public dataset directory listing LeRobot datasets available for policy training
dataset-versioninglerobotreproducibilityhugging-facemlops

Versioning Robot Datasets: Hashes, Revisions, Reproducible Runs

AY-Robots ResearchAugust 23, 202621 min read

How to pin a LeRobot dataset to an immutable commit: Hugging Face revisions, meta/info.json, content hashes, and the dataset pointer every checkpoint needs.

A checkpoint is a directory of weights. Weights do not remember where they came from. Six weeks after a training run you have one policy that picks up the cube and one that hovers next to it, both produced from the same dataset repository, and no way to tell them apart, because that repository has been pushed to four times since.

This is not a filing problem. It is a correctness problem. If you cannot name the exact bytes a policy was trained on, you cannot reproduce a good result, you cannot bisect a bad one, and you cannot honestly say what the model learned. This article covers the mechanics: what a LeRobot dataset records about itself, how Hugging Face revisions actually behave (including the two places they behave differently from what you expect), what hashes verify, and the four-line pointer that belongs in every checkpoint you keep.

What you need to know

  • A dataset repo id such as lerobot/svla_so101_pickplace is not a version. It is an address that resolves differently every time someone pushes.
  • The only immutable pointer on the Hugging Face Hub is a full-length commit SHA. Branches move, and tags can be deleted and recreated at a new commit.
  • LeRobot does not default to main. LeRobotDataset sets revision to the current codebase version tag, which is v3.0 in the current main branch of lerobot.
  • LeRobotDataset.push_to_hub() has tag_version=True by default, and that path deletes the existing version tag and recreates it at the new commit. Your v3.0 tag moves under you.
  • codebase_version inside meta/info.json is a format version, not a content version. Two datasets with different episodes, different cameras and different calibration all say v3.0.
  • lerobot writes train_config.json into the checkpoint, and its dataset block has a revision field. Fill it in with a SHA and the checkpoint carries its own provenance.
  • GR00T fine-tuning exposes no seed, so even a perfectly pinned dataset does not make a GR00T run bit-for-bit reproducible. lerobot's default seed is 1000.

The three failures this prevents

These are not hypotheticals. Each one has a specific mechanism and a specific fix, and each one is cheap to prevent and expensive to diagnose after the fact. If you have ever filed a ticket on a policy that only works in one setup or loss that falls while the policy does nothing, at least one of these is probably in the causal chain.

What you observeThe mechanismWhat a pinned revision would have done
Retraining the same config gives a worse policySomeone appended episodes, re-encoded video, or fixed a task string and pushed. main moved.The rerun pulls the identical commit and reproduces the original result, or proves the code changed instead.
Two checkpoints from the same week disagree and you cannot say whyBoth checkpoints record only a repo id. The repo id resolved to different commits at the two start times.A SHA in each train_config.json turns the comparison into a two-line diff.
A dataset that trained fine last month now crashes the loaderThe version tag was recreated at a commit in a newer format, or the loader was upgraded past the dataset format.The SHA still resolves to the old bytes, so the failure is isolated to the code side.
Episode metadata references files that are not in the repoA partial upload or an interrupted push left meta/ describing shards that were never committed.The last known-good SHA is still downloadable while you repair the head.

That last row is documented upstream rather than folklore. NVIDIA ships a repair script in Isaac-GR00T, scripts/repair_lerobot_metadata.py, whose docstring says plainly that some Hugging Face dataset snapshots can contain episode metadata for files that are not present in the remote repo. Its job is to drop those broken episodes from meta/episodes.jsonl, update the summary fields in meta/info.json, and regenerate stats for the datasets it changed.

What a LeRobot dataset says about itself

Every LeRobot dataset carries a metadata directory. In the v3.0 layout the canonical files are meta/info.json (schema, frame rate, path templates), meta/stats.json (per-feature mean, std, min and max used for normalisation), meta/tasks.parquet (task strings mapped to integer ids) and meta/episodes/ (per-episode lengths, task ids and offsets into the shared shards, stored as chunked parquet). One caveat if you check this against the docs: the upstream v3.0 page still lists meta/tasks.jsonl, which was the v2.1 filename. The live repository tree and the docstring of LeRobotDatasetMetadata both say meta/tasks.parquet, so the doc page is the stale one. info.json is the file that claims to be a version.

bash
# The real metadata of a public SO-101 pick-and-place dataset, at the v3.0 tag
curl -sL https://huggingface.co/datasets/lerobot/svla_so101_pickplace/resolve/v3.0/meta/info.json

{
  "codebase_version": "v3.0",
  "robot_type": "so100_follower",
  "total_episodes": 50,
  "total_frames": 11939,
  "total_tasks": 1,
  "chunks_size": 1000,
  "fps": 30,
  "splits": { "train": "0:50" },
  "data_path": "data/chunk-{chunk_index:03d}/file-{file_index:03d}.parquet",
  "video_path": "videos/{video_key}/chunk-{chunk_index:03d}/file-{file_index:03d}.mp4",
  "features": {
    "action":                { "dtype": "float32", "shape": [6], "names": ["shoulder_pan.pos", ...] },
    "observation.state":     { "dtype": "float32", "shape": [6], "names": ["shoulder_pan.pos", ...] },
    "observation.images.up":   { "dtype": "video", "shape": [480, 640, 3], "info": { ... } },
    "observation.images.side": { "dtype": "video", "shape": [480, 640, 3], "info": { ... } }
    // plus timestamp, frame_index, episode_index, index, task_index
  },
  "data_files_size_in_mb": 100,
  "video_files_size_in_mb": 500
}
Fetched 2026-08-24, abridged: the per-feature names arrays, the video codec info blocks and the five bookkeeping features are elided. Six joints, two cameras, 50 episodes, 11939 frames at 30 fps.

Now fetch the same file from the same repository at the v2.1 tag. The episode and frame counts are identical, the robot is identical, but the storage layout is a different thing entirely. That is the point where a bare repo id stops being a description of anything.

info.json fieldAt tag v2.1At tag v3.0
codebase_versionv2.1v3.0
total_episodes / total_frames50 / 1193950 / 11939 (unchanged)
data_path templatedata/chunk-{episode_chunk:03d}/episode_{episode_index:06d}.parquetdata/chunk-{chunk_index:03d}/file-{file_index:03d}.parquet
Files under data/50 parquet files, one per episode, 12890 to 18560 bytes each, 759090 bytes in total1 parquet file of 369943 bytes, under half the v2.1 total
Fields only in this versiontotal_videos (100), total_chunks (1)data_files_size_in_mb (100), video_files_size_in_mb (500)
Commit the tag resolved toe18e43de5e31997effab6abbe3810ff0e2e637d5, committed 2025-05-23f641879e22172be7e8161d5e6c1503c2d2feb657, committed 2025-09-27
codebase_version is a format version, not a content version

The string v3.0 in info.json tells a loader how to parse the directory. It says nothing about which episodes are in it, which cameras were plugged in, or which calibration convention the joint values follow. Two datasets recorded eight months apart on two different arms both say "codebase_version": "v3.0". If you are treating that field as a version pointer, you are treating a file-format magic number as provenance.

The AY-Robots glossary entry for the LeRobot dataset format, describing episodes, camera streams and joint states
The LeRobot dataset format in the glossary. The format defines the directory layout; the revision defines which bytes are in it.

Four ways to name a dataset, one of which is stable

A Hugging Face dataset repository is a git repository with large files kept outside the object graph. That means the whole git vocabulary applies: branches, tags, commit SHAs, and pull request refs. The revision argument in hf_hub_download, snapshot_download and LeRobotDataset all take the same set of values. What differs is how long each one keeps meaning the same thing.

PointerExampleImmutable?Use it for
Repo id onlylerobot/svla_so101_pickplaceNoBrowsing. Never for a run you intend to reproduce.
BranchmainNoFollowing ongoing work. Resolves to whatever HEAD is right now.
Tagv3.0No, despite appearancesHuman-readable milestones, if nothing recreates the tag.
Pull request refrefs/pr/104NoReviewing a proposed change before it lands.
Full commit SHAf641879e22172be7e8161d5e6c1503c2d2feb657YesEvery training run whose result you want to keep.

The Hub exposes all of these through a single unauthenticated endpoint, which makes it trivial to snapshot the pointer state before a run. This is the single most useful command in the whole workflow, and it costs one HTTP request.

bash
curl -s https://huggingface.co/api/datasets/lerobot/svla_so101_pickplace/refs | python3 -m json.tool

{
  "tags": [
    { "name": "v3.0", "ref": "refs/tags/v3.0",
      "targetCommit": "f641879e22172be7e8161d5e6c1503c2d2feb657" },
    { "name": "v2.1", "ref": "refs/tags/v2.1",
      "targetCommit": "e18e43de5e31997effab6abbe3810ff0e2e637d5" }
  ],
  "branches": [
    { "name": "main", "ref": "refs/heads/main",
      "targetCommit": "f641879e22172be7e8161d5e6c1503c2d2feb657" }
  ],
  "converts": [
    { "name": "parquet", "ref": "refs/convert/parquet",
      "targetCommit": "08e04aae19a9c18006d32f2e807ea23b1253ea7b" }
  ]
}
Real response, 2026-08-24. refs/convert/parquet is generated by the Hub's dataset viewer, not by you.

Run the same query against lerobot/pusht and the numbers stop agreeing with intuition. Its main branch points at commit 7628202a from 2025-09-27, and its v3.0 tag points at commit b1c3ecba from 2026-01-29. The tag is four months newer than the branch it looks like it should be on. Loading that dataset with revision="main" and loading it with the LeRobot default are two different requests.

The trap that eats a day: LeRobot does not default to main, and the tag moves

Two behaviours in src/lerobot/datasets/ compound each other. First, both LeRobotDatasetMetadata (in dataset_metadata.py) and LeRobotDataset (in lerobot_dataset.py) run self.revision = revision if revision else CODEBASE_VERSION, where CODEBASE_VERSION = "v3.0" in the current main branch. Passing no revision does not give you main, it gives you the tag matching the library you happen to have installed. Second, LeRobotDataset.push_to_hub() defaults to tag_version=True, and that path calls hub_api.delete_tag followed by hub_api.create_tag at the branch it just pushed to. Push an extra ten episodes and the v3.0 tag now points somewhere else, so every collaborator who omitted revision silently gets different data on their next cache miss. There is a third, quieter one: if the exact version tag is missing, the loader calls get_safe_version(), which falls back to the highest tag sharing the major version with a lower or equal minor version, and only logs a warning.

Pin the dataset before the run, not after

The whole discipline is five steps, done once at the start of a run. None of them require a GPU, an account upgrade, or a tool you do not already have. This is the manual path; the platform path is in the tabs further down.

  1. 1
    Resolve the pointer you were given into a SHA

    Whatever a colleague sent you, turn it into a 40-character hex string before anything else touches it. Note the full length: the Hub download helpers require the full-length hash, not an abbreviated seven-character one.

    bash
    REPO=lerobot/svla_so101_pickplace
    REF=v3.0
    
    SHA=$(curl -s "https://huggingface.co/api/datasets/$REPO/refs" \
      | python3 -c "import json,sys;d=json.load(sys.stdin);\
    print(next(t['targetCommit'] for t in d['tags']+d['branches'] if t['name']=='$REF'))")
    
    echo "$REPO @ $SHA"
    # lerobot/svla_so101_pickplace @ f641879e22172be7e8161d5e6c1503c2d2feb657
  2. 2
    Download at that SHA, into a directory you control

    The hf CLI ships with huggingface_hub. Use --revision with the SHA, and --dry-run first if you want to know the transfer size before committing to it.

    bash
    pip install -U huggingface_hub
    
    hf download "$REPO" --repo-type dataset --revision "$SHA" --dry-run
    hf download "$REPO" --repo-type dataset --revision "$SHA" \
      --local-dir ./data/svla_so101_pickplace@${SHA:0:8}
  3. 3
    Verify the bytes against the Hub checksums

    hf cache verify recomputes checksums for a cached snapshot and compares them to what the Hub says they should be. Turn the default warnings into failures so a partial download cannot pass silently. The same command takes --local-dir if you downloaded outside the cache.

    bash
    # Verify the cached snapshot at that exact revision
    hf cache verify "$REPO" --repo-type dataset --revision "$SHA" \
      --fail-on-missing-files --fail-on-extra-files
    
    # Or verify a directory you downloaded with --local-dir
    hf cache verify "$REPO" --repo-type dataset \
      --local-dir ./data/svla_so101_pickplace@${SHA:0:8}
  4. 4
    Pass the SHA to the trainer, do not let it default

    lerobot's DatasetConfig has a revision field that defaults to None, which means the codebase version tag. Setting it explicitly is the difference between a run you can repeat and a run you cannot. Set the seed too: it defaults to 1000, but writing it down costs nothing.

    bash
    lerobot-train \
      --dataset.repo_id="$REPO" \
      --dataset.revision="$SHA" \
      --policy.type=act \
      --seed=1000 \
      --output_dir=outputs/act_${SHA:0:8}
  5. 5
    Confirm the pointer landed in the checkpoint

    lerobot writes train_config.json into the checkpoint directory. Read it back and check the dataset block. If revision is null there, the checkpoint does not know what it was trained on and you have to fix that now, not in six weeks.

    bash
    python3 -c "import json;c=json.load(open('outputs/act_f641879e/train_config.json'));\
    print(c['dataset']['repo_id'], c['dataset']['revision'], c['seed'])"
    # lerobot/svla_so101_pickplace f641879e22172be7e8161d5e6c1503c2d2feb657 1000

What the hashes actually verify

A commit SHA identifies a tree, not the file contents directly. The large files in a dataset repository are stored outside git, and each one is tracked by its own content hash. You can read those hashes from the same public API, which gives you a per-file fingerprint independent of the commit.

bash
# Per-file content hashes at two revisions of the same repository
curl -s "https://huggingface.co/api/datasets/lerobot/svla_so101_pickplace/tree/v2.1/data?recursive=true"
# 50 files, e.g. data/chunk-000/episode_000000.parquet  16271 bytes
#                oid 35926c287e3943cd647f0f32...

curl -s "https://huggingface.co/api/datasets/lerobot/svla_so101_pickplace/tree/v3.0/data?recursive=true"
# 1 file,    data/chunk-000/file-000.parquet          369943 bytes
#                oid 579ad57e2454359fa9f2c0e8...
Same 50 episodes and 11939 frames, two revisions, zero shared file hashes.

Underneath, repositories on the Hub's Xet storage backend are deduplicated at chunk level rather than file level. Xet splits every file with content-defined chunking at roughly 64 KB per chunk, so chunk boundaries follow the content and survive an insertion anywhere in the file. New chunks are grouped into 64 MB blocks and each block is stored once in a content-addressed store. The practical consequence for dataset versioning is good: appending ten episodes to a 20 GB dataset transfers the changed chunks, not the whole file, so keeping many pinned revisions of the same dataset costs far less storage than the file sizes suggest. The consequence that catches people is that a cheap push is a frequent push, and frequent pushes are exactly what makes an unpinned pointer dangerous.

The provenance block that belongs in every checkpoint

lerobot already writes a full config snapshot next to the weights, under the filename train_config.json, and its dataset section is a serialised DatasetConfig. That dataclass carries repo_id, repo_type, root, episodes, exclude_episodes and revision, plus flags like use_imagenet_stats and streaming. The fields that matter for provenance are a short list, and every one of them is something you already know at launch time.

FieldWhere it comes fromWhy the run is unreproducible without it
dataset.repo_idYou chose itNames the repository. Necessary, nowhere near sufficient.
dataset.revisionResolved SHA, defaults to None (the codebase version tag)The only field that fixes which bytes were read.
dataset.episodes / exclude_episodesYour allowlist or your drop listDropping four bad episodes changes the training set. Nothing else records that you did.
seedlerobot default 1000Controls model init and data shuffling. Two seeds on identical data give two different policies.
Trainer versionpip freeze, or a git SHA of the trainerPR-level changes to normalisation and calibration alter what identical data means.
Dataset codebase_versionmeta/info.jsonTells you which loader can read it, and which conversions were applied on the way in.
Pinning every run to a commit SHA
Advantages
  • A regression becomes a diff between two SHAs instead of an argument about what changed.
  • Reruns are actually reruns. A repeat with the same SHA and the same seed either matches or proves the code moved.
  • Chunk-level deduplication on the Hub means keeping many pinned revisions costs a fraction of their nominal size.
  • Anyone reading the checkpoint six months later can fetch exactly the training set, with no access to you.
  • Ablations become honest: hold the dataset SHA fixed, vary one hyperparameter, and the comparison means something.
Trade-offs
  • You give up automatic freshness. A pinned run will not pick up the twelve good episodes a teammate added yesterday.
  • SHAs are unreadable. You still need a tag or a card entry so a human can tell v3.0-plus-ten-episodes from v3.0.
  • Deleting a Hub repository or force-rewriting history takes the pinned commit with it. Pinning is not archiving.
  • It adds a resolve step to every launch, which is exactly the kind of step people skip under time pressure.
  • It buys you nothing against non-determinism in the trainer itself, which is a separate problem with a separate fix.

Two routes to the same pinned run

The manual route gives you full control and full responsibility. The platform route removes the GPU provisioning and the format juggling, but it does not remove your responsibility for the pointer. Both are laid out below for the same goal: train ACT or SmolVLA on an SO-100 dataset whose exact contents you can name a year from now.

You install lerobot, resolve the SHA, rent or own a GPU, and run the trainer yourself. Everything in the previous section applies literally. The work you own is the resolve step, the conversion step if your model needs an older format, and the record-keeping.

bash
# 1. install
pip install lerobot huggingface_hub

# 2. resolve and record the pointer
REPO=lerobot/svla_so101_pickplace
SHA=f641879e22172be7e8161d5e6c1503c2d2feb657

# 3. train against the SHA, with an explicit seed
lerobot-train \
  --dataset.repo_id=$REPO \
  --dataset.revision=$SHA \
  --policy.type=act \
  --seed=1000 \
  --output_dir=outputs/act_${SHA:0:8}

# 4. archive the resolved pointer alongside the weights
cat > outputs/act_${SHA:0:8}/PROVENANCE.txt <<EOF
dataset  $REPO@$SHA
lerobot  $(pip show lerobot | awk '/^Version/{print $2}')
seed     1000
date     $(date -u +%FT%TZ)
EOF
  • Full control over the trainer version, which matters more than it sounds like it does.
  • You are responsible for the v3.0 to v2.1 conversion if you are training GR00T.
  • You are responsible for noticing that a version tag moved.
  • Nothing stops you from launching with an unpinned repo id at 11pm.

Conversion is where the chain quietly breaks

The moment you convert a dataset between formats, you create a second artifact that no longer has the original's commit history. This is not an edge case on this platform. GR00T N1.7 and GR00T N1.5 expect LeRobot v2.0 or v2.1, while Pi0.5, SmolVLA and ACT expect v3.0. A v3.0 dataset crashes the GR00T loader and has to be converted down. Both directions have a real script.

bash
# Upgrade an old dataset: v2.1 -> v3.0 (lerobot)
# Pushes to main and re-tags v3.0; add --push-to-hub=false to convert in place.
python src/lerobot/scripts/convert_dataset_v21_to_v30.py --repo-id=lerobot/pusht

# Downgrade for GR00T: v3.0 -> v2 (Isaac-GR00T)
# Run from this directory: it has its own pyproject.toml, which pins lerobot to one commit.
cd scripts/lerobot_conversion
uv venv && source .venv/bin/activate
uv pip install -e . --verbose
python convert_v3_to_v2.py --repo-id BobShan/double_folding_towel_v3.0
Both invocations as upstream documents them: the lerobot one from the docstring of convert_dataset_v21_to_v30.py, the GR00T one from scripts/lerobot_conversion/README.md.

GR00T also requires a file that plain LeRobot datasets do not have: meta/modality.json, which maps semantic field names onto index ranges in the concatenated state and action arrays, maps standardised video keys onto the actual video files, and lists annotation fields. That file is authored, not derived. If you hand-edit index ranges and do not commit the result, the exact input to your fine-tuning run exists only on the machine where you edited it.

Record the conversion, not just the result

A converted dataset needs three pointers, not one: the source repo and SHA, the converter and its version, and the SHA of the converted output once you push it. Isaac-GR00T models this correctly in its own conversion environment: scripts/lerobot_conversion/pyproject.toml does not depend on lerobot main, it depends on lerobot at commit c75455a6, which is the commit that bumped lerobot to 0.4.1. That is what a reproducible converter looks like. If you can only keep one pointer, keep the source SHA plus the exact converter command, because those two reconstruct the output. See dataset rejected as v3 for what the failure looks like from the trainer side.

The AY-Robots tutorial page for recording your first LeRobot dataset from a teleoperation session
Recording is where provenance starts. The tutorial for a first dataset is at /learn/record-your-first-dataset.

The change that no version number catches

Here is the case that defeats every scheme described so far. lerobot pull request 777 redesigned the hardware API and, with it, the joint convention. Before it, joint values were degrees in the range -180 to 180 and the zero position for an SO-100 or SO-101 was the arm fully extended horizontally. After it, joints are normalised to -100 to 100, the gripper to 0 to 100, and zero sits in the middle of each joint's range. The upstream docs state plainly that trajectories recorded before that change replay incorrectly if loaded directly.

python
# From the lerobot backward-compatibility guide: fixing up a pre-PR-777 dataset
key = f"{name.removeprefix('main_')}.pos"        # key naming changed too
action[key] = action_array[i].item()

# shoulder_lift: zero moved by -90 degrees AND the direction reversed
action["shoulder_lift.pos"] = -(action["shoulder_lift.pos"] - 90)

# elbow_flex: zero moved by -90 degrees
action["elbow_flex.pos"] -= 90
Replaying an old episode also needs --robot.use_degrees=true. The same two corrections must be applied to the outputs of any policy trained on that data.

Nothing in meta/info.json distinguishes a dataset recorded on either side of that line. Both say codebase_version v3.0 after conversion. Both have six float32 action dimensions. The numbers are simply on a different convention, and a policy trained on one and deployed against the other will move to the wrong place with complete confidence. This is the same class of problem as a drifted calibration, and it is why the trainer version belongs in your provenance record next to the dataset SHA.

Semantics are not covered by format versions

A format version answers "can my loader parse this directory". It does not answer "do these numbers mean what my policy thinks they mean". Joint conventions, camera key naming, units, gripper sign, and normalisation defaults all live outside the format version. Pin the trainer and the recorder version, or write the convention down in the dataset card in plain language. Preferably both.

A dataset card that is actually enough

The documentation practice here predates robot learning. Datasheets for Datasets (Gebru and colleagues, submitted 2018, last revised December 2021) argued that every dataset should ship a document covering its motivation, composition, collection process and recommended uses, by analogy with the datasheet on an electronic component. Model Cards for Model Reporting (Mitchell and colleagues, 2018) made the parallel argument for trained models. The NeurIPS 2019 reproducibility program, reported by Pineau and colleagues, turned that into a submission checklist. None of this is exotic. For a robot dataset it collapses to a README you can write in ten minutes.

markdown
---
license: apache-2.0
task_categories: [robotics]
tags: [LeRobot, so100, pick-and-place]
---

# so100_cube_pick

| Field | Value |
|---|---|
| Frozen revision | `f641879e22172be7e8161d5e6c1503c2d2feb657` |
| Format | LeRobot v3.0 (`codebase_version` in `meta/info.json`) |
| Episodes / frames | 50 / 11939 |
| Frame rate | 30 fps |
| Arm | SO-100 follower, 6 joints |
| Cameras | `observation.images.up`, `observation.images.side`, 480x640 RGB |
| Joint convention | Normalised -100..100, gripper 0..100 (post lerobot PR #777) |
| Recorded with | lerobot <version>, <commit sha> |
| Excluded episodes | 12, 31 (operator reset mid-episode) |
| Known defects | Episodes 4-9 recorded under different lighting |

## What is NOT in here

No failure recoveries. Single cube colour. One table height.
Do not expect a policy trained on this to generalise past that.
The two sections that matter most are the frozen revision and the list of what is missing.
  • State the frozen revision as a full SHA, not a tag, because the tag can be recreated.
  • State the joint convention in words. This is the field that saves you six months from now.
  • List excluded episodes explicitly. An exclude_episodes list in a config is invisible to anyone reading the repository.
  • Say what the dataset does not cover. See collecting high-quality VLA training data for what usually goes missing.
  • Keep the counts. If total_episodes in the card and in meta/info.json disagree, someone pushed without updating the card.

Where this platform does not help

Honesty is cheaper than support tickets. There are five places where none of the above is solved for you, and pretending otherwise would only cost you a day later.

  • Resolving a mutable pointer into a commit SHA is your step, not the platform's. Hand the training form a bare repo id and a bare repo id is all the run has to go on.
  • Datasets recorded locally by the desktop client are files on your machine. They have no revision at all until you push them somewhere that gives them one.
  • The v3.0 to v2.1 conversion for GR00T still has to happen, and the converted dataset is a new artifact with its own history. Nothing infers the link back to the source for you.
  • GR00T's fine-tuning entry point exposes no seed, so GR00T runs are not bit-for-bit reproducible no matter how precisely you pin the data. lerobot-based training (ACT, SmolVLA, Pi0.5) does take a seed, defaulting to 1000.
  • Pinning is not archiving. If the upstream Hub repository is deleted, your SHA points at nothing. Keep your own copy of anything a published result depends on.

The last one deserves emphasis because it is the one people get wrong in the direction that hurts. A commit SHA is a promise about identity, not about availability. If a result matters, mirror the dataset. If you are working from a large public corpus such as DROID, at minimum record the revision and the exact subset filter you applied, because re-deriving a subset from memory is not reproduction.

The AY-Robots public dataset directory listing LeRobot datasets available for training
The public dataset directory. Everything listed here is a repo id, which is an address, not a version. Resolve it before you train on it.

If you are starting from nothing, the order is: record a dataset following the recording tutorial, read the format notes in the dataset documentation, pin the revision, then run your first training job. The full hardware path from parts to a first imitation learning run is covered in the SO-100 complete guide, and the per-model launch details live in the training docs and the model-and-arm guides such as GR00T N1.7 on SO-100.

Record datasets whose provenance you control

The AY-Robots desktop client records LeRobot-format datasets straight from a teleoperation session: episodes, camera streams and joint states. A dataset you recorded yourself is one whose contents, cameras and joint convention you can still describe a year later.

Get the desktop client
Is a Hugging Face tag like v3.0 an immutable version?

No. A tag is a movable git ref. LeRobot's own push_to_hub() defaults to tag_version=True and that path calls delete_tag followed by create_tag at the new commit, so pushing a dataset update moves the v3.0 tag onto the new commit. Only a full-length commit SHA is immutable.

What does codebase_version in meta/info.json actually tell me?

It tells a loader which directory layout to expect: v2.1 stores one parquet file per episode, v3.0 concatenates many episodes into shared parquet and mp4 shards and resolves episode boundaries through metadata. It says nothing about which episodes are inside, which cameras were connected, or which joint convention the numbers follow.

If I pass no revision to LeRobotDataset, do I get main?

No. In the current main branch of lerobot, LeRobotDataset and LeRobotDatasetMetadata both set revision to CODEBASE_VERSION, which is the string v3.0. You get the tag matching your installed library. On lerobot/pusht, for example, that tag currently points at a different and newer commit than main does.

How do I check that what I downloaded matches the Hub?

Use hf cache verify with the repo id, --repo-type dataset, and --revision set to the SHA. It recomputes checksums for a cache snapshot or a local directory and compares them against the Hub. Add --fail-on-missing-files and --fail-on-extra-files so a partial download fails loudly instead of warning.

Does pinning the dataset make my training run reproducible?

It makes the data side reproducible, which is the half people usually get wrong. You still need the seed and the trainer version. lerobot's default seed is 1000 and it is written into train_config.json. GR00T's fine-tuning CLI exposes no seed at all, so GR00T runs are not bit-for-bit reproducible even with a perfectly pinned dataset.

Why does storing many versions of the same dataset not blow up my storage?

The Hub's Xet backend deduplicates at chunk level rather than file level. Files are split by content-defined chunking at roughly 64 KB per chunk, new chunks are grouped into 64 MB blocks, and only chunks that are not already stored get uploaded. Appending episodes to a large dataset therefore transfers and stores the delta, not the whole file.

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started