Parallel Random Number Generation

There are three strategies implemented that can be used to produce repeatable pseudo-random numbers across multiple processes (local or distributed).

Independent Streams

Currently only pcg32 and pcg64 support independent streams, and, due to the limited period of pcg32 (\(2^{64}\)), only pcg64 should be used. This example shows how many streams can be created by passing in different index values in the second input while using the same seed in the first.

from randomstate.entropy import random_entropy
import randomstate.prng.pcg64 as pcg64

entropy = random_entropy(4)
# 128-bit number as a seed
seed = reduce(lambda x, y: x + y, [long(entropy[i]) * 2 ** (32 * i) for i in range(4)])
streams = [pcg64.RandomState(seed, stream) for stream in range(10)]

Jump/Advance the PRNG state

Jump

jump advances the state of the PRNG as-if a large number of random numbers have been drawn. The specific number of draws varies by PRNG, and ranges from \(2^{64}\) to \(2^{512}\). Additionally, the as-if draws also depend on the size of the default random number produced by the specific PRNG. The PRNGs that support jump, along with the period of the PRNG, the size of the jump and the bits in the default unsigned random are listed below.

PRNG Period Jump Size Bits
dsfmt \(2^{19937}\) \(2^{128}\) 53
mrg32k3a \(\approx 2^{191}\) \(2^{127}\) 32
xorshift128 \(2^{128}\) \(2^{64}\) 64
xorshift1024 \(2^{1024}\) \(2^{512}\) 64
mt19937 \(2^{19937}\) | \(2^{128}\) 32

jump can be used to produce long blocks which should be long enough to not overlap.

from randomstate.entropy import random_entropy
import randomstate.prng.xorshift1024 as xorshift1024

entropy = random_entropy(2).astype(np.uint64)
# 64-bit number as a seed
seed = entropy[0] * 2**32 + entropy[1]
blocks = []
for i in range(10):
    block = xorshift1024.RandomState(seed)
    block.jump(i)
    blocks.append(block)

Advance

advance can be used to jump the state an arbitrary number of steps, and so is a more general approach than jump. Only pcg32 and pcg64 support advance, and since these also support independent streams, it is not usually necessary to use advance.

Advancing a PRNG updates the underlying PRNG state as-if a given number of calls to the underlying PRNG have been made. In general there is not a one-to-one relationship between the number output random values from a particular distribution and the number of draws from the core PRNG. This occurs for two reasons:

  • The random values are simulated using a rejection-based method and so, on average, more than one value from the underlying PRNG is required to generate an single draw.
  • The number of bits required to generate a simulated value differs from the number of bits generated by the underlying PRNG. For example, two 16-bit integer values can be simulated from a single draw of a 32-bit PRNG.

Advancing the PRNG state resets any pre-computed random numbers. This is required to ensure exact reproducibility.

This example uses advance to advance a pcg64 generator 2 ** 127 steps to set a sequence of random number generators.

import randomstate.prng.pcg64 as pcg64
rs = pcg64.RandomState()
rs_gen = pcg64.RandomState()
rs_gen.set_state(rs.get_state())

advance = 2**127
rngs = [rs]
for _ in range(9):
    rs_gen.advance(advance)
    rng = pcg64.RandomState()
    rng.set_state(rs_gen.get_state())
    rngs.append(rng)