lerobot-lancedb wraps that into a ready-made dataset class for video features: same items, same training pipeline. Image-backed features are not supported — re-encode them as video first.
What you get:
- Train from object storage while keeping your GPUs fed. Lance reads random frames straight from S3, GCS, the HF Hub or HF Storage Buckets, fetching only the bytes each batch needs, so every batch is a global shuffle: any frame, from anywhere in the dataset. In the table below (samples/s, 8 workers, batch 64), Lance and upstream local both draw every batch from the whole dataset. Upstream streaming does a reservoir shuffle, reading the dataset in order and drawing each batch from a pool of recently seen frames, so those two columns are not the same job:
The shuffle window is 0.05% of the dataset, at least 5,000 frames: 5,000 on the small sets, 13,815 on DROID. Streaming DROID at that window in upstream default needs 122 GB, so those two runs used a 256 GB machine with the same decode speed. The DROID pair is noisy: Lance 684 and 1,124, streaming 303 and 653.
Same batches: Lance from an object store is 1.9x faster than upstream streaming on DROID and 5x to 15x on the other datasets, with 8 GB of RAM against 122 GB on DROID. Global shuffle: Lance with nothing downloaded is 1.1x to 1.7x faster than upstream reading local NVMe after the download. berkeley_rpt, toto and soarm101 are left out because their metadata lists frames the videos do not have;
lerobot-lance-doctor reports that before you train.
- Faster iteration, end to end. 10,000 steps of SmolVLA on DROID, 8xH100, lerobot’s default
num_workers=4, same seed:
Same model, same loss, 33 minutes sooner, and no 384 GB download first. The difference is time the GPUs spend waiting on data, so it grows with the run: at 100k steps it is over five hours, and every re-run and sweep point pays it again.
- The training table is also the index. Parquet has no secondary indexes, so finding frames instead of scanning them usually means another system per question: a vector database for embeddings, a search service for instructions, a feature store for scores. With Lance those are columns and indexes on the same table the DataLoader reads. One query spans all of them, at a version the trainer can reopen.
Install
Convert a dataset
Any LeRobot v3.0 dataset, from the Hub or a local directory:meta/ as is and adds three Lance tables (frames, videos, meta). lerobot-lance-doctor audits the result and often finds problems in the source: DROID carries 44% orphaned rows, and three of eight public datasets we converted have frame-count defects that only random access exposes.
Host the result anywhere: an HF dataset repo (hf upload, tag it v3.0), an HF Storage Bucket (hf buckets sync), or any object store (aws s3 sync).
Load and train
lerobot-lancedb provides one map-style dataset class for both image and
video features, reading from a local path or an s3://, gs:// or hf://
URI with nothing downloaded up front. Pair it with lance_mp_context() so
multi-worker DataLoaders share Lance’s own multiprocessing context instead
of the default one:
Python
LeRobotDataset, and delta_timestamps configures
temporal windows the same way.
Or from the CLI. Held-out evaluation on a remote dataset needs random access, so it only works on the Lance path:
Visualize TB-scale datasets in Foxglove
LeRobot’s dataset viewer can serve an episode to Foxglove over its WebSocket protocol (lerobot#4542), and with a Lance dataset it works wherever the data lives:ws://localhost:8765, and scrub. Cameras, joint state and actions show up as topics you can lay out however you like:

Query, curate, enrich
The frames table is a LanceDB table like any other:Python
Python
add_columns registers a @udf function and its input columns as a new column and computes nothing; backfill computes and stores the values later, in parallel. No copy of the dataset, and the videos are never touched:
videos table:
emb_siglip2 vectors are nearly identical; LanceDB Enterprise’s
native dedupe collapses them into one
representative per cluster, directly over the index, without a pairwise scan:
jerk_score, the episode and
camera metadata, the video blob reference — since dedupe returns full
rows, not just which ones matched. Wrap it in a materialized view, as above,
to keep the deduplicated set current as new episodes land; see
Materialized views.
Search is an index on those columns: a vector index over the embeddings built in 29.7 s across 27.6M rows, a full-text index over the instructions in 7.0 s, queries in 10 to 54 ms.
Curation is one query across all of it. Semantic search with a predicate on the derived score, no join, no second system:
tbl.search().where("jerk_score > 1.2875 AND success = false") pulls the rough, failed frames out of 27.6M rows in milliseconds, ready to become a fine-tuning set or a labeling queue.
EDA is a scan of the tabular columns: a pass over all 27.6M rows reads 2.52 GB in 190 s and decodes no video.
Then drop what you found from training with episodes=[...] instead of rewriting files. The reader ignores columns it does not know, and the table is versioned, so curation and training share one copy of the data at a version you can reopen.
Lance datasets are read-only in LeRobot: record and edit in the default format, then convert.
lance-format/pusht-lance and friends under the lance-format org.