inference/research

10 — Testing methodology: differential oracles that actually catch silent corruption

This document exists to close one specific gap, stated in 05-known-traps.md §"What the records cannot answer about traps", item 1:

No systematic fuzzing or differential-test record. Every trap here came from a user report. There is no record of an automated cross-backend differential harness, so the absence of a record for (engine, hardware, format) means untested, not safe.

The repo now has ~103 gotchas. Almost all of them are silent corruption: no crash, no exception, no error in the log, plausible-looking output. A test suite that only checks "did it return 200" and "is the text fluent" passes on every single one of them. What follows is the set of oracles that would have caught them, each tied to the specific existing record that demonstrates the need for it.

Read the oracles in the order below. They are ordered by signal-to-cost: the cheapest oracle that catches a whole class of bugs comes first, and the most expensive (and most conclusive) comes last.


The core problem, stated precisely

Two facts govern everything in this document.

1. The reference is a model, not an assertion. There is no closed form for "the correct logits" of a 70B transformer. So the question is never "is this output right?" but "does this output change when it should not?" Every useful oracle here is a differential: two runs that a correct implementation must make agree, and that a broken one may not.

2. Bit-exactness and numerical accuracy are different tests, and a build can fail either one independently. vLLM issue 55131 reports that tl.dot uses TF32 by default for fp32 inputs on Ampere/Hopper, which is wrong for precision-sensitive modules (the DeepSeek Sparse Attention Indexer, the MoE router). A TF32 matmul can be perfectly batch-invariant and still be numerically wrong. Conversely, a loose tolerance like rtol=1e-2 will happily pass a build whose output text visibly diverges. Run both a bit-exactness check and a tolerance-based accuracy check; never substitute one for the other.


Oracle 1 — Batch invariance (the strongest single check)

The check. Take one fixed prompt. Run it (a) alone at batch size 1, then (b) inside batches of 8, 64, 256, at several different positions within the batch. Assert the logits are bit-identical every time.

Why it is so strong. It requires no reference model and no golden output. It is a self-consistency property that any correct implementation must satisfy by construction: the arithmetic for a given token does not depend on how many unrelated tokens share its batch. A single flipped ULP anywhere in prefill or decode breaks it. This is why it is the primary oracle: it converts "is the model correct?" — which is unanswerable — into "is the engine self-consistent?", which is checkable on every request in a smoke test.

The evidence it is needed. The same prompt at temperature 0, sampled 1000 times on Qwen3-235B-A22B, produced 80 unique completions (Qwen3-235B-A22B-Instruct-2507, temperature 0, 1000 completions of 1000 tokens: 80 unique completions, first divergence at token 103; all 1000 identical with batch-invariant kernels). All 1000 agreed for the first 102 tokens and first diverged at token 103. With batch-invariant kernels all 1000 became identical.

The mechanism, so you know what you are testing. It is not floating-point non-associativity in general — the same matmul run 1000× on identical data is bitwise identical. It is batch-dependent reduction strategy. Thinking Machines identify exactly which strategies break invariance: split-K matmul, stream-K (which additionally breaks batch-position invariance by splitting K differently per output tile), split-KV/FlashDecoding with a per-call split count, FlashInfer's "balanced scheduling" which picks the largest split size that saturates the cores, and small-batch split reductions in RMSNorm. A kernel library that picks any of those per call, based on load, is non-invariant by construction.

Which existing gotchas this would have caught — and note that most of them are not batch-size bugs at all:

How to run it upstream. vLLM ships this as a beta feature with a per-op test suite at tests/v1/determinism/ — 10 files covering batch invariance for VLM, CUTLASS matmul, NVFP4, RMSNorm, XPU, plus an online end-to-end test (vLLM documentation: Batch Invariance (docs/features/batch_invariance.md, read from the vllm repo at main)). Run the per-op suite, not a single end-to-end check.

