DAY 56 / PHASE 6 · ENGINEERING

Determinism Engineering

For LLM inference — the real drift source · what seed guarantees · steadying evals · the cost trade-off

2026-07-08 · BigCat

temperature=0 was never the same as reproducible — what makes two runs diverge isn't sampling, it's the batch size you don't control.

Prerequisite → Day 14 Inference Optimization (KV cache · dynamic batching · kernels)

// WHY THIS MATTERS

Almost anyone who has run an LLM eval has hit this wall: same prompt, temperature=0, seed pinned — and two runs still differ; prompt A beats B by 2 points and you can't tell if that's a real gain or noise. Most people file it under "model voodoo" and either ignore it or frantically add seeds. The widely-shared 2025 Thinking Machines Lab blog dug out the real culprit: the drift isn't from sampling randomness, it's floating-point non-associativity + server-side dynamic batching — the batch your request lands in is out of your hands, and batch size changes the reduction order of matmul/attention, nudging the per-token logits. This issue doesn't explain "what temperature is" (that's 101) — only four things that bite in production: where the drift actually enters, what seed/system_fingerprint really guarantee, how to make evals stop being flaky, and the throughput bill you pay for determinism.

// 01

The real drift source: not sampling, but batch-invariance

Claim: setting temperature to 0 only kills one random source — sampling. What makes the same prompt differ across runs is floating-point non-associativity × a batch size you can't control.

Background & principle

Break one intuition first: temperature=0 is greedy decoding (take the argmax logit at each step), which in theory is fully deterministic. So where does drift come from? Floating-point addition is not associative: (a+b)+c ≠ a+(b+c) at finite precision differs in the last few bits. And the GPU kernels for matmul, RMSNorm, attention pick different reduction strategies and tiling orders depending on the current batch size — this is the lack of batch-invariance Thinking Machines names: the same sample, run alone vs. packed into a batch of 32 requests, reduces in a different order and produces logits that differ ever so slightly. On most tokens that error is harmless, but hit a near-tie token where two logits are nearly equal and argmax flips, choosing a different word — then autoregression amplifies that fork infinitely and the two trajectories fully diverge. The key: a production inference server's batch size floats with live load, entirely outside a caller's control, so "same request, different moment" simply drifts.

Same prompt, two calls, temperature=0 ┌────────────────────────────────────────────────────┐ │ low-load moment │ high-load moment │ │ your request batch=1 │ batch=48 (same request) │ │ │ │ │ │ │ ▼ │ ▼ │ │ matmul reduction A │ matmul reduction B │ │ (fp non-associative → differ in last few bits) │ │ │ │ │ │ │ ▼ │ ▼ │ │ logits: 0.4999 … │ logits: 0.5001 … │ │ │ │ │ │ │ ▼ argmax │ ▼ argmax │ │ token = " the" │ token = " a" ← FLIP! │ │ │ │ │ │ │ ▼ │ ▼ │ │ autoregression amplifies the fork → different text │ └────────────────────────────────────────────────────┘ drift is not in "sampling", it's in "which batch you fall into"

Hands-on

Reproduce it yourself first — turn "voodoo" into something observable by running a fixed input N times and locating the first divergent token:

# Diagnose: run the same request N times, see where outputs fork
outs = [call(prompt, temperature=0, seed=0) for _ in range(20)]
uniq = set(outs)
print(f"{len(uniq)} distinct outputs / 20 runs")   # >1 = nondeterministic
# Find the first divergence: align outputs by token, locate first mismatch index
# Divergence usually sits late and on near-tie tokens — confirms batch-reduction drift

Make this the first health check when you adopt any new provider or self-hosted stack: measure how nondeterministic it is before deciding whether to pay for determinism.

Failure modes: ① assuming seed=0 + temperature=0 reproduces — stable on a single low-load box, drifts the moment you hit production concurrency; ② filing nondeterminism as a model bug — it's a systemic property of fp + batching, not a defect; ③ concluding from a single run.
Going deeper · Thinking Machines Lab · Defeating Nondeterminism in LLM Inference (Horace He et al., 2025 — with a batch-invariant kernel library)
// 02

The truth about seed / system_fingerprint: mostly, not guaranteed

Claim: OpenAI's seed offers "best-effort" reproduction bound to a system_fingerprint; one backend change voids it, and longer outputs drift more — it is not a promise of bit-level reproduction.

Background & principle

The seed parameter on closed APIs is often misread as a "determinism switch." The official wording is far more restrained: same seed and same other params yield "mostly deterministic" output; and it's bound to a system_fingerprint — a fingerprint of the backend weights + infrastructure config. The moment the provider changes inference config (which happens a few times a year), the fingerprint changes and seed's reproduction guarantee is void. A finer trap: the docs state that even when seed and fingerprint match, you may still observe differences; and larger max_tokens / longer output means worse reproducibility (the §1 fork amplifies — more tokens, more chances to hit a near-tie). So position seed correctly: a variance-reduction tool, not a variance-elimination guarantee.

Hands-on

resp = client.chat.completions.create(
    model="...", messages=msgs,
    temperature=0,
    seed=42,                       # pin the seed
    max_tokens=256,                # shorter = steadier: don't gamble reproduction at 4k
)
fp = resp.system_fingerprint       # record it!
log(fp)                            # fp changed = your baseline is void, re-run the benchmark
# Right use: pin seed + monitor fingerprint changes + keep output short
# Wrong expectation: bit-level regression assertions / cross-fingerprint comparison
Failure modes: ① writing one seeded output into a strict string assertion as the "golden answer" — the next fingerprint change turns them all red; ② expecting seed to reproduce long-form generation; ③ comparing results across fingerprints / model versions and misreading it as model regression.
Going deeper · OpenAI Cookbook · Reproducible outputs with the seed parameter (seed + system_fingerprint usage and limits)
// 03

Making evals non-flaky: don't chase bit reproduction, control variance

Claim: eval score jitter is usually not model regression — it's nondeterministic inference flipping pass/fail on borderline samples; the fix isn't pretending variance is gone, it's measuring and reporting it.

Background & principle

Connect §1–2 to evals: since the same request drifts, your pass/fail verdict flips on samples sitting right at the threshold. You get 82 one run, 85 the next; judging "the new prompt is better" from a single run is likely reading noise. Two engineering exits — pick one, don't mix: (A) want true reproduction — self-host, use batch-invariant kernels (vLLM has a demo), pin down the systemic drift, evals align bit-for-bit; (B) accept nondeterminism — sample each case n≥5 times, turn a single score into a distribution + confidence interval, and judge A/B by whether intervals overlap, not by which of two single points is larger. Most teams on closed APIs can only take (B), so stop treating a single run as ground truth.

# Robustifying eval: repeated sampling → report mean±std, not a single point
scores = [grade(run(case)) for _ in range(5)]   # n≥5
mean, std = stat(scores)
report(case, mean, std)
# A/B: check if confidence intervals overlap; don't conclude from 82 vs 85
# Bonus: flag near-tie samples (pass/fail flips across repeats) — noise hotspots
Failure modes: ① a single eval drops 3 points and you declare "the model regressed" — it's within variance; ② pinning a seed and believing variance is gone — you only hid it behind the fingerprint; ③ using (A)'s tactic (disable batching) while on a closed API — you can't touch its kernels.
Going deeper · Thinking Machines batch-invariant kernel library + vLLM repro demo (see §1 link) · This site Day 29 Real-World Eval (variance & confidence intervals)
// 04

When you want determinism, when you don't, and what it costs

Claim: determinism is not a free default — it's an engineering choice paid for in throughput: debug/audit/cache/RL training want it, creative generation doesn't.

Background & principle

Determinism has a real price. Batch-invariant kernels give up "pick the fastest reduction adaptively per batch," and in Thinking Machines' measurements the deterministic mode is markedly slower (throughput nearly halved when unoptimized), closed only later with engineering. So don't flip determinism on globally — decide by scenario: debug / reproduce a bug / compliance audit / regression baseline — yes, reproducibility is mandatory; on-policy RL training — yes, if the sampler (inference) and trainer are numerically inconsistent, on-policy quietly degrades into off-policy and the gradient is biased; creative generation / brainstorming / diverse candidates — no, you want diversity; forcing temp=0 for "stability" only makes output dull, handing you the same mediocre answer across calls.

// Determinism decision table

Want · debug / reproduce a prod bug — must replay the same trajectory or you can't localize.

