rml / Random numbers

Random numbers

rml/random is one generator and the distributions built on it. The generator is Philox4x32-10, a counter-based design: the n-th random word is a pure function of (seed, stream, n), so any part of a stream can be computed on its own and a generator's whole state is three integers.

On this page
  1. Why a counter-based generator
  2. Philox4x32-10
  3. Generator
  4. Unbiased integers
  5. Normals
  6. Distributions
  7. Categorical sampling
  8. Entropy and KL edge cases
  9. Who draws from it
  10. Tests

In short: Generator encrypts a 128-bit counter with a 64-bit key (the seed) through ten Philox rounds and hands out the four 32-bit lanes of each block as words. Skipping ahead is O(1) and so is saving or restoring state. On top of the words sit exact conversions to float and double, unbiased integers by rejection, and Box-Muller normals. distributions.h adds sampled tensors, Fisher-Yates permutations, and normal, Bernoulli and categorical distributions with log-probability, entropy and KL divergence in nats.

The headers link reproducibility, probability and distributions and entropy and KL divergence on /ml-models. Every sampler in rml takes a random::Generator&, so a fixed seed makes a whole run reproducible (who draws from it).

Why a counter-based generator

A conventional generator such as std::mt19937 carries a large state and steps it once per draw, so draw n exists only after draws 0 through n − 1. Philox (Salmon et al., "Parallel Random Numbers: As Easy as 1, 2, 3", SC 2011) is instead a keyed bijection on 128-bit counters (philox.h L4–12). Block i of the stream is Philox(counter = i, key = seed), computed directly with nothing carried over from block i − 1. Three things follow:

  • Any element of a stream can be produced on its own, so Skip costs nothing.
  • Work split across threads can draw the same numbers a serial run would.
  • A generator's entire state is (seed, stream, position): three uint64s, trivially saved in a checkpoint.
How words map to Philox blocks Word position w selects block w divided by 4 and lane w modulo 4. The counter is the 64-bit block number in its first two lanes and the 64-bit stream id in the last two; the key is the 64-bit seed. Ten Philox rounds turn the counter into four output words, which the generator returns in lane order. counter (4 × 32 bits) block lo block hi stream lo stream hi key (2 × 32 bits) seed lo seed hi Philox4x32: 10 rounds lane 0 lane 1 lane 2 lane 3 one block word w = lane (w mod 4) of block ⌊w / 4⌋ a new block is computed only when w mod 4 = 0
The word layout from philox.h L25–27. A different stream with the same seed gives an independent sequence.

Philox4x32-10

Philox4x32 (philox.cc L33–42) runs ten rounds. Each round (L24–29) multiplies two of the four lanes by fixed odd constants, keeps both halves of each 64-bit product, and mixes them with the other two lanes and the key:

Output laneValue
0hi(M1 · c2) ⊕ c1 ⊕ k0
1lo(M1 · c2)
2hi(M0 · c0) ⊕ c3 ⊕ k1
3lo(M0 · c0)

M0 = 0xD2511F53 and M1 = 0xCD9E8D57 (L12–16). Between rounds the key is bumped by two Weyl constants, 0x9E3779B9 (the golden ratio) and 0xBB67AE85 (√3 − 1), each as a 32-bit fraction. The high half of each product depends on every bit of its input lane, which is where the mixing comes from. The implementation is checked against three known-answer vectors from the Random123 reference distribution.

Generator

Generator holds the seed, the stream, the number of words consumed, and the current four-word block. Refill (philox.cc L46–52) recomputes the block for position / 4. Everything else is arithmetic on position:

MethodWhat it returnsWords usedSource
NextU32The next lane, refilling when the position is a multiple of 4.1L54
NextU64Two words, the first as the high half.2L59
NextFloatThe top 24 bits × 2⁻²⁴: uniform in [0, 1), and every value is an exact float.1L64
NextDoubleThe top 53 bits of a u64 × 2⁻⁵³: uniform in [0, 1) at full double precision.2L66
UniformInt(n)Uniform in [0, n) with no modulo bias (below). n = 0 aborts.2 or moreL68
NextNormalA standard normal by Box-Muller (below).4L78
Skip(words)Advances the position in O(1), refilling only if it lands mid-block.—L85
state(), set_stateThe (seed, stream, position) triple, and restoring it.—L90

Unbiased integers

Taking x % n of a uniform 64-bit x would make the smallest residues slightly more likely whenever n does not divide 2⁶⁴. UniformInt rejects the bottom 2⁶⁴ mod n values of the range, computed in unsigned arithmetic as -n % n, and redraws. What remains is a whole multiple of n, so every residue is equally likely. For n = 3 the threshold is 1: only x = 0 is rejected, a chance of 1 in 2⁶⁴, so a redraw almost never happens.

Normals