Two limits you must plan around.

  1. Support is bounded to NVIDIA cc8.0+ and Intel XPU with the Triton backend. There is no ROCm/HIP, Metal, TPU or CPU claim (vLLM batch invariance is beta and its hardware support is bounded: NVIDIA compute capability 8.0+ and Intel XPU with Triton backend only - no ROCm/HIP, Metal, TPU or CPU claim). For the AMD, Apple, Gaudi, TPU and Ascend records in this repo there is no flag to test against — which is exactly why Oracles 3, 4 and 5 below exist.
  2. The flag is not a guarantee. Enabling vLLM's batch-invariance flag does not make the engine deterministic: it silently does not hold for fused-MoE experts, for AWQ on sm8x, and TF32 breaks the matmul invariant - all with no warning — with VLLM_BATCH_INVARIANT=1 set, the guarantee silently does not hold for fused-MoE experts (issue #57016) or for AWQ on sm8x in default mode (#59086), and TF32 breaks the matmul invariant (#55131). No warning in any case. Assert invariance on the exact (architecture × quantization × kernel-family) config you ship; a pass on one config is no evidence about another.

Cost. Determinism is not free: 26 s → 55 s (2.11×) → 42 s (1.62× with an improved attention kernel) for 1000 sequences of 90–110 tokens on one GPU with Qwen-3-8B (Enabling batch-invariant (deterministic) kernels in vLLM costs 1.6x-2.1x wall clock: Qwen-3-8B, 1 GPU, 1000 sequences at 90-110 output tokens). So: run this as a test oracle on a small config, not as a production mode.


Oracle 2 — Cross-backend agreement (ROCm/HIP vs Vulkan, and friends)

The check. Same model file, same quantization, byte-identical flags (verify with diff), on the same machine — run it on two backends and compare.

Why it works here specifically. On a single backend you have no ground truth. On two backends written by different code paths, a shared bug is unlikely while an independent one shows up immediately. This is the repo's most-productive oracle by far: both of the gotchas below were found this way, by users, not by CI.

Which existing gotchas demand this oracle — this is the pair the task refers to:

Two disciplines these records teach, both non-obvious:

  1. Read the backend from the running process, not from the launcher. The reporter of On gfx1151 the llama.cpp HIP backend produces corrupt output where Vulkan is correct with byte-identical flags, so cross-backend comparison is the only clean detector found an earlier "Vulkan is correct" result was actually the HIP server still answering after a boot script silently failed. Check /proc/<pid>/exe on every run. A differential harness that records which backend it believes ran is worse than useless — it will confidently mis-attribute.
  2. Two SHAs agreeing is not corroboration. In that same record, reproduction on b21e4de74567f and 3a653fea932e proves nothing, because no commits between them touch the relevant source. Pin and diff the actual backend directories.

Cost. One command. llama-bench on the same GGUF files across two backends. This is the best value in the document.

Bonus use — performance, not just correctness. On gfx1201 the Vulkan backend loses 4.7-6.7x decode throughput to HIP at hidden_size >= 4096, and effective bandwidth collapses to ~70-100 GB/s on a 640 GB/s card was diagnosed by converting tok/s into effective bandwidth: Qwen3-8B Q4_K_M on Vulkan gives 21.0 t/s ≈ 98 GB/s on a 640 GB/s card, while HIP gives 98.8 t/s ≈ 464 GB/s. That conversion turns "Vulkan is slow" into a diagnosable shape problem (hidden 2560 fast; 4096 and 5120 slow) and is worth doing on every cross-backend measurement.


Oracle 3 — Perplexity / likelihood on a fixed corpus

The check. Score a fixed text corpus (e.g. WikiText-2, a fixed 2-item set) and compare PPL across configurations, backends and builds.

Why. Generation hides corruption behind fluency; PPL does not. It is scalar, deterministic, and needs no reference model — only itself, compared across time.

Which existing gotchas demand it: A llama.cpp ROCm commit raised WikiText-2 perplexity from 7.72 to 3024 on a Q4_0 model with no error, and only llama-per perplexity caught it (the canonical case), llama.cpp HIP on gfx1151 returns wrong-but-plausible logits for any prompt longer than n_ubatch, and the default n_ubatch makes it almost universal (ppl 93.86 on HIP vs 7.40 on Vulkan for 31B — "which reads as a weak model rather than a broken backend"), and Enabling MTP speculative decoding together with prefix caching silently costs ~20% accuracy.

Caveat that makes PPL a necessary but insufficient oracle: PPL is an aggregate. It can be unchanged while specific long-context or long-prompt requests are badly broken — which is exactly the split in llama.cpp HIP on gfx1151 returns wrong-but-plausible logits for any prompt longer than n_ubatch, and the default n_ubatch makes it almost universal, where BF16 is affected too (ruling out the quantized-matmul kernel and making it a scheduling/batching bug) and the default n_ubatch of 512 means "almost every real prompt" is affected. PPL over short contexts would miss it. Pair PPL with Oracle 4.


Oracle 4 — KV-cache hit path vs cold path equivalence

The check. Run a set of requests twice against the same warm server:

  • run 1: fresh server, no prefix cache populated → output A
  • runs 2..N: same requests, cache now warm → output B

Assert A == B bitwise, and assert the cache hit-rate is non-zero in run 2 (otherwise the test is vacuous — you proved nothing, because nothing was cached).

Why this is a distinct oracle. It is the only check that covers the stateful half of the system: cache insertion, block layout, eviction, connector serialization, and cross-instance transfer. It is the highest-yield oracle in this repo by bug count.

Which existing gotchas demand it — the largest cluster in the repo:

What "identical" must mean here. Bitwise, at temperature 0, on the same request. Note from Oracle 2's evidence that temperature 0 alone is not sufficient — see Oracle 6.


Oracle 5 — One-parameter differential diagnosis

The check. Change exactly one parameter, observe one output.

Why. When several things are suspect, bisecting one at a time isolates the layer without a debugger. It is the cheapest diagnostic in the repo and it is already the documented instinct in three records.

Which existing gotchas demonstrate it — all three, verbatim from 05-known-traps.md Pattern 4:

Note the direction of the last one: sometimes the differential is between metrics, not configurations. Keep prefill and decode numbers separate; a single aggregate tok/s hides a 5× cliff.


Oracle 6 — What NOT to trust (the false-pass list)

Explicit, because these are the checks people reach for first:

Tempting check Why it fails
"Temperature 0 makes it deterministic" 80 unique completions from 1000 runs at temperature 0, first diverging at token 103, all fluent. See Temperature-0 greedy decode is not a correctness oracle: the same prompt at temperature 0 produced 80 distinct completions across 1000 runs, all fluent and plausible, with first divergence at token 103.
"Compare against a stored golden string" Inherits the 7.9% minority-branch rate above; the divergent branch reads as correct to a human. Compare across batch sizes and positions, not to one golden.
"Is the output fluent English?" Every one of the 80 divergent completions is coherent. This is why all the corrupt-output records pass a fluency check.
"It returns HTTP 200 and /health is green" mlx-lm --kv-bits passes its startup check, then crashes on the first request for hybrid sliding-window models passes startup and health, then raises on first request.
"It works under transformers" Forcing the FlashInfer attention backend fails on head_dim 256/512 models — same model, fine under a different attention backend. Engine-level clearance requires engine-level tests.
"Set the determinism flag and ship" Enabling vLLM's batch-invariance flag does not make the engine deterministic: it silently does not hold for fused-MoE experts, for AWQ on sm8x, and TF32 breaks the matmul invariant - all with no warning — silently non-invariant for fused-MoE and AWQ on sm8x.
"A passing end-to-end test on one model clears the engine" The upstream determinism suite is per-op (RMSNorm, CUTLASS matmul, NVFP4, attention) precisely because invariance does not transfer across kernel families.

The minimum viable suite

For a given (engine, hardware, quantization) tuple, in this order. Anything less leaves a documented class of bug uncaught.

  1. Oracle 1 — batch invariance at bs ∈ {1, 8, 64, 256} × several in-batch positions, bitwise on logits. (NVIDIA cc8.0+ / XPU-Triton only; elsewhere mark as UNAVAILABLE rather than passed.)
  2. Oracle 4 — cold vs warm, ≥2 runs, bitwise on output, plus a non-zero hit-rate assertion. Issue real requests.
  3. Oracle 2 — cross-backend, byte-identical flags, verified process identity.
  4. Oracle 3 — PPL on a fixed corpus, compared against a stored baseline.
  5. Oracle 5 — one-parameter bisection on any failure above.

Then record the tuple as tested or untested. The point of the exercise is to make "untested, not safe" (05-known-traps.md gap 1) a distinction the repo can actually represent* — because an untested tuple and a clean one currently look identical.

Known coverage holes in this methodology

Related documents