PyTorch DataLoader reseeds global RNGs per worker, not RNG objects your Dataset holds
Measured on torch 2.15.0+cpu, CPython 3.12.16, NumPy 2.5.4, num_workers=4, batch_size=1 (indices go round-robin, so the first 4 items come from 4 different workers). Under both fork and multiprocessing_context='spawn', the global np.random, the stdlib random functions and torch.rand gave 4 of 4 distinct values. But generators created in Dataset.__init__ (self.g = np.random.default_rng(), np.random.RandomState(), random.Random()) gave 1 of 4: the Dataset is copied into each worker by fork or by pickling, state included. So 'DataLoader seeds NumPy for you' covers only the global legacy API. Augmentation code that keeps its own generator duplicates across workers, and without persistent_workers every epoch replays the same draws, because the parent's copy never advances. Fix: in worker_init_fn, reseed the dataset's generators from torch.initial_seed(), via get_worker_info().dataset. Or create the generators lazily inside the worker.
Reproduced on torch 2.15.0+cpu / CPython 3.12.16 / NumPy 2.5.4, 8 items, batch_size=1, num_workers=4, two epochs without persistent_workers. fork and spawn behaved the same. Global np.random, random and torch.rand gave 4/4. self.g = default_rng(), self.rs = RandomState() and self.r = random.Random() created in __init__ gave 1/4, and epoch 1 was element-for-element identical to epoch 0 for all three. With worker_init_fn reseeding get_worker_info().dataset's generators from torch.initial_seed() % 2**32: 4/4 for all six sources, and the epochs differed.
One ordering detail I checked, which completes the "global API only" point: under spawn the Dataset is unpickled before _worker_loop reseeds. So a Dataset whose __setstate__ calls np.random.seed(1) and random.seed(1) still gets 3 of 3 distinct np.random and random values across 3 workers (torch 2.15.0+cpu, CPython 3.12.16, NumPy 2.5.4). torch's reseed overwrites that seed. The reseed does not reach other process-global RNGs set the same way, e.g. sentencepiece's set_random_generator_seed, which a tokenizer transform may call while it is unpickled. Those keep one seed in every worker. That part is from reading the source; I did not run it.
Also holds on an older torch: 2.2.2+cpu, CPython 3.12.3, NumPy 1.26.4, num_workers=4, one item per worker. Global np.random 4/4 and a default_rng() created in Dataset.__init__ 1/4, under fork and spawn. On the __setstate__ ordering point, the override is specific to the DataLoader. A text-transform class from a translation toolkit that calls np.random.seed(opts.seed) in __setstate__ gave 4/4 distinct drop counts inside a spawn DataLoader (seed 1234 and -1). Under a plain multiprocessing.Pool, where each task unpickles the transform and nothing reseeds afterwards, the toolkit's vocab-building step gave 1/4 with seed 1234. With the default seed -1 it gave 1/4 too, under fork, from the inherited state. So a reseed done during unpickling is harmless under DataLoader and harmful under Pool.
Confirmed with a real augmentation library's default pipeline, not just a toy Dataset. torch 2.15.0+cpu, CPython 3.12.3, NumPy 2.5.4, 64 copies of one image, num_workers=4: 16/64 distinct outputs under fork, spawn and forkserver, and 16 distinct over 3 epochs with persistent_workers=False (every epoch identical). One extra gotcha when you audit a fix: the library's worker-aware reseed (seed + torch.initial_seed() when get_worker_info() is not None) is only applied if the user passed a seed. The default seed=None path returns early, so the common case stays duplicated while the seeded case is fixed. When you check a library, test seed=None separately. Also, a version bisect located the regression exactly at the release where the library moved from the global np.random to its own Generator: the global API had been rescued by torch's per-worker reseed, the private Generator is not.