Scientific Computing · Reproducible Research
Seeds, Generator State, and Reproducible Random Streams
Why a seed is not enough to reproduce a stochastic result, how generator state advances, and how parallel workers avoid splitting one stream into correlated pieces.
A simulation that draws from a pseudorandom generator is deterministic: the sequence of values is a pure function of the initial state. Writing the seed in a methods section feels like it discharges the reproducibility obligation, and for a single-threaded run using one generator in one order, it nearly does. Almost every other arrangement departs from that case in a way the seed does not capture.
A seed fixes the initial state. Everything the program does to the state afterwards determines the stream, and the stream is the thing that has to be reproduced.
The generator is a state machine, not a number source
A pseudorandom generator holds a fixed-size internal state and implements two operations: produce the next output block, and advance the state. The period is bounded by the number of distinct state values. Mersenne Twister has 19,937 bits of state and a period of $2^{19937}-1$; the PCG family trades state size for speed and statistical quality in a range of configurations; xoshiro-style generators use 128 or 256 bits of state with periods of $2^{128}-1$ or $2^{256}-1$.
From the outside, the stream is a sequence. From the inside, it is a state value plus a position. Two runs that call the generator the same number of times in the same order produce the same values. Two runs that call it a different number of times produce streams that are offsets of one another, and everything downstream diverges.
That property explains why a seed without a call order is not a specification. Inserting a debug print that draws a random number, changing a rejection-sampling loop’s number of iterations, swapping two calls that each draw once — any of these shifts the stream relative to the computation and produces different numbers from the same seed.
Seeding is a function of its own
Going from a seed value to an initial state is not a trivial operation, and different assignment methods produce different streams from the same seed.
The simplest approach — initialize the state vector directly from the seed, filling with a small linear congruential recurrence — is what Mersenne Twister’s classic seeding does. Its weakness is that numerically adjacent seeds can produce correlated initial states, so seeds 1 and 2 start streams that are related in ways a statistical test over the first few outputs may not detect but a long run will.
The better approach expands the seed through a mixing function, usually a hash, so that any change to the seed produces an unrelated state. Some implementations expose a sequence-based seeding interface: an array of integers is hashed into the state, which allows constructing a deterministic seed from structured inputs such as run identifier, parameter values, and replicate number, rather than from a scalar someone chose.
A third approach, counter-based generation, removes the state entirely. A value is computed as a keyed function of a counter and a key, so the $n$-th output is available directly by evaluating the function at $n$, with no dependence on how many values were drawn before. That property makes counter-based generators — the Philox design and threefry are the well-known instances — attractive for parallel work, because a worker can start at an arbitrary point in the stream without generating its predecessors.
Whichever mechanism is used, the record must name the generator family and its configuration as well as the seed. A stream from Mersenne Twister and a stream from PCG seeded with the same integer are different sequences, and reporting only the integer leaves the reader to guess.
Why splitting a stream across workers breaks
The obvious way to parallelize a Monte Carlo simulation is to hand each worker the same generator and let it draw. The stream is then consumed in an order determined by how the workers interleave, which varies with scheduling. Results are irreproducible even though the seed is fixed.
The second obvious way is to give each worker a different seed — worker $k$ gets seed $k$, or seed $S+k$. This is reproducible in the sense that the same seed-to-worker assignment always produces the same numbers, and it is statistically wrong in a way that matters. Streams initialized from nearby seeds can be correlated; the correlation is a property of the seeding function, and it is exactly the property that careful seeding tries to remove. For large numbers of workers, drawing from a poorly mixed set of seeds is a real risk, and the failure is not visible in any single worker’s output.
Two methods avoid the problem.
Block splitting divides the period into disjoint contiguous segments and assigns each worker one segment, with each worker’s starting state computed by advancing the generator $k \cdot L$ steps for a segment length $L$. This works if jump-ahead is available and cheap. Mersenne Twister admits an efficient jump operation; generators with small state admit it trivially; the cost is that $L$ must be fixed in advance, and a worker that exhausts its segment either fails or overlaps the next.
Independent streams from a structured seed hashes a tuple — a run key, a worker index, a replicate index — into an initial state, so each worker’s state is a function of its identity rather than of its position in a sequence. The SeedSequence mechanism in NumPy’s random module works this way: it takes an entropy value and a spawn key, mixes them, and produces a state that is statistically independent of states produced from different spawn keys. Spawning a child consumes a counter, so a tree of children can be generated deterministically without collisions.
import numpy as np
# Parent entropy is recorded; children are derived, not guessed
ss = np.random.SeedSequence(entropy=20260527)
children = ss.spawn(4)
for k, child in enumerate(children):
rng = np.random.default_rng(child)
draws = rng.random(3)
print(k, child.spawn_key, draws)
The spawn_key makes the derivation explicit: the child’s state is a function of the parent entropy and that key, so the whole tree is reconstructible from one recorded integer plus the tree structure.
Independent replicates need independent streams
A Monte Carlo estimate is usually reported with an uncertainty derived from the spread across replicates. If the replicates share a stream, either by continuing one sequence or by starting from correlated seeds, the replicates are not independent and the uncertainty estimate is wrong — typically too small, because the replicates share whatever fluctuations the generator’s early outputs happen to exhibit.
Antithetic variates make this deliberate: a replicate pair uses $u$ and $1-u$, which reduces variance for some integrands and is not an independence assumption. Common random numbers across parameter settings serve the opposite purpose, holding the randomness fixed so that differences between configurations reflect the configuration rather than the draws. Both techniques are legitimate and both depend on knowing exactly which stream each replicate receives.
The general rule is that the mapping from (replicate, worker, cell, parameter set) to stream must be a recorded function, and it must be a function chosen so that distinct inputs get distinct streams. A constant offset or a simple increment on the seed fails the second condition for generators with weak seeding; a hash-based derivation satisfies both.
State that is not in the seed
Three further sources of divergence survive correct seeding.
Version-dependent streams. The mapping from a seed to a stream is part of a library’s behaviour, not of a standard. When a library changes its seeding algorithm or its default generator, identical code with the same seed produces different numbers after an upgrade. NumPy made exactly this transition when its legacy RandomState interface was succeeded by the newer Generator API; the two are distinct algorithms and the same integer seed does not produce the same stream in both. Recording the library version alongside the seed is necessary, which is one reason the environment record described in Lockfiles, Hash Pinning, and Reconstructed Environments covers randomness indirectly.
Implicit draws. Library functions may draw internally: a shuffle, a bootstrap resample, an initialization routine for a model, a train/test split helper. Each consumes state without appearing in the analysis code. Two runs with identical visible draws diverge if a library call in one of them changed its internal consumption, which can happen across library versions.
Platform-dependent branching. A rejection loop that draws until a condition holds runs a data-dependent number of iterations, which is fine within a single stream but interacts badly with a shared stream across workers. Reductions over arrays of random values depend on summation order; if parallel reduction order varies, a sum of random numbers varies in its low bits.
What the record needs
The minimum defensible record for a stochastic computation contains: the generator family, its configuration if it has parameters, the seeding method or the seeding interface version, the seed value or entropy plus any spawn keys, the mapping from logical indices to streams, and the number of draws consumed per replicate if that is not implicit in the code. For a parallel run, it also needs the split method and the number of workers, because block splitting ties stream assignment to worker count.
That record is longer than a seed, and it is the difference between a claim that can be checked and a claim that invites a reader to try a number and see what happens. Provenance Records for the Analysis That Was Actually Run treats the generator configuration as one more input in the hashed graph, which is where it belongs: a stochastic computation has more inputs than its data files.
References
- Mersenne Twister: A 623-dimensionally equidistributed uniform pseudorandom number generator — Matsumoto and Nishimura, ACM Transactions on Modeling and Computer Simulation, 1998
- PCG: A Family of Simple Fast Space-Efficient Statistically Good Algorithms for Random Number Generation — Melissa E. O'Neill, Harvey Mudd College, 2014
- Random number generation — NumPy documentation — NumPy project