Source code for omniloader.utils.seeding
"""Worker seeding helpers for reproducible data loading.
:func:`seed_worker` is a ``DataLoader(worker_init_fn=...)`` callback that gives
each worker process a deterministic, distinct RNG state derived from PyTorch's
per-worker seed, so any randomness that falls back to the global NumPy/Python/torch
RNGs (rather than an explicit :class:`torch.Generator`) is reproducible. It also
resets any HDF5-backed dataset reachable from the worker's dataset, since HDF5
handles are not fork-safe (see :meth:`omniloader.data.datasets.HDF5Dataset.reset_for_worker`).
"""
from __future__ import annotations
import random
from typing import Any
import numpy as np
import torch
def _resettable_datasets(dataset: Any, seen: set[int] | None = None) -> list[Any]:
"""Return every dataset reachable from ``dataset`` that defines ``reset_for_worker``.
Walks ``OmniLoader``-style composition (a ``datasets`` sequence of source
datasets) as well as plain ``torch.utils.data.Subset``/``ConcatDataset``
(``dataset``/``datasets`` attributes), so a worker can reset every leaf
dataset regardless of how many layers of wrapping sit above it.
Args:
dataset: The dataset instance handed to the worker (``get_worker_info().dataset``).
seen: Internal set of already-visited object ids, to guard against cycles.
Returns:
The list of leaf datasets that expose a ``reset_for_worker`` method.
"""
seen = set() if seen is None else seen
if id(dataset) in seen:
return []
seen.add(id(dataset))
found = [dataset] if hasattr(dataset, "reset_for_worker") else []
for name in ("datasets", "dataset"):
child = getattr(dataset, name, None)
if child is None:
continue
children = child if isinstance(child, (list, tuple)) else [child]
for entry in children:
found.extend(_resettable_datasets(entry, seen))
return found
[docs]
def seed_worker(worker_id: int) -> None: # noqa: ARG001 (DataLoader worker_init_fn API)
"""Seed a DataLoader worker's RNGs and give it its own HDF5 handles.
Pass as ``DataLoader(worker_init_fn=seed_worker)``. PyTorch already sets a
distinct ``torch.initial_seed()`` per worker (derived from the base seed and
worker id); this propagates it to the other RNGs.
A ``fork``-started worker (the default on Linux/macOS) also inherits
whatever HDF5 file handles were already open in the parent process, and
those handles are not fork-safe — reading them from more than one process
hangs rather than raising. This clears any such handle so the lazy reopen
happens inside the worker, as intended by
:meth:`omniloader.data.datasets.HDF5Dataset._handle`.
Args:
worker_id: The worker index supplied by the DataLoader (unused; the seed
is taken from ``torch.initial_seed`` which already encodes it).
"""
worker_seed = torch.initial_seed() % 2**32
np.random.seed(worker_seed)
random.seed(worker_seed)
info = torch.utils.data.get_worker_info()
if info is None:
return
for dataset in _resettable_datasets(info.dataset):
dataset.reset_for_worker()