Pseudorandom numbers#
In most numerical libraries, drawing a random number means consulting a hidden
generator that updates itself between calls. JAX has no such generator, and no
hidden update. Randomness in JAX is driven by keys: ordinary immutable
array values that you pass around explicitly. Every function in
jax.random is a pure, deterministic function of its key argument: call
it twice with the same key and you get the same number twice, by design.
Why not a global generator?#
NumPy’s numpy.random is the familiar stateful design: a global generator,
seeded once, silently advanced by every sampling call:
import numpy as np
np.random.seed(0)
print(np.random.uniform())
print(np.random.uniform()) # different: the hidden state advanced
0.5488135039273248
0.7151893663724195
The problem is that “the hidden state advanced” makes your results depend on the exact order and number of sampling calls everywhere in your program:
np.random.seed(0)
def bar(): return np.random.uniform()
def baz(): return np.random.uniform()
def foo(): return bar() + 2 * baz()
print(foo())
1.9791922366721637
This value is only reproducible because NumPy promises to run bar() before
baz(). Such sequencing promises are what JAX needs to avoid: an optimizing
compiler should be free to reorder and parallelize work across devices. JAX
needs random number generation that is reproducible, parallelizable,
and vectorizable, which rules out sampling functions that secretly read
and write shared state.
The solution isn’t to make the generator state an explicit argument that you shuttle in and out of functions. Instead, sampling is just a pure function of key values, and no updated state is threaded back out.
Keys are values#
Create a key from an integer seed with jax.random.key():
import jax
from jax import random
key = random.key(42)
key
Array((), dtype=key<fry>) overlaying:
[ 0 42]
A key is a rank-0 array (shape ()) with a special element type: its JAX
type is key<fry>[], where key<fry> names the default Threefry PRNG
implementation.
print(jax.typeof(key))
key<fry>[]
Passing a key to a sampling function doesn’t modify it or “use it up” in any physical sense; the sample is a deterministic function of the key:
print(random.normal(key))
print(random.normal(key)) # same key, same value — necessarily
-0.028304616
-0.028304616
This means reproducibility is automatic: results depend only on the key values your program constructs, never on execution order, call counts, or which device ran what.
The flip side is that distinct random numbers require distinct keys, which leads to the one rule of JAX randomness:
Never reuse a key (unless you want identical outputs). Feeding the same key to two different samplers produces correlated results, depriving your program of lifegiving chaos.
Deriving new keys#
To get fresh keys, derive them from a key you already have.
jax.random.split() deterministically produces any number of new keys,
each of which can be used to generate statistically independent samples:
key = random.key(42)
key, subkey = random.split(key)
print(random.normal(subkey))
0.60576403
split produces as many keys as you ask for in one shot:
key = random.key(42)
subkeys = random.split(key, num=4)
[float(random.normal(k)) for k in subkeys]
[0.07592553645372391,
0.6057640314102173,
0.4323064982891083,
-0.281894713640213]
And jax.random.fold_in() derives a new key from a key and an integer,
which is ideal for generating a per-step or per-example key without carrying
any key-threading through your loop:
key = random.key(42)
for step in range(3):
step_key = random.fold_in(key, step)
print(f"step {step}: {random.normal(step_key)}")
step 0: 0.07592553645372391
step 1: 0.6057640314102173
step 2: 0.4323064982891083
Note the shape of this pattern: every step_key is derived directly from one
parent key, not from its predecessor. The next section explains why.
Keep the key tree wide, not deep#
Your program’s keys form a tree, rooted at the seed, growing by either split
or fold_in operations. A bad pattern is to grow that tree as a long chain,
splitting off each step’s key from the previous step’s:
for step in range(num_steps):
key, subkey = random.split(key) # each key derived from the last: avoid!
...
Prefer wide trees instead: all step keys hanging off one parent, via a
single split(key, num_steps) or via fold_in(key, step) as above. The
chained version has two problems, one computational and one statistical:
It serializes. Each key depends on the previous one, so a chain of a million steps means a million sequential hash applications. Wide key derivation is a single batched operation, free to vectorize and parallelize.
It courts collisions. For a fixed key, the PRNG’s underlying hash is a pseudorandom permutation of its input, so the keys produced by one
split(or byfold_inover distinct integers) are guaranteed distinct. But viewed as a function of the key, the hash is not a permutation: it behaves like a random function. Every derivation hop is therefore an independent chance for two keys in your tree to coincide, and over a long chain in the default 64-bit key space, collision probability accumulates toward the birthday bound. A collision means identical random streams from the point of collision onward.
A handful of chained splits is harmless; the collision math only bites at scale, and plenty of correct code splits a key a few times in sequence. But for anything proportional to the length of training or the size of a dataset, derive keys widely from a common parent.
No sequential equivalence#
NumPy guarantees that sampling N numbers one at a time yields the same sequence as sampling N at once. JAX deliberately makes no such promise:
key = random.key(42)
subkeys = random.split(key, 3)
print("individually:", np.stack([random.normal(k) for k in subkeys]))
key = random.key(42)
print("all at once: ", random.normal(key, shape=(3,)))
individually: [0.07592554 0.60576403 0.4323065 ]
all at once: [-0.02830462 0.46713185 0.29570296]
Sequential equivalence would impose the kind of ordering constraint JAX’s design exists to avoid. Giving it up means samples drawn from independent keys don’t depend on each other in any order, so generation can be freely vectorized and sharded.
Since keys are just arrays, they compose with everything else in JAX. You can
vmap a sampler over a batch of keys:
import jax
jax.vmap(random.normal)(subkeys)
Array([0.07592554, 0.60576403, 0.4323065 ], dtype=float32)
With the default PRNG implementation, this is exactly equivalent to calling
random.normal on each key separately; vectorizing over keys doesn’t change
the values.
Note
These docs use the typed keys created by jax.random.key(). You may also
encounter older code using jax.random.PRNGKey(), which produces a raw
uint32 array — it still works, but it’s easy to misuse (nothing stops you
from doing arithmetic on it) and it doesn’t record which PRNG implementation
it belongs to. Prefer jax.random.key in new code, and convert at boundaries
with jax.random.key_data() and jax.random.wrap_key_data() when
interfacing with systems that need raw arrays. See the typed PRNG keys
JEP for the full
story.
Design and implementations#
JAX’s PRNG is a counter-based Threefry hash combined with a functional splitting model, chosen so that generation has no sequencing constraints at all. See the PRNG design JEP for the design rationale.
Threefry is the default of several available implementations. Alternatives
(selected per-key via the impl argument to jax.random.key(), or
globally via the jax_default_prng_impl config) trade off generation speed on
TPUs, shardability and sharding-invariance, bit-for-bit identical results
across platforms, and exact vmap-over-keys semantics. The jax.random
module documentation has the full comparison table; the default is the right
choice unless PRNG generation shows up in your profiles.
jax.random itself offers samplers for a wide range of distributions —
uniform, normal, categorical, permutations, and many more — all taking a key
as their first argument.
Next steps#
Keys handle randomness while keeping every function pure. The remaining topic is state: values that evolve as a program runs, and genuine in-place mutation. It’s covered in Stateful computations.