Skip to content

[Perf] [AMD] gdn: Tune the ROCm decode Conv1D update and recurrent state streaming - #47157

Draft
vorapolsiloai wants to merge 1 commit into
sgl-project:mainfrom
vorapolsiloai:perf/qwen35-rocm-gdn-decode-tuning
Draft

vorapolsiloai wants to merge 1 commit into
sgl-project:mainfrom
vorapolsiloai:perf/qwen35-rocm-gdn-decode-tuning

Conversation

@vorapolsiloai

@vorapolsiloai vorapolsiloai commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

On ROCm, two of the GDN kernels that run in every decode step leave performance unused:

  • _fused_qkvzba_causal_conv1d_update_contiguous_kernel launches 8 warps per 256-feature block. On 64-lane wavefronts that is 512 lanes for 256 features, so every feature is held twice. It also reloads two state values after the output store.
  • fused_recurrent_gated_delta_rule_packed_decode_kernel streams the whole fp32 recurrent state through the caches. At TP1 that is about 3 MB per request per GDN layer, so roughly 100-800 MB per layer per decode step at batch 32-256. Each tile is read and written exactly once per step, so caching it only evicts the weights the next GEMMs read.

Modifications

  • kernels/ops/attention/triton_gdn_fused_proj.py: on ROCm, the launcher picks a new _fused_qkvzba_causal_conv1d_update_reuse_state_kernel at 4 warps, one feature per lane. It does the same operations in the same order as the contiguous kernel. The only difference is that it keeps the shifted state values from its first loads instead of reloading them; each lane is the only one touching those elements. The kernel is a separate copy so the CUDA path stays byte-for-byte unchanged.
  • kernels/ops/attention/fla/fused_recurrent.py: a new constexpr STREAM_STATE (on for ROCm) loads the state tile with evict_first and stores it with .cs. Tile shape, layout and op order are unchanged.
  • srt/environ.py: SGLANG_DISABLE_ROCM_GDN_DECODE_TUNING=1 (read at import, so set it before launch) restores both stock launches. CUDA is unaffected.

Dependency: Qwen3.5/Qwen3.8 only reach the fused decode projection + Conv1D launcher on ROCm with #41677, which enables that path on HIP. On main alone, only the recurrent-state streaming change is active on ROCm. This PR is not stacked on #41677 and shares no files with it. It is a single commit on main.

Accuracy

Both changes are bit-exact: outputs and state pools are identical with the tuning on and off.
GSM8K (1319 questions), MI350X, TP2, Qwen3.8-27B: 0.917 (on) vs 0.917 (off).

Speed

MI350X, same image, tuning on vs off:

Conv1D µs/call recurrent µs/call GPU busy µs/step
TP2 c128 13.22 → 7.62 84.03 → 80.64 18573 → 17919 (−3.5%)
TP1 c32 8.05 → 4.95 49.86 → 46.52 10982 → 10597 (−3.5%)

Serving, MI350X, median TPOT change (two runs):

  • TP1 c32: −4.0% / −3.3%
  • TP2: c32 −2.4% / −1.5%, c64 −2.7% / −3.0%, c128 −2.1% / −3.0%, c256 −1.4% / −2.8%

MI355X, TP1, bs256, from traces (not an isolated A/B): the Conv1D update goes from 1,935 µs/step to 1,025 µs/step.
MI355X, TP1, the same build with the tuning on vs off (SGLANG_DISABLE_ROCM_GDN_DECODE_TUNING=1), 8k input / 1k output tokens: median time per output token 8.14 → 8.00 ms (-1.7%) at 16 concurrent requests, 19.19 → 18.75 ms (-2.3%) at 64, 57.96 → 56.83 ms (-1.9%) at 256; output tokens/s +2.3% / +1.5% / +1.6%.

Setup for the MI355X numbers:

  • MI355X (gfx950), Qwen3.8-27B MXFP4 (amd/Qwen3.8-27B-Quark-AWQ-MXFP4).
  • SGLang TP1 with --attention-backend aiter, FP8 KV cache, --enable-amd-monokernel, chunked prefill 16384 (TP2 runs add INT4 quick reduce).
  • On vs off = the same build with the tuning enabled vs disabled (SGLANG_DISABLE_ROCM_GDN_DECODE_TUNING=1), on the same GPU, averaged over 2 GPUs.
  • Decode: bench_serving random, 8,192 input / 1,024 output tokens, 64 / 128 / 256 prompts at 16 / 64 / 256 concurrent requests, median TPOT.
  • Repeat noise of the full stack on one GPU: TTFT 0.1-0.8%; TPOT 0.1-0.8%, except 2.4% at 16 concurrent requests on one GPU.

Relation to #41879

#41879 adds opt-in AITER FlyDSL kernels for the prefill conv and the decode recurrence. Those need AITER's FlyDSL ops and are not bit-exact, because they split K across lanes. This PR keeps the Triton kernels, is bit-exact and on by default on ROCm, and also covers the decode Conv1D update, which #41879 does not touch. The two PRs share no files.

Tests

New: test/registered/kernels/ops/attention/test_rocm_gdn_decode_tuning.py, registered for AMD jit-kernel-unit. It calls the public launchers with the tuning on and off and compares outputs and whole state pools bit for bit, including untouched slots. Coverage:

  • Qwen3.8-27B TP1 and TP2 head shards
  • decode batch widths 1 to 256, 3 consecutive steps
  • pad and out-of-range slot indices
  • Conv1D widths 4, 3 and 2, with and without bias and SiLU

Results:

  • MI350X: 2 passed (70 subtests).
  • MI355X: 2 passed (70 subtests).

Checklist

  • pre-commit run --all-files
  • Unit test added
  • MI355X serving numbers

On ROCm:
- the fused QKVZ/BA Conv1D decode update runs one feature per lane (4
  wavefronts per 256-feature block instead of 8, which held every feature
  twice on 64-lane wavefronts) in a copy of the kernel that keeps the shifted
  state values from its first loads instead of reloading them;
- the packed recurrent decode kernel loads and stores its fp32 state tile with
  streaming hints (evict_first / .cs), since each tile is touched once per step.

Both produce bit-identical outputs and states (new kernel test). Setting
SGLANG_DISABLE_ROCM_GDN_DECODE_TUNING=1 restores the stock launches; CUDA is
unchanged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant