Repository navigation
perf(qwen4-exp): stack index heads into M for the QSA decode scorer - #2651
ZLkanyo009 wants to merge 2 commits into
Conversation
Decode-sized QSA index scoring (the target's MTP verify and the draft, at most 256 rows) ran on the CUDA-core kernel, which reads every request's compressed keys once per query row. The new Triton MFMA kernel stacks 4 rows x 4 index heads into one M = 16 operand, so a request's keys are read once per verify and meet every head in a single dot; the per-head ReLU is applied before the heads are summed back out of M. On Qwen3.8-Flash-Next (MI355X, TP1, MTP 3) the target's decode scoring drops from 1130 to 320 us a step and the draft's from 147 to 85 us, -6.2% of decode kernel time. ATOM_QSA_STACKED_DECODE_SCORER=0 keeps the old kernel. Co-authored-by: Cursor <cursoragent@cursor.com>
🏷️ CI GuideRuns automatically on every eligible PR before approval:
Heavy model tests:
|
Conflict in atom/utils/envs.py only: main reformatted the adjacent ATOM_PA_FLYDSL entry. Keep main's form and add ATOM_QSA_STACKED_DECODE_SCORER after it as before. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Merged Conflict: Verified (MI355X,
|
…o lingzha/qwen38-qsa-fp8-kv-index Brings in the updated base branch (#2651), which now contains origin/main c34e7d0. The only conflict with main was atom/utils/envs.py next to the reformatted ATOM_PA_FLYDSL entry; with #2651 resolved the same way, git merges it cleanly: ATOM_QSA_STACKED_DECODE_SCORER, then ATOM_QSA_DRAFT_KV_BF16, after main's ATOM_PA_FLYDSL. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Folded into #2652. #2652 was stacked on this PR and already contains its commits (branch |
Summary
Decode-sized QSA index scoring (the target's MTP verify and the draft, at most 256 query rows) now runs on a Triton MFMA kernel that stacks every row's 4 index heads into the M dimension. A request's compressed keys are read once per verify instead of once per row, and the target's decode scoring drops from 1130 to 320 µs a step (-6.2% of decode kernel time on Qwen3.8-Flash-Next).
Motivation
Part of the Qwen3.8-Flash-Next (
amd/Qwen3.8-Flash-Next-Quark-MXFP4) optimization on MI355X / ATOM: https://github.com/wuhuikx/LLM-Booster/issues/263At the decode work point,
_qsa_paged_mqa_logits_kernel(CUDA-core, one program per query row) was the third-largest decode kernel. With MTP 3 a request contributes 4 verify rows that share the same keys, and each of them streamed the request's whole compressed-key cache. The existing MFMA kernel is not used below 256 rows because one row per request leaves M mostly empty.Changes
atom/model_ops/qwen4_exp/ops/qsa.py_qsa_paged_mqa_logits_stacked_kernel: a program's BMT rows x NUM_HEADS heads form one M = BMT * heads operand, so onetl.dotper key tile scores every head of every row; the per-head ReLU is applied before the heads are summed back out of M. BMT = 4 with 4 heads is one 16-row MFMA tile and, in decode, exactly one request's verify rows. Programs stride over column tiles up to the block's widest visible count; rows of different requests in one block (the draft: one row per request) walk their requests as the prefill MFMA kernel does.qsa_paged_mqa_logitsroutes calls with at most 256 rows to it when the index heads and head dim are powers of two (heads >= 4); everything else is unchanged. Tile(BMT, BN, warps, column programs) = (4, 32, 2, 256), chosen with an L2-flushed microbench at 36 rows over 9 requests and confirmed on a live server.atom/utils/envs.py,docs/environment_variables.md:ATOM_QSA_STACKED_DECODE_SCORER(default 1;0restores the CUDA-core kernel).tests/model_ops/test_qsa_stacked_scorer.py(GPU): stacked vs CUDA-core vs a torch reference, 1 and 4 rows per request, 4- and 16-row pages (block 16 / 64), padded rows, uneven contexts,max_columns.Accuracy
Selection is unchanged: logits match the CUDA-core kernel to FP32 summation order (the unit test holds them to rtol / atol 5e-5). End to end (lm_eval
gsm8k5-shot through the chat API withfewshot_as_multiturn, temperature 0, real MTP verification, MTP 3):--limit 200(branch code)(stderr ≈ 0.004 on the full set; the first 200 questions score 0.975-0.99 across all earlier full runs.)
Performance
Kernel time per step from a torch trace, MI355X, TP1, MTP 3, BF16 KV, block 16 (
rocm/atom-dev:latest566aa3dc9d5d, ATOM53e4fb524, AITER948dd267f). Decode =decode[bs=9/16 tok=36 d=9 spec=3]averaged over 400+ steps; prefill =prefill[bs=1 tok=16384 ctx=98304]. Both from a controlled profile (decode: 9 concurrent requests on 64k-128k prompts; prefill: the 7th chunk of a 130k prompt). "before" = this branch withATOM_QSA_STACKED_DECODE_SCORER=0, i.e. main's kernels (mean of two runs).With
--block-size 64the target's decode scoring is 285 µs (16 rows a page, fewer page-table lookups).AgentX C16 (
--scenario agentx, 3600 s, golden AL 2.32), BF16 KV, block 16:Not a clean kernel A/B: the BF16 pool (1.61 M tokens) is too small for 16 AgentX sessions, so the realized prefix hit, which depends on eviction timing in this closed-loop load, moves by several points between runs and drives most of the difference. The two runs were also on different MI355X nodes. Read it as "no regression"; the kernel gain is the decode profile above.
How to use
On by default.
ATOM_QSA_STACKED_DECODE_SCORER=0keeps the CUDA-core scorer. It serves block 16 and block 64 alike; the recommended Qwen3.8-Flash-Next config is--block-size 64(with--kv_cache_dtype fp8 --index-cache-dtype bf16from #2652), where block 64 also moves BF16 index prefill scoring to FlyDSL.Test plan
pytest tests/model_ops/test_qsa_stacked_scorer.py tests/model_ops/test_triton_dynamic_shapes.py tests/test_envs.pyon MI355X: 128 passed.pre-checks.yamlversions).--limit 200and MTP acceptance above; kernel profile and AgentX above.Depends on #2606 at runtime for
amd/Qwen3.8-Flash-Next-Quark-MXFP4(MTP weights), not for the code itself. #2652 (FP8 KV / FP8 / FP4 index caches for QSA) builds on this PR.