PyTorch Tensors

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.

On this page 5
  1. Why it exists
  2. How it works
  3. A real example you have seen
  4. Remember this
  5. What to learn next

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 start

The 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

Developer — Code and libraries.

Setup

bash
pip install torch

Verified 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

seeds.py
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))
Output
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:

seed_everything.py
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 too

torch.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:

python
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

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