Skip to content

[AMD] Optimize gpt-oss-120B performance#27063

Merged
HaiShaw merged 23 commits into
sgl-project:mainfrom
HaiShaw:feat/plan-a-5d-kv-pa-decode-gluon
Jun 8, 2026
Merged

[AMD] Optimize gpt-oss-120B performance#27063
HaiShaw merged 23 commits into
sgl-project:mainfrom
HaiShaw:feat/plan-a-5d-kv-pa-decode-gluon

Conversation

@kkHuang-amd

@kkHuang-amd kkHuang-amd commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Integrate Atom's pa_decode_gluon paged-attention decode kernel into
SGLang's AITER backend to accelerate gpt-oss-120b decode on ROCm
(MI300 / MI355). The kernel requires a SHUFFLE 5D vectorized KV
cache layout that aiter's mha_batch_prefill_func also consumes
natively, so we add the layout as a new opt-in pool format
(SGLANG_AITER_KV_CACHE_LAYOUT=vectorized_5d) and route the AITER backend
through it on both prefill and decode.

End result on gpt-oss-120b @ MI300, TP=1, page=64, c=128:

  • +14 % TPOT and +15 % output throughput vs the NHD baseline
    on 8K/1K decode; pa_decode_gluon itself is ~46 % faster than the
    legacy unified_attention decode kernel.
  • GSM8K (lm_eval, 3-shot, chat template) stays within seed noise of
    baseline for both bf16 and fp8 KV cache.

The layout is fully opt-in — when SGLANG_AITER_KV_CACHE_LAYOUT is unset
(default "nhd") every code path is bit-for-bit identical to current
main.

Modifications

New env var

  • SGLANG_AITER_KV_CACHE_LAYOUT (environ.py) selects the physical KV pool
    layout. "nhd" (default) keeps today's behaviour; "vectorized_5d"
    enables the SHUFFLE 5D layout.

Memory pool (mem_cache/)

  • MHATokenToKVPool allocates the 5D layout when the flag is set:
    • K: (num_blocks, H_kv, head_dim/X, page_size, X)
    • V: (num_blocks, H_kv, page_size/X, head_dim, X),
      where X = 16 / store_dtype.itemsize (8 for bf16, 16 for fp8).
  • SWAKVPool propagates the layout to both full-attn and SWA sub-pools
    so SWA decode can also go through pa_decode_gluon.
  • set_kv_buffer already does per-tensor fp8 quant on the host before
    the writer kernel, so the kernel just byte-copies into the cache —
    no scale plumbing needed on the writer side.

Writer kernel (layers/attention/utils.py)

  • reshape_and_cache_shuffle_5d / launch_reshape_and_cache_shuffle_5d:
    Triton scatter from per-token (num_tokens, num_heads, head_size)
    K/V into the 5D SHUFFLE layout. Used by MHATokenToKVPool.set_kv_buffer
    when the fused QKV+RoPE+set_kv writer is unavailable.

Reader kernel (layers/attention/utils.py)

  • gather_shuffle_5d_to_linear / launch_gather_shuffle_5d_to_linear:
    bit-exact triton inverse of the writer. Materialises per-token
    (T, num_heads, head_size) K/V from a list of pool slot ids. Used
    by the chunked-prefill gather-and-linearize fallback (see below).

AITER backend wiring (layers/attention/aiter_backend.py + new

layers/attention/aiter_utils.py)

Two 5D-specific entry points live in aiter_utils.py and are called
from forward_extend / forward_decode only when the active pool is
5D, keeping the backend file focused on dispatch and the legacy NHD
path:

  • forward_extend_vectorized_5d

    • Fresh-prompt shortcut: when every request in the batch has
      extend_prefix_lens == 0 (first prefill chunk, no prefix reuse)
      skip pool reads entirely and feed the raw (k, v) to
      mha_batch_prefill_func in 3D LINEAR mode (page_size=1) on bf16.
    • Gather-and-linearize: otherwise gather the full per-request
      K/V from the SHUFFLE 5D pool via launch_gather_shuffle_5d_to_linear
      into a contiguous (T, H, D) buffer in store_dtype, then run
      the same LINEAR prefill. fp8-store layers forward the raw fp8
      tensors with per-tensor descales — aiter's LINEAR-mode prefill
      supports fp8 K/V/Q natively, no host-side dequant needed.
    • Without this path aiter's paged mha_batch_prefill_func aborts
      with "no matching kernel found" for our
      (page_size=64, bf16/fp8, SHUFFLE 5D) configuration whenever the
      scheduler produces a non-first chunked-prefill batch.
  • forward_decode_vectorized_5d

    • Routes BOTH full-attn and SWA layers through pa_decode_gluon
      (SWA sub-pool is also allocated 5D). Full layers pass
      kv_indices + sliding_window=0; SWA layers pass
      swa_page_table + sliding_window=layer.sliding_window_size.
      fp8 KV cache forwards layer.k_scale / layer.v_scale as
      key_scale / value_scale so the kernel dequantises correctly.

Fused RoPE + KV write (layers/rotary_embedding/base.py, models/utils.py)

  • The HIP fused fused_qk_rope_reshape_and_cache kernel already
    supports the 5D SHUFFLE layout (flash_layout=False). Re-enable
    enable_fused_set_kv_buffer for the 5D pool and have the arg
    builder pass the raw 5D buffer when k_buffer.ndim == 5. This
    collapses RoPE + KV write into a single launch per layer per step.

UX

  • server_args._handle_page_size defaults page_size=64 when
    SGLANG_AITER_KV_CACHE_LAYOUT=vectorized_5d, since that is the page size
    the 5D kernels are tuned for.

Accuracy Tests

All numbers below are GSM8K (3-shot, chat template) on gpt-oss-120b @
MI300, TP=1, run with the following commands:

# Server (vary --kv-cache-dtype between auto / fp8_e4m3; drop the
# SGLANG_AITER_KV_CACHE_LAYOUT env for the NHD baseline rows).
SGLANG_AITER_KV_CACHE_LAYOUT=vectorized_5d \
SGLANG_USE_AITER_MOE_GU_ITLV=False \
python3 -m sglang.launch_server \
  --model-path /path/to/gpt-oss-120b/ \
  --tp 1 --trust-remote-code \
  --mem-fraction-static 0.85 \
  --prefill-attention-backend aiter --decode-attention-backend aiter \
  --chunked-prefill-size 131072 --max-running-requests 256 \
  --page-size 64 --disable-radix-cache --port 8000 \
  --kv-cache-dtype fp8_e4m3   # or `auto` for bf16

