Spaces:
Running on Zero
Running on Zero
File size: 9,001 Bytes
a1833c8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: OpenMDW-1.1
"""Built-in BatchCollator implementations."""
from __future__ import annotations
import torch
import torch.utils.data
from torch.utils.data.dataloader import default_collate
from cosmos_framework.data.generator.dataflow.base import BatchCollator
class DefaultBatchCollator(BatchCollator):
"""Stacks samples with torch's default_collate — stock DataLoader behavior."""
def collate(self, samples: list[dict]) -> dict:
return torch.utils.data.default_collate(samples)
# ---------------------------------------------------------------------------
# VFMListCollator — reproduces the legacy PackingDataLoader packed-batch structure:
# for each raw sample, inner-collate at batch_size=1 (custom_collate_fn copy),
# split per _get_next_sample i=0 rules, then _update_output_batch-accumulate.
#
# This produces list[list[Tensor]] for _MULTI_ITEM_KEYS (not flat list[Tensor]).
# ---------------------------------------------------------------------------
_TIMING_KEYS = {"_sample_time", "_aug_time", "_pre_aug_time", "_aug_step_times"}
_BATCH_TIMING_KEYS = {
"_worker_batch_time",
"_worker_aug_time",
"_worker_io_time",
"_worker_aug_step_times",
"_worker_id",
}
# Verbatim copy of JointDataLoader._MULTI_ITEM_KEYS
_MULTI_ITEM_KEYS = {"text_token_ids", "images", "video", "action", "sound"}
# Verbatim copy of JointDataLoader._FLATTEN_LIST_KEYS
_FLATTEN_LIST_KEYS = {"image_size"}
def _vfm_inner_collate(batch):
"""
Verbatim copy of custom_collate_fn from joint_dataloader.py.
Collate function that works like default_collate for all keys other than "text_token_ids", "images", and "video".
For "text_token_ids", "images", and "video" it simply returns them in a list, instead of stacking them as a tensor.
"""
list_collate_keys = {
"text_token_ids",
"images",
"video",
"action",
"domain_id",
"sequence_plan",
"sound",
"raw_action_dim",
"image_size",
}
# Data keys where a per-sample value of ``None`` is a meaningful signal
# (e.g. audio extraction failed for that sample → ``sound=None`` paired
# with ``plan.has_sound=False``). These keys must be kept as a list with
# ``None`` placeholders so the model can align per-sample data 1:1 with
# per-sample plans. Dropping the entire key on any None would leave the
# remaining sound tensors mis-aligned with the plans whose ``has_sound``
# flag was set BEFORE collation, causing ``sequence_packing`` to index
# past the end of ``x0_tokens_sound``.
sparse_data_keys = {"sound"}
# Handle the case where the batch is already a dictionary (e.g. column-wise batching)
if isinstance(batch, dict):
return {key: (value if key in list_collate_keys else default_collate(value)) for key, value in batch.items()}
# Handle standard list of samples
elem = batch[0]
if isinstance(elem, dict):
# Some Action datasets add optional metadata keys (for example
# ``additional_view_description`` for concat-view captions) only for a
# subset of samples. PyTorch can batch such samples together when
# DataLoader batch_size > 1; collating only elem's keys and indexing
# every sample by that key turns the optional field into a fatal
# KeyError. Use the union of keys and skip optional keys that are not
# present in every sample. Required training keys still fail loudly via
# downstream assertions if actually missing.
result = {}
keys = set().union(*(d.keys() for d in batch))
for key in keys:
if key in _TIMING_KEYS:
continue
values = [d.get(key) for d in batch]
if any(value is None for value in values):
# Sparse data keys keep their None placeholders to preserve
# 1:1 alignment with sequence_plan. Other (optional metadata)
# keys not present in every sample are dropped.
if key in sparse_data_keys:
result[key] = values
continue
if key in list_collate_keys:
result[key] = values
else:
result[key] = default_collate(values)
result.update(_aggregate_worker_timing(batch))
return result
else:
return default_collate(batch)
def _aggregate_worker_timing(samples: list[dict]) -> dict:
"""Extract per-sample timing keys, aggregate into per-batch scalars."""
info: dict[str, float | int] = {}
if "_sample_time" in samples[0]:
info["_worker_batch_time"] = sum(s.get("_sample_time", 0.0) for s in samples)
if "_aug_time" in samples[0]:
aug_total = sum(s.get("_aug_time", 0.0) for s in samples)
info["_worker_aug_time"] = aug_total
if "_worker_batch_time" in info:
info["_worker_io_time"] = info["_worker_batch_time"] - aug_total
if "_aug_step_times" in samples[0]:
agg: dict[str, float] = {}
for s in samples:
for step_name, t in s.get("_aug_step_times", {}).items():
agg[step_name] = agg.get(step_name, 0.0) + t
info["_worker_aug_step_times"] = agg
worker_info = torch.utils.data.get_worker_info()
info["_worker_id"] = worker_info.id if worker_info is not None else 0
return info
def _split_one(batch: dict) -> dict:
"""Port of _get_next_sample split rules for i=0 (verbatim from joint_dataloader.py lines 470-490).
Splitting rules:
- _BATCH_TIMING_KEYS: passed through as-is.
- _MULTI_ITEM_KEYS with list value: elem = v[0]; if elem is a list → sample[k]=elem,
else → sample[k]=v[0:1] (single-element list wrapping the tensor).
- Other list values: sample[k] = v[0] (bare element, direct-indexed).
- Non-list (tensor/scalar) values: sample[k] = v[0:1] (preserve batch dim).
"""
sample = {}
for k, v in batch.items():
if k in _BATCH_TIMING_KEYS:
sample[k] = v
elif isinstance(v, list) and k in _MULTI_ITEM_KEYS:
elem = v[0]
sample[k] = elem if isinstance(elem, list) else v[0:1]
elif isinstance(v, list):
sample[k] = v[0]
else:
sample[k] = v[0:1]
return sample
def _accumulate(output_batch: dict, output: dict) -> None:
"""Port of _update_output_batch from joint_dataloader.py lines 405-418."""
for key, value in output.items():
if key in _BATCH_TIMING_KEYS:
if key not in output_batch:
output_batch[key] = value
elif key in _FLATTEN_LIST_KEYS and isinstance(value, list):
if key not in output_batch:
output_batch[key] = value
else:
output_batch[key].extend(value)
elif key not in output_batch:
output_batch[key] = [value]
else:
output_batch[key].append(value)
# Keep _vfm_collate as an alias for the inner collate (used by legacy callers).
_vfm_collate = _vfm_inner_collate
class VFMListCollator(BatchCollator):
"""Reproduces the legacy PackingDataLoader packed-batch structure.
For a group of N raw SFTDataset samples, the packed output has:
- ``_MULTI_ITEM_KEYS`` (``text_token_ids``, ``video``, ``images``,
``action``, ``sound``): ``list[list[Tensor]]`` — each inner list
is a single-element list ``[tensor]``, matching the
``v[i:i+1]`` slice from ``_get_next_sample``.
- Metadata list keys (``sequence_plan``, ``domain_id``,
``raw_action_dim``): flat ``list[element]``.
- ``image_size`` (``_FLATTEN_LIST_KEYS``): flat ``list[Tensor]``
(extended, not appended).
- Non-list tensor keys: ``list[Tensor(1,...)]``.
- ``_BATCH_TIMING_KEYS``: set once from the first sample.
Implementation: for each sample, inner-collate at batch_size=1 via
``_vfm_inner_collate`` (verbatim ``custom_collate_fn``), split
sample 0 per ``_split_one`` (verbatim ``_get_next_sample`` i=0
rules), then accumulate via ``_accumulate`` (verbatim
``_update_output_batch``). Byte-identical to the legacy packer.
"""
def collate(self, samples: list[dict]) -> dict:
# Reproduce the legacy PackingDataLoader packed batch: for each sample,
# inner-collate at batch_size=1, split sample 0 per _MULTI_ITEM_KEYS /
# list / tensor rules, then _update_output_batch-accumulate across the group.
output_batch: dict = {}
for s in samples:
collated = _vfm_inner_collate([s]) # verbatim custom_collate_fn copy
split = _split_one(collated) # i=0 split (rules from _get_next_sample)
_accumulate(output_batch, split) # _update_output_batch copy
return output_batch
|