Repository navigation
fix(inference): fused_sample_probs differs by 1 ULP at temperature 1.0 on sm_70 #1563
Description
Found while implementing #1544 (part of epic #1536). Not caused by that change, and out of its scope. Same pattern as #1558: a pre-existing failure that surfaced only because a wider test scope finally ran.
What happens
On a Tesla V100 (sm_70, CUDA 12.9.41):
cargo test -p mlxcel-core --release --features cuda --lib -- --test-threads=1
gives 1672 passed, 1 failed. The failure is sampling::tests::temperature_one_support_unchanged, on a 1 ULP difference in 5 of 64 fused_sample_probs entries at T = 1.0.
It predates the change that found it
Verified by reverting src/lib/mlx-cpp/patches/mlx/backend/cuda/gemms/grouped_gemm_unaligned.cu to c2e54939, rebuilding, and re-running: the failure reproduces byte-identically. The grouped-GEMM work is not implicated, and neither is anything else in this epic's merged units, which touch qmv, qmm_naive tile selection, and the grouped-GEMM arch tag rather than the sampler.
Why it surfaced now
This was the first full library-suite run in the epic. #1559 ran only a filtered subset, and the earlier units narrowed scope to the modules they touched, because a full cargo test --lib exceeds the sub-agent stream-idle watchdog on this host. The test itself is not new and is not skipped; it simply had not been executed on this hardware before.
Scope
- Determine whether the 1 ULP divergence at
T = 1.0is a genuine defect infused_sample_probson sm_70 or an over-tight assertion. The test's name says it checks that temperature one leaves the support unchanged, so the question is whether a 1 ULP move can change the sampled support at all, or whether the comparison should be on support membership rather than bitwise equality. - Establish whether sm_80 and later are affected. The kernel is shared, so this may not be Volta-specific, and nobody has run this suite on an sm_121 host as part of this work.
- If the assertion is correct and the kernel is wrong, fix the kernel. If the kernel is right and the assertion is too strict for a fused path, fix the assertion and say what tolerance is defensible and why.
Acceptance criteria
- A written determination of whether this is a kernel defect or a test defect, with the mechanism.
- The same question answered for sm_80 and later, or an explicit statement that it could not be tested and why.
-
cargo test -p mlxcel-core --release --features cuda --libgreen on sm_70. - If the kernel changes: sampled output unchanged at temperature 0, and no throughput regression on the decode path.
Related
- fix(cuda/quant): quantized prefill is not bitwise reproducible on sm_70, and its determinism test never runs #1558, quantized prefill not bitwise reproducible on sm_70, found the same way and still open.
- Both are arguments for running the full library suite on this host at least once per epic rather than only filtered subsets. The watchdog constraint that prevents sub-agents from doing it is real, but the orchestrator can run it in the background, where the watchdog does not apply.
Activity
perf(cuda): explain the Volta fixed first-token cost, and instrument …
fix(downloader): let a test isolate the legacy models probe
fix(downloader): let a test isolate the legacy models probe
inureyes commentedon Oct 15, 2026
Determination: test defect, not a kernel defect. Closing.
Mechanism. At
T = 1.0,fused_sample_probsnever launches a sampling kernel. It builds the support (top-k, top-p) and then takes the MLX graph softmax over it (fused_sample_probs_implinmlx_cxx_bridge.cpp). The softmax sum and exp are reassociated differently per backend and per GPU generation, so the last ULP of a probability moves with the hardware while the support and every value the sampler compares stay the same. The sm_70 report fits that: 5 of 64 entries moved by 1 ULP, and none left or joined the support. The assertion pinned exact bits from a Metal capture, which is a hardware-order tripwire rather than a correctness check.Fix already on main. #1567 (e278d01) replaced the exact-bit comparison with
assert_probs_match_snapshot_within_ulp(src/lib/mlxcel-core/src/sampling.rs), and 1b6760d raised the stock-row bound from 2 to 4 ULP, citing this issue, and made the helper report the worst drift. The helper asserts the exact support set first, requires every in-support entry to stay positive and finite, and allows 2 ULP on the routed top-p rows and 4 ULP on the stock top-k row. The distribution itself is checked separately by the chi-square tests against exact softmax (sampling_rejection_tests.rs,sampling_gumbel_tests.rs,sampling_fixed_key_tests.rs). 4 ULP covers the worst measured drift so far (3 ULP on M5 Max) with one ULP of headroom, and the helper reports the worst drift on failure so a real arithmetic change is still visible.sm_80 and later. Measured on sm_121 (NVIDIA GB10, driver 580.178.04, main at e09bd1c) with a temporary print of the worst drift, three runs, identical every time: (40, 0.9) and (0, 0.9) drift 1 ULP at token 26, and the stock (40, 1.0) row drifts 2 ULP at token 12. Support is unchanged in every case. So sm_121 drifts too, by the same mechanism, and stays inside both bounds.
temperature_one_support_unchangedpasses, and the wholesamplingmodule passes on this host (210 passed, 0 failed,--test-threads=1).sm_70. Not re-run: no V100 is available, and the CI
cuda-sm70-compilejob only compiles. The reported sm_70 drift (1 ULP, support unchanged) is within the 4 ULP stock bound, so the test is expected to pass there; the first sm_70 run of the library suite should confirm it.No kernel change, so the temperature-0 and throughput criteria do not apply.