C.W.K.
Stream
Lesson 06 of 06 · published

Common Mistakes and Reproducibility

~13 min · random, jax, tutorial

Level 0Curious
0 XP0/73 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete

PRNG keys are simple, but there are a few mistakes that even experienced JAX users make. Let's walk through them so you can avoid them.

Mistake 1: Reusing Keys (Correlated Randomness)

import jax
import jax.numpy as jnp

key = jax.random.key(0)

# WRONG: same key → same values → correlated randomness
w1 = jax.random.normal(key, (100, 50))
w2 = jax.random.normal(key, (50, 10))
# w1 and w2 share the same random pattern (scaled to shape)!

# RIGHT: split first
k1, k2 = jax.random.split(key)
w1 = jax.random.normal(k1, (100, 50))
w2 = jax.random.normal(k2, (50, 10))
# Fully independent

Mistake 2: Forgetting to Split in Loops

key = jax.random.key(0)

# WRONG: same key every iteration
samples_bad = []
for i in range(5):
    samples_bad.append(jax.random.normal(key, (3,)))
# All five samples are IDENTICAL

# RIGHT: split at each iteration
samples_good = []
for i in range(5):
    key, subkey = jax.random.split(key)
    samples_good.append(jax.random.normal(subkey, (3,)))
# All five samples are different

Mistake 3: Using Python's random or NumPy random with JAX

import random
import numpy as np

# WRONG: these are not JIT-compatible
# x = random.random()           # Python random — not traced
# x = np.random.randn(3)        # NumPy random — not traced

# RIGHT: always use jax.random inside jitted functions
@jax.jit
def generate(key):
    return jax.random.normal(key, (3,))

Reproducibility Guarantees

JAX provides strong reproducibility: same key + same function = same output, every time, on the same hardware and JAX version.

key = jax.random.key(123)

# Run 1
a = jax.random.normal(key, (5,))

# Run 2 — same key, same result
b = jax.random.normal(key, (5,))

assert jnp.array_equal(a, b)  # True — always

# This works even under jit
@jax.jit
def sample(key):
    return jax.random.normal(key, (1000,))

c = sample(key)
d = sample(key)
assert jnp.array_equal(c, d)  # True

⚠️ Pure Function Check

Cross-version caveat: JAX guarantees same distribution but not same values across JAX versions. When JAX 0.5.0 switched to the partitionable PRNG, jax.random.key(42) started producing different values than in 0.4.x. If your tests assert specific random values, pin your JAX version. If you only test statistical properties (mean ≈ 0, std ≈ 1), you're safe across versions.

💡 Why This Matters

Reproducibility is critical for scientific research and debugging. With JAX's explicit keys, you can reproduce any experiment exactly: save the initial key with your checkpoint, and you can replay the entire training run — including dropout masks, data augmentation, and weight initialization — identically. Try doing that with PyTorch's torch.manual_seed() across multiple GPUs.

Here's a complete example tying it all together — implementing dropout and data augmentation with proper PRNG management:

import jax
import jax.numpy as jnp

def create_train_state(key, input_dim, hidden_dim, output_dim):
    """Initialize model with reproducible randomness."""
    k1, k2, k3 = jax.random.split(key, 3)
    params = {
        'w1': jax.random.normal(k1, (input_dim, hidden_dim)) * 0.01,
        'b1': jnp.zeros(hidden_dim),
        'w2': jax.random.normal(k2, (hidden_dim, output_dim)) * 0.01,
        'b2': jnp.zeros(output_dim),
    }
    return params

@jax.jit
def train_step(params, batch, dropout_key):
    """Single training step with dropout."""
    x, y = batch
    k1, k2 = jax.random.split(dropout_key)

    def loss_fn(params):
        h = jax.nn.relu(x @ params['w1'] + params['b1'])
        mask = jax.random.bernoulli(k1, 0.7, h.shape)
        h = jnp.where(mask, h / 0.7, 0.0)
        logits = h @ params['w2'] + params['b2']
        return jnp.mean((logits - y) ** 2)

    loss, grads = jax.value_and_grad(loss_fn)(params)
    params = jax.tree.map(lambda p, g: p - 0.01 * g, params, grads)
    return params, loss

