fix(kernel): launch GGUF MoE GEMV token axis on grid.x, not grid.z - #185
fix(kernel): launch GGUF MoE GEMV token axis on grid.x, not grid.z#185erichstuntebeck wants to merge 1 commit into
Conversation
moe_vec_*_q8_1_cuda launched the flat (token, top-k) index as grid.z:
const dim3 block_nums(block_num_y, 1, tokens * top_k);
maxGridDimZ is 65535 on every CUDA compute capability, so any batch with
tokens * top_k > 65535 fails the launch with cudaErrorInvalidValue. With
top_k=8 that caps a prefill batch at 8191 tokens, and --max-extend-tokens
defaults to 8192, so a single long prompt -- or a few concurrent ones the
scheduler packs into one batch -- reproducibly killed the engine.
Move the flat index to grid.x (2^31-1) and rows to grid.y, whose extent is
ceil(nrows / GGML_CUDA_MMV_Y) and stays far below the cap. The neighbouring
quantize_row_q8_1_cuda already tiles its y axis at 65535 for the same reason.
None of the 19 launch sites checked the launch return code, so the failure
only set the error flag and was reported by whatever unrelated CUDA call ran
next -- flashinfer's gelu_tanh_and_mul, the following ggml_moe_a8_vec, even a
torch::zeros. Add FT_MOE_VEC_LAUNCH_CHECK() after each launch so a rejected
configuration reports itself, with the geometry that caused it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Independent confirmation on Ada (sm_89) with a different model, plus a data point on the Before the patchFreeToken 0.1.2 (PyPI wheel), RTX 4080 SUPER 16 GB, driver 610.57.04 / CUDA UMD 13.3, Any prompt long enough to fill a default 8192-token prefill chunk killed the worker, and it I spent an afternoon on this before finding #186, and landed in exactly the trap your PR Client-visible behaviour is also worth noting: the request does not error, it hangs — the Measured boundary, one prompt per server since the first failure is terminal:
After the patchApplied to the installed tree (the
Engine still Caveat on attribution: that run also carried #103 ( Also worth flagging for anyone hitting this on a GGUF MoE: the practical trigger is not |
|
Follow-up to remove the attribution caveat in my comment above: I re-ran with only this
Engine |
Fixes #186.
Problem
moe_vec_*_q8_1_cudalaunches the flat(token, top-k)index as grid.z:maxGridDimZis 65535 on every CUDA compute capability, so a batch withtokens * top_k > 65535fails the launch withcudaErrorInvalidValue.With
top_k = 8that caps a prefill batch at 8191 tokens — and--max-extend-tokensdefaults to exactly 8192. So a GGUF MoE model reliably dies on the first prompt long
enough to fill one chunk. It is not limited to one long prompt either: the scheduler's token
budget is per batch, so a few concurrent medium prompts packed together trip it identically
(observed here as
Prefill batch, #new-seq: 3, #new-token: 7299— three streams, one batch).This cost me a long time to find, because none of the 19 launch sites check the launch
return code. A rejected launch only sets the error flag, so it was reported by whatever
unrelated CUDA call ran next. I saw the same single fault blamed on three different places:
flashinfer.activation.gelu_tanh_and_mul(the very next CUDA call, viaact_fninfused_q4_0.py)ggml_moe_a8_vec(once flashinfer was gated off)torch.zerosin an unrelated allocationCUDA_LAUNCH_BLOCKING=1does not help here — the failure is in the launch configuration,not in execution.
Fix
Move the flat index to
grid.x(limit 2³¹−1) and rows togrid.y, whose extent isceil(nrows / GGML_CUDA_MMV_Y)and stays far below the cap.quantize_row_q8_1_cudanextdoor already tiles its y axis at 65535 for the same reason, so the limit is known in this file's
neighbourhood —
moe_vec.cuhjust missed it.Also adds
FT_MOE_VEC_LAUNCH_CHECK()after each of the 19 launches, so a rejectedconfiguration reports itself with the geometry that caused it instead of latching.
In short, three things: the flat
(token, top-k)index moves togrid.x; every launch sitegains a return-code check; and a regression test is added that fails on
mainand passes here.Test
tests/kernels/test_gguf_moe_vec.pyruns a batch one token over the old cap(
8192 * 8 = 65536) and compares against the same batch split in half, each half under theold limit — so it checks the result is correct, not merely that nothing raised.
Both runs used a fresh JIT build cache. Worth flagging for reviewers: ninja compares
mtimes, so swapping
moe_vec.cuhin place over a warm~/.cache/torch_extensionssilentlyreuses the old
.soand the test passes when it should fail.Tested on
-std=c++17(built clean)ggml-org/gemma-4-26B-A4B-it-GGUF→gemma-4-26B-A4B-it-Q4_0.gguf(128 experts/layer,
top_k=8, 30 layers), served via--moe-backend offloadcompletes, and a 3-needle recall test passes 3/3 at 14,329 tokens. Server logs show
Prefill batch, #new-token: 8192succeeding — the exact geometry that used to fail.Performance
Not the goal, but it measures faster rather than slower. Isolated kernel benchmark, median of
10 after 3 warmups, two independent runs per arm, each arm built from a clean cache
(
nrows=1408,hidden=2816,top_k=8):The mechanism is not root-caused, and this PR claims none. Swapping the axes changes which
blocks are co-scheduled, but the per-token expert varies, so the obvious weight-reuse story does
not straightforwardly hold. The numbers are recorded here as a measurement, not as a
justification for the change — the fix stands on the correctness bug alone.
Notes
q4_0is exercised on my hardware, but all 19 launchers in the file shared theidentical grid expression and are changed identically.
moe.cuhputstokens/mmq_xin grid.yand
mmvq.cuhputsnvecsin grid.y, both of which can overflow in principle but neitheris reachable on my config. Left alone to keep this to one change.