Parallel Random Number Generation ================================= There are five strategies implemented that can be used to produce repeatable pseudo-random numbers across multiple processes (local or distributed). The final method is suitable for parallel applications where reproducibility is not a concern. * :ref:`using-seed-sequence` * :ref:`distinct-cryptographic-keys` * :ref:`advancing` * :ref:`jumping` * :ref:`weyl-sequences` * :ref:`non-reproducible-sequences` .. _using-seed-sequence: Using a SeedSequence -------------------- Using a :class:`~numpy.random.SeedSequence` is a universal way to initialize many streams. When using with a bit generator with a moderately large state (say 256-bits), the probability of a collision is negligible. This said, some generators can have difficult to detect correlation when producing many streams in corner cases. .. code-block:: python import numpy as np from randomgen import SFC64 NUM_STREAMS = 8196 seed_seq = SeedSequence(603484028490308141) children = seed_seq.spawn(NUM_STREAMS) streams = [SFC64(child) for child in children] .. _distinct-cryptographic-keys: Distinct Keys in Cryptographic Generators ----------------------------------------- The cryptographic pseudo-random number generators (PRNGs) support using distinct keys to produce distinct sequence. Generators that support this form of parallelization include :class:`~randomgen.aes.AESCounter`, :class:`~randomgen.chacha.ChaCha`, :class:`~randomgen.hc128.HC128`, :class:`~randomgen.threefry.ThreeFry`, :class:`~randomgen.philox.Philox`, and :class:`~randomgen.speck128.SPECK128`. 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. .. code-block:: python import numpy as np from randomgen import AESCounter, random_entropy NUM_STREAMS = 8196 keys = random_entropy(4 * NUM_STREAMS).view(np.uint64) keys = set([tuple(key.tolist()) for key in keys.reshape((-1, 2))]) # Essentially 0 probability this is needed while len(keys) < NUM_STREAMS: new_keys = random_entropy(4 * (NUM_STREAMS - len(keys))).view(np.uint8) new_keys = set([tuple(key.tolist()) for key in new_keys.reshape((-1, 2))]) keys.update(new_keys) # Distinct 128-bit numbers as a key encoded as 2 uint64 values streams = [AESCounter(key=np.array(key,dtype=np.uint64)) for key in keys] .. _advancing: Advancing the PRNG's state -------------------------- Most of the cryptographic PRNGs are counter-based, and so support advancing which increments the counter. 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 a 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. .. code-block:: python import numpy as np from randomgen import SPECK128, SeedSequence PHI = (np.sqrt(5) - 1) / 2 STEP = int(PHI * 2**96) NUM_STREAMS = 8196 seed_seq = SeedSequence(603484028490308141) base = SPECK128(seed_seq) streams = [base] for i in range(1, NUM_STREAMS): next_gen = SPECK128(seed_seq) streams.append(next_gen.advance(i * STEP)) In addition to the cryptographic PRNGs, the PCG-based generators also support ``advance``: :class:`~randomgen.pcg64.PCG64`, :class:`~randomgen.pcg64.LCG128Mix`, and :class:`~randomgen.pcg32.PCG32`. Note that :class:`~randomgen.hc128.HC128` is based on a stream cipher and so does not support advancing a counter. .. _jumping: Jumping the PRNG state ---------------------- ``jumped`` advances the state of the PRNG as if a large number of random numbers have been drawn, and returns a new instance with this state. Jumping is more universal than ``advance`` since the multiplier needed to jump some PRNGs is expensive to compute. However, this multiplier can be pre-computed for a fixed step size when the PRNG does not support an arbitrary advance which enables the jump. The specific number of as if draws varies by PRNG, and ranges from around :math:`2^{64}` to :math:`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 ``jumped``, 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 | +=================+=========================+=========================+=========================+ | AESCounter | :math:`2^{128}` | :math:`2^{64}` | 64 | +-----------------+-------------------------+-------------------------+-------------------------+ | ChaCha | :math:`2^{128}` | :math:`2^{64}` | 64 | +-----------------+-------------------------+-------------------------+-------------------------+ | LCG128Mix | :math:`2^{128}` | :math:`\phi` | 64 | +-----------------+-------------------------+-------------------------+-------------------------+ | DSFMT | :math:`2^{19937}` | :math:`2^{128}` | 53 | +-----------------+-------------------------+-------------------------+-------------------------+ | LXM | :math:`2^{256}` | :math:`2^{128}` | 64 | +-----------------+-------------------------+-------------------------+-------------------------+ | MT19937 | :math:`2^{19937}` | :math:`2^{128}` | 32 | +-----------------+-------------------------+-------------------------+-------------------------+ | PCG32 | :math:`2^{64}` | :math:`\phi` | 32 | +-----------------+-------------------------+-------------------------+-------------------------+ | PCG64 | :math:`2^{128}` | :math:`\phi` | 64 | +-----------------+-------------------------+-------------------------+-------------------------+ | Philox | :math:`2^{256}` | :math:`2^{128}` | 64 | +-----------------+-------------------------+-------------------------+-------------------------+ | SFMT | :math:`2^{19937}` | :math:`2^{128}` | 64 | +-----------------+-------------------------+-------------------------+-------------------------+ | SPECK128 | :math:`2^{128}` | :math:`2^{64}` | 64 | +-----------------+-------------------------+-------------------------+-------------------------+ | ThreeFry | :math:`2^{256}` | :math:`2^{128}` | 64 | +-----------------+-------------------------+-------------------------+-------------------------+ | Xoroshiro128 | :math:`2^{128}` | :math:`2^{64}` | 64 | +-----------------+-------------------------+-------------------------+-------------------------+ | Xorshift1024 | :math:`2^{1024}` | :math:`2^{512}` | 64 | +-----------------+-------------------------+-------------------------+-------------------------+ | Xoshiro256 | :math:`2^{256}` | :math:`2^{128}` | 64 | +-----------------+-------------------------+-------------------------+-------------------------+ | Xoshiro512 | :math:`2^{512}` | :math:`2^{256}` | 64 | +-----------------+-------------------------+-------------------------+-------------------------+ ``jumped`` can be used to produce long blocks that are long enough to not overlap. A jump size of :math:`\phi` is the integer value of :math:`\sqrt{5}/2 - 1`, the golden ratio, times the full period. .. code-block:: python from randomgen.entropy import random_entropy from randomgen import Xoshiro512, SeedSequence NUM_STREAMS = 8196 seed = SeedSequence() blocked_rng = [] last_rng = rng = Xoshiro512(seed) for i in range(NUM_STREAMS): blocked_rng.append(last_rng) last_rng = last_rng.jumped() .. _weyl-sequences: Weyl Sequences -------------- :class:`~randomgen.sfc.SFC64` uniquely supports generating streams using distinct Weyl sequences. Distinct sequences are produced by setting unique values in ``k``` when initializing :class:`~randomgen.sfc.SFC64`. These increments must be odd. There is some indication that sparse bit patterns in the Weyl sequence with 50% or fewer non-zero bits perform best. .. code-block:: python import numpy as np from randomgen.entropy import random_entropy from randomgen import SFC64, SeedSequence NUM_STREAMS = 8196 seed_seq = SeedSequence() # Helper function to generate values with 32 non-zero bits # The number of non-zero bits is configurable weyl_inc = SFC64(seed_seq).weyl_increments(1000) streams = [SFC64(seed_seq, k=k) for k in weyl_inc] .. _non-reproducible-sequences: Non-reproducible Sequences -------------------------- :class:`randomgen.rdrand.RDRAND` uses a hardware based random number generator to produce non-reproducible sequences on random numbers. These can be used on any system that supports the `RDRAND` instruction, subject to the key limitation that results cannot be reproduced without storing the values generated. .. code-block:: python from randomgen import RDRAND NUM_STREAMS = 8196 streams = [RDRAND() for _ in range(NUM_STREAMS)] .. end block