Repository navigation
[Perf] [AMD] qwen3-next: Consume the AITER fused GDN prefill kernel - #39873
karverma-amd wants to merge 3 commits into
Conversation
75c0984 to
a98b6e3
Compare
| # model shape the AITER kernel hard-codes. The per-call tensor contract | ||
| # is checked in the backend, which falls back rather than raising. | ||
| _avail = gdn_fused_prefill_aiter.available() | ||
| self._gdn_fused_prefill_ready = ( |
There was a problem hiding this comment.
Please add a guard with if_hip or if_rocm to make sure your change doesn't affect the NVIDIA common path. Use if_hip if this kernel can be used by all AMD GPUs; use use_aiter if this kernel imports from the AITER library.
|
Thanks @yichiche — good call, addressed in ae2083a. Guard the NVIDIA common path. In qwen3_next.py, the fused-prefill defaults (_gdn_fused_prefill_ready=False, etc.) are now No bespoke flag / on-by-default via use_aiter. Dropped the SGLANG_QWEN3_NEXT_GDN_FUSED_BACKEND opt-in env var. The adapter now gates on the standard switch: |
yichiche
left a comment
There was a problem hiding this comment.
Please also added unit test for this modification.
|
|
||
| return (core_attn_out, z) if return_z else core_attn_out | ||
|
|
||
| _aiter_gdn_prefill_reject_logged = False |
There was a problem hiding this comment.
_aiter_gdn_prefill_reject_logged: we should remove this log from production code.
| or forward_batch.forward_mode.is_target_verify() | ||
| ): | ||
| return None | ||
| if get_tc_piecewise_forward_context() is not None: |
There was a problem hiding this comment.
The fused prefill path skips the DP-padding trim in forward. This docstring says DP-padding envelopes are declined, but the only extra guard is the piecewise-graph check.
forward narrows extend inputs to global_num_token_non_padded_cpu and zeros the pad tail, because those rows go into the residual, MoE, and collectives. try_fused_gdn_prefill runs on the full projected_qkvz. AITER allocates outputs with torch.empty((M, ...)) and does not require cu_seqlens[-1] == M, so a padded prefill either folds the pad rows into conv/SSM state or leaves an uninitialized tail.
Please return None (unfused chain) when global_num_token_non_padded_cpu is set and smaller than projected_qkvz.shape[0], or when cu_seqlens[-1] != projected_qkvz.shape[0].
Two related holes on the same path:
covered()does not passA_log,dt_bias, ornorm_weight. AITER re-checks those insiderun()and raisesValueError(dt_biasmust be bf16[8],norm_weightbf16[128]). Pass them intocovered()so a mismatch falls back instead of failing the request._gdn_out_proj_fp8is anyFp8LinearMethod.block_quant, including MXFP8 and block K other than 128. The kernel scales are fp32 group-128[M, 8]. Gate the direct FP8 feed on block K == 128 and not MXFP8, and clearattn._gdn_fp8_outafterout_proj.
Also, the PR body still documents SGLANG_QWEN3_NEXT_GDN_FUSED_BACKEND=aiter, which this commit removed. The repro should use --enable-aiter.
|
Thanks @yichiche for the careful review — all points addressed across two commits 1. Remove the Done — dropped the class flag and the 2. Fused path skips the DP-padding trim ( Fixed. The backend now declines any batch whose projected rows exceed the real 2.1 Fixed.
To keep the fused path enabled (the model stores 2.2 Fixed via 3. Add a unit test Added 4. PR body still documented the removed env var Updated — the flag is gone; the repro now uses |
yichiche
left a comment
There was a problem hiding this comment.
#39873 Consume the AITER fused GDN prefill kernel — +703/−0 across 5 files. It reaches shared code only additively (gdn_backend.py, radix_linear_attention.py), so nothing that already ships changes behaviour. Guarded by use_aiter (SGLANG_USE_AITER and is_hip()), with the NVIDIA path fenced by _is_hip and gfx950 / Triton 3.8 inside available(). No new env var, no new global, no shared signature rewritten. /sglang-pr-review found no CRITICAL (0 high, 0 medium, 1 low — none blocking). The PR body carries accuracy and perf numbers, and a test lands in the diff. LGTM.
This comment has been minimized.
This comment has been minimized.
Rebased onto current main to resolve the merge conflict. Content is the approved PR unchanged except for import drift that accumulated on main: - gdn_backend.py: fused_gdn_gating moved fla/ -> linear/ (context only) - radix_linear_attention.py: re-add the get_tc_piecewise_forward_context import (main dropped it from this file when its last in-file use went away; the fused-prefill decline path needs it back) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6c85b64 to
46efb51
Compare
[Qwen3-Next] Consume the AITER fused GDN prefill kernel
Motivation
The Qwen3-Next GDN (gated delta-net) prefill runs as a four-kernel chain
today (
causal_conv1d_split_qkv→ fused gating → the fp32 chunked delta-rulescan → gated RMSNorm), so the large intermediate tensors round-trip through HBM
between each launch. AITER now ships a fused single-kernel prefill
(
fused_gdn_prefill_qkvz, ROCm/aiter#5606) that folds that chain into a tight setof Gluon launches and emits the per-head group-128 FP8 activations a block-FP8
out_projconsumes directly.This PR wires sglang to use it. It is the prefill sibling of #39554 (the decode
consumer of ROCm/aiter#5520) and rides the standard AMD switch, so one flag enables
both phases:
On by default wherever AITER is enabled on a gfx950 host — there is no bespoke
per-kernel flag. Everything is probed, nothing assumed: unsupported platform,
older Triton, or an uncovered call runs the existing unfused chain unchanged.
What it does
kernels/ops/attention/gdn_fused_prefill_aiter.py— fail-closedavailable()/covered()/run(): platform +SGLANG_USE_AITER, a Triton ≥ 3.8Gluon probe, and a per-call shape/dtype contract.
covered()delegates the tensorcontract to AITER's own predicate so the two can't drift, then additionally checks
the parameters that predicate does not see — the SiLU output gate and the
A_log/dt_bias/norm_weightelement counts/dtypes — so a mismatch falls backinstead of raising deep inside the kernel. Two small pure helpers
(
out_proj_accepts_group128_fp8,prefill_rows_match_real_tokens) hold theout_proj-compat and DP-padding decisions and are unit-tested directly.
GDNAttnBackend.try_fused_gdn_prefill— builds the ragged-batch args(
cu_seqlensfromquery_start_loc,has_initial_statefromextend_prefix_lens,cache_indicesfrom the mamba pool) and the conv/state pools, callscovered() → run(), and stashes the post-norm output + group-128 FP8 activations.Declines MIS / target-verify / tracked-conv batches, and declines DP-padded
batches — the fused kernel has no padding trim, so it must not run when the
projected rows exceed the real varlen tokens (
global_num_token_non_padded_cpubelow the row count, or
cu_seqlens[-1] != rows).RadixLinearAttention.try_fused_gdn_prefill— unwrapsHybridLinearAttnBackendto reach the GDN backend; guards extend / not-verify / not-piecewise.
qwen3_next.py— the fused path is AMD/AITER-only and stays off the NVIDIAcommon path (
_is_hipfence + lazy import). Static readiness gate (available()+num_v_heads == 2·num_k_heads+head_k == head_v == 128+conv == 4), publishesthe bf16 norm weight after load, attempts the fused path first in
forward. It feedsthe kernel's group-128 FP8 activations straight into
out_projonly when thatGEMM is a group-128 block-FP8 (block K == 128, not MXFP8); every other method takes
the bf16 normalized output and re-quantizes as usual. The FP8 stash is cleared right
after it is consumed.
Files changed
python/sglang/kernels/ops/attention/gdn_fused_prefill_aiter.pypython/sglang/srt/models/qwen3_next.pypython/sglang/srt/layers/attention/linear/gdn_backend.pytry_fused_gdn_prefilldispatch + DP-padding decline + stashpython/sglang/srt/layers/radix_linear_attention.pytest/registered/attention/test_gdn_fused_prefill_guards.pyDependency
Requires the AITER fused prefill kernel ROCm/aiter#5606. Fail-closed, so this
merges safely before that lands — the fused path simply activates once the kernel
is importable.
Accuracy — run command
Same model/args with the AITER fused kernel present vs absent; GSM8K must stay at
parity. Needs a gfx950 (MI350-series) host with Triton ≥ 3.8.
Result (MI355X): GSM8K parity — fused 0.953 vs stock 0.950.
Perf
End-to-end A/B, fused kernel present vs absent, same Triton-3.8 image (isolates the
kernel), TP4, Qwen3-Next-80B FP8, MI355X.
Prefill-focused (ISL 6144 / OSL 8):
Balanced 1k/1k, concurrency sweep (Median TTFT, ms):
TPOT is identical on both arms (decode path untouched); the win is prefill-side,
largest where prefill is a bigger fraction of the request (low concurrency / long
prompts) and diluting as the decode-heavy steady state dominates.
CI States
Latest PR Test (Base): 🚫 Run #41252100316
Latest PR Test (Extra): ❌ Run #41252100257
Latest PR Test (AMD ROCm 10): ❌ Run #41252100266
Tip
Base CI only covers the core cases, and its workload may not exercise your change. Use
/rerun-testto run the tests your change touches, or copy this prompt for an agent: