Source code for omniloader.sampling.bucketing
"""Length-bucketed batching to minimize padding waste.
:class:`LengthBucketBatchSampler` wraps an index sampler (e.g.
:class:`~omniloader.sampling.sampler.OmniSampler`) and a per-sample length array,
and yields *batches* of indices whose sequences have similar length. Used as
``DataLoader(batch_sampler=...)`` together with
:class:`~omniloader.collate.DynamicCollator`, it keeps each batch's padded length
close to its true length.
The classic pooling scheme is used: the sampler's index stream is cut into pools
of ``batch_size * bucket_multiplier``; each pool is sorted by length and split
into batches; the batch order is then shuffled so length still varies across
steps (only within-batch length is homogenised).
This only reduces padding waste in the **native-length** regime
(``OmniLoader(pad_features=False)`` + :class:`~omniloader.collate.DynamicCollator`).
When every sample is padded/cropped to a fixed ``time_dim`` the batches are already
equal length, so grouping by length changes no shapes and bucketing is a no-op.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Protocol, runtime_checkable
import torch
from torch.utils.data import Sampler
if TYPE_CHECKING:
from collections.abc import Iterator, Sequence
[docs]
@runtime_checkable
class SizedSampler(Protocol):
"""An index sampler that reports its length (as PyTorch samplers do)."""
[docs]
def __iter__(self) -> Iterator[int]:
"""Iterate the sampler's indices."""
...
[docs]
def __len__(self) -> int:
"""Number of indices produced per epoch."""
...
[docs]
class LengthBucketBatchSampler(Sampler[list[int]]):
"""Yield batches of indices grouped by sequence length.
Only meaningful in the native-length regime (``pad_features=False`` +
:class:`~omniloader.collate.DynamicCollator`); under a fixed ``time_dim`` all
batches are already equal length and this changes nothing.
Args:
sampler: An index sampler yielding global indices (its length defines the
epoch size). If it exposes an ``epoch`` attribute, it seeds the batch
shuffle for reproducibility across epochs.
lengths: Per-index sequence lengths, indexable by the sampler's indices
(e.g. from :meth:`~omniloader.loader.OmniLoader.sequence_lengths`).
batch_size: Number of samples per batch.
bucket_multiplier: Pool size in batches; larger pools bucket more tightly
but reduce cross-length shuffling.
drop_last: Drop a trailing batch smaller than ``batch_size``.
shuffle: Shuffle the order of the produced batches each epoch.
seed: Base seed for the batch-order shuffle.
Raises:
ValueError: If ``batch_size`` or ``bucket_multiplier`` is not positive.
"""
def __init__(
self,
sampler: SizedSampler,
lengths: Sequence[int],
batch_size: int,
bucket_multiplier: int = 100,
drop_last: bool = False,
shuffle: bool = True,
seed: int = 0,
) -> None:
if batch_size <= 0 or bucket_multiplier <= 0:
raise ValueError("batch_size and bucket_multiplier must be positive")
self.sampler = sampler
self.lengths = lengths
self.batch_size = batch_size
self.bucket_multiplier = bucket_multiplier
self.drop_last = drop_last
self.shuffle = shuffle
self.seed = seed
def _batches(self) -> list[list[int]]:
"""Build this epoch's batches: pool, sort by length, split."""
indices = list(self.sampler)
pool_size = self.batch_size * self.bucket_multiplier
batches: list[list[int]] = []
for start in range(0, len(indices), pool_size):
pool = sorted(indices[start : start + pool_size], key=lambda i: self.lengths[i])
for j in range(0, len(pool), self.batch_size):
batch = pool[j : j + self.batch_size]
if self.drop_last and len(batch) < self.batch_size:
continue
batches.append(batch)
return batches
[docs]
def __iter__(self) -> Iterator[list[int]]:
"""Iterate length-bucketed batches for the current epoch."""
batches = self._batches()
if self.shuffle:
epoch = int(getattr(self.sampler, "epoch", 0))
gen = torch.Generator()
gen.manual_seed(self.seed + epoch)
order = torch.randperm(len(batches), generator=gen).tolist()
batches = [batches[i] for i in order]
yield from batches
[docs]
def __len__(self) -> int:
"""Number of batches produced per epoch."""
n = len(self.sampler)
if self.drop_last:
return n // self.batch_size
return -(-n // self.batch_size) # ceil