Seeds and reproducible runs
Setting a seed makes PyTorch's randomness repeat on demand, so a result you got once can be gotten again — with honest limits on how far that promise goes.
- 7 min read
- 3 reading levels
- Published
Read these first
On this page 5
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
A seed is a starting point for the computer's randomness — same seed, same "random" numbers, every time.
Think of shuffling a deck of cards by cutting it the same way, the same number of times, from the same starting order. Do that exactly, and the "shuffled" deck comes out identical every time. Computer randomness works like that: it looks shuffled, but it follows steps from a starting point. That starting point is the seed.
Why it exists
Training a model uses randomness everywhere — the starting weights, the shuffling of data, the dropped neurons. Run twice, get two different models, and two different scores.
Now suppose yesterday's run scored well and today's scored badly. Did your code change break it, or was it luck? Without a seed you cannot tell. With one, both runs draw identical randomness — so any difference is caused by your change. That single ability is what makes experiments mean something.
How it works
seed 42 -> [random generator] -> 0.88, 0.91, 0.38, ... today
seed 42 -> [random generator] -> 0.88, 0.91, 0.38, ... tomorrow
seed 7 -> [random generator] -> 0.53, 0.19, 0.65, ... different startThe generator is a recipe that turns its current state into a number, and moves to the next state. Set the seed, and you have set the whole future sequence.
One honest caution: a seed makes runs repeatable on your machine with your versions. It is not a promise that every computer in the world produces identical numbers.
A real example you have seen
Minecraft worlds are made from seeds. Share a world's seed with a friend, and their game generates the same mountains, caves and villages. Nobody stored that world — the seed regrew it. PyTorch regrows your "random" weights the same way.
Remember this
- A seed fixes the starting point of pretend-randomness.
- Same seed, same machine, same code → the same run again.
- Seeds enable fair comparisons: change one thing, and only that thing differs.
What to learn next
- How the autograd graph is built and freed — the next section: what PyTorch records while you compute.
- Creating tensors and choosing a dtype — where the random creation functions live.
- PyTorch basics — the training loop all this reproducibility serves.
Developer — Code and libraries.
Setup
pip install torchVerified with torch 2.5.1, CPU. The specific digits below depend on the torch version — a different version may draw a different (but equally repeatable) sequence.
Seeds in action
import torch
torch.manual_seed(42)
print(torch.rand(3))
torch.manual_seed(42) # reset the sequence to the start
print(torch.rand(3)) # the same three numbers again
print(torch.rand(3)) # the sequence has moved on
g = torch.Generator().manual_seed(7) # a private stream, untouched by others
print(torch.rand(3, generator=g))tensor([0.8823, 0.9150, 0.3829]) tensor([0.8823, 0.9150, 0.3829]) tensor([0.9593, 0.3904, 0.6009]) tensor([0.5349, 0.1988, 0.6592])
The walkthrough
Lines 1 and 2 match; line 3 does not. Every random draw advances the generator. Re-seeding rewinds it to the start. This also means the order of random calls is part of your experiment — insert one extra torch.rand(1) anywhere and everything downstream shifts.
The private Generator is underused. Give your data shuffling its own generator, and your model initialisation another. Then adding a dropout layer (which consumes global randomness) no longer silently changes your data order. Isolation of streams is what makes ablations clean.
Seeding for a full training script takes more than one call, because randomness enters through several doors:
import random
import numpy as np
import torch
def seed_everything(seed: int):
random.seed(seed) # Python's own generator
np.random.seed(seed) # NumPy's generator
torch.manual_seed(seed) # PyTorch CPU, and all GPUs tootorch.manual_seed covers CUDA devices as well; separate cuda.manual_seed calls are unnecessary in torch 2.x. What it does not cover: Python's random, NumPy, and worker processes in a DataLoader (which need a worker_init_fn or generator argument — see PyTorch basics for the loader itself).
Seeds are not full determinism — the honest part
This confuses almost everyone at first, and it is worth reading twice.
With a seed, both runs draw the same random numbers. But some GPU operations execute additions in a timing-dependent order, and float addition cares about order. Result: same inputs, same seed, answers differing in the seventh decimal — which after thousands of steps can become visibly different curves.
If you need bit-for-bit repeatability:
torch.use_deterministic_algorithms(True)This switches every operation to a deterministic implementation — or raises an error naming the operation that has none, so you can avoid it. Determinism costs speed; measure before adopting it permanently. cuDNN's convolution auto-tuner (torch.backends.cudnn.benchmark) also picks algorithms by timing races, so leave it off for reproducible work.
Common mistakes
Seeding once, comparing across code changes that alter call order. Same seed, but your new augmentation consumes two draws where the old one consumed one — everything after diverges. Compare seeds and be aware the stream is positional. Private generators per component reduce this coupling.
Reporting one seeded run as "the" result. A seed makes luck repeatable; it does not remove luck. Score three to five seeds and report the range — a habit that saves public embarrassment when someone else's seed lands differently.
Expecting identical numbers across machines or versions. CPU versus GPU, different GPU models, different torch versions — each may legitimately draw or accumulate differently. Reproducibility claims should name the environment: "torch 2.5.1, CPU" is a claim; "always" is not.
Forgetting the loader workers. Multi-process data loading has per-worker random state. Without seeding it, augmentations differ run to run even though everything else is pinned.
Try it yourself
Write a ten-line script: seed, build torch.nn.Linear(4, 2), print its weights. Run it twice and confirm identical weights. Then move the seed line below the layer creation and run twice more — watch repeatability vanish, and explain to yourself exactly why.
What to learn next
- How the autograd graph is built and freed — the next section: what PyTorch records while you compute.
- Creating tensors and choosing a dtype — where the random creation functions live.
- PyTorch basics — the training loop all this reproducibility serves.
Researcher — Mathematics and papers.
The generators
CPU: Mersenne Twister (Matsumoto and Nishimura, 1998, Mersenne Twister: A 623-dimensionally equidistributed uniform pseudorandom number generator, ACM TOMACS) — period 2^19937 − 1, enormous state, sequential.
CUDA: Philox4×32-10 (Salmon et al., 2011, Parallel Random Numbers: As Easy as 1, 2, 3, SC'11) — a counter-based generator. Output is a bijective function of (key, counter): random access into the stream costs O(1), so thousands of GPU threads generate disjoint substreams without communicating. Counter-based design is the enabling trick of parallel RNG, and why torch.manual_seed can cheaply cover every CUDA device.
Neither is cryptographically secure; both are statistically excellent for simulation (pass BigCrush-class batteries — L'Ecuyer and Simard, 2007, TestU01).
Where nondeterminism actually enters
With seeds fixed, remaining variance comes from operation schedules, not draws:
- Floating-point non-associativity meets atomic operations:
scatter_add_,index_add_, embedding-bag backward, and some pooling backwards use CUDA atomicAdd, whose arrival order is scheduler-dependent. Same values, different summation tree, different last bits. - cuDNN algorithm selection: with
benchmark=True, algorithm choice is a timing race; the chosen algorithm can differ per run and per shape, and algorithms differ in accumulation order. - Reduced-precision paths: TF32 on Ampere+ tensor cores (
torch.backends.cuda.matmul.allow_tf32) changes matmul rounding relative to true float32 — a cross-machine reproducibility hazard masquerading as a nondeterminism one.
torch.use_deterministic_algorithms(True) swaps deterministic implementations where they exist and raises RuntimeError naming the op where they do not; some ops additionally require the CUBLAS_WORKSPACE_CONFIG=:4096:8 environment variable, per the PyTorch reproducibility notes — the authoritative, version-specific list.
Statistical honesty about seed variance
Seed-to-seed variance is a first-class effect, not noise to hide: on small benchmarks, reported architecture gains are regularly within it. Bouthillier et al. (2021, Accounting for Variance in Machine Learning Benchmarks, MLSys) and Picard (2021, torch.manual_seed(3407) is all you need, arXiv:2109.08203 — the title is a joke; the variance data is not) both quantify this. Minimum defensible practice: k ≥ 3 seeds, report mean ± range or a bootstrap interval, and never tune the seed itself.
Physical randomness (torch.seed() with entropy, /dev/urandom) is for when you want fresh draws — production sampling — and has no place in experiments you intend to compare.
What to learn next
- How the autograd graph is built and freed — the next section: what PyTorch records while you compute.
- Creating tensors and choosing a dtype — where the random creation functions live.
- PyTorch basics — the training loop all this reproducibility serves.