Repository navigation
perf: speculative-decode verify on gfx1151 (QMoE bursts, LinearAttention burst, TopK) + TopK largest=0 fix - #1264
Conversation
Speculative verify presents 2-8 tokens per MoE layer, which used to take the grouped path (one pass over the union of experts in padded WMMA tiles). Bursts up to HIPDNN_EP_QMOE_FUSED_MAX_TOKENS (default 8) now take the fused decode path instead. The dp4a kernels take num_tokens and run all token*k slots in the same launches (quantize inputs, FC1+SwiGLU, quantize slots, FC2, plus the per-token reduce); bursts autotune separately from decode. The fp16 kernel is iterated a token at a time. HIPDNN_EP_QMOE_BURST=0 restores the per-token dp4a loop and HIPDNN_EP_QMOE_FUSED_MAX_TOKENS=1 the decode-only behaviour, for A/B. Ports the burst dispatch of #1067. Strix Halo, Qwen3.6-35B-A3B int4, target call T(w) in ms: w 2 3 4 5 6 8 loop 21.97 26.09 29.03 32.48 - - burst 20.73 24.04 25.94 28.52 30.53 33.15 grouped 24.46 27.98 29.95 31.69 33.37 35.97 test_qmoe adds 2, 5, 8 (fused) and 9 (grouped) tokens. The fused cases, 1 token included (the same dp4a decode path as on main), exceeded the fixed atol=0.1: the default W4A8 dp4a path quantizes activations to int8, an error that scales with |output| (~100 at the GPT-OSS shapes; max diff 0.49-0.75, cosine >= 0.99996). They are now bounded at 2% of max |output| with cosine >= 0.9999 instead of 0.99; grouped cases keep atol=0.1. Burst and loop produce the same error stats on every case. 11/11 pass on gfx1151. qmoe_decode_fc1_dp4a_kernel gains a num_tokens parameter, so its entries in kernel_resources_*.json (baseline and allow list) move to the new mangled name unchanged; with the same SDK check_kernel_resources.py reports the same resources for it as on main. Co-authored-by: Cursor <cursoragent@cursor.com>
… short bursts Every S column evolves independently under all four update rules, so the (batch, kv_head) state is split into 32-column tiles across blockIdx.y (128 blocks instead of 32 for Qwen3.6's 32 kv heads) and each thread keeps its rows of S in registers: one read and one write of S per launch instead of a past->present copy plus two reads and a write per step. seq_len <= HIPDNN_EP_LA_BURST_MAX_TOKENS (default 16) runs all steps in that launch with S in fp32 between steps (operands staged in LDS, two barriers per step) and reads past_state directly, so the separate state copy is skipped for decode and speculative verify. HIPDNN_EP_LA_COLS=0 or HIPDNN_EP_LA_BURST_MAX_TOKENS=0 restores the old paths. Standalone, 30 gated-delta layers (Hq=16, Hkv=32, d=128, ping-pong state): w 1 2 4 8 old 1.77 2.34 2.53 2.87 ms new 1.13 1.32 1.55 1.91 ms In the full 35B-A3B target the saving is 0.2-0.7 ms per call at w<=4. test_linear_attention passes, now also at seq_len 2, 5 and 16. Co-authored-by: Cursor <cursoragent@cursor.com>
onnx.TopK declares largest and sorted as SI64Attr, but the conversion read them with getAttrOfType<BoolAttr>, which never matches, so both stayed at their default of true: every smallest-k TopK returned the largest values (test_topk_axis1 with largest=0 failed on main). Read them as IntegerAttr, as axis already is, and add a lit case that checks largest = false and sorted = false reach hip.top_k. Co-authored-by: Cursor <cursoragent@cursor.com>
The generic TopK assigns one thread per row and scans the axis serially. For a greedy-decode argmax over a 248k vocab that is ~31 ms per call on gfx1151, more than the rest of the 35B-A3B forward, and 2 <= k <= 32 was as slow (2.4 s for k=16 over 7x248320). - k=1: one 1024-thread block per row with a shared-memory (value, index) reduction; ties keep the lowest index, matching numpy argmax. - 2 <= k <= 32: one 256-thread block per row with per-thread register top-K lists; rows longer than 8192 take a chunked first pass into a cached scratch buffer plus a per-row merge. Values and lowest-index tie order match a stable sort. Measured on Strix Halo (Qwen3.6-35B-A3B int4, GPU-argmax graph): T(1) 49.1 -> 16.9 ms, vs 18.1 ms for host argmax. Token-identical to host argmax on 10 Spec-Bench prompts x 128 tokens. k=16 over 7x248320: 0.80 ms; k=32 over 8x32000: 0.37 ms. test_topk covers each new path in both directions: k=1 over 32000, k=16 over 1000 (one pass) and k=32 over 20000 (two pass). 9/9 pass on gfx1151. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Thanks for opening a PR! This project follows LLVM's incremental-development and AI-tool-use Before requesting review, please check that:
Reviewers are assigned through |
|
/run-test models=all |
L2 Accuracy Results (EP vs CPU)
Threshold: 0.01 | Run: 8092 - Commit: |
Binary sizesBaseline: run 40801212116 -
MorphiZen EP Performance Results
EPContext Export Performance
EPContext Import Performance
OGA Benchmark Results
OGA Wheel Smoke (Python benchmark_e2e.py)
VLM Benchmark Results
TTFT / TPS vs mainBaseline: run 40801212116 -
Run: 8092 - Commit: |
Resolve test_qmoe.py: keep the fused-path tolerance helper for the skewed-routing test and add main's router_weights test unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
|
/run-test models=all |
Kernel Unit Test Results
Leaves: 16 (0 failed) | Cases: 289 (0 failed) Mode: lookup | Coverage: 1 | Run: 8092 - Commit: |
Replace the global mutex-guarded scratch buffer with the RuntimeState workspace, following the qgemm pattern. Add hip_top_k_workspace_bytes(), pass workspace/workspace_bytes into hip_top_k, size it in wrap_top_k after K is read, and fall back to the one-pass topk_small_last_axis_kernel when no workspace is available. Co-authored-by: Cursor <cursoragent@cursor.com>
Clear stale HIP error state before launching, then log hipGetErrorString and return -1 on launch failure. Co-authored-by: Cursor <cursoragent@cursor.com>
Update TN=4/TN=8 baseline keys to the new mangled signature (extra int parameter). Co-authored-by: Cursor <cursoragent@cursor.com>
|
ROCm GenAI CI finished with result: UNSTABLE.
|
|
/run-test models=all |
|
ROCm GenAI CI finished with result: UNSTABLE.
|
Resolve kernel_resources_{gfx11-generic,gfx1170}.json: take main's regenerated baselines and reapply this branch's rename of the qmoe_decode_fc1_dp4a_kernel keys to the new signature (extra int parameter); apply the same rename to main's new kernel_resources_gfx1151.json.
Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve qmoe.cpp: keep this branch's fused burst gate (num_tokens <= hipdnn_ep_qmoe_fused_max_tokens()) and add main's swiglu requirement, so relu2 experts skip the SwiGLU fused path. Resolve test_qmoe.py imports: keep both os and dataclass. Co-authored-by: Cursor <cursoragent@cursor.com>
|
/run-test models=all |
|
/run-test models=all |
|
/run-test models=all |
…uncated The two-pass TopK accepted only k <= 32, so larger k over a vocab row fell back to the one-thread-per-row kernel. The two-pass kernels stay exact for larger k as long as no thread is handed more than kTopKSmallMax candidates in either pass (chunk pass: kTopKChunk / block, merge pass: chunks * k / block). Allow 32 < k <= 256 under that bound. This covers the low-rank candidate head of EAGLE-4 drafts (k = 128 over a 248320 vocab: 61 chunks, 31 candidates per thread in the merge). test_topk: adds k=128 over 248320 and k=256 over 100000, both directions. Co-authored-by: Cursor <cursoragent@cursor.com>
|
/run-test models=all |
|
/run-test models=all node=xsjnucstrhalo09 |
Conflict in lib/Runtime/real/qmoe.cpp with #1264, which split the fused decode into a whole-burst dp4a launch (HIPDNN_EP_QMOE_BURST) and a per-token loop. Kept main's code and placed the step marks on the new paths: "decode.dp4a" on the burst launch, and one mark before the per-token loop, "decode.dp4a.per_token" or "decode.fp", since a mark names every launch until the next one. Co-Authored-By: Cursor Claude Opus 5.5 <cursoragent@cursor.com> Made-with: Cursor
Summary
Speeds up the speculative-decoding verify pass (2-8 tokens per target call) and greedy argmax on gfx1151, and fixes TopK
largest=0/sorted=0being dropped during conversion. Four areas:past_statedirectly (no past->present copy).onnx.TopKdeclareslargest/sortedasSI64Attr, but the conversion read them asBoolAttr, so they were alwaystrueand every smallest-k TopK returned the largest values.Related issue or design
None. The QMoE burst dispatch ports #1067.
Why
Speculative verify on MoE / hybrid targets (Qwen3.6-35B-A3B int4) is dominated by fixed per-call costs at small widths. The grouped QMoE path pads every expert to WMMA tiles, the LinearAttention decode path copies state and launches per step, and the generic TopK scans a 248k vocab row with one thread (~31 ms per greedy argmax).
What
lib/Runtime/debug_log.h. Each one defaults to the new behaviour and can restore the old path for A/B:HIPDNN_EP_QMOE_FUSED_MAX_TOKENS(default 8; 1 restores decode-only fused)HIPDNN_EP_QMOE_BURST(default on; 0 restores the per-token dp4a loop)HIPDNN_EP_LA_BURST_MAX_TOKENS(default 16; 0 disables)HIPDNN_EP_LA_COLS(0 disables the column-split kernel)hip_qmoe_decode_fused_dp4agains anum_tokensargument; newhip_linear_attention_burstentry point.kernel_resources_*.json: theqmoe_decode_fc1_dp4a_kernelbaseline and allow-list entries move to the new mangled name, with values unchanged.Test plan
On gfx1151 (Strix Halo, Radeon 8060S), Windows, Release, branch on
ffeed9de(since rebased onto991b9859, a CI-only ORT update touching none of these files):pytest tests/test_qmoe.py: 11 passed. Adds 2, 5, 8 (fused) and 9 (grouped) tokens. The fused cases, including the 1-token case that takes the same dp4a path onmain, exceeded the fixedatol=0.1. The default W4A8 path quantizes activations to int8, an error that scales with |output| (~100 at these shapes; max diff 0.49-0.75, cosine >= 0.99996). Fused cases are now bounded at 2% of max |output| with cosine >= 0.9999 (tightened from 0.99); grouped cases keepatol=0.1.pytest tests/test_linear_attention.py: 21 passed (adds seq_len 2, 5, 16).pytest tests/test_topk.py: 13 passed (ondd010ba3). Adds k=1 over 32000, k=16 over 1000, k=32 over 20000 (two-pass), and k=128 over 248320 and k=256 over 100000 (two-pass with k > 32), in both directions.test_topk_axis1[largest=0]fails onmainbecause of the conversion bug.Conversion/onnx-to-hip(149 tests) plus the TopK hip-to-llvm and e2e tests and LinearAttention hip-to-llvm, all passed. New lit case forlargest = 0, sorted = 0.pre-commit run --from-ref origin/main --to-ref HEAD: all hooks pass.check_kernel_resources.py --sources top_k_kernel,qmoe_kernel,linear_attention_kernel: the local SDK is not the baseline compiler, somainitself reports 11 errors / 14 warnings. Run onmainand on this branch with the same SDK, the reports are identical except for the renamed kernel (same numbers) and one warning:qmoe_decode_reduce_kernel+15% instructions (105 -> 121) for the token loop. The new TopK and LinearAttention kernels use no scratch.Performance
Strix Halo, Qwen3.6-35B-A3B int4, measured per commit (details in each commit message):
End-to-end speculative decoding: best speedup per drafter
W4A16 ONNX drafters on Strix Halo: Radeon 8060S iGPU (gfx1151), Windows, ONNX Runtime + hip-ep, strict iGPU mode (
HIPDNN_EP_STRICT=1, CPU fallback disabled),HIPDNN_EP_MATMUL_DP4A_MAX_M=8. Spec-Bench, 240 prompts (first 40 of each of 6 subtasks), greedy, 128 new tokens, decode time only. Speedup = pooled tok/s over plain decode measured in the same process.Bold rows: best drafter for each target. GPT-OSS-20B runs use
HIPDNN_EP_QMOE_FUSED_MAX_TOKENS=1(its large experts favour the grouped path). The Nemotron target export runs all 128 experts per token (no QMoE), so its speedups are not comparable with the QMoE targets. Qwen3.8-27B is a dense hybrid target (48 Gated DeltaNet + 16 GQA layers, AMD int4 ONNX export) with the EAGLE-Corp/Qwen3.8-27B-EAGLE4 drafter (eagle4:5:c:t0.5: k <= 5, cap, τ = 0.5); it needs the k = 128 two-pass TopK above. Its 240-prompt run used this branch at7316a9daplus that TopK change; on the current head (dd010ba3) a one-prompt check runs end to end in strict mode. Drafter, target graph and harness: amd/Qwen3.8-27B-EAGLE4-hip-ep.Notes for reviewers
largest=0tests depend on it.Checklist
AI assistance: Cursor implemented the kernels, runtime dispatch, conversion fix and tests, ran the measurements and validation listed above, and is credited in the
Co-authored-bytrailers.