perf(cuda): GPU-side argmax for batched decode + BM=32 MMQ tile (#205, #203) - #263
Conversation
… item 1) WIP / parked: bit-identical +2.5-2.9% e2e at N=5-8 on Qwen3-8B Q4_K_M @ 4070 Ti — below the 3-4% bar on its own, so NOT for standalone merge. To be bundled with #204 (Q6_K decode MMQ, the larger lever) to clear the threshold per umbrella #203. The #201 n16 decode MMQ runs BM=64 (256 threads) → ceil(rows/64) blocks. For the [4096xcols] shapes (Q/O proj, the Q4_K ffn_down half) that's 64 blocks on a 60-SM 4070 Ti — under 2 waves, grid-starved. Emit the kernel at a second row-tile size (BM=32, 128 threads, 4 warps) from one template; MatMulBatchedDecodeMmq routes shapes with ceil(rows/64) < 2*SM there (high-row gate/up/lm-head keep BM=64 — they fill the grid and BM=32 would double activation re-staging). SM count from cudaDevAttrMultiProcessorCount. SHARPI_DECODE_MMQ_BM32=0 kill-switch. Output bit-identical to BM=64 (same fragments/mma/accumulation order; only the block->row mapping changes). Validated: CudaDecodeMmqTests covers both tiles vs the fp32 CPU reference; N=6 MMQ oracle green. Measured (the nsys ~8% per-shape estimate in #205 did NOT translate e2e — the [4096x*] Q4_K shapes overlap with larger work and aren't dominant; the Q6_K WS-sw shapes #204 targets are the bigger chunk). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…203) Pushes multi-user batched decode clearly over the 3-4% bar by stacking two orthogonal levers on Qwen3-8B Q4_K_M @ 4070 Ti (aggregate t/s, vs baseline): N baseline +BM32 +argmax +both 6 264.3 271.1 273.6 281.7 (+6.6%) 7 298.6 306.4 310.0 317.3 (+6.3%) 8 321.1 328.9 332.9 342.3 (+6.6%) 1) GPU-side greedy argmax tail (the larger lever, +3.5-3.8% ALONE). Batched decode downloaded the full [N x vocab] logits every step (4.86 MB at N=8, vocab 151936), host-copied it, and allocated N per-row float[]. New BatchForwardMultiArgmax reuses the shipped #219 ArgmaxRows kernel to compute the per-sequence argmax on-device and copy back only rows*8 bytes. Unlike #219's single-user case (1-row download = "within noise"), the batched download is N x larger, so eliding it is a real win. The trunk is hoisted into RunBatchedTrunk; BatchForwardMulti keeps the full-logits tail. Bit-exact to a host argmax of the same logits (lowest-index tie-break == Sampler.Greedy). ContinuousBatchingEngine takes it only when every active seq is greedy this step (temp<=0, not force-closing </think>); any sampled/forced seq reverts the whole step to the full path. Gated by SupportsBatchedGpuArgmax (SHARPI_GPU_ARGMAX kill-switch; off under the profiling variants). Single-user spec-decode BatchVerify is untouched (stays bit-exact WS). 2) BM=32 decode-MMQ tile (#205 item 1, +2.4-2.6% alone, parked here from e7d7cbc). Emits llm_mmq_q4k_soa_acts_n16 at a second row-tile size from one template; routes grid-starved shapes (ceil(rows/64) < 2*SM: Q/O proj + the Q4_K ffn_down half, 64 blocks on 60 SMs) to BM=32/128-thread. High-row shapes keep BM=64. Bit-identical to BM=64. SHARPI_DECODE_MMQ_BM32=0 kill-switch. The two are orthogonal (argmax = host/PCIe tail; BM=32 = device matmul time), so they stack additively. #205 item 2 (cp.async) dropped — adversarial design panel + the #176/#152 prefill precedent rate it ~0-1% / e2e-neutral on decode. Tests: new Qwen3_8B_BatchForwardMultiArgmax_MatchesBatchForwardMulti_N6 (argmax == host Argmax of full logits, exact); CudaDecodeMmqTests covers both MMQ tiles vs the fp32 CPU reference; N=6 MMQ oracle + all CudaBatchForwardMultiTests green (guard the trunk refactor). Build clean (TreatWarningsAsErrors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request optimizes batched decode performance by introducing a BM=32 tile size for decode-MMQ to prevent grid starvation on low-row shapes, and implementing an on-device greedy argmax tail (BatchForwardMultiArgmax) to elide full N×vocab logits downloads when all active sequences are greedy. These changes include kernel generation updates, SM count queries, engine integration, and new unit tests and benchmarks. As there are no review comments, I have no feedback to provide.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Code Review
This pull request optimizes the batched decode path by introducing a smaller BM=32 tile size for decode-MMQ to prevent grid starvation on low-row shapes, and an on-device greedy argmax tail (BatchForwardMultiArgmax) to avoid downloading full logits when all sequences are greedy. Feedback suggests checking the return value of CuBlasInterop.DeviceGetAttribute when querying the SM count to ensure robustness and consistency.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| int smCount = 0; | ||
| CuBlasInterop.DeviceGetAttribute(out smCount, CuBlasInterop.CudaDevAttrMultiProcessorCount, 0); |
There was a problem hiding this comment.
The return value of CuBlasInterop.DeviceGetAttribute is ignored here. While smCount is initialized to 0 and a failure will safely fall back to disabling the BM=32 route (since _smCount > 0 is checked later), it is more robust and consistent with other DeviceGetAttribute calls in this method to check the return value and handle or log any failure.
int smCount = 0;
if (CuBlasInterop.DeviceGetAttribute(out smCount, CuBlasInterop.CudaDevAttrMultiProcessorCount, 0) != 0)
{
smCount = 0;
}
Summary
Pushes multi-user batched decode clearly over the 3-4% bar by stacking two orthogonal levers. Measured on Qwen3-8B Q4_K_M @ RTX 4070 Ti (aggregate t/s vs baseline, all-greedy):
The levers target different constraints (host/PCIe tail vs device matmul), so they add.
Lever 1 — GPU-side argmax for batched decode (the larger one, +3.5–3.8% alone)
BatchForwardMultidownloaded the full[N×vocab]logits every step (4.86 MB at N=8, vocab 151936), host-copied it, and allocated N per-rowfloat[]. NewBatchForwardMultiArgmaxreuses the already-shipped #219ArgmaxRowskernel to compute the per-sequence argmax on-device and copy back onlyrows*8bytes.RunBatchedTrunk;BatchForwardMultikeeps the full-logits tail unchanged.Sampler.Greedy).ContinuousBatchingEnginetakes it only when every active sequence is greedy this step (temp≤0, not force-closing</think>); any sampled/forced seq reverts the whole step to the full path.SupportsBatchedGpuArgmax(the existingSHARPI_GPU_ARGMAXkill-switch; off under the profiling variants). Single-user spec-decodeBatchVerifyis untouched (stays bit-exact WS).Lever 2 — BM=32 decode-MMQ tile (#205 item 1, +2.4–2.6% alone)
Emits
llm_mmq_q4k_soa_acts_n16at a second row-tile size from one template; the dispatcher routes grid-starved shapes (ceil(rows/64) < 2·SM: Q/O proj + the Q4_K ffn_down half — 64 blocks on a 60-SM card) to the BM=32/128-thread tile. High-row shapes (gate/up, lm-head) keep BM=64. Bit-identical to BM=64. SM count fromcudaDevAttrMultiProcessorCount;SHARPI_DECODE_MMQ_BM32=0kill-switch.#205 item 2 (cp.async) dropped — an adversarial design panel plus the #176/#152 prefill precedent rate it ~0–1% / e2e-neutral on decode; not worth the kernel complexity.
Validation
Qwen3_8B_BatchForwardMultiArgmax_MatchesBatchForwardMulti_N6: GPU argmax token == hostArgmaxof the full logits, and the returned logit == that row's value (exact).CudaDecodeMmqTestscovers both MMQ tiles vs the fp32 CPU reference (SimdKernels.DotQ4K).CudaBatchForwardMultiTests(N=2, two-step, empty-batch, prefill) green — guard the trunk refactor.Closes #205. Refs #203 (umbrella), #219 (extends GPU argmax to the batched path).
🤖 Generated with Claude Code