Box-Muller turns two uniforms into a standard normal: z = √(−2 ln u₁) · cos(2π u₂). NextNormal takes u₁ = 1 − NextDouble(), which lies in (0, 1], so the logarithm is always finite. It computes in double and rounds to float. The paired sine value is not kept, so each normal costs two doubles (four words), and a draw never depends on whether an earlier call left a cached value behind.

Distributions

distributions.h fills tensors from a Generator in row-major element order, so a fixed seed gives a fixed tensor. Entropy and KL divergence are in nats.

APIBehaviorSource
Uniform(shape, gen, low, high)low + (high − low) · NextFloat() per element. Requires low ≤ high.L47
Normal(shape, gen, mean, stddev)mean + stddev · NextNormal(). Requires stddev ≥ 0.L55
Permutation(n, gen)Fisher-Yates: from the end, swap each slot i with a uniform j in [0, i]. Every one of the n! orders is equally likely.L63
NormalDistributionScalar mean and stddev (stddev > 0). LogProb = −z²/2 − ln σ − ln(2π)/2, entropy = ln(σ√(2πe)).L76
BernoulliOne probability per element, checked to be in [0, 1]. FromLogits applies a sigmoid. Samples are 0 or 1 as f32 (NextFloat() < p). LogProb uses log1p(−p) for outcome 0 and rejects outcomes other than 0 and 1.L112
CategoricalOver the last axis; leading axes are a batch. The constructor normalizes each row (non-negative, nonzero sum). FromLogits is a softmax. Sample returns i32 indices with the batch shape, and LogProb takes them back.L170
KlDivergence(p, q)For each pair of the same family. Zero exactly when p = q, and not symmetric..h L85

Categorical sampling

Categorical::Sample (L192–213) inverts the CDF. It draws u = NextDouble() and walks the row, adding probabilities, until u falls below the running sum. Zero-probability categories are skipped, so they can never be chosen. If rounding leaves u above the final sum, the last category with nonzero probability is taken. For the row [0.25, 0.5, 0, 0.25], u = 0.6 passes 0.25, stops at 0.75, and returns index 1.

Entropy and KL edge cases

Two helpers (L23–31) carry the limits that keep entropy and KL finite where they should be. x log x is taken as 0 at x = 0, so a category with probability 0 adds nothing to the entropy. x log(x / y) is 0 when x = 0, and infinity when x > 0 but y = 0. That is, KL(p ‖ q) is infinite when p puts mass where q has none. Both sums are accumulated in double.

FamilyKL(p ‖ q)Source
Normalln(σ_q / σ_p) + (σ_p² + (μ_p − μ_q)²) / 2σ_q² − 1/2L102
Bernoullip ln(p/q) + (1 − p) ln((1 − p)/(1 − q)), per elementL156
CategoricalΣₖ pₖ ln(pₖ / qₖ) over the last axisL247

Who draws from it

No module keeps a hidden global generator. Every caller takes a random::Generator& or owns one, so the seed is always visible at the call site:

CallerWhat it samplesSource
Weight initXavier and Kaiming, uniform and normal; the Linear, Conv2d, Embedding, attention and recurrent constructors take a generator.nn/init.h L36
DropoutOwns a generator seeded at construction and samples a Bernoulli(1 − p) keep mask.nn/dropout.cc L15
Data splitsTrain/test, stratified and k-fold splits.data/split.h L25
Variational lossesReparameterizedSample: z = mean + exp(log_var / 2) · ε with ε ~ N(0, I).losses/variational.h L30
Classical MLk-means++ seeding, and feature subsampling when a tree grows (GrowTree draws only then).classical/kmeans.h L64
LLM samplerOwns a generator for temperature, top-k, top-p and min-p sampling.llm/sampler.h L80

Those modules are covered on Neural network layers, Data and metrics, Classical ML and Generation.

Tests

FileTestsWhat they pin down
philox_test.cc8Three Random123 known-answer vectors; words equal to the counter blocks for a given seed and stream; the same seed and stream repeating, and a different seed or stream not; Skip and state restore matching sequential draws; floats in [0, 1) with mean 0.5; UniformInt filling bins evenly and aborting on 0; normal mean, variance and fourth moment (3, a Gaussian's kurtosis).
distributions_test.cc13Uniform range, moments and seeding; normal moments; permutations hold every index once and all 6 orders of 3 elements appear about equally often in 36,000 draws; normal log-probability, entropy and KL against the closed form and a Monte Carlo estimate; Bernoulli samples, log-probability (−∞ for an impossible outcome), entropy, logits and KL; categorical normalization, sampling in proportion, log-probability, entropy, KL and softmax logits; invalid inputs aborting.

Randomized tests elsewhere in rml use fixed seeds of this generator, so their failures reproduce. How these run under sanitizers is on Testing.