Fuse Qwen3 dense-decode q-norm + RoPE into one kernel (opt-in)#90
Draft
yeahdongcn wants to merge 2 commits into
Draft
Fuse Qwen3 dense-decode q-norm + RoPE into one kernel (opt-in)#90yeahdongcn wants to merge 2 commits into
yeahdongcn wants to merge 2 commits into
Conversation
Qwen3 dense attention issues three separate launches per decode step for q_norm, k_norm, and neox RoPE. Fuse them into one TileLang kernel that normalizes and rotates q and k in place; the attention backend keeps reshape_and_cache. The fused op is registered opaque (direct_register_custom_op) so Dynamo sees a single call, and it mutates only q and k, so it captures cleanly under a full CUDA graph. Wiring is a category-6 runtime object patch, off by default and enabled with VLLM_MUSA_QWEN3_FUSED_QKNORM=1.
🤖 Augment PR SummarySummary: This PR adds an optional fused decode fast path for Qwen3 dense attention on MUSA by combining q-norm, k-norm, and Neox RoPE into a single TileLang kernel launch. Changes:
Technical Notes: The fused path is designed to be capture-safe (single GPU op per call) and remains off by default; it primarily targets launch overhead reduction in 1-token decode. 🤖 Was this summary useful? React with 👍 or 👎 |
Ignore VLLM_MUSA_QWEN3_FUSED_QKNORM on any non-MUSA runtime so an accidentally-set flag cannot apply the MUSA-only TileLang kernel on a CPU or CUDA process.
yeahdongcn
marked this pull request as draft
July 12, 2026 01:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Qwen3 dense attention issues three separate GPU launches per decode step —
q_norm,k_norm, and neoxrotary_embedding. All three are registered withdirect_register_custom_op(opaque to Dynamo, asrope.pydocuments), soInductor cannot fuse across them: they stay three distinct kernel launches
even under
torch.compile+FULL_DECODE_ONLY. Hand-fusing them is thereforenot redundant with the compile path.
This adds one TileLang kernel that does q-norm + k-norm + neox RoPE in a single
launch, writing q and k in place. This is the 3→1 variant that ships wired
here: the attention backend keeps its own
reshape_and_cache(no backendsurgery). Wiring is a category-6 runtime object patch, off by default,
enabled with
VLLM_MUSA_QWEN3_FUSED_QKNORM=1.A 4→1 variant that also writes the paged KV cache in the same kernel
(
qk_norm_rope_kv_insert,write_kv=True, correctness-verified) ships in thesame module but is not wired — it needs a small backend patch so the
attention backend skips its own
reshape_and_cache. It is left as a follow-up(numbers for both variants below).
Why
On S5000 each tiny decode kernel has an irreducible ~3.5–6.4 µs device floor — a
1-token op moving a few KB cannot amortize grid-launch / scheduling
(8 KB / 1.6 TB/s ≈ 0.005 µs ≪ the ~5 µs floor). Fusing pays that launch floor
once instead of three times. Because the win comes from removing whole
kernel-node floors (not a faster inner loop), it survives CUDAGraph capture —
unlike an absorbed compute win the graph would hide.
Microbench (cold cache, 8 GB L2 flush,
mate.bench_kineto, Qwen3-8B 32/8/128, bs1)Per-launch device time (µs):
q_norm + k_norm + rope = 11.65 µswith one fusedlaunch (~9 µs);
reshape_and_cache(5.89 µs) stays separate. Net decode-attnpreamble ~17.55 µs → ~15.0 µs — the fusion pays 3 launch floors once instead of
three times.
1.8× on the op (up to ~2.2× at the op-level wall including launch overhead).
At bs≥16 the fused kernel becomes occupancy-bound and the edge narrows (32B bs16
→ ~1.0×); single-stream decode is the target. The op is launch/occupancy-bound at
M=1 (0.2–2.5 % of the 1600 GB/s ceiling) — the lever is op-count/fusion, not the
inner loop.
Correctness (fused vs torch fp32 reference, bf16 envelope atol 1e-2 rtol 2e-2)
v is a pure copy (bit-exact). No NaN/Inf, no poison (all-zero/constant) output.
Capture-safety + serve correctness (Qwen3-8B-FP8, TP1, compile ON + FULL_DECODE_ONLY)
FULL_DECODE_ONLYgraphs captured, zerooperation not permitted when stream is capturing, noSKIPPED INLINING. The1-op wrapper does no host→device scalar copy and writes q in place; JIT prewarm
runs during the eager dummy pass before capture.
apply()runsvia the
vllm.general_pluginsentry point in every worker(
Applied object-patch vllm.model_executor.models.qwen3in the EngineCore log),so it works under normal V1 multiprocessing.
identical first tokens (one prompt byte-identical). One prompt's long-tail greedy
divergence is bf16-ULP sensitivity, bounded by the rel-err 0.0015 above.
E2E — honest read
These are profile-grounded projections, not measured serve A/B numbers —
because the delta is below the serve-benchmark noise floor (3–5 % same-session,
~15 % shared-node drift), so a serve A/B would be noise-dominated. The credible
evidence is the isolated cold-cache microbench. Grounded in the Qwen3-8B-FP8
decode profile (10.62 ms/step):
So this PR delivers ~1 % TPOT at bs1 — small, positive, and capture-durable.
The larger ~2.6 % needs the 4→1 backend integration. Consistent with the finding
that vLLM-MUSA dense decode is already at/near the S5000 roofline: there is no
single big compute lever here, only a set of ~1–3 % launch-floor fusions, of
which this is one.
Test plan
py_compileall four files (local + in-container)Risk
Low. Off by default. When enabled, one attention class (
Qwen3Attention) ispatched; q and k are mutated in place; the attention backend keeps
reshape_and_cache, so there is no backend surgery. Pure-Python + JIT kernel —no native reinstall. Dormant for every non-Qwen3 model and whenever the env flag
is unset.