# Client
lm_eval --model local-chat-completions --apply_chat_template \
  --model_args model=/path/to/gpt-oss-120b/,base_url=http://localhost:8000/v1/chat/completions,num_concurrent=65,max_retries=3,max_gen_toks=2048,tokenized_requests=False \
  --tasks gsm8k --num_fewshot 3
Layout KV dtype flexible-extract strict-match
NHD (baseline) bf16 0.8855 0.3647
NHD fp8_e4m3 0.8893 0.3381
SHUFFLE 5D (this PR) bf16 0.8870 0.3389
SHUFFLE 5D (this PR) fp8_e4m3 0.8848 0.3374

Specific path coverage:

  • Chunked-prefill gather-and-linearize path (forced by the repro in
    the bugfix commit): bf16 → 0.8863 / 0.3442, fp8 → 0.8848 / 0.3374.
  • fp8 with the fused write path disabled (forces the standalone 5D
    writer): 0.8848 / 0.3480 — confirms set_kv_buffer's host-side
    per-tensor quant feeds the writer correctly.

TP > 1 caveat: GSM8K at TP=2 / TP=4 fails on this branch and
on pre-Plan-A main (the NHD baseline also drops to ~0.32 / 0.16 at
TP=2). Suspected upstream MoE FlyDSL kernel dispatch bug at
per-rank shapes. TP=1 and TP=8 are unaffected. Tracked in
KNOWN_ISSUES.md (A1, local doc) — not introduced by this PR.

Speed Tests and Profiling

InferenceX-style sweep on 8K/1K decode (256 prompts, MI300, TP=1,
--page-size 64 --kv-cache-dtype auto --disable-radix-cache):

Concurrency Layout Median TPOT (ms) Output throughput (tok/s)
4 NHD baseline ~4.75 780
4 SHUFFLE 5D (this) ~4.17 (-12 %) ~920 (+18 %)
8 NHD baseline ~6.06 1220
8 SHUFFLE 5D (this) ~4.88 (-19 %) ~1530 (+26 %)
128 NHD baseline ~7.05 2007
128 SHUFFLE 5D (this) ~6.06 (-14 %) ~2307 (+15 %)

For full-context fp8 + 8K c=64 we re-verified single-run noise: the
SHUFFLE 5D + pa_decode_gluon path matches ATOM throughput within
±6 % across the sweep and beats it by 18-26 % at low concurrency.

pa_decode_gluon itself is ~46 % faster than unified_attention on
the same (bs, num_kv_heads, head_dim, ctx_len) for our gpt-oss
shapes.

Known issues / how to use

SGLANG_AITER_KV_CACHE_LAYOUT=vectorized_5d \
SGLANG_USE_AITER_MOE_GU_ITLV=False \
python3 -m sglang.launch_server \
  --model-path /path/to/gpt-oss-120b/ \
  --tp 1 --trust-remote-code \
  --mem-fraction-static 0.85 \
  --prefill-attention-backend aiter --decode-attention-backend aiter \
  --page-size 64 --disable-radix-cache \
  --max-running-requests 256 \
  --kv-cache-dtype fp8_e4m3
  • --max-running-requests 256 (or omit; auto-tuned default is

    = 256) is required for full accuracy on gpt-oss-120b. With
    --max-running-requests < 256 the AITER MoE dispatch in
    aiter/fused_moe.py selects a bf16-activation path (governed by
    GPTOSS_SWIGLU_MXFP4_BF16_BOUND, default 256) that produces
    measurably wrong logits on gpt-oss MXFP4 — independently of this
    PR. Workaround for smaller batches:
    export GPTOSS_SWIGLU_MXFP4_BF16_BOUND=128 (don't set to 0 — that
    crashes the server at init since warmup hits tiny M).

Checklist

  • Format your code according to the Format code with pre-commit.
  • Add unit tests according to the Run and add unit tests.
  • Update documentation according to Write documentations.
  • Provide accuracy and speed benchmark results according to Test
    the accuracy and Benchmark the speed.
  • Follow the SGLang code style guidance.

CI States

Latest PR Test (Base): ⏳ Run #27113289949
Latest PR Test (Extra): ❌ Run #27113289843

wunhuang and others added 12 commits June 1, 2026 04:49
    The AITER MXFP4 fused-MoE kernels (FlyDSL `gate_mode="separated"` and CK
    `preshuffle_on`) expect the standard `shuffle_weight((16,16))` tile layout
    on top of the *separated* (gate-then-up) weight half ordering, with
    `w.is_shuffled=True` so AITER picks the preshuffle_on codegen. The previous
    path used `shuffle_weight_a16w4` (gate/up-interleaved layout) and never set
    `is_shuffled`, so AITER fell back to `preshuffle_off` kernels that
    interpreted shuffled bytes as un-shuffled and produced silently wrong
    output (gsm8k flexible-extract dropped from 0.88 to ~0.21).

    Changes:
    - `Mxfp4MoEMethod.process_weights_after_loading` (`_use_aiter` branch):
      replace `shuffle_weight_a16w4` / `shuffle_scale_a16w4` with
      `shuffle_weight(..., is_guinterleave=False, gate_up=...)` /
      `shuffle_scale(..., is_guinterleave=False, gate_up=...)` to match the
      layout used by `gptoss_fp4_tuned_fmoe.csv` flydsl entries. Set
      `is_shuffled=True` on both `w13_weight` and `w2_weight`.
    - `Mxfp4MoEMethod.apply` (`_use_aiter` branch): re-tag `is_shuffled=True`
      on the `.view(torch.float4_e2m1fn_x2)` tensors (view() drops the marker).
      Forward `swiglu_limit` to AITER via the quant_info so OAI swiglu is
      applied correctly.
    - `AiterRunnerCore.run`: gate the `GateMode.INTERLEAVE` selection behind
      `SGLANG_USE_AITER_MOE_GU_ITLV` and default to `GateMode.SEPARATED`
      (matches the new weight layout and the gptoss_fp4 tuned config).
    - `environ.py`: register `SGLANG_USE_AITER_MOE_GU_ITLV` (EnvBool, default
      False) in the AMD & ROCm section.

    Verified with `lm_eval gsm8k --num_fewshot 3` on gpt-oss-120b @ MI355X:
    flexible-extract 0.8810 (ATOM baseline 0.8825), strict-match 0.3541
… MI35x test

Previous commit defaulted the AITER MoE gate/up tile layout to SEPARATED
because that is what `Mxfp4MoEMethod` and the gptoss_fp4 tuned FlyDSL
kernels need. However other AITER MXFP4 paths still rely on the
INTERLEAVE layout that the pre-fix code produced, so flipping the global
default would silently regress them.

Restore INTERLEAVE as the default and have the gpt-oss path explicitly
opt out via `SGLANG_USE_AITER_MOE_GU_ITLV=0`:

- `environ.py`: change `SGLANG_USE_AITER_MOE_GU_ITLV` default from False
  to True; update the docstring to describe the new semantics.
- `aiter.py`: update the inline comment near the gate_mode selection to
  describe the new default and the opt-out semantics; logic unchanged.
- `test/registered/amd/accuracy/mi35x/test_gpt_oss_eval_mi35x.py`: add
  `SGLANG_USE_AITER_MOE_GU_ITLV=0` to the env_vars of both MXFP4
  ModelConfigs (`openai/gpt-oss-20b`, `openai/gpt-oss-120b`) so the
  AITER MXFP4 + gpt-oss accuracy path keeps using the SEPARATED layout
  that matches the new Mxfp4MoEMethod shuffle. Other ROCm gpt-oss tests
  (`mi30x` BF16, NVIDIA CUDA paths) do not touch AITER MXFP4 and are
  left unchanged.
The MXFP4 MoE method pads hidden_size up to a multiple of 256
(2880 -> 3072 for gpt-oss-120b) via torch.nn.functional.pad inside
Mxfp4MoEMethod.apply, which fires a separate pad kernel per layer per
forward pass on top of the post-attention RMSNorm + residual add.

Fold the pad into the preceding RMSNorm by:

* RMSNorm gains an optional `x_pad_to_multiple` ctor arg that wires the
  aiter Triton `fused_add_rmsnorm_pad` kernel in `forward_aiter`. The
  fused kernel produces a (M, padded_N) output and an (M, N) residual_out
  in one launch.
* GptOssDecoderLayer constructs `post_attention_layernorm` with the
  MoE-input alignment (256 for MXFP4) only when env
  `SGLANG_FUSE_RMSNORM_PAD=1`, the aiter backend is active, the quant
  method is MXFP4, and TP=1 (the only attn-scatter mode where the
  pre-padded layernorm output flows through `_simple` without touching
  AllReduce/scatter helpers that haven't been taught the padded width).
* `Mxfp4MoEMethod.apply` skips the explicit pad when the input already
  arrives at `self.hidden_size`.
* `GptOssSparseMoeBlock.forward_normal` slices the (pre-padded) input
  back to the unpadded width for the router/topk GEMMs, and trims the
  experts output to the unpadded width before postprocess_layer (which
  pairs hidden_states with the unpadded residual).

Gated behind `SGLANG_FUSE_RMSNORM_PAD` (default off) so other models /
TP>1 / non-aiter setups keep the existing code path bit-for-bit.

Numerical: per-call RMSNorm output matches the baseline bit-exact at
decode batch sizes and within one bf16 ULP at 16k-token prefill
(reduction-order difference between the two Triton kernels). End-to-end
GSM8K 3-shot on gpt-oss-120b is within ±1 sigma of baseline:

  flexible-extract  baseline 0.8855 ± 0.0088  fused 0.8863 ± 0.0087
  strict-match      baseline 0.3654 ± 0.0133  fused 0.3525 ± 0.0132

Co-authored-by: Cursor <[email protected]>
End-to-end integration of Atom's pa_decode_gluon decode kernel into SGLang
via a SHUFFLE 5D-vectorized KV cache layout, opt-in behind the new env var
SGLANG_KV_CACHE_LAYOUT=vectorized_5d.

Pool layout (memory_pool.py, swa_memory_pool.py):
  K: (num_blocks, H_kv, D/X, page, X)
  V: (num_blocks, H_kv, page/X, D, X)        X = 16 / dtype_bytes (=8 bf16)
Both the full-attn and SWA sub-pools follow the env-driven layout; aiter
mha_batch_prefill_func and pa_decode_gluon consume this shape natively.

New writer (layers/attention/utils.py reshape_and_cache_shuffle_5d +
launch_reshape_and_cache_shuffle_5d): scatters per-token (num_tokens, H_kv, D)
inputs into the 5D pool slots.

aiter_backend wiring:
 - save_kv_cache routes through token_to_kv_pool.set_kv_buffer when the
   pool is 5D so the SHUFFLE writer is invoked (SWAKVPool dispatches per
   layer to the right inner pool with the proper full-to-swa loc translation).
 - mha_batch_prefill_func gains an extend_no_prefix shortcut: with
   --disable-radix-cache (no prefix reuse) we feed the kernel the raw
   incoming K/V in 3D linear mode, bypassing the 5D pool on the prefill
   read path.
 - forward_decode adds a 5D branch that calls pa_decode_gluon with Atom
   ps=True / get_recommended_splits convention. Full layers pass
   forward_metadata.kv_indices + seq_lens; SWA layers pass swa_page_table
   + seq_lens + sliding_window=sliding_window_size (ctx_part=128,
   max_part_num=1, mirroring Atom).

models/utils.py: disable enable_fused_set_kv_buffer when the pool is 5D.
The fused QKV+RoPE+set_kv writer .view()s the pool as 4D NHD, which
silently passes numel checks against a 5D buffer but corrupts the data.

GSM8K (lm_eval, 3-shot, chat template, gpt-oss-120b on MI355x TP=1):
  Plan A v2:    flexible-extract 0.8908 / strict-match 0.3381
  Baseline NHD: flexible-extract 0.8855 / strict-match 0.3647

Perf (8K/1K, c=128, 256 prompts):
  Plan A v2:    median TPOT 6.39 ms, output throughput 2210 tok/s
  Baseline NHD: median TPOT 7.06 ms, output throughput 2006 tok/s
  ~10% TPOT improvement; pa_decode_gluon kernel itself is ~46% faster
  than unified_attention on this workload.

Co-authored-by: Cursor <[email protected]>
Profile of Plan A v2 decode (8K/1K c=128) showed ~38ms of elementwise
overhead per 5 decode steps coming from two per-layer copies:

  1. MHATokenToKVPool.set_kv_buffer in vectorized_5d mode called
     .contiguous() on cache_k / cache_v. The writer kernel uses
     key.stride(0) directly for the source token stride and assumes only
     that head/dim are contiguous within each token (stride(1)=head_size,
     stride(2)=1). Both hold for K/V from a QKV split + RoPE in the
     upstream attention path even when the outer per-token stride is
     non-canonical, so the protective copies were no-ops semantically
     while still launching a large elementwise kernel per layer.

  2. forward_decode in the vectorized_5d branch allocated a private
     o_out = empty_like(q_in) for pa_decode_gluon and then did
     o.copy_(o_out.view_as(o)) per layer. Replaced by a zero-copy
     o.view(-1, num_q_heads, v_head_dim) passed directly as the kernel
     output.

Profile diff (8K/1K c=128, 5 decode steps):
  vectorized_elementwise:  25341 us -> 542 us   (-98%)
  manual_unroll:           13068 us -> 868 us   (-93%)

Benchmark (8K/1K c=128, 256 prompts):
  v2:           median TPOT 6.39 ms, 2210 tok/s
  v3 (this):    median TPOT 6.14 ms, 2300 tok/s
  NHD baseline: median TPOT 7.06 ms, 2007 tok/s
  -> v3 is 13% faster TPOT and 14.6% higher output throughput than
     baseline NHD.

GSM8K (lm_eval, 3-shot, chat template) holds:
  v3:           flexible 0.8878 / strict 0.3525
  v2:           flexible 0.8908 / strict 0.3381
  NHD baseline: flexible 0.8855 / strict 0.3647

Co-authored-by: Cursor <[email protected]>
… pool

The HIP fused rotary-embedding + KV-cache-write kernel
(fused_qk_rope_reshape_and_cache) already supports the SHUFFLE 5D cache
layout natively:

  flash_layout=True:  key_cache shape (T_cache, page, KH, D)        [NHD 4D]
  flash_layout=False: key_cache shape (T_cache, KH, D//x, page, x)  [5D]
                      value_cache supports the matching transposed
                      5D shape (T_cache, KH, page//x, D, x).

We had disabled enable_fused_set_kv_buffer for 5D pools because the
HIP-path arg builder reshaped k_buffer / v_buffer with the 4D NHD
.view(), which silently passes numel checks against a 5D buffer but
corrupts the data. With this commit:

 * enable_fused_set_kv_buffer no longer special-cases 5D — both HIP and
   CUDA paths can use the fused kernel for any pool layout.
 * create_fused_set_kv_buffer_arg detects k_buffer.ndim == 5 and passes
   the raw 5D buffer (no view) for the SHUFFLE layout; otherwise it
   keeps the legacy NHD 4D paged view.
 * forward_cuda in rotary_embedding/base.py detects key_cache.ndim==5
   and flips flash_layout accordingly.

Effect: per layer we now run ONE fused kernel instead of separate
rotary_embedding_kernel + reshape_and_cache_shuffle_5d launches.

Benchmark (8K/1K c=128, 256 prompts):
  v3 (no fused): median TPOT 6.139 ms, 2300 tok/s
  v4 (fused):    median TPOT 6.058 ms, 2307 tok/s
  NHD baseline:  median TPOT 7.055 ms, 2007 tok/s
  -> v4 is 14.1% faster TPOT and 15% higher output throughput than NHD.

GSM8K (lm_eval, 3-shot, chat template):
  v4:           flexible 0.8840 / strict 0.3541
  v3:           flexible 0.8878 / strict 0.3525
  NHD baseline: flexible 0.8855 / strict 0.3647

Co-authored-by: Cursor <[email protected]>
Two fixes to unblock fp8_e4m3 KV cache on the 5D vectorized layout, plus
a small UX default.

1) pa_decode_gluon was missing key_scale / value_scale on fp8 KV.
   The kernel reads fp8 bytes and, without per-tensor dequant scales,
   feeds the raw fp8 values straight into the dot product. The write
   side (fused_qk_rope_reshape_and_cache with apply_scale=True) was
   already correct, so this is purely a decode-side regression.

   Fix: when self.kv_cache_dtype == fp8_dtype, pass layer.k_scale /
   layer.v_scale (falling back to the backend's per-tensor scales) into
   pa_decode_gluon. Shape [1] per-tensor scales are supported natively.

2) MHATokenToKVPool 5D path asserted store_dtype == dtype, which blocked
   fp8 storage with bf16 compute. The X vectorization width is a
   property of the on-pool *storage* dtype (16 / store_dtype.itemsize),
   so compute the width from store_dtype and drop the assert. For bf16
   storage X stays 8, for fp8 storage X becomes 16.

3) server_args _handle_page_size: default page_size to 64 when
   SGLANG_KV_CACHE_LAYOUT=vectorized_5d (the layout the kernel is tuned
   for), so users don't have to pass --page-size 64 explicitly.

GSM8K (lm_eval, 3-shot, chat template, gpt-oss-120b, 1xMI300):
  NHD       + fp8  : flexible 0.8893 / strict 0.3381  (control)
  Plan A 5D + fp8  : flexible 0.8802 / strict 0.3306  (after fix)
  Plan A 5D + fp8  : ~0.01 / ~0.00                    (before fix)

Co-authored-by: Cursor <[email protected]>
The pre-Plan-A note said SWA layers stayed on the NHD unified_attention
path because the SWA sub-pool was forced to NHD. This hasn't been true
since SWAKVPool started honoring SGLANG_KV_CACHE_LAYOUT for both
sub-pools — SWA layers now go through pa_decode_gluon with
swa_page_table + sliding_window=layer.sliding_window_size, same as
full-attn but with a non-zero sliding_window arg. Update the comment so
it reflects what the code actually does.

Co-authored-by: Cursor <[email protected]>
forward_extend on the SHUFFLE 5D KV pool was crashing whenever a batch
arrived with non-zero extend_prefix_lens (chunked-prefill 2nd+ segment,
prefix-cache hit, multi-turn, …). The fallback aiter
mha_batch_prefill_func paged path it dropped into doesn't ship a
compiled kernel for (page_size=64, bf16/fp8, paged 5D) and aborts with
"no matching kernel found", which surfaces as a HSA_STATUS_ERROR_EXCEPTION
hardware exception. The repro is just:

  SGLANG_KV_CACHE_LAYOUT=vectorized_5d SGLANG_USE_AITER_MOE_GU_ITLV=False \
  python3 -m sglang.launch_server \
    --model-path /path/to/gpt-oss-120b/ --tp 1 --trust-remote-code \
    --mem-fraction-static 0.85 \
    --prefill-attention-backend aiter --decode-attention-backend aiter \
    --page-size 64 --disable-radix-cache --port 8000

  lm_eval --model local-chat-completions ... --tasks gsm8k --num_fewshot 3

(--disable-radix-cache is irrelevant — the scheduler still produces
non-first chunks during chunked prefill.)

Fix: for the 5D pool route the non-no-prefix case through a new
gather-and-linearize path that mirrors the SHUFFLE writer in reverse:

  * Add gather_shuffle_5d_to_linear / launch_gather_shuffle_5d_to_linear
    in layers/attention/utils.py. Same source addressing as the writer
    so it is bit-exact symmetric. Output is (T, num_heads, head_size)
    in the cache's store_dtype (uint8 bytes for fp8 pools, bf16 for
    bf16 pools).
  * In aiter_backend.forward_extend, when self.kv_cache_is_vectorized_5d
    take the per-token slot ids straight from
    forward_metadata.kv_indices[:seq_lens_sum] (or .swa_page_table for
    SWA layers — both are already per-token slot lists populated by
    create_flashinfer_kv_indices_triton, no page-table indirection),
    triton-gather K/V from the right sub-pool, then run
    mha_batch_prefill_func in 3D LINEAR mode (page_size=1) which does
    have a working kernel.
  * For fp8 store layers we reinterpret the gathered uint8 tensor as
    fp8 (zero-copy view), cast q to fp8, and pass q/k/v_descale to
    aiter — its LINEAR mha_batch_prefill_func supports fp8 K/V/Q
    natively so no host-side dequant is needed.

GSM8K (lm_eval, 3-shot, chat template, gpt-oss-120b, TP=1, MI300, the
exact repro command above, but with --kv-cache-dtype auto vs fp8_e4m3):
  bf16 KV: flexible 0.8863 / strict 0.3442
  fp8  KV: flexible 0.8848 / strict 0.3374
Matches the pre-A4 single-chunk baseline (0.8802 / 0.3306) within
seed noise, and the server no longer crashes on the 2nd chunk.

Co-authored-by: Cursor <[email protected]>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@kkHuang-amd

Copy link
Copy Markdown
Collaborator Author

@amd-bot ci-status

@amd-bot

amd-bot commented Jun 6, 2026

Copy link
Copy Markdown

@kkHuang-amd

I have enough to compose the report. Summary of findings:

  • AMD CI ran and is GREEN (all stage-a/b/c AMD jobs success; 2 still queued). But the PR's new code paths are gated behind env vars that default OFF and no test sets them → coverage gap.
  • NVIDIA base-b: 18 failing jobs, but 17 are fast-fail cascade victims. The single root cause is the EAGLE spec test crash (speculative code, untouched by PR).
  • XPU: infra (git checkout EACCES).
  • NPU: perf-threshold assertions (Ascend), unrelated to AMD/gpt-oss.

CI Status for PR #27063

PR: [AMD] Optimize gpt-oss-120B performance
Changed files: aiter_utils.py (new, +309), attention/utils.py (+1185, new triton reshape_and_cache_flash), memory_pool.py (+118/-18), gpt_oss.py (+74/-5), aiter_backend.py (+69/-13), layernorm.py (+35), models/utils.py (+25/-7), server_args.py (+17/-1), environ.py (+20), rotary_embedding/base.py (+12/-7), mxfp4.py (+10/-3). No test files added.

Warning

The PR's core optimizations are NOT exercised by any CI test — a green AMD run does not verify them. All new fast paths are gated behind env vars that default OFF, and the PR adds zero tests:

  • SGLANG_AITER_KV_CACHE_LAYOUT=vectorized_5d (SHUFFLE 5D KV pool + pa_decode_gluon) — default "nhd"; no registered test sets vectorized_5d.
  • SGLANG_FUSE_RMSNORM_PAD (fused_add_rmsnorm_pad) — EnvBool(False) by default; no test enables it.
  • --page-size 64 auto-bump only triggers under vectorized_5d (HIP).

Grep of test/ and python/sglang/ on main finds no reference to AITER_KV_CACHE_LAYOUT, vectorized_5d, pa_decode_gluon, or SGLANG_FUSE_RMSNORM_PAD. AMD CI green only proves the default (nhd) path is not regressed. Recommend adding an AMD test that runs gpt-oss-120B with SGLANG_AITER_KV_CACHE_LAYOUT=vectorized_5d (+ SGLANG_FUSE_RMSNORM_PAD=1) before merging, or manually validating the new path on MI325.

AMD: 0 failures | Others: 6 distinct failures (0 related) + 18 cascade/aggregator jobs

AMD CI (run 27051282095) ran the full stage-a/b/c MI325 + MI35x matrix — all completed jobs passed; 2 jobs still queued (stage-c 8-gpu shard 3, mi35x disaggregation). No AMD CI failures, so the AMD CI table is omitted.

Other CI Failures

Job Test File Test Function Error Related? Explanation Log
base-b-test-1-gpu-small (1) test/registered/spec/eagle/test_spec_eagle_page.py (server scheduler crash) scheduler subprocess crashed exit -6 (SIGABRT/CUDA) in eagle_utils.py:64 apply_eagle_prefill_input_rotationeagle_worker.py:1157 forward_draft_extend; server died → Connection refused 🟢 Unlikely Crash is in speculative decoding code, which this PR does not touch. The NVIDIA path is unchanged: all generic-file edits (memory_pool.py, rotary_embedding/base.py, layernorm.py, models/utils.py) are _is_hip-gated or purely additive; on CUDA they take the unchanged legacy branch. main has recent EAGLE scatter/OOB churn (#27360, #27428, #27338), indicating this area is unstable independent of this PR. Log
17× base-b (1-gpu-small/large, 2-gpu, 4-gpu-b200) Fast-fail: skipping — root cause job(s): wait-for-base-b, base-b-test-1-gpu-small (1) 🟢 Unlikely Cascade victims of the single EAGLE crash above; not independent failures.
stage-a-test-1-gpu-xpu N/A N/A EACCES: permission denied, unlink '.../python/sglang.egg-info/PKG-INFO' during actions/checkout cleanup 🟢 Unlikely Pure runner/infra issue (stale file owned by another user on the self-hosted XPU runner). No test ran. Log
stage-b-test-1-npu-a2 (0) test/registered/ascend/basic_function/quant/test_npu_w8a8_quantization.py (perf assert) AssertionError: 590.01 not >= 700 (throughput threshold) 🟢 Unlikely Ascend NPU w8a8 quant perf threshold; different backend/codepath from the AMD/gpt-oss changes. Log
multimodal-gen-test-1-npu-a3 multimodal_gen/test/server/ascend/test_server_1_npu.py test_diffusion_generation[flux_image_t2i_npu] assert 46065 <= 25838 (E2E latency, failed all 7 retries) 🟢 Unlikely Ascend NPU diffusion latency regression; unrelated to AMD LLM-serving changes. Log
multimodal-gen-test-2-npu-a3 multimodal_gen/test/server/ascend/* (NPU diffusion) container exit 1 (same NPU multimodal suite) 🟢 Unlikely Same Ascend NPU multimodal suite as above; not on the PR's code path. Log

Aggregator jobs that merely report children's status (not independent failures): pr-test-finish, wait-for-base-b, finish, pr-test-extra-finish, pr-test-npu-finish.

Details

Do you need to fix something? For the visible failures — no. None of the 6 distinct failures are on a code path this PR changes:

  • The EAGLE crash, XPU infra error, and 3 NPU perf failures are all in code/backends untouched by this PR. The 17 other red base-b jobs are fast-fail cascades from the one EAGLE crash.
  • Every generic file the PR edits is either HIP-gated (memory_pool.py _use_aiter = ... and _is_hip; rotary_embedding/base.py inside if ... and _is_hip; layernorm.py inside if _use_aiter) or purely additive (attention/utils.py adds a new unused-on-CUDA triton kernel). On the NVIDIA base-b path these reduce to the pre-existing behavior, so they cannot explain the EAGLE crash.

But the bigger risk is what CI did not test. This PR's entire value (SHUFFLE 5D KV layout, pa_decode_gluon decode, fused rmsnorm-pad for gpt-oss MXFP4) lives behind default-off env vars with no covering test. The green AMD run validates only the unchanged default path. Re-running AMD CI will not close this gap — a new test (or manual MI325 validation) that actually sets SGLANG_AITER_KV_CACHE_LAYOUT=vectorized_5d is needed before merge to verify the optimization works and is numerically correct.

Generated by amd-bot using Claude Code CLI

@kkHuang-amd

Copy link
Copy Markdown
Collaborator Author

@amd-bot ci-status

@amd-bot

amd-bot commented Jun 6, 2026

Copy link
Copy Markdown

@kkHuang-amd

I have enough to write the report. Key findings: AMD CI ran, but both AMD failures are in code paths untouched by the PR, and the PR's actual target (gpt-oss-120B + the new SHUFFLE-5D/fused-rmsnorm-pad paths) is gated behind default-off env vars and nightly-only AMD tests — so PR CI does not verify the optimization.

CI Status for PR #27063

PR: [AMD] Optimize gpt-oss-120B performance
Changed files (all source, no tests): environ.py (+20), layers/attention/aiter_backend.py (+69/-13), layers/attention/aiter_utils.py (+309), layers/attention/utils.py (+1185), layers/layernorm.py (+35), layers/quantization/mxfp4.py (+10/-3), layers/rotary_embedding/base.py (+12/-7), mem_cache/memory_pool.py (+118/-18), models/gpt_oss.py (+74/-5), models/utils.py (+25/-7), server_args.py (+17/-1)

Caution

The PR's actual optimization is untested on PR CI. All new behavior is gated behind default-OFF env vars (SGLANG_FUSE_RMSNORM_PAD=False, SGLANG_AITER_KV_CACHE_LAYOUT=nhd), and the only tests that exercise the gpt-oss-120B target — test/registered/amd/accuracy/mi30x/test_gpt_oss_eval_amd.py (suite nightly-amd-accuracy-8-gpu-gpt-oss) and test/registered/amd/accuracy/mi35x/test_gpt_oss_eval_mi35x.py (suite nightly-amd-8-gpu-mi35x) — are both nightly=True, so they did not run on this PR. The changed code in gpt_oss.py, mxfp4.py, the layernorm.py fused-pad path, the rotary_embedding/base.py SHUFFLE-5D path, and the memory_pool.py 5D layout have no PR-CI coverage. A green PR CI does not mean the optimization is verified — run the nightly AMD gpt-oss suites (or a manual run with the new env vars enabled) before merging.

AMD: 2 failures (0 likely related) | Others: 2 root-cause failures + cascade (0 related)

AMD CI Failures

Job Test File Test Function Error Related? Explanation Log
stage-c-test-large-8-gpu-amd (mi325, 3) test/registered/ops/test_aiter_allreduce_fusion_amd.py test_fused_ar_rms_residual_accuracy AssertionError: Residual accuracy check failed (fused allreduce+rmsnorm not bit-exact vs unfused) 🟡 Possibly Bit-exact accuracy test of tensor_model_parallel_fused_allreduce_rmsnorm (in distributed/communication_op.py, not modified by this PR). Thematically AMD-fusion-adjacent, but the PR's layernorm.py change touches the RMSNorm.forward_aiter class path, not this allreduce-fusion kernel. Bit-exact tests are sensitive to aiter version/env. Log
stage-b-...-mi35x-disaggregation-amd test/registered/amd/disaggregation/test_mori_transfer_engine_e2e.py test_generate_smoke_hybrid_mamba 500 Internal Server Error; scheduler crashed exit -6 — RegisterRdmaMemoryRegion failed! ... errno:22 (Invalid argument) 🟢 Unlikely Crash in disaggregation/mori/conn.py:475 bootstrap_worker_send_mamba_state (conn.py:1106) → mori batch_write. The mori/disaggregation/mamba-state code path is not touched by this PR. RDMA registration errno:22 on the mi35x fabric runner points to mori-engine/RDMA/hardware. Log

Other CI Failures

Job Test File Test Function Error Related? Explanation Log
base-b-test-1-gpu-small (1) (NVIDIA) test/registered/spec/eagle/test_spec_eagle_page.py TestEagleLlama2Page4Topk8.test_radix_attention scheduler crashed exit -6 (SIGABRT, CUDA coredump) in EAGLE apply_eagle_prefill_input_rotation (eagle_utils.py:64) → forward_draft_extend 🟢 Unlikely Pure EAGLE spec-decode crash (Llama2, page_size=4, topk=8) on NVIDIA. The speculative/eagle_* path is not modified by this PR, and all PR changes are gated behind _is_hip/aiter/gpt-oss. Recent main carries EAGLE topk>1 + page_size>1 instability work (#27360, #27338) — looks pre-existing/flaky on main. Log
stage-a-test-1-gpu-xpu (XPU) N/A N/A EACCES: permission denied, unlink '...sglang.egg-info/PKG-INFO' + Container name cannot be empty 🟢 Unlikely Runner/infrastructure error (filesystem permissions + container setup), not a test failure. Unrelated to PR code. Log

The remaining ~24 failing jobs (base-b-test-1-gpu-small/large shards 2–8, base-b-test-2-gpu-large, base-b-test-4-gpu-b200, and the various *-finish jobs) are fast-fail cascade, not independent failures — their logs show Fast-fail: skipping — root cause job(s): wait-for-base-b, base-b-test-1-gpu-small (1).

Details

  • No failure is in code modified by this PR. The two AMD failures live in distributed/communication_op.py (allreduce fusion) and disaggregation/mori/conn.py (mamba-state RDMA transfer); the NVIDIA failure is in speculative/eagle_*. None of these files are in the diff, and the PR's source changes are all gated behind _is_hip + the default-off env vars above.
  • Biggest risk is the coverage gap, not the red X's. Because the gpt-oss path and the new SHUFFLE-5D / fused-rmsnorm-pad code are nightly/env-gated, the existing red failures tell you nothing about whether the optimization works. Trigger the nightly AMD gpt-oss suites (or a dispatch run with SGLANG_FUSE_RMSNORM_PAD=1 / SGLANG_AITER_KV_CACHE_LAYOUT=vectorized_5d --page-size 64) before merging.
  • Suggested next steps for the failures you can see: re-run the AMD stage-c and stage-b mi35x jobs to confirm the allreduce-bit-exact and mori-RDMA failures reproduce (both smell like pre-existing/infra issues independent of this PR); the NVIDIA EAGLE crash and XPU infra error can be ignored from this PR's perspective.

Generated by amd-bot using Claude Code CLI

@HaiShaw HaiShaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we apply _use_aiter more consistently cross code changes - producer, consumers, utilities?

Comment thread python/sglang/srt/models/utils.py Outdated
Comment thread python/sglang/srt/models/utils.py
Comment thread python/sglang/srt/environ.py Outdated
@HaiShaw

HaiShaw commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

@amd-bot ci-status

@amd-bot

amd-bot commented Jun 7, 2026

Copy link
Copy Markdown

@HaiShaw

CI Status for PR #27063

Merge verdict: No failing job is caused by this PR — every red X is an infra cascade, hang, or unrelated-backend flake. But do not read green-or-unrelated-red as "verified": the PR's entire value (the 5D KV layout + pa_decode_gluon decode + fused rmsnorm-pad) sits behind two default-off env vars that no test sets, so PR CI never executed any of the ~1,800 new lines. Functionally untested by CI.

Caution

This PR's new code is not exercised by any PR-CI test. All new behavior is gated behind SGLANG_AITER_KV_CACHE_LAYOUT=vectorized_5d and SGLANG_AITER_FUSE_RMSNORM_PAD=1 (both default off), and the PR adds zero test files. A grep of test/ for either env var or for vectorized_5d returns nothing. Green CI proves only that the default "nhd" path still works — it does not verify aiter_utils.py (309 lines), the 1,185 lines of new Triton 5D writer/reader kernels in utils.py, the 5D pool in memory_pool.py, or the 5D forward paths in aiter_backend.py. Before merge, run gpt-oss-120b on MI300/MI355 with SGLANG_AITER_KV_CACHE_LAYOUT=vectorized_5d SGLANG_AITER_FUSE_RMSNORM_PAD=1 --page-size 64 (bf16 + fp8 KV), plus a GSM8K accuracy check.

Changed files: aiter_utils.py (+309 new), utils.py (+1185), memory_pool.py (+118/-18), gpt_oss.py (+74/-5), aiter_backend.py (+69/-13), models/utils.py (+25/-7), environ.py, server_args.py, layernorm.py, rotary_embedding/base.py, mxfp4.py. All check runs are completed (0 in-progress, 0 queued).

AMD: 2 failures (0 related) · Others (NVIDIA): 19 failures (0 related, 1 root cause + 18 cascade) · NPU: 3 failures (0 related)

AMD CI Failures

Job Test File Test Function Error Related? Why
stage-b-test-1-gpu-small (9) test/registered/tokenizer/test_skip_tokenizer_init.py setUpClass (hang) ✗ TIMEOUT after 2400s (est. 117s); py-spy could not dump 🟢 Tokenizer-init hang; does not touch the AITER KV pool. Runs on default "nhd" layout — 5D path not taken.
stage-b-test-large-8-gpu-mi35x-disaggregation test/registered/amd/disaggregation/test_mori_transfer_engine_e2e.py test_generate_smoke_hybrid_mamba AssertionError: 500 != 200; ConnectionRefusedError 127.0.0.1:11120 🟢 Mori transfer-engine + hybrid-mamba disagg; unrelated to gpt-oss KV cache.

Other CI Failures (NVIDIA)

Job Test File Test Function Error Related? Why
base-b-test-2-gpu-large (0) test/registered/dp_attn/test_dp_attention.py setUpClass (0/5) CUDA unknown error - CUDA initialization; ValueError: Expected a cuda device, but got: cpu; Rank 0 scheduler died 🟢 Runner infra failure — CUDA failed to init, devices set to zero. Not a code defect. This is the single root cause.
18× base-b-* + base-b-test-4-gpu-b200 (1) N/A N/A Fast-fail: skipping — root cause job(s): base-b-test-2-gpu-large (0) 🟢 Fast-fail cascade from the infra failure above. Collapsed into the one root-cause row.

NPU CI Failures

Job Test File Test Function Error Related? Why
stage-b-test-1-npu-a2 (0) test/registered/ascend/basic_function/quant/test_npu_w8a8_quantization.py test_gsm8k AssertionError: 473.77 not >= 700 (throughput) 🟢 Ascend NPU w8a8 perf threshold; PR has no NPU code.
multimodal-gen-test-1-npu-a3 multimodal_gen/test/server/ascend/test_server_1_npu.py test_diffusion_generation[flux_image_t2i_npu] Diffusion check failed 🟢 NPU diffusion image-gen; unrelated to LLM KV cache.
multimodal-gen-test-2-npu-a3 multimodal_gen/test/server/ascend/test_server_2_npu.py test_diffusion_generation[flux_2/qwen_*_2npu] Diffusion check failed 🟢 Same NPU diffusion suite, 2-NPU.

Details / what to do before merge

The coverage gap is the only real action item — none of the failures are.

  • Verify the new feature manually (mandatory). The 5D layout, pa_decode_gluon decode, and the new Triton kernels are unreachable in CI. Author must run gpt-oss-120b on MI300/MI355 with SGLANG_AITER_KV_CACHE_LAYOUT=vectorized_5d SGLANG_AITER_FUSE_RMSNORM_PAD=1 --page-size 64 for both bf16 and fp8 KV, plus a GSM8K accuracy run, and paste results. The PR body cites such numbers — link or attach them on the PR.
  • Default-path safety looks sound (spot-checked, not a substitute for the above): memory_pool.py defaults kv_cache_layout="nhd" and gates 5D allocation; layernorm.py x_pad_to_multiple defaults 0; rotary_embedding/base.py and models/utils.py branch on ndim==5 (false on NHD); mxfp4.py branches on tensor shape; gpt_oss.py MoE pad is gated by SGLANG_AITER_FUSE_RMSNORM_PAD + mxfp4 + TP==1 + HIP. The "bit-for-bit identical when unset" claim is plausible — but consider adding a CI test that sets the env var on an AMD shard so this doesn't regress silently.
  • Failures need no PR action. The NVIDIA base-b-test-2-gpu-large (0) CUDA-init failure is a runner infra issue (re-run the workflow). The AMD tokenizer-init timeout, AMD disagg 500, and the three NPU failures are unrelated flakes/perf on backends this PR doesn't touch — re-run to clear, or rely on a maintainer override.

Generated by amd-bot using Claude Code CLI

@HaiShaw
HaiShaw merged commit 1c73ff8 into sgl-project:main Jun 8, 2026
69 of 109 checks passed

@HaiShaw HaiShaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment on lines +291 to +294
# is then trimmed back to the unpadded width so postprocess_layer
# can pair it with the (M, hidden_dim_unpadded) residual.
num_tokens = hidden_states.shape[0]
hidden_dim_unpadded = self.experts.hidden_size

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This contradicts the comment: hidden_dim_unpadded should be the model hidden size (config.hidden_size). This regresses GPT-OSS with FlashInfer MXFP4 on SM10X because self.experts.hidden_size is padded to 3072, while config.hidden_size is 2880.

@mmangkad

mmangkad commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

@kkHuang-amd @HaiShaw this broke GPT-OSS serving on NVIDIA SM10X with FlashInfer MXFP4. Instead of reverting, this should be a quick fix; I have it here, please check: #27528

@fzyzcjy

fzyzcjy commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

#25647 (comment)

Oasis-Git added a commit to Oasis-Git/sglang that referenced this pull request Jun 8, 2026
PR sgl-project#27063's hidden_dim_unpadded = self.experts.hidden_size assumption
breaks on non-AMD MXFP4 paths. FusedMoE.__init__ at fused_moe_triton/
layer.py:241-247 rounds hidden_size up to a multiple of 256 when
quant_config.get_name() == "mxfp4" and the runner backend is
flashinfer_mxfp4. For gpt-oss-120b (hidden=2880) on a CUDA flashinfer
MXFP4 path, self.experts.hidden_size becomes 3072 even though the
upstream layernorm does *not* prepad — _resolve_moe_input_pad_multiple
returns 0 unless AMD + AITER + MXFP4 + TP=1.

That mismatch flipped is_prepadded = True incorrectly: the slice
[..., :3072] was a no-op on the 2880-wide input, the experts produced
(M, 2880) output, and the final ans.view(num_tokens, 3072) blew up
with "shape '[4096, 3072]' is invalid for input of size 11796480".

Restoring the pre-PR-sgl-project#27063 form: trust hidden_states.shape, route it
through router/experts unchanged, view back at the same width. Loses
the small router-input slicing optimization that PR sgl-project#27063 wanted on
the AMD MXFP4 prepad path, but FusedMoE.forward already handles
prepadded inputs correctly internally on that path.

The _resolve_moe_input_pad_multiple helper is kept — it still gates
the layernorm-side prepad at line 548.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@yuan-luo

Copy link
Copy Markdown
Collaborator

@kkHuang-amd @HaiShaw this broke GPT-OSS serving on NVIDIA SM10X with FlashInfer MXFP4. Instead of reverting, this should be a quick fix; I have it here, please check: #27528

I also encountered this error in CI of #27449

@geraldstanje1

Copy link
Copy Markdown

hi any plans to make gpt-oss-20b faster on nvidia rtx 6000 pro?

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.

8 participants