Repository navigation
perf(attention): run up to 8 query rows per request in the triton decode kernel - #900
Conversation
…ode kernel Speculative verify (MTP, DFlash) and the MTP draft extend sent q_len > 1 to the extend kernel, which streams the whole context per (request, query head) with no KV split: 6.6-60x the single-row decode time at 32K-128K on sm_120. - the grouped decode packs query rows x GQA heads in the M axis, limits each row to its own position and window, and keeps the KV split - the triton backend sends batches of at most 8 rows per request without multimodal blocks to it - a program's query heads divide the GQA group, which also fixes wrong output for groups above 16 that are not a multiple of 16 (e.g. 48/2)
…on sm_80 to sm_120 - pass maxnreg=255 to multi-row stage-1 launches on CUDA: without .maxnreg ptxas holds these tiles at 80-128 registers for occupancy and spills 8-16 B on sm_80 to sm_120 - pick block_n and warps from a launch sweep under maxnreg, among the variants that do not spill on sm_80, 86, 89, 90, 100 and 120; quantized head_dim 512 takes 16 rows x heads again
Gemma-4 MTP at long contextWith #895 merged, Gemma-4's assistant-draft MTP verifies k+1 rows on triton too. Its full-attention layers have head_dim 512 and 1 kv head, so on main those rows took the generic
At 128K the k=3 verify step goes from 128.6 to 27.5 ms (a no-spec decode step is 20.1 ms), so MTP on main was slower than no spec there. The greedy outputs are identical between main and branch at every context and config. Accept length matches too, except k=7 at 128K (5.43 -> 5.64, same text). |
Merge main through 66190a9, retaining the multi-row decode changes from FlashML-org#900. Move architecture and launch selection into attention.py, reuse the shared architecture helper, and remove the packed reader's test-only generic arm. Use four warps and require 16-byte aligned cache bases and strides to avoid packed-reader register spills. Keep CUDA address expressions inline and document the gfx12 VOP3 conversion workaround. Validate D128 GQA4/8 against main and same-launch generic controls. Confirm zero packed spills, 14 layout/Graph cases, 58 final operator cases and 14 forced-emulation cases. All 81 RTX 5090 main/head codegen comparisons match. Preserve four unrelated AMD suite failures reproduced on pinned main.
Part of #893 (triton multi-token verify).
The triton backend sent every batch with more than one query row per request to the extend kernel, which has no KV split: one program per (request, query head) streams the whole context. Speculative verify (MTP, DFlash) and the MTP draft extend (which runs on triton even when the target uses fi) were therefore 6.6-60x slower than single-row decode at 32K-128K.
Changes:
decode_paged_attentiontakes up to 8 causal query rows per request (qo_indptr,max_q_len). Stage 1 puts query rows x GQA heads on the M axis and keeps the KV split; each row is limited to its own position and window. Stage 2 merges per row.rows_per_req > 1get per-row split scratch.maxnreg=255on CUDA. Without it, ptxas holds these tiles at 80-128 registers for occupancy and spills 8-16 B on sm_80 to sm_120. block_n and num_warps come from a sweep among the variants that do not spill on any of these archs.End-to-end A/B (main vs this branch)
Qwen/Qwen3.6-35B-A3B bf16, bs 1, greedy, 256 output tokens,
--moe-cache-size 10240. Each cell has 2 runs per side (order main, branch, branch, main); the runs agree within 0.3%.--attention-backend triton, MTP--kv-cache-dtype fp8_per_token_head(triton), MTP--attention-backend triton, no specOutput tokens/s. In the fi row the target model's attention stays on fi. The MTP draft layer's attention runs on triton unless the target uses fa (
spec/mtp/proposer.py), so its (k+1)-row extend after each verify no longer uses the extend kernel: at 128K the verify step goes from 28.4 to 14.1 ms. Accept length is within 0.12 between main and branch in every MTP cell (3.31-3.54). The greedy outputs are identical at 128K. At 32K, 2 cells take another wording at token ~67 of 256, a near-tie; both texts are coherent. AIME-25 problem 1 pass@3 with fp8 KV + MTP on this branch: 70 / 70 / 70.Server, as launched by the client (
ft=python -m freetoken.cli; 132352 instead of 34048 at 128K):The client is
benchmarks/bench_decode_moe.py --context 32768 --greedy --cache 10240, plus two flags that it passes through toft serve(--speculative-algorithm,--speculative-draft-model-path). The no-spec row runs on the script as it is on main.Kernel time
Stage 1 + stage 2 in a CUDA graph with L2 flushed before each rep, RTX PRO 6000. Shape 16 query / 2 kv heads, head_dim 256 (Qwen3.6-35B-A3B full attention), bf16 KV, times in us.
Before this PR, R > 1 ran the extend kernel. FlashInfer fa2
BatchPrefillWithPagedKVCacheWrapperis shown for reference. The R=1 code is unchanged. "after" is the interquartile mean; the other columns are the medians of an earlier run on the same GPU.Tested on
NVIDIA RTX PRO 6000 Blackwell Server Edition 96 GB (sm_120), driver 595.91.07; Intel Xeon Platinum 8559C; torch 2.14.1+cu130, triton 3.8.0, flashinfer 0.6.18.post1. Checkpoint
Qwen/Qwen3.6-35B-A3B.pytest tests/kernels/test_triton_attention.py tests/attention tests/spec tests/scheduler/test_spec_drain.py: all pass, except the 2facases intest_capture_rows_per_req.py. Those fail on main too, becausefadoes not support sm_120.pytest tests -m "not slow": 91 failures, all of which fail the same way on main (ggml JIT 81, local-checkpoint quant configs 8,faon sm_120 2).