# Training loop with proper key management
key = jax.random.key(0)
key, init_key = jax.random.split(key)
params = create_train_state(init_key, 784, 256, 10)

for step in range(1000):
    key, step_key = jax.random.split(key)
    # params, loss = train_step(params, batch, step_key)

Code

import jax
import jax.numpy as jnp

key = jax.random.key(0)

# WRONG: same key → same values → correlated randomness
w1 = jax.random.normal(key, (100, 50))
w2 = jax.random.normal(key, (50, 10))
# w1 and w2 share the same random pattern (scaled to shape)!

# RIGHT: split first
k1, k2 = jax.random.split(key)
w1 = jax.random.normal(k1, (100, 50))
w2 = jax.random.normal(k2, (50, 10))
# Fully independent
key = jax.random.key(0)

# WRONG: same key every iteration
samples_bad = []
for i in range(5):
    samples_bad.append(jax.random.normal(key, (3,)))
# All five samples are IDENTICAL

# RIGHT: split at each iteration
samples_good = []
for i in range(5):
    key, subkey = jax.random.split(key)
    samples_good.append(jax.random.normal(subkey, (3,)))
# All five samples are different
import random
import numpy as np

# WRONG: these are not JIT-compatible
# x = random.random()           # Python random — not traced
# x = np.random.randn(3)        # NumPy random — not traced

# RIGHT: always use jax.random inside jitted functions
@jax.jit
def generate(key):
    return jax.random.normal(key, (3,))
key = jax.random.key(123)

# Run 1
a = jax.random.normal(key, (5,))

# Run 2 — same key, same result
b = jax.random.normal(key, (5,))

assert jnp.array_equal(a, b)  # True — always

# This works even under jit
@jax.jit
def sample(key):
    return jax.random.normal(key, (1000,))

c = sample(key)
d = sample(key)
assert jnp.array_equal(c, d)  # True
import jax
import jax.numpy as jnp

def create_train_state(key, input_dim, hidden_dim, output_dim):
    """Initialize model with reproducible randomness."""
    k1, k2, k3 = jax.random.split(key, 3)
    params = {
        'w1': jax.random.normal(k1, (input_dim, hidden_dim)) * 0.01,
        'b1': jnp.zeros(hidden_dim),
        'w2': jax.random.normal(k2, (hidden_dim, output_dim)) * 0.01,
        'b2': jnp.zeros(output_dim),
    }
    return params

@jax.jit
def train_step(params, batch, dropout_key):
    """Single training step with dropout."""
    x, y = batch
    k1, k2 = jax.random.split(dropout_key)

    def loss_fn(params):
        h = jax.nn.relu(x @ params['w1'] + params['b1'])
        mask = jax.random.bernoulli(k1, 0.7, h.shape)
        h = jnp.where(mask, h / 0.7, 0.0)
        logits = h @ params['w2'] + params['b2']
        return jnp.mean((logits - y) ** 2)

    loss, grads = jax.value_and_grad(loss_fn)(params)
    params = jax.tree.map(lambda p, g: p - 0.01 * g, params, grads)
    return params, loss

# Training loop with proper key management
key = jax.random.key(0)
key, init_key = jax.random.split(key)
params = create_train_state(init_key, 784, 256, 10)

for step in range(1000):
    key, step_key = jax.random.split(key)
    # params, loss = train_step(params, batch, step_key)

External links

Exercise

Write a 30-line training script (model + loss + 50 steps). Run it twice with the same PRNGKey(0). Confirm bit-identical outputs. Then change one line to use jax.random.PRNGKey(int(time.time())) and observe the drift. Reproducibility is built, not hoped for.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.