nida-tts / src /nida /train /sampler.py
Bilal7Dev's picture
Nida v0.5 (research preview): weights, 16 voices, code, model card
c0bce59 verified
Raw History Blame Contribute Delete
6.55 kB
"""Frame-budget dynamic batching: deterministic, bucketed, resumable.
An *epoch* visits every item exactly once. For epoch ``e`` the sampler
1. draws a permutation of all items from ``default_rng([seed, stream, e])``;
2. cuts it into pools of ``pool_size`` items and sorts each pool by length (stable, so ties
keep the shuffled order) -- items inside a batch therefore have similar length;
3. greedily packs each sorted pool into batches whose padded size
``max_len * batch_size`` stays ``<= frame_budget`` (and ``<= max_items`` items);
4. shuffles the order of the batches.
The result depends only on ``(lengths, frame_budget, pool_size, max_items, seed, stream,
epoch)``, so a run can be resumed from a :class:`SamplerPosition` ``(epoch, batch_index)``.
Nothing is ever dropped or split: an item longer than the budget raises
:class:`BudgetError` at construction.
``stream`` separates independent uses (train = 0, eval = 1) that share one ``seed``.
"""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
import numpy as np
__all__ = ["BatchStream", "BudgetError", "FrameBudgetSampler", "SamplerPosition"]
class BudgetError(ValueError):
"""An item does not fit into the frame budget (it would have to be split or dropped)."""
@dataclass(frozen=True)
class SamplerPosition:
"""Where the stream is: the *next* batch to be produced is ``batch_index`` of ``epoch``."""
epoch: int = 0
batch_index: int = 0
class FrameBudgetSampler:
"""Deterministic epoch -> list of batches (each batch: item indices).
Args:
lengths: ``num_frames`` of every item, in dataset order (all >= 1).
frame_budget: maximum padded frames per batch (``max_len * batch_size``).
seed: non-negative base seed.
pool_size: items per sorting pool (bigger = more similar lengths, less randomness).
max_items: optional cap on the batch size.
stream: independent random stream id (non-negative).
Raises:
ValueError: invalid arguments or an empty dataset.
BudgetError: some item is longer than ``frame_budget``.
"""
def __init__(
self,
lengths: Sequence[int],
*,
frame_budget: int,
seed: int,
pool_size: int,
max_items: int | None = None,
stream: int = 0,
) -> None:
if len(lengths) == 0:
raise ValueError("cannot sample from an empty dataset")
if min(lengths) < 1:
raise ValueError("every length must be >= 1")
if seed < 0 or stream < 0:
raise ValueError("seed and stream must be >= 0")
if frame_budget < 1 or pool_size < 1 or (max_items is not None and max_items < 1):
raise ValueError("frame_budget, pool_size and max_items must be >= 1")
too_long = [(i, n) for i, n in enumerate(lengths) if n > frame_budget]
if too_long:
index, frames = too_long[0]
raise BudgetError(
f"{len(too_long)} item(s) exceed frame_budget={frame_budget}; "
f"first: item {index} with {frames} frames"
)
self._lengths = np.asarray(lengths, dtype=np.int64)
self.frame_budget = frame_budget
self.seed = seed
self.pool_size = pool_size
self.max_items = max_items
self.stream = stream
self._cache: tuple[int, list[list[int]]] | None = None
@property
def num_items(self) -> int:
return int(self._lengths.shape[0])
def epoch_batches(self, epoch: int) -> list[list[int]]:
"""All batches of ``epoch`` (item indices; each item appears exactly once)."""
if epoch < 0:
raise ValueError("epoch must be >= 0")
if self._cache is not None and self._cache[0] == epoch:
return self._cache[1]
rng = np.random.default_rng([self.seed, self.stream, epoch])
order = rng.permutation(self.num_items)
batches: list[list[int]] = []
for start in range(0, self.num_items, self.pool_size):
pool = order[start : start + self.pool_size]
pool = pool[np.argsort(self._lengths[pool], kind="stable")]
current: list[int] = []
for index in pool.tolist():
length = int(self._lengths[index]) # ascending, so it is the batch max
full = bool(current) and (
length * (len(current) + 1) > self.frame_budget
or (self.max_items is not None and len(current) >= self.max_items)
)
if full:
batches.append(current)
current = []
current.append(index)
if current:
batches.append(current)
permutation = rng.permutation(len(batches))
result = [batches[i] for i in permutation.tolist()]
self._cache = (epoch, result)
return result
def num_batches(self, epoch: int) -> int:
return len(self.epoch_batches(epoch))
class BatchStream:
"""Infinite stream of batches over epochs with a resumable position."""
def __init__(
self, sampler: FrameBudgetSampler, position: SamplerPosition | None = None
) -> None:
self._sampler = sampler
self._epoch = 0
self._batch_index = 0
if position is not None:
self.set_position(position)
@property
def position(self) -> SamplerPosition:
return SamplerPosition(self._epoch, self._batch_index)
def set_position(self, position: SamplerPosition) -> None:
"""Jump to ``position``.
Raises:
ValueError: ``batch_index`` is beyond the number of batches of that epoch.
"""
count = self._sampler.num_batches(position.epoch)
if not 0 <= position.batch_index <= count:
raise ValueError(
f"batch_index {position.batch_index} outside 0..{count} for epoch {position.epoch}"
)
self._epoch, self._batch_index = position.epoch, position.batch_index
def next_batch(self) -> list[int]:
"""Item indices of the next batch; rolls over to the next epoch when exhausted."""
batches = self._sampler.epoch_batches(self._epoch)
if self._batch_index >= len(batches):
self._epoch += 1
self._batch_index = 0
batches = self._sampler.epoch_batches(self._epoch)
batch = batches[self._batch_index]
self._batch_index += 1
return list(batch)