You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
quantized_matmul(transpose=False) wrong results on Metal (gs 32/64, bits 2/4/8) #4201
Bug: quantized_matmul(..., transpose=False) returns wrong results on Metal
Summary
On current main (31b5cbb09), the non-transposed path of mx.quantized_matmul produces garbage on Metal. transpose=True is fine; transpose=False fails for group_size 32 and 64 and bits 2/4/8. This also shows up as 48 failing subtests in TestQuantized::test_qmm.
Repro (M5 Max, MLX built from main, Metal backend)
TestQuantized::test_qmm FAILED/SUBFAILED shapes: {M∈(8,33,64)} x {N∈(128,256)} x {K∈(128,256)}, bits 2/4/8, group_size 32, transpose=False (same class at gs=64).
Happy to dig into the kernel (local Metal build available) and bring a fix PR.
Root cause: the non-transposed NAX qmm path (#4051) tiles 64 columns wide but dequantizes a 64-column block with one per-group scale, so group_size < 64 (e.g. 32) scales columns beyond the first group incorrectly -> garbage/NaN on Metal. transpose=True unaffected.
Fix: gate the non-transposed NAX path on group_size >= 64; smaller group sizes fall back to the legacy kernel.
Verified on a local Metal build (M5 Max, macOS 27): gs=32/64/128 all ~1e-2 vs reference; test_quantized went from 48 failed subtests to 36 passed / 3079 subtests passed.
Root cause (in #4051's non-transposed NAX path): the group_size=32 QuantizedBlockLoader specialization advanced the scale/bias pointer by n_groups * group_stride per reduction tile. With the scale plane laid out as [K, N/32] and a 64-row tile = 2N scale entries, the double step read out of bounds from the second K-tile on — so gs=32, transpose=False returned garbage/NaN on Metal for any K > 64 (K=64, a single tile, was correct; gs=64/128 and transpose=True use other paths and were fine).
Fix: advance by group_stride in the reduction_dim==0 branch (also fixed the identical pattern in the fp8/MX loader). Added test_qmm_non_transposed_group_size_lt_64 (single-vs-multi K-tile). Verified on a local Metal build: test_quantized went 48 failed subtests -> 37 passed / 3106 subtests passed.
Activity
PhilipJohnBasile commentedon Aug 12, 2026
Root cause found and fixed in PR #4202.
Root cause: the non-transposed NAX qmm path (#4051) tiles 64 columns wide but dequantizes a 64-column block with one per-group scale, so group_size < 64 (e.g. 32) scales columns beyond the first group incorrectly -> garbage/NaN on Metal. transpose=True unaffected.
Fix: gate the non-transposed NAX path on group_size >= 64; smaller group sizes fall back to the legacy kernel.
Verified on a local Metal build (M5 Max, macOS 27): gs=32/64/128 all ~1e-2 vs reference;
test_quantizedwent from 48 failed subtests to 36 passed / 3079 subtests passed.Fix non-transposed NAX quantized matmul at group_size < 64
PhilipJohnBasile commentedon Aug 12, 2026
Resolved with the root-cause fix in PR #4202.
Root cause (in #4051's non-transposed NAX path): the group_size=32
QuantizedBlockLoaderspecialization advanced the scale/bias pointer byn_groups * group_strideper reduction tile. With the scale plane laid out as [K, N/32] and a 64-row tile = 2N scale entries, the double step read out of bounds from the second K-tile on — so gs=32, transpose=False returned garbage/NaN on Metal for any K > 64 (K=64, a single tile, was correct; gs=64/128 and transpose=True use other paths and were fine).Fix: advance by
group_stridein the reduction_dim==0 branch (also fixed the identical pattern in the fp8/MX loader). Addedtest_qmm_non_transposed_group_size_lt_64(single-vs-multi K-tile). Verified on a local Metal build: test_quantized went 48 failed subtests -> 37 passed / 3106 subtests passed.