Want · compliance audit / regression assertion — conclusions must be reproducible; use (A) self-hosting or at least pin seed + short output.

Want · on-policy RL — sampler and trainer numerically aligned, else the training target shifts.

Depends · prefix caching hits — you only reap the cache dividend when the input prefix is bit-stable; drift = miss.

Don't · creative / multi-candidate / ensemble voting — diversity is the value; temp>0 is a feature, not a bug.

Failure modes: ① nailing temperature=0 on a creative task for "consistency" — output turns uniform and rigid; ② disabling batching globally in production for "reproducibility" — throughput halved, cost doubled, and nobody actually needed it; ③ doing RL but running inference and training on two numerical paths — train/inference mismatch makes the curve diverge weirdly.
Going deeper · The RL / on-policy section of Thinking Machines' "Defeating Nondeterminism…" (2025) · This site Day 14 Inference Optimization (batching & prefix caching)

// DEEPER QUESTIONS

Why is temperature=0 greedy decoding the "least stable," while temperature>0 sometimes looks more robust?
Because greedy is a hard argmax: when two logits differ by 0.0001, whichever is larger wins, so a few bits of floating drift can flip it, and autoregression amplifies the fork into entirely different output. It turns "tiny numerical noise on a near-tie token" into a discrete, irreversible choice. Sampling at temperature>0 already treats tokens as a probability distribution — you've already accepted run-to-run difference, so you don't mistakenly expect consistency; and two near-tie tokens have near-equal probabilities under sampling, so either pick is within the distribution, unlike greedy which manufactures the "was supposed to be fixed but changed" illusion. So the real lesson isn't "turn off randomness for determinism" but: greedy's determinism is on paper only, and it's unusually sensitive to underlying floating noise — for true reproduction you must fix the kernel's batch-invariance, not just crank temperature to 0.
If a batch-invariant kernel nearly halves throughput, why is it non-negotiable in RL training?
Because on-policy RL's correctness depends on "the policy used to sample" and "the policy whose gradient you update" being the same one. In production the sampler (inference engine, chasing throughput, dynamic batching) and the trainer (forward pass, a different kernel) run two numerical paths — the §1 drift creates a systematic bias in token probabilities. So you think you're doing on-policy, but the samples you collected no longer belong to the policy distribution you're updating, quietly becoming biased off-policy, the gradient carrying an unmodeled correction term, the training curve strangely unstable or diverging. Here determinism isn't "nice-to-have reproducibility" — it's a precondition for the algorithm's assumptions to hold — so you'd rather halve throughput than misalign sampler/trainer numerics. This also explains why the blog came from a lab doing RL/post-training: they got bitten by this bug.
Closed APIs can never give bit-level reproduction — does "reproducible AI research/eval" still hold on an API?
It holds, but you must downgrade "reproducible" from bit-level to statistical. Expecting a provider to emit byte-identical output on a given day is fantasy — fingerprints change, batch is out of your hands. Pragmatic reproducibility is: ① lock what you can — record model version, system_fingerprint, all sampling params, prompt hash; ② report a distribution, not a point — n≥5 repeats, give mean±std and a confidence interval so others can reproduce your conclusion statistically; ③ make conclusions robust to fingerprint changes — if your "A beats B by 2 points" can't survive one backend update, it was never a finding, just noise. The only route to true bit reproduction is self-hosting + batch-invariant kernels, removing the provider as an uncontrolled variable entirely.
Can you just cache answers to sidestep the whole nondeterminism problem?
You can dodge part of it, but it depends on what you cache and where the edge is. Exact prefix caching (same prompt returns the stored output) does make repeated requests 100% consistent — but it only covers identical inputs, and in an agent the context changes every turn, so hit rate is often low. KV/prefix caching (caching a prefix's intermediate activations) saves compute, not determinism: it requires the prefix to be bit-stable to hit, and the §1 drift makes a supposedly-identical prefix compute slightly differently and miss instead. Semantic caching (similar question returns an approximate answer) trades accuracy for consistency, handing you "stable but wrong" on borderline samples. So caching is a tool for throughput and consistency, not a substitute for correctness and reproducibility — it lets you compute less but doesn't fix why the underlying drift happens. To cure it, you're back to kernel batch-invariance.

// FURTHER READING