Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b756b5e
Add LanceDB-powered dataloaders for the 3 Cosmos training loaders
AyushExel Jun 23, 2026
1c1d9ce
docs: add action-loader disk-footprint table to README
AyushExel Jun 23, 2026
80bb43e
experiments: filtered-sampling (predicate pushdown) benchmark + docs
AyushExel Jun 23, 2026
8835733
validate: train-equivalence test (Lance vs base loader, same model)
AyushExel Jun 23, 2026
068c57c
validate: multi-GPU per-epoch timing (training is compute-bound here)
AyushExel Jun 23, 2026
9a93c21
validate: data-bound regime demo (S3 + tiny compute) shows Lance 1.74…
AyushExel Jun 23, 2026
c40983d
validate: decent-epochs real training, base vs lance outputs identical
AyushExel Jun 23, 2026
1509d7d
experiments: borrow episode-shuffle + lance-optimal knobs (audit)
AyushExel Jun 23, 2026
4e935d5
merge working branch into experiments (bring train_* demos)
AyushExel Jun 23, 2026
64d3757
experiments: add lance-episode mode to data-bound demo + honest re-me…
AyushExel Jun 23, 2026
00183ce
perf: batch take_blobs across a batch's episodes (was per-episode loop)
AyushExel Jun 23, 2026
5351a06
fix: honest faithful action numbers (base-episode baseline, local vs S3)
AyushExel Jun 23, 2026
44e7e40
benchmark: self-contained combined loader (local/S3/default-mixed reg…
AyushExel Jun 24, 2026
253a29e
cleanup: remove superseded benches + redundant docs
AyushExel Jun 24, 2026
bc381ca
docs: honest numbers (local 3.11x / S3 2.64x / default 2.66x) + REPRO…
AyushExel Jun 24, 2026
791edf3
Lance dataloaders: plain-binary S3 fast path, worker rebalancing, e2e…
AyushExel Jun 24, 2026
b605f2e
docs: add combined-mixer row to the per-loader tables (BENCHMARKS.md)
AyushExel Jun 24, 2026
cf592ed
docs: add measured dataset size chart for the recreated combined-view…
AyushExel Jun 24, 2026
b83575c
benchmarks: add portable run_matrix.sh + run_e2e.sh drivers (env-var …
AyushExel Jun 24, 2026
c760fef
Lance dataloaders: consolidated, verified drop-in for Cosmos training
AyushExel Jun 29, 2026
4e4bb74
lance: module-level imports, trim redundant test, drop unused code
AyushExel Jun 29, 2026
23d86a8
Merge remote-tracking branch 'origin/main' into lancedb-dataloader
AyushExel Jun 29, 2026
6d603b1
lance: fix add_special_tokens import after main's sequence_packing re…
AyushExel Jun 29, 2026
e053b78
lance: drop the raw-bytes DROID loader + e2e training example (minima…
AyushExel Jun 30, 2026
69b02ef
lance: read via the Permutation API (plain large_binary); drop the un…
AyushExel Jun 30, 2026
d59bf90
lance: order-safe takes, pylance-free VLM scan, drop-in builders + re…
AyushExel Jun 30, 2026
517c41b
lance: inline _rows drop (remove single-use mixin); honest memory not…
AyushExel Jun 30, 2026
4f5db66
lance README: per-loader benchmarks + "How it works" (schema, torchco…
AyushExel Jul 1, 2026
c0f02f2
lance README: per-loader table with Local + S3 columns
AyushExel Jul 1, 2026
ae941a7
lance README: note where conversion scripts live (+ VLM convert)
AyushExel Jul 1, 2026
3afb54c
lance README: per-loader detail + full schemas, drop ad-style bullets…
AyushExel Jul 1, 2026
2d588a2
lance README: drop O(1) claim, describe shuffle plainly
AyushExel Jul 1, 2026
7d38c8d
lance: fix VLM e2e chat-template format; report VLM standalone (base …
AyushExel Jul 1, 2026
9aee58d
lance README: add dataset-size comparison (composed gop=1 vs original…
AyushExel Jul 2, 2026
af27602
lance README: state benchmark dataset source (lerobot/droid_1.0.1 sub…
AyushExel Jul 2, 2026
a81c5f1
Update README.md
AyushExel Jul 2, 2026
663df96
lance README: document action loader is a hybrid (Lance video + base …
AyushExel Jul 9, 2026
1ab1899
lance: action loader fully Lance (labels + video); add design.md
AyushExel Jul 9, 2026
06a5536
Merge remote-tracking branch 'origin/main' into lancedb-dataloader
AyushExel Jul 9, 2026
e271927
lance: adapt to the rewritten lazy-LeRobot action base (post main merge)
AyushExel Jul 9, 2026
33c4ed1
lance: full-branch audit — correctness fixes + cleanup
AyushExel Jul 9, 2026
b14094e
lance design.md: tighten prose, schemas as tables
AyushExel Jul 9, 2026
fd22b56
lance: explicit per-table schemas in design.md; table_sizes.py + visi…
AyushExel Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,7 @@ cython_debug/
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore

# Lance dataloader benchmarks: generated run artifacts
benchmarks/lance/train_out/
benchmarks/lance/matrix_results.txt
161 changes: 161 additions & 0 deletions benchmarks/lance/base_standins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# SPDX-License-Identifier: OpenMDW-1.1
"""Benchmark standins over base Cosmos loaders.

Subclasses genuine Cosmos loaders to measure performance in storage regimes
not natively supported by the base classes.
"""

from __future__ import annotations

import os
import tempfile
from contextlib import contextmanager
from pathlib import Path
from types import SimpleNamespace
from typing import Any

import boto3
from transformers import AutoTokenizer

from cosmos_framework.data.generator.action.datasets.droid_lerobot_dataset import DROIDLeRobotDataset
from cosmos_framework.data.generator.local_datasets.sft_dataset import (
SFTDataset,
_load_sft_metadata_from_s3,
)

_QWEN_TOKENIZER = "Qwen/Qwen2.5-7B"


@contextmanager
def hf_online_preserved():
"""Constructing the action base flips HF Hub offline process-wide (env + constant);
restore both so HF-dependent loaders (tokenizers, streaming) keep working."""
import huggingface_hub.constants as hfc

prev_const, prev_env = hfc.HF_HUB_OFFLINE, os.environ.get("HF_HUB_OFFLINE")
try:
yield
finally:
hfc.HF_HUB_OFFLINE = prev_const
if prev_env is None:
os.environ.pop("HF_HUB_OFFLINE", None)
else:
os.environ["HF_HUB_OFFLINE"] = prev_env


class S3DROIDLeRobotDataset(DROIDLeRobotDataset):
"""DROIDLeRobotDataset that materializes the S3-hosted videos, then runs the genuine base.

Builds a shadow root (same versioned dir name, so the base's version registry
resolves): metadata/labels are symlinked from the local tree, the mega-mp4s
under ``videos/`` are downloaded from ``s3://{bucket}/{prefix}/videos/...``.
"""

def __init__(
self,
root: str,
s3_bucket: str,
s3_prefix: str,
*,
region: str | None = None,
cache_dir: str | None = None,
**kwargs: Any,
) -> None:
src = Path(root)
key = s3_prefix.strip("/").replace("/", "_")
cache = Path(cache_dir or os.path.join(tempfile.gettempdir(), "_s3base_droid", key)) / src.name
success = cache / "success"
success.mkdir(parents=True, exist_ok=True)
for sub in ("meta", "data"):
link = success / sub
if not (link.exists() or link.is_symlink()):
link.symlink_to((src / "success" / sub).resolve())

s3 = boto3.client("s3", region_name=region) if region else boto3.client("s3")
pref = s3_prefix.strip("/") + "/videos/"
paginator = s3.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket=s3_bucket, Prefix=pref):
for obj in page.get("Contents", []):
rel = obj["Key"][len(pref) - len("videos/") :] # keep the videos/ prefix
dst = success / rel
if dst.exists():
continue
dst.parent.mkdir(parents=True, exist_ok=True)
tmp = str(dst) + f".part{os.getpid()}"
s3.download_file(s3_bucket, obj["Key"], tmp)
os.replace(tmp, dst)

super().__init__(root=str(cache), **kwargs)


def _qwen_tokenizer_config():
return SimpleNamespace(tokenizer=AutoTokenizer.from_pretrained(_QWEN_TOKENIZER))


def load_sft_metadata(
jsonl_path: str, *, s3_bucket: str | None = None, s3_prefix: str | None = None, min_frames: int = 61
) -> list[dict]:
meta = _load_sft_metadata_from_s3(None, jsonl_path, min_frames=min_frames)
if s3_bucket and s3_prefix:
base_dir = os.path.dirname(os.path.abspath(jsonl_path))
pref = s3_prefix.strip("/")
for m in meta:
vp = m["vision_path"]
if os.path.isabs(vp) or os.path.exists(vp):
rel = os.path.relpath(vp, base_dir)
else:
rel = vp
m["vision_path"] = f"s3://{s3_bucket}/{pref}/{rel}"
return meta


class BenchSFTDataset(SFTDataset):
"""SFTDataset driver for throughput benchmarks."""

def __init__(
self,
metadata: list[dict],
*,
num_video_frames: int = 16,
resolution: str = "256",
temporal_interval_mode: str = "entire_chunk",
frame_selection_mode: str = "first",
temporal_compression_factor: int = 4,
skip_tokenize: bool = False,
) -> None:
super().__init__(
metadata=metadata,
num_video_frames=num_video_frames,
resolution=resolution,
s3_credentials={},
temporal_interval_mode=temporal_interval_mode,
frame_selection_mode=frame_selection_mode,
tokenizer_config=_qwen_tokenizer_config(),
cfg_dropout_rate=0.0,
temporal_compression_factor=temporal_compression_factor,
)
self.skip_tokenize = bool(skip_tokenize)
self.shard_world_size = 1
self.shard_rank = 0
self.shard_id = 0

def _tokenize_caption(self, caption: str):
if self.skip_tokenize:
return ([], caption)
return super()._tokenize_caption(caption)

def __iter__(self):
if not hasattr(self, "_meta0"):
self._meta0 = list(self.metadata)
self.metadata = list(self._meta0)
self.is_initialized = False
return super().__iter__()

@classmethod
def from_jsonl(
cls, jsonl_path: str, *, s3_bucket: str | None = None, s3_prefix: str | None = None, **kw
) -> "BenchSFTDataset":
return cls(load_sft_metadata(jsonl_path, s3_bucket=s3_bucket, s3_prefix=s3_prefix), **kw)


__all__ = ["S3DROIDLeRobotDataset", "BenchSFTDataset", "load_sft_metadata", "hf_online_preserved"]
141 changes: 141 additions & 0 deletions benchmarks/lance/bench_action_faithful.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# SPDX-License-Identifier: OpenMDW-1.1
"""Faithful action-loader dataloader-throughput benchmark.

The production base loader uses EPISODE-SHUFFLE (`ActionIterableShuffleDataset`,
`iterable_shuffle=True`), not RandomSampler. So the apples-to-apples comparison is
episode-shuffle on BOTH sides. We also include lance-random to show that batched
takes + concurrency (LANCE_IO_THREADS) make random S3 reads competitive too.

Pure dataloader throughput (no model). Stressful config: many episodes (decoder cache
<< episodes), 8+ workers, batch 16, long steady-state. Set LANCE_IO_THREADS=256 for S3.

modes: base-episode | lance-episode | lance-random
"""

from __future__ import annotations

import argparse
import multiprocessing as mp
import os
import time

import torch
from base_standins import S3DROIDLeRobotDataset

from cosmos_framework.data.generator.action.datasets.action_sft_dataset import ActionIterableShuffleDataset
from cosmos_framework.data.generator.action.datasets.droid_lerobot_dataset import DROIDLeRobotDataset
from cosmos_framework.data.lance import LanceDROIDComposedDataset

_KW = dict(action_space="joint_pos", use_state=True, mode="policy", chunk_length=16)


def _collate(items):
return torch.stack([s["video"] for s in items])


def _build(mode, root, uri, region, cache, s3_bucket=None, s3_prefix=None):
so = {"region": region} if region else None

def _base():
# genuine DROIDLeRobotDataset; for S3 the standin materializes the mega-mp4s first.
if s3_bucket and s3_prefix:
return S3DROIDLeRobotDataset(
root=root, s3_bucket=s3_bucket, s3_prefix=s3_prefix, region=region, use_success_only=True, **_KW
)
return DROIDLeRobotDataset(root=root, use_success_only=True, **_KW)

if mode == "base-random":
return _base(), "random"
if mode == "base-episode":
return ActionIterableShuffleDataset(_base()), None # the genuine production shuffle
comp = LanceDROIDComposedDataset(uri, decode_device="cpu", decoder_cache_size=cache, storage_options=so, **_KW)
if mode == "lance-episode":
return ActionIterableShuffleDataset(comp), None
return comp, "random" # lance-random -> RandomSampler


def _measure(ds, sampler_kind, *, batch_size, num_workers, num_batches, warmup):
kw = dict(
batch_size=batch_size,
num_workers=num_workers,
collate_fn=_collate,
persistent_workers=num_workers > 0,
prefetch_factor=4 if num_workers > 0 else None,
multiprocessing_context="spawn" if num_workers > 0 else None,
)
if sampler_kind == "random":
g = torch.Generator()
g.manual_seed(0)
loader = torch.utils.data.DataLoader(ds, sampler=torch.utils.data.RandomSampler(ds, generator=g), **kw)
else:
loader = torch.utils.data.DataLoader(ds, **kw) # IterableDataset (episode-shuffle)
seen, t0 = 0, None
for i, _ in enumerate(loader):
if i == warmup:
t0 = time.perf_counter()
if i >= warmup:
seen += 1
if seen >= num_batches:
break
return seen * batch_size / (time.perf_counter() - t0)


def _mode_entry(mode, a, q):
"""Subprocess entrypoint: build+measure one mode, return its samples/s. Each mode runs
in its own process so the torchcodec/lance C++ teardown can't SIGABRT a later mode."""
ds, sk = _build(
mode, a["root"], a["uri"], a["region"], a["cache_size"], s3_bucket=a["s3_bucket"], s3_prefix=a["s3_prefix"]
)
sps = _measure(
ds,
sk,
batch_size=a["batch_size"],
num_workers=a["num_workers"],
num_batches=a["num_batches"],
warmup=a["warmup"],
)
q.put(sps)
q.close()
q.join_thread()
os._exit(0)


def main():
ap = argparse.ArgumentParser()
ap.add_argument("--root", required=True)
ap.add_argument("--uri", required=True)
ap.add_argument("--region", default=None)
ap.add_argument(
"--s3-bucket", default=None, help="if set, base materializes mega-mp4s from this bucket (S3 regime)"
)
ap.add_argument("--s3-prefix", default=None, help="key prefix the DROID videos/ tree lives under")
ap.add_argument("--cache-size", type=int, default=16)
ap.add_argument("--batch-size", type=int, default=16)
ap.add_argument("--num-workers", type=int, default=8)
ap.add_argument("--num-batches", type=int, default=60)
ap.add_argument("--warmup", type=int, default=10)
ap.add_argument("--modes", nargs="+", default=["base-episode", "lance-episode", "lance-random"])
args = ap.parse_args()

a = vars(args)
print(
f"batch={args.batch_size} workers={args.num_workers} cache={args.cache_size} "
f"num_batches={args.num_batches} LANCE_IO_THREADS={os.environ.get('LANCE_IO_THREADS', 'default')}\n"
)
print(f"{'mode':<16}{'samples/s':>12}{'vs base':>10}")
ctx = mp.get_context("spawn")
base = None
for mode in args.modes:
q = ctx.Queue()
p = ctx.Process(target=_mode_entry, args=(mode, a, q))
p.start()
sps = q.get()
p.join()
if mode == "base-episode":
base = sps
spd = f"{sps / base:.2f}x" if base else "-"
print(f"{mode:<16}{sps:>12.1f}{spd:>10}", flush=True)


if __name__ == "__main__":
main()
Loading