Fix int16 overflow in the gather_qmm sorted row bound - #4010
Closed
erwinzhang7 wants to merge 1 commit into
Closed
Conversation
The NAX sorted gather kernel narrowed the per-simdgroup row and column bounds to short before taking the min: const short sgp_sm = align_M ? SM : min(SM, short(max(0, (M - (y_row + tm))))); For a row count above the int16 range the inner cast wraps negative, so the min returns a negative bound and the tile is loaded with garbage limits. The guard only runs when align_M is false, so both a large M and an M that is not a multiple of BM are required to reach it. Taking the min in int and narrowing afterwards is safe because the result is bounded by SM. This matches fp_quantized_nax.h and every other site. In a quantized MoE the row count is tokens * experts_per_token, so this is reachable at long context. For Qwen3-Coder-30B-A3B (8 experts per token) it corrupts any single prefill whose length is not a multiple of 8, past about 4k tokens.
erwinzhang7
force-pushed
the
fix-gather-qmm-int16-row-overflow
branch
from
August 5, 2026 13:49
e314c7e to
bbd40f8
Compare
This was referenced Aug 5, 2026
Member
|
Closing since there are already PRs with similar changes. |
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.
Fixes #3856.
A quantized MoE silently returns wrong results for a single long prefill on M5. The same tokens fed through in chunks are correct, so the corruption is invisible unless you compare the two.
This is independent of #4009. I verified that by running the issue's repro on 0.32.0 and on a build carrying #4009, and the outputs were identical, so the two defects do not interact.
Root cause
In
affine_gather_qmm_rhs_nax:The narrowing to
shorthappens inside themin. OnceMpasses the int16 range the inner expression wraps negative, theminreturns that negative value, and the tile is then loaded with a garbage row bound. Taking themininintand narrowing after is safe, since the result is bounded bySM.Two details make this hard to hit by accident:
align_Mis false, soM % BM != 0is required as well asMbeing large.fp_quantized_nax.h:864and every othersgp_smsite in the tree already use themin(int(SM), ...)form. These two lines were the only outliers.Trigger condition, and a correction to the issue title
The issue reports the trigger as sequence length
% 32 != 0. The actual condition is the kernel row countM > 32767andM % 64 != 0.In
SwitchGLUthe rows are sorted and flattened, soM = tokens * experts_per_token. Qwen3-Coder-30B-A3B routes to 8 experts per token, givingM = 8L, soM % 64 != 0reduces toL % 8 != 0. The lengths sampled in the issue (16000, 16032, 16064 clean; 16065, 16066 corrupt) happen to satisfy both% 32and% 8, so they cannot distinguish the two rules.L % 8is the correct one.Evidence
Synthetic, no model required.
gather_qmmwithsorted_indices=Trueagainstsorted_indices=Falseon the same already sorted input, E=4, N=256, K=512, group_size 64, 8 bit. K is aligned here so #4009 is not involved. Max difference relative tomean(abs(unsorted)):Every corrupt case satisfies both conditions and no clean case satisfies both.
End to end on Qwen3-Coder-30B-A3B-Instruct 8 bit, M5 Max, comparing a single prefill against 2048 token chunks. Max absolute difference in the final position logits:
Worst KV cache difference at the two corrupt lengths drops from 43.75 and 43.0 to 20.0, matching the aligned baseline.
One note on reading that table: a residual argmax flip remains at L=16000, unchanged by this patch. It is not corruption. At that position the top two logits are exactly tied in fp16 (both 9.6250, gap 0.0000), so which one wins is decided by ordinary accumulation noise between the one shot and chunked paths. Every length now sits in the same 0.15 to 0.37 band.
Test
Added
test_gather_qmm_sorted_large_m, covering M of 32704 (under the int16 range, as a control), 32800 and 40001. It fails on 0.32.0 on the latter two and passes with this change.test_quantized.py,test_blas.py,test_nn.pyandtest_ops.pypass, 279 tests.Performance
No measurable change. The edit only widens a scalar min computed once per thread, and the difference is well inside the run to run variance of a
gather_qmmbenchmark.