Spaces:
Running on L40S
Running on L40S
| # Custom Datasets for Generator and Reasoner Training | |
| This guide explains how to bring your own dataset into Cosmos training using | |
| `DataPackerDataLoader` and `JointDataPackerDataLoader` β the OSS-facing data | |
| layer that works without any internal infrastructure (no WebDataset, no | |
| object-store credentials). | |
| --- | |
| ## Contents | |
| 1. [Overview](#overview) | |
| 2. [DataPackerDataLoader](#datapackerdataloader) | |
| - [Step 1 β Prepare your data source](#step-1--prepare-your-data-source) | |
| - [Step 2 β Write your DataPacker](#step-2--write-your-datapacker) | |
| - [Step 3 β Wire into an experiment config](#step-3--wire-everything-into-an-experiment-config) | |
| - [Key parameters](#key-parameters) | |
| - [Shuffle and stateful checkpoint/resume](#shuffle-and-stateful-checkpointresume) | |
| - [Data-parallel sharding](#data-parallel-sharding) | |
| 3. [JointDataPackerDataLoader](#jointdatapackerdataloader) | |
| - [When to use it](#when-to-use-it) | |
| - [How to wire it up](#how-to-wire-it-up) | |
| - [Stateful checkpoint/resume](#stateful-checkpointresume) | |
| 4. [Real-world examples](#real-world-examples) | |
| 5. [Checklist](#checklist-for-a-new-dataset) | |
| --- | |
| ## Overview | |
| The data pipeline has two parts you control: | |
| ``` | |
| Your dataset (Dataset or IterableDataset) | |
| β | |
| βΌ | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| β DataPackerDataLoader β | |
| β β | |
| β map-style Dataset (any shuffle setting): β | |
| β ββββββββββββββββββββββββββββββββββββββββββββ β | |
| β β _ShuffledMapIterableDataset β β | |
| β β β’ per-epoch randperm (shuffle=True) β β | |
| β β or sequential (shuffle=False) β β | |
| β β β’ DP Γ worker sharding β β | |
| β β β’ position metadata for stateful resume β β | |
| β ββββββββββββββββββββ¬ββββββββββββββββββββββββ β | |
| β β β | |
| β IterableDataset: β | |
| β ββββββββββββββββββββββββββββββββββββββββββββ β | |
| β β _IterableWrapper β β | |
| β β β’ DP Γ worker sharding only β β | |
| β β β’ no stateful resume β β | |
| β ββββββββββββββββββββ¬ββββββββββββββββββββββββ β | |
| β β raw item β | |
| β ββββββββββββββββββββΌββββββββββββββββββββββββ β | |
| β β _DataPackerIterableDataset β β | |
| β β (subclass of PackingIterableDataset) β β | |
| β β β β | |
| β β β’ fill pool (pool_size samples) β β | |
| β β β’ greedy bin-pack within max_tokens β β | |
| β β β’ cap at max_batch_size β β | |
| β β β β | |
| β β β DataPacker.sft_process_sample β you β β | |
| β β β DataPacker.compute_num_tokens β you β β | |
| β β β DataPacker.sft_collate_fn β you β β | |
| β ββββββββββββββββββββββββββββββββββββββββββββ β | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| β fully-collated batch dict | |
| βΌ | |
| Trainer / model.forward() | |
| ``` | |
| Key point: **all map-style datasets** (whether `shuffle=True` or `shuffle=False`) | |
| are routed through `_ShuffledMapIterableDataset`, which attaches position | |
| metadata to every sample. This means stateful checkpoint/resume works regardless | |
| of whether shuffle is enabled. | |
| --- | |
| ## DataPackerDataLoader | |
| ### Step 1 β Prepare your data source | |
| `DataPackerDataLoader` accepts either a **map-style** `torch.utils.data.Dataset` | |
| or an **iterable-style** `torch.utils.data.IterableDataset`. Plain lists and | |
| generators are rejected with a `TypeError`. | |
| | Type | Notes | | |
| | ----------------------------------------- | ------------------------------------------------------------------------------------- | | |
| | `torch.utils.data.Dataset` (map-style) | Pass directly. Supports `shuffle=True/False` and stateful checkpoint/resume. | | |
| | `torch.utils.data.IterableDataset` | Pass directly. No shuffle, no stateful resume β shuffle externally if needed. | | |
| | HuggingFace `Dataset` | Is a `torch.utils.data.Dataset` subclass β pass directly, `shuffle=True` works. | | |
| | HuggingFace `IterableDataset` (streaming) | Is a `torch.utils.data.IterableDataset` β pass directly, use `.shuffle()` externally. | | |
| #### Loading from HuggingFace (simplest) | |
| ```python | |
| from cosmos_framework.data.vfm.data_packer_dataloader import load_data_source | |
| # HuggingFace Hub dataset (downloaded, map-style) | |
| data_source = load_data_source("liuhaotian/LLaVA-Instruct-150K", split="train") | |
| # Dataset saved with dataset.save_to_disk() | |
| data_source = load_data_source("/path/to/my_saved_dataset", split="train") | |
| # Then pass with shuffle for per-epoch shuffling + stateful resume | |
| DataPackerDataLoader(data_source=data_source, ..., shuffle=True, seed=42) | |
| ``` | |
| #### Streaming from HuggingFace (no disk space) | |
| ```python | |
| from datasets import load_dataset | |
| data_source = load_dataset( | |
| "lmms-lab/LLaVA-OneVision-Data", name="si", split="train", streaming=True | |
| ) | |
| # shuffle before passing β IterableDataset does not support internal shuffle | |
| data_source = data_source.shuffle(seed=42, buffer_size=10_000) | |
| ``` | |
| #### Custom map-style dataset | |
| ```python | |
| class MyMapDataset(torch.utils.data.Dataset): | |
| def __len__(self): return 10_000 | |
| def __getitem__(self, idx): return {"video": ..., "text": ...} | |
| # Pass directly β DataPackerDataLoader handles sharding and shuffle internally | |
| DataPackerDataLoader(data_source=MyMapDataset(), ..., shuffle=True, seed=42) | |
| ``` | |
| --- | |
| ### Step 2 β Write your DataPacker | |
| `DataPacker` is an abstract base class. Implement all three methods, then place | |
| the class in the same experiment config file that uses it. | |
| ```python | |
| from cosmos_framework.data.vfm.data_packer import DataPacker | |
| class MyDataPacker(DataPacker): | |
| def sft_process_sample(self, item: dict) -> dict: | |
| """ | |
| Convert one raw item from data_source into a training-ready sample. | |
| Called inside DataLoader workers β tokenization, decoding, transforms go here. | |
| """ | |
| ... | |
| return {"input_ids": ..., "labels": ..., ...} | |
| def compute_num_tokens(self, sample: dict) -> int: | |
| """ | |
| Return the token cost of one processed sample. | |
| Used by the packing engine to decide how many samples fit in a batch. | |
| """ | |
| return int(sample["input_ids"].shape[0]) | |
| def sft_collate_fn(self, samples: list[dict], max_len: int, | |
| ignore_label_id: int = -100) -> dict: | |
| """ | |
| Collate a list of processed samples into one batch dict. | |
| max_len is the longest token sequence in this batch (for padding). | |
| """ | |
| ... | |
| return {"input_ids": ..., "labels": ..., ...} | |
| ``` | |
| > **Note on extra batch keys**: For map-style datasets, `DataPackerDataLoader` | |
| > automatically appends `sample_worker_id`, `sample_epoch`, and `sample_index` to | |
| > every batch dict. These are used by `DataLoaderStateCallback` for stateful | |
| > checkpoint/resume and are transparent to the model as long as `training_step` | |
| > accesses the batch by key (not `**kwargs` unpack). | |
| #### Token counting for Generator models | |
| ```python | |
| import math | |
| def compute_num_tokens(self, sample: dict) -> int: | |
| tokens = 1 + len(sample.get("text_token_ids", [])) | |
| v = sample.get("video") # shape [C, T, H, W] | |
| if v is not None: | |
| _, T, H, W = v.shape | |
| latent_h = math.ceil(H / (self.spatial_compression * self.patch_spatial)) | |
| latent_w = math.ceil(W / (self.spatial_compression * self.patch_spatial)) | |
| latent_t = 1 + (T - 1) // self.temporal_compression | |
| tokens += latent_h * latent_w * latent_t + 2 | |
| return tokens | |
| ``` | |
| Typical values: `spatial_compression=16`, `temporal_compression=4`, `patch_spatial=2`. | |
| --- | |
| ### Step 3 β Wire everything into an experiment config | |
| ```python | |
| from cosmos_framework.utils.lazy_config import LazyCall as L, LazyDict | |
| from cosmos_framework.data.vfm.data_packer_dataloader import DataPackerDataLoader, load_data_source | |
| from cosmos_framework.callbacks.dataloader_state import DataLoaderStateCallback | |
| from hydra.core.config_store import ConfigStore | |
| cs = ConfigStore.instance() | |
| my_experiment = LazyDict(dict( | |
| defaults=[...], # inherit model, optimizer, scheduler from a base | |
| trainer=dict( | |
| callbacks=dict( | |
| # Tracks per-worker (epoch, position) for checkpoint/resume. | |
| # Works with both shuffle=True and shuffle=False for map-style datasets. | |
| dataloader_state=L(DataLoaderStateCallback)(distributor_type="data_packer"), | |
| ), | |
| ), | |
| dataloader_train=L(DataPackerDataLoader)( | |
| data_source=L(load_data_source)(name="my-org/my-dataset", split="train"), | |
| data_packer=L(MyDataPacker)(...), | |
| max_tokens=16000, | |
| pool_size=16, | |
| max_batch_size=1, | |
| shuffle=True, # per-epoch randperm, different order every epoch | |
| seed=42, # epoch e uses seed+e β reproducible permutations | |
| num_workers=4, | |
| prefetch_factor=4, | |
| persistent_workers=True, | |
| pin_memory=True, | |
| ), | |
| dataloader_val=None, | |
| ), flags={"allow_objects": True}) | |
| cs.store(group="experiment", package="_global_", name="my_experiment", node=my_experiment) | |
| ``` | |
| Launch: | |
| ```bash | |
| torchrun --nproc_per_node=8 -m cosmos_framework.scripts.train \ | |
| --config=cosmos_framework/configs/base/config.py -- \ | |
| experiment=my_experiment \ | |
| trainer.max_iter=1000 | |
| ``` | |
| --- | |
| ### Key parameters | |
| | Parameter | What it controls | | |
| | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | |
| | `max_tokens` | Token budget per batch. Packing stops when adding one more sample would exceed this. For Generator, counts video latent tokens; for Reasoner, counts `input_ids` length. | | |
| | `pool_size` | Samples to buffer before bin-packing. Larger pool β better packing efficiency, more memory. Default: 16. | | |
| | `max_batch_size` | Hard cap on samples per batch regardless of token budget. Use `1` for Reasoner (one image per step), `128`β`256` for action policy training. | | |
| | `shuffle` | `True` β per-epoch `randperm` shuffle for map-style datasets (no effect on `IterableDataset`, a warning is logged). `False` β sequential, still resumable. | | |
| | `seed` | Base seed for the shuffle permutation. Epoch `e` uses `seed + e` β reproducible, different ordering every epoch. Default: `0`. | | |
| | `name` | Optional string that namespaces resume env vars. **Required** when multiple `DataPackerDataLoader` instances share the same process (i.e., inside `JointDataPackerDataLoader`). Each inner loader must have a unique `name` matching its key in the `dataloaders` dict. Leave empty (default) for single-loader setups. | | |
| | `long_threshold` | Samples with token count β₯ this are emitted as singleton batches, bypassing packing. Default: 6400. | | |
| | `batching_strategy` | `"prefer_closest"` (default) picks candidates nearest in token length. `"prefer_first"` picks the first that fits. | | |
| | `num_workers` | DataLoader workers for `sft_process_sample`. Use `0` for debugging. | | |
| | `persistent_workers` | Automatically promoted to `True` for all map-style datasets when `num_workers > 0` (required for correct resume behaviour). | | |
| --- | |
| ### Shuffle and stateful checkpoint/resume | |
| For map-style datasets, `DataPackerDataLoader` tracks each worker's position and | |
| resumes training from **exactly** where it left off after a checkpoint. This works | |
| for both `shuffle=True` and `shuffle=False`. | |
| #### How it works | |
| 1. Each epoch, a permutation is generated with `torch.randperm(n, generator=torch.Generator().manual_seed(seed + epoch))` (or `list(range(n))` when `shuffle=False`). | |
| 2. Each `(dp_rank, worker_id)` pair sees a disjoint stride: `perm[stream_id :: total_streams]` where `stream_id = dp_rank * num_workers + worker_id`. | |
| 3. After each training step, `DataLoaderStateCallback` reads `sample_epoch` and `sample_index` from the batch and tracks the high-water mark per worker. | |
| 4. At checkpoint, the DCP checkpointer saves the state to `iter_XXXXXXXXX/dataloader/rank_{rank}.pkl`. | |
| 5. On resume, `load_state_dict` sets `DP_STATE_WORKER_{worker_id}_EPOCH/INDEX` env vars before workers start, and workers fast-forward past already-seen samples. | |
| **At most `pool_size` (default 16) samples are re-processed** at each resume (they pass through `sft_process_sample` again but are trained on only once). | |
| #### Required wiring | |
| ```python | |
| from cosmos_framework.callbacks.dataloader_state import DataLoaderStateCallback | |
| exp["trainer"]["callbacks"]["dataloader_state"] = L(DataLoaderStateCallback)( | |
| distributor_type="data_packer" | |
| ) | |
| ``` | |
| Use `ckpt_type=dcp` (the default) β not `ckpt_type=dummy` which disables all checkpointing. | |
| #### Limitations | |
| - **Map-style datasets only.** Stateful resume is not supported for `IterableDataset` sources. | |
| - **`fork` start method required** (the default for Linux/CUDA). `spawn` is not supported. | |
| - **`persistent_workers=True` required** when `num_workers > 0` (auto-enforced for all map-style datasets). | |
| --- | |
| ### Data-parallel sharding | |
| `DataPackerDataLoader` automatically shards `data_source` across ranks **and** | |
| DataLoader workers. Each `(dp_rank, worker_id)` pair receives a disjoint subset β | |
| a strided slice of the (shuffled) permutation. | |
| **If your dataset already shards internally** (like `SFTDataset`), disable its | |
| sharding before passing it to `DataPackerDataLoader`: | |
| ```python | |
| def get_my_dataset_no_dp(**kwargs): | |
| dataset = MyDataset(**kwargs) | |
| dataset.shard_world_size = 1 # disable internal sharding | |
| dataset.shard_rank = 0 | |
| return dataset | |
| ``` | |
| **For FSDP + TP/PP**: pass `parallel_dims` so the correct DP rank is used | |
| (global rank β DP rank in these setups): | |
| ```python | |
| DataPackerDataLoader(..., parallel_dims=parallel_dims) | |
| ``` | |
| --- | |
| ## JointDataPackerDataLoader | |
| ### When to use it | |
| `JointDataPackerDataLoader` wraps **multiple** `DataPackerDataLoader` instances | |
| with ratio-based seeded selection. Use it when training on multiple datasets with | |
| different modalities or formats β for example, video + action data at a 3:1 ratio. | |
| Semantics mirror `IterativeJointDataLoader`: | |
| - **One batch = one dataset** β samples from different datasets never share a packed batch. | |
| - Ratios control how frequently each dataset is visited (per batch, not per sample). | |
| - Selection is deterministic: step `i` always picks the same dataset given the same `seed`. | |
| - Stateful checkpoint/resume: both the outer step counter (`global_id`) and each inner | |
| loader's per-worker position are saved and restored. | |
| ### How to wire it up | |
| Each inner `DataPackerDataLoader` must be given a unique `name` that matches its | |
| key in the `dataloaders` dict. The `name` namespaces the resume env vars to | |
| prevent conflicts between concurrent loaders. | |
| ```python | |
| from cosmos_framework.data.vfm.data_packer_dataloader import DataPackerDataLoader, JointDataPackerDataLoader | |
| from cosmos_framework.callbacks.dataloader_state import JointDataLoaderStateCallback | |
| from cosmos_framework.utils.lazy_config import LazyCall as L | |
| # Build the joint loader | |
| joint_loader = JointDataPackerDataLoader( | |
| dataloaders={ | |
| "video": { | |
| "dataloader": DataPackerDataLoader( | |
| data_source=MyVideoDataset(...), | |
| data_packer=MyVideoDataPacker(...), | |
| max_tokens=45056, | |
| shuffle=True, | |
| seed=0, | |
| name="video", # must match the key above | |
| num_workers=4, | |
| persistent_workers=True, | |
| pin_memory=True, | |
| ), | |
| "ratio": 3, # video 3Γ, action 1Γ | |
| }, | |
| "action": { | |
| "dataloader": DataPackerDataLoader( | |
| data_source=MyActionDataset(...), | |
| data_packer=MyActionDataPacker(...), | |
| max_tokens=999_999, | |
| max_batch_size=128, | |
| shuffle=True, | |
| seed=0, | |
| name="action", # must match the key above | |
| num_workers=4, | |
| persistent_workers=True, | |
| pin_memory=True, | |
| ), | |
| "ratio": 1, | |
| }, | |
| }, | |
| seed=42, # controls outer dataset selection sequence | |
| ) | |
| # Wire into the experiment config | |
| exp["dataloader_train"] = joint_loader | |
| exp["trainer"]["callbacks"]["dataloader_state"] = JointDataLoaderStateCallback( | |
| outer_loader=joint_loader, | |
| distributor_type="data_packer", | |
| ) | |
| ``` | |
| > **Reserved name**: `"global_id"` cannot be used as a dataset name β it is | |
| > reserved by the checkpoint state format. | |
| #### `JointDataPackerDataLoader` parameters | |
| | Parameter | What it controls | | |
| | ------------- | ------------------------------------------------------------------------------------------------------------------------ | | |
| | `dataloaders` | Dict mapping dataset name β `{"dataloader": DataPackerDataLoader, "ratio": int}`. Entries with `ratio <= 0` are skipped. | | |
| | `seed` | Base seed for outer dataset selection. Step `i` uses `np.random.RandomState(seed + i)` β same sequence on every rank. | | |
| #### `JointDataLoaderStateCallback` | |
| This single callback replaces the per-inner-loader `DataLoaderStateCallback` | |
| instances. It saves: | |
| - `global_id` β the outer step counter, which determines which dataset fires at each step on resume. | |
| - Per-dataset, per-worker `(epoch, index)` β each inner loader's position. | |
| All state is written to a single DCP checkpoint entry (`checkpoint_component="dataloader"`). | |
| ### Stateful checkpoint/resume | |
| At checkpoint step `N`: | |
| - `global_id = N` is saved. | |
| - Each inner loader saves its per-worker `(epoch, index)` under its `name` key. | |
| On resume: | |
| 1. `JointDataLoaderStateCallback.load_state_dict` calls `set_start_iteration(N)` on the outer loader β selection sequence resumes from step `N`. | |
| 2. Each inner `DataLoaderStateCallback.load_state_dict` sets namespaced env vars (`DP_STATE_{name}_WORKER_{id}_EPOCH/INDEX`) β workers fast-forward to the saved position. | |
| Inner loader iterators are created lazily on the **first** `__iter__` call (not at | |
| `__init__` time), ensuring workers fork **after** env vars have been set. | |
| --- | |
| ## Real-world examples | |
| ### Reasoner β HuggingFace image-text dataset | |
| **File**: `cosmos_framework/configs/base/vlm/experiment/llava_ov_datapacker_experiment.py` | |
| ``` | |
| data_source: lmms-lab/LLaVA-OneVision-Data (streaming IterableDataset) | |
| DataPacker: VLMDataPacker | |
| sft_process_sample: ShareGPT β OpenAI messages β Qwen3-VL processor | |
| compute_num_tokens: len(input_ids) | |
| sft_collate_fn: unsqueeze batch dim, keep pixel_values flat | |
| max_batch_size: 1 | |
| max_tokens: ~16000 | |
| shuffle: False (streaming IterableDataset β use .shuffle() externally) | |
| ``` | |
| ### Action Policy β Robot learning (LIBERO) | |
| **File**: `cosmos_framework/configs/base/experiment/action/posttrain_config/libero_policy_datapacker_experiment.py` | |
| ``` | |
| data_source: LIBERODataset (map-style Dataset, passed directly) | |
| DataPacker: ActionDataPacker | |
| sft_process_sample: full ActionTransformPipeline (resize, tokenize, pad action) | |
| compute_num_tokens: VAE video tokens + text tokens | |
| sft_collate_fn: action/domain_id/sequence_plan fields + video + text | |
| max_batch_size: 128 (token budget disabled β batch bounded by max_batch_size) | |
| max_tokens: 999999 | |
| shuffle: True, seed=0 | |
| ``` | |
| `LIBERODataset` is a map-style `Dataset` passed directly. `shuffle=True` enables | |
| per-epoch shuffling and stateful checkpoint/resume. This pattern (high `max_tokens` | |
| - bounded `max_batch_size`) is standard for action policy training where you want | |
| a fixed number of demonstrations per step. | |
| --- | |
| ## Checklist for a new dataset | |
| ### Single dataset (`DataPackerDataLoader`) | |
| - [ ] Choose a `data_source`: map-style `Dataset` or `IterableDataset` (no plain lists/generators) | |
| - [ ] For map-style: pass directly; use `shuffle=True, seed=<N>` for per-epoch shuffle | |
| - [ ] For iterable: shuffle externally before passing (e.g. `.shuffle(buffer_size=N)`) | |
| - [ ] If dataset has internal DP sharding, disable it (`shard_world_size=1`) | |
| - [ ] Subclass `DataPacker` and implement `sft_process_sample`, `compute_num_tokens`, `sft_collate_fn` | |
| - [ ] Choose `max_tokens` and `max_batch_size` for your modality | |
| - [ ] Add `DataLoaderStateCallback(distributor_type="data_packer")` to the experiment's callbacks (works for both `shuffle=True` and `shuffle=False` on map-style datasets) | |
| - [ ] Use `ckpt_type=dcp` (not `dummy`) for real checkpoint/resume | |
| - [ ] Register in Hydra ConfigStore with `cs.store(group="experiment", ...)` | |
| - [ ] Smoke-test with `ckpt_type=dummy trainer.max_iter=10` before a full run | |
| ### Multiple datasets (`JointDataPackerDataLoader`) | |
| - [ ] Give each inner `DataPackerDataLoader` a unique `name` matching its key in `dataloaders` | |
| - [ ] Set appropriate `ratio` for each dataset (controls visit frequency per batch) | |
| - [ ] Use `JointDataLoaderStateCallback(outer_loader=joint_loader)` instead of `DataLoaderStateCallback` | |
| - [ ] Do **not** also register standalone `DataLoaderStateCallback` for inner loaders β `JointDataLoaderStateCallback` handles all of them | |
| - [ ] Avoid using `"global_id"` as a dataset name (reserved) | |
| - [ ] Use `ckpt_type=dcp` for real checkpoint/resume | |