Skip to content

fix(inference): fused_sample_probs differs by 1 ULP at temperature 1.0 on sm_70 #1563

Closed

Description

@inureyes

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

  1. Determine whether the 1 ULP divergence at T = 1.0 is a genuine defect in fused_sample_probs on 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.
  2. 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.
  3. 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 --lib green on sm_70.
  • If the kernel changes: sampled output unchanged at temperature 0, and no throughput regression on the decode path.

Related

Activity

  1. added
    type:bugBug fixes, error corrections, or issue resolutions
    area:inferenceGeneration, sampling, decoding (incl. speculative, DRY)
    platform:linuxLinux (CUDA / packaging) specific
    on Sep 1, 2026
  2. inureyes commented on Oct 15, 2026

    @inureyes
    MemberAuthor

    Determination: test defect, not a kernel defect. Closing.

    Mechanism. At T = 1.0, fused_sample_probs never launches a sampling kernel. It builds the support (top-k, top-p) and then takes the MLX graph softmax over it (fused_sample_probs_impl in mlx_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_unchanged passes, and the whole sampling module 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-compile job 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.

  3. added and removed on Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:inferenceGeneration, sampling, decoding (incl. speculative, DRY)platform:linuxLinux (CUDA / packaging) specificpriority:mediumMedium prioritystatus:doneCompletedtype:bugBug fixes, error corrections, or issue resolutions

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions