Repository navigation
Conversation
The CPU QuantizedMatmul counted its matrices as x.size() / (K * M),
which divides by zero when x has no rows or K is 0. x86-64 traps with
SIGFPE there. On arm64 the quotient is 0, so at K = 0 no kernel ran
and the [M, N] output was never written. Each operand's offset comes
from elem_to_loc, which divides by every dimension, so an operand with
a 0 dimension traps on x86 too: in QQMatmul and GatherQMM as well, and
in quantized_matmul with a 2-D x and a w whose batch is empty.
quantized_matmul's check for its mixed bfloat16/float16 path computed
x.size() / x.shape(-1) while the op was built.
This commit's tests run quantized_matmul, its mixed path, qqmm and
gather_qmm on the CPU with M, N or K of 0, and quantized_matmul with
a 2-D x and a w of shape [0, 64, 8]. Before its K = 0 case, the first
test frees a buffer of ones of the output's size, so an output that
is not written fails on arm64 too. On x86-64 Linux each case crashed
without the fix, as the first did:
/work/src-bf-qe/tests/quantized_stream_tests.cpp:207: FATAL ERROR: test case CRASHED: SIGFPE - Floating point error signal
QuantizedMatmul, GatherQMM and QQMatmul now return at once for an
empty output, and for K = 0 they fill the output with zeros, the empty
sum, as Matmul does. The dispatchers count matrices from the output.
The mixed-path check needs K > 0. quantized_matmul now rejects a w
whose batch is empty when x has no batch dimensions: the output then
has the shape of x and comes from one matrix of w, and w has none.
This was referenced Oct 28, 2026
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 x86-64, the CPU quantized matmuls crash the process with SIGFPE when M, N or K is 0. On arm64 they do not crash, but with K = 0 the output is never written. Line numbers below are at
main=4a22e4fed._qmm_dispatch_typed(mlx/backend/cpu/quantized.cpp:398, affine) andfp_qmm_dispatch_mode(:636, mxfp4, nvfp4, mxfp8) count the matrices asx.size() / (K * M).[0, K]or[M, 0]that is0 / 0. x86'sdivtraps.[M, N], an empty sum) is left as allocated.QQMatmulreaches:636throughqqmm(:1363).elem_to_loc(mlx/backend/common/utils.h:16-25), which divides by every dimension. So any operand with a 0 dimension traps on x86 too::408-411,:645-647);gather_qmm(:728-739,:829-838).quantized_matmulwith a 2-D x and a 3-D w whose batch is 0: validation allows it, the output is[M, N](batch dimensions broadcast only when x has them too,mlx/ops.cpp:4831-4835), K > 0, andelem_to_loc(0, w.shape())divides by 0 (quantized.cpp:409).quantized_matmul's check for its mixed bfloat16/float16 path (mlx/ops.cpp:4802) computesx.size() / x.shape(-1). For a bfloat16 x with float16 scales and biases, group 64 and 4 or 8 bits, a K = 0 input crashes while the op is built, on any device.Why no test caught it
test empty gather output constructs without dividing by zerobuilds agather_qmmwith N = 0, but it does not evaluate it. No test evaluates a quantized matmul with M, N or K of 0.The test
Five new cases in
tests/quantized_stream_tests.cpp, on the CPU stream. The first four check the output's shape and that it is all zeros:quantized_matmul, affine and mxfp4, transposed and not, with x of shape[0, 64]and[2, 0].quantized_matmulwith K = 0.qqmm(mxfp4) with K = 0, and with N = 0.gather_qmm, affine and mxfp4, with M = 0, N = 0 and K = 0.quantized_matmul, affine and mxfp4, with x of shape[2, 64]and w of shape[0, 64, 8]: it must throwstd::invalid_argument.Before the K = 0 case of the first test,
clear_cache()runs and an array of ones of the output's size is evaluated and dropped, so the allocator hands that buffer to the output: an output that is not written then reads as ones, not as fresh zeros. That case compares the output with a scalar 0, so that no other array takes the buffer. The mixed-path test also checks the output dtype, float32 (the promoted path).Without the fix each case crashes on x86-64 Linux. The first failure:
Before the first test freed a buffer of ones, the cases passed on arm64 Linux and on macOS: the unwritten K = 0 outputs happened to read as zeros. On arm64 and macOS the new case fails without the fix: it does not throw (
bf-qe-vred5-{arm,mac}.tests.log).The fix
QuantizedMatmul,GatherQMMandQQMatmul::eval_cpufirst call one helper. For an empty output it sets the data and returns, as other CPU primitives do (out.set_data(allocator::malloc(0))). For K = 0 it fills the output with zeros, asMatmul::eval_cpudoes (matmul.cpp:111-118). Both return before anyelem_to_loc.out.size() / (M * N), which does not depend on K.x.shape(-1) > 0, asgather_qmm's check requiresexperts > 0. A K = 0 input takes the promoted path.QQMatmul's NYI throw (quantized.cpp:1367) and the dtype throw of itsdispatch_quantize_dequantize(:1153).quantized_matmulrejects a w whose batch is empty when x has no batch dimensions (std::invalid_argument, on every device). Batch dimensions broadcast only when both x and w have more than two. Otherwise the output has the shape of x, with N as its last dimension, and comes from one matrix of w: the CPU computes one output matrix and reads w's first matrix. With a batch of 0, w has no matrix, and K > 0, so the op's rules define no value for the[M, N]output. Zeros would not be an empty sum here, and an output of[0, M, N]would contradict the same rule for a batch of 3. So the shape is invalid, not a case to handle.The other divisions in
quantized.cppcannot see a zero:group_sizeis validated, and the rest are compile-time constants.Verification
At the branch tip:
mainpasses 264 and 280 cases (3992 and 4085 assertions) in the same builds, so nothing newly fails. Logs:bf-qe-vsuite-{x86,arm,mac}.tests.log. The one new build warning is libstdc++'sresult_ofdeprecation for the newencoder.dispatchlambda, which every dispatch lambda in the file also gets.The test was mutation-checked, each case run alone. On x86-64, each removed guard crashes with SIGFPE in the cases it protects:
[0, 64]case,qqmmwith N = 0, andgather_qmmwith M = 0.ops.cpp'sx.shape(-1) > 0: the mixed-path case, while the op is built.ops.cpp's empty-batch check: the new case (bf-qe-vmutR-x86.tests.log).Logs:
bf-qe-mutA{1..4}-x86.tests.log,bf-qe-mutK{1..4}-x86.tests.log(the first four cases, before the buffer of ones was added).With the zero fill removed but the K = 0 return kept, the K = 0 case of the first test fails on all three platforms, four times (affine and mxfp4, transposed and not), with no crash (
bf-qe-vmutZ-{x86,arm,mac}.tests.log).Upstream
ml-explore/mlx
mainat32a183e12has the same two divisions (quantized.cpp:403and:641), the sameelem_to_locoffsets, the same gather paths and the sameQQMatmulroute (:1345,:1377), so the same crashes. Itsops.cpphas no mixed bfloat16/float16 path, so:4802is this fork's alone. The CPU part will be reported there with a link to this PR.🤖 Generated with Claude Code