Repository navigation
Conversation
qqmm passes the nvfp4 global scales of x and w as two more inputs,
but QQMatmul::eval_cpu read only x, w and the scales of w. So the CPU
quantized x as if g_x were 2688 and dequantized w without g_w / 2688:
with a global scale of w other than 2688 the product was 2688 / g_w
times too large, and x blocks whose e4m3 scale needs g_x became 0.
This commit's test runs qqmm on the CPU with exact nvfp4 inputs and
compares it with a float32 matmul of the op-level dequantize of x and
w. With g_w = 336 the CPU gave 8 times the product, for x scaled by
2^-12 with g_x = 0.65625 it gave 0, and for a float16 x with g_w =
10.5 it gave 256 times the product, or inf where that is out of the
float16 range. On x86-64 Linux, each as:
/work/src-bf-gs/tests/quantized_stream_tests.cpp:269: ERROR: CHECK( array_equal(y, expected, false, s).item<bool>() ) is NOT correct!
values: CHECK( false )
The CPU now reads the two global scales. x's quantize-dequantize
encodes each block with 2688 / g_x, as the op-level quantize does.
Each block scale of w is multiplied by g_w / 2688 before the sum, as
the op-level dequantize does. With the factor on the sum instead, the
float16 case gives inf on Linux, where that sum is in float16.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What fails
On the CPU,
qqmmwith nvfp4 global scales ignores them and gives no error. Line numbers below are atmain=4a22e4fed.qqmmvalidatesglobal_scale_xandglobal_scale_w(mlx/ops.cpp:4890-4901: nvfp4 only, one float32 element each, both or neither). It passes them as two more inputs after the scales of w (:4975-4976), and CUDA and Metal read them there.QQMatmul::eval_cpu(mlx/backend/cpu/quantized.cpp:1331-1369) reads onlyinputs[0..2](:1337-1339)::1362,fp_quantize_dequantize:1103) takes each block scale ase4m3(amax / 6). That is the encoding for a global scale of 2688, whateverg_xis.:1363) uses the raw e4m3 block scales of w, withoutg_w / 2688.g_wis not 2688, the product is2688 / g_wtimes too large. And x blocks whose scale is under e4m3's range withoutg_xbecome 0.The constant is
F8E4M3_MAX * F4E2M1_MAX = 448 * 6 = 2688. The op-levelquantizeencodes with2688 / g(ops.cpp:5149-5155), and the op-leveldequantizedecodes withg / 2688(:5455-5458). CUDA and Metal do the same: CUDAfp_quantize.cuh:80-83andqmm_naive.cuh:205-207, Metalkernels/fp_quantized.h:215-216.Why no test caught it
python/tests/test_quantized.py:281passes the global scales toqqmmonly on the GPU, with "TODO: Fix qmv with global scale in CPU backend." No C++ test runsqqmm.The test
A new case in
tests/quantized_stream_tests.cpp, on the CPU stream: nvfp4qqmmwith x of shape[1, 32]and w of shape[4, 32]. Every value is on the e2m1 grid, and every group of 16 holds 6 and -6, so every step is exact. It runs four cases:g_x = 2688,g_w = 336. The block scales of w are stored as 8, and onlyg_w / 2688 = 1/8gives w back.g_x = 0.65625(an encode factor of 4096),g_w = 2688. Withoutg_xthe block scales of x, 2^-12, round to 0 in e4m3.g_x = 2688,g_w = 10.5. The block scales of w are stored as 256 and decoded with exactly 2^-8. The product is[282, 188, 115, 45], exact in float16, but 256 times it is out of float16's range for the first element (72192).The reference is the op-level
dequantizeof x's and w's quantization, with the global scales, and a float32 matmul. It does not use the CPU qqmm kernels. The test also checks that thisdequantizegives back x and w exactly. All comparisons usearray_equal.Without the fix, A, B and D fail on x86-64 Linux, arm64 Linux and macOS. A gives 8 times the product, B gives 0, and D gives 256 times the product (inf for the first element):
(the same for
c.name := Bandc.name := D; logsbf-gs-redbase-{x86,arm,mac}.tests.log).The fix
QQMatmul::eval_cpureadsinputs[3]andinputs[4]when the mode is nvfp4 and both are given. The task reads them, asscale_enc = 2688 / g_xandscale_dec = g_w / 2688. Both are 1 without global scales.quantizedoes:s = e4m3(amax / 6 * scale_enc),q = e2m1(x * scale_enc / s),x_hat = s * q / scale_enc, and 0 whensis 0.scale_decinfp_qmm,fp_qmm_tandfp_qmm_t_simd:T(float(e4m3(s)) * scale_dec).dequantize's order (from_fp8(scales) * (g / 2688),ops.cpp:5455-5458), and the code of Add support for x86 SIMD with Highway ml-explore/mlx#3019 (mlx/backend/cpu/quantized.cpp:1371at7c614ee17).max_size<bfloat16_t>), the CPU takesfp_qmm_t, which sums in T. A factor on the sum would carry sums that are2688 / g_wtimes too large. In float16 they overflow wheng_wis small. bfloat16 has float32's exponent range, so in practice only float16 overflows. The test's case D shows it (see the mutation checks).scale_dec = 1, the block scale is the same value converted T to float to T. Soquantized_matmuland qqmm without global scales compute as before, bit for bit. Withscale_enc = 1, x's round trip is also unchanged:x * 1and/ 1are exact.scale_decdefaults to 1 infp_qmm_dispatchandfp_qmm_dispatch_transpose, so thequantized_matmulandgather_qmmcalls are unchanged.GatherQQMM::eval_cpustill throws NYI, so it does not drop its global scales silently.Verification
At the branch tip:
mainpasses 264 and 280 cases (3992 and 4085 assertions) in the same builds, so nothing newly fails. Logs:bf-gs-suite2-{x86,arm,mac}.tests.log. The build warnings ofquantized.cpp.oare as onmain(14 on Linux, 0 on macOS).The new case alone passes on all three (
bf-gs-green2-{x86,arm,mac}.tests.log). For float32 and float16, the Linux runs usefp_qmm_tand the macOS runs usefp_qmm_t_simd, so both kernels are covered.Mutation checks, the new case alone:
g_w / 2688moved from each block scale onto the sum, infp_qmm_tandfp_qmm_t_simd: only D fails on x86-64 and arm64 Linux, where the float16 sum overflows to inf (bf-gs-mutsum-{x86,arm}.tests.log). It passes on macOS, wherefp_qmm_t_simdsums in float (bf-gs-mutsum-mac.tests.log).scale_decleft at 1 (g_wdropped,g_xkept), on the first version of the test (cases A, B and C, as named now, before the float16 case D was added): only A failed, on all three platforms (bf-gs-mutgw-{x86,arm,mac}.tests.log).Upstream
ml-explore/mlx
mainat7d2d94a55has a byte-identicalQQMatmul::eval_cpu(quantized.cpp:1346-1384) and the same CPU TODO (python/tests/test_quantized.py:362), so it has the same bug. ml-explore#3019 (open) would fix it there: it applies2688 / g_xin x's round trip andg_w / 2688to each block scale.Merge note
This PR and #30 (
fix/cpu-qmm-empty) both add test cases at the end oftests/quantized_stream_tests.cpp, so that file conflicts.git merge-treeagainst #30's head (a5b93e484) mergesmlx/backend/cpu/quantized.cppcleanly. Whichever merges second rebases.🤖 Generated with Claude Code