Download src/nida/train/sampler.py from Bilal7Dev/nida-tts: direct link, hf CLI and curl.
- Browser
- Download file 6.55 kB
-
https://huggingface.co/Bilal7Dev/nida-tts/resolve/main/src/nida/train/sampler.py
- Command line
-
hf download hf://Bilal7Dev/nida-tts/src/nida/train/sampler.py
-
curl -L -o sampler.py https://huggingface.co/Bilal7Dev/nida-tts/resolve/main/src/nida/train/sampler.py
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).""" | |
| 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 | |
| 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) | |
| 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) | |