Source code for omniloader.collate
"""Collate functions for batching unified samples.
Two collators are provided:
* :func:`unified_collate` — the default: every sample already shares the same
tensor shapes (the unified schema, with sequences padded to a fixed ``time_dim``),
so it simply stacks tensors and gathers string metadata into lists.
* :class:`DynamicCollator` — pads each sequence key only to the batch's longest
sample instead of a fixed ``time_dim``. Use it with
``OmniLoader(pad_features=False)`` to avoid wasting memory/compute on padding
when sequence lengths vary a lot across the batch.
Masks vs. cost: a ``<name>_mask`` guarantees *correctness* (padded steps never
corrupt the model output or the loss), but a padded step still costs compute and
memory (self-attention is ``O(T²)``). Dynamic padding targets that cost — so it is
only useful in the native-length regime. Under the default fixed ``time_dim``
(``pad_features=True``) every batch is already one length and :class:`DynamicCollator`
is a no-op.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import torch
from omniloader.utils.padding import pad_or_crop_time_dim
if TYPE_CHECKING:
from collections.abc import Sequence
from omniloader.schema.spec import UnifiedSchema
[docs]
def unified_collate(batch: Sequence[dict[str, Any]]) -> dict[str, Any]:
"""Collate a list of unified samples into a batched dict.
Args:
batch: Samples produced by :class:`~omniloader.loader.OmniLoader`.
Returns:
A dict where each tensor key holds a stacked tensor of shape
``(B, ...)`` and each non-tensor key holds a list of length ``B``.
Raises:
ValueError: If the batch is empty.
"""
if not batch:
raise ValueError("Cannot collate an empty batch")
keys = batch[0].keys()
out: dict[str, Any] = {}
for key in keys:
values = [sample[key] for sample in batch]
if isinstance(values[0], torch.Tensor):
out[key] = torch.stack(values, dim=0)
else:
out[key] = list(values)
return out
[docs]
class DynamicCollator:
"""Collate with per-batch dynamic padding of sequence keys.
Sequence values (those whose spec sets ``time_dim``) are padded to the
longest sample in the batch — ``(B, L_batch, F)`` instead of a fixed
``(B, time_dim, F)`` — and their ``<name>_mask`` is rebuilt accordingly.
Vectors, scalars and metadata are batched exactly as
:func:`unified_collate` does. Pair with ``OmniLoader(pad_features=False)`` so
samples reach the collator at their native length; under the default fixed
``time_dim`` every sample already shares one length, so this collator has
nothing to shrink and behaves like :func:`unified_collate`.
Args:
schema: The unified schema, used to identify sequence keys and their
placeholder fill values.
keys: Optional subset of sequence keys to pad dynamically. When ``None``,
every sequence key in the schema is padded dynamically.
"""
def __init__(self, schema: UnifiedSchema, keys: Sequence[str] | None = None) -> None:
seq = {spec.name: spec for spec in schema.specs if spec.is_sequence}
self.seq_specs = seq if keys is None else {k: seq[k] for k in keys if k in seq}
[docs]
def __call__(self, batch: Sequence[dict[str, Any]]) -> dict[str, Any]:
"""Collate ``batch``, dynamically padding the configured sequence keys.
Args:
batch: Samples to collate.
Returns:
The batched dict.
Raises:
ValueError: If the batch is empty.
"""
if not batch:
raise ValueError("Cannot collate an empty batch")
out: dict[str, Any] = {}
for key in batch[0]:
# Sequence masks are produced alongside their value key; skip them.
if key.endswith("_mask") and key[:-5] in self.seq_specs:
continue
values = [sample[key] for sample in batch]
if key in self.seq_specs:
spec = self.seq_specs[key]
masks = [sample[f"{key}_mask"] for sample in batch]
length = max(v.shape[0] for v in values) # batch max sequence length
# (T_i, ...) -> (L, ...) padded with the spec placeholder value.
out[key] = torch.stack(
[pad_or_crop_time_dim(v, length, spec.placeholder)[0] for v in values]
)
# (T_i,) bool mask -> (L,) padded with False.
out[f"{key}_mask"] = torch.stack(
[pad_or_crop_time_dim(m.to(torch.float32), length)[0].bool() for m in masks]
)
elif isinstance(values[0], torch.Tensor):
out[key] = torch.stack(values, dim=0)
else:
out[key] = list(values)
return out