Skip to content

[ROCm][Perf] Qwen3.8-2.4T specialized decode on MI355X (--monokernel) - #60833

Draft
mjkvaak-amd wants to merge 1 commit into
vllm-project:mainfrom
mjkvaak-amd:rocm/qwen38-mono-decode
Draft

mjkvaak-amd wants to merge 1 commit into
vllm-project:mainfrom
mjkvaak-amd:rocm/qwen38-mono-decode

Conversation

@mjkvaak-amd

@mjkvaak-amd mjkvaak-amd commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Opt-in --monokernel decode for Qwen3.8-2.4T (amd/Qwen3.8-2.4T-A95B-Quark-MXFP4, Qwen3_5MoeForCausalLM) at TP8 on MI355X. Laid out per the specialized-decode RFC #60904 under vllm/models/qwen3_5/amd/specialized_decode/, as Qwen3.8-Flash-Next's (#63032). A decode step of 1–16 rows runs two persistent FlyDSL launches a layer; everything else runs vLLM's forward. No core changes beyond the --monokernel docstring.

Claims

  • Decode TPOT at 1–16 concurrent requests on MI355X TP8: −33% at 1 (8.36 → 5.61 ms, 8k/1k), −18% at 8, −9% at 16; above 16 rows steps run stock (within 1%).
  • Accuracy within noise of stock: GSM8K 0.976 vs 0.973, GPQA Diamond (thinking, 198 questions) 0.919 vs 0.934 (±0.019 per run). GSM8K under VLLM_GPU_SYNC_CHECK=error (Triton GDN prefill, since AITER's FlyDSL GDN prefill metadata syncs the host on prefills) completes with 0.968: no host syncs in the decode path. RULER NIAH at 64k / 128k: being measured.
  • Follows RFC [RFC]: A standard for Specialized (MonoKernels) Decodes in vllm/models #60904: --monokernel switch, default-deny refusals in three stages (each listing every reason, TP ranks agreeing), one plan() per step, MRV2 only, model-owned code, weights and caches read in place.
  • A rank whose cross-rank wait gives up (10 s) marks itself failed; the TP ranks raise together within 16 steps, so the engine stops rather than hangs (tested with a stalled rank).
  • Kernel numerics tests (TP8, MI355X) and CPU refusal/routing tests, with a Buildkite MI355 (8 GPU) step.

Validation

Serving (MI355X TP8, vllm/vllm-openai-rocm:nightly-5d35ce3934a7804b298559d2d99eec33792e1eb9 with this branch's vllm/models/qwen3_5/amd/ and model hook overlaid; both arms VLLM_ROCM_USE_AITER=1, AITER fused shared experts, FULL CUDA graphs, FP8 KV cache, prefix caching off, --max-num-seqs 256; stock adds nothing, the candidate adds --monokernel; vllm bench serve random dataset, about 4× concurrency prompts (at least 8), as many warmups as concurrency; mean TPOT, ms):

concurrency 8k/1k stock 8k/1k --monokernel 1k/1k stock 1k/1k --monokernel
1 8.36 5.61 (−33%) 8.33 5.59 (−33%)
2 8.82 6.45 (−27%) 8.81 6.46 (−27%)
4 10.47 8.38 (−20%) 10.35 8.15 (−21%)
8 13.78 11.30 (−18%) 12.80 10.39 (−19%)
12 18.51 16.98 (−8%) 16.34 14.83 (−9%)
16 19.23 17.55 (−9%) 16.51 15.00 (−9%)
24 19.25 19.28
32 21.99 22.14
48 26.51 26.71
64 29.55 29.76

Per launch (µs, TP8 on MI355X, each launch in a CUDA graph of 46, weights rotated over enough copies that the 256 MB MALL does not hold a layer's weights across launches, random routing):

rows GDN block layer block (out_proj MXFP4) layer block (o_proj BF16, gated)
1 13.3 32.2 35.9
2 15.3 38.2 42.9
4 19.8 54.8 59.0
8 25.4 74.1 78.8
12 30.6 99.0 104.7
16 34.7 120.8 127.2

From 8 rows up the layer block's routed experts stream about 6 TB/s of MXFP4 weights; the rest is the all-reduce, norm and route hand-offs (about 3 µs each between CTAs).

Tests (MI355X, vllm/vllm-openai-rocm:nightly-5d35ce3934a7804b298559d2d99eec33792e1eb9 with this branch's vllm/models/qwen3_5/amd/ overlaid, VLLM_ROCM_USE_AITER=1 pytest -v -s tests/models/qwen3_5/amd_specialized_decode): all 41 tests pass.

  • test_kernels.py: the GDN block against the Gemma residual add + norm, in_proj, AITER's conv update and fused gated delta rule and RMSNormGated at widths 1, 5, 8, 9, 16 (pad rows included, first layer included), on either MXFP4 GEMM layout; the layer block against o_proj / out_proj, the all-reduce, fused_add_rms_norm, the router's softmax top-10, the MXFP4 experts with the fused shared expert and the MoE all-reduce at widths 1, 3, 8, 9, 15, 16 (each twice: both epoch parities), on all 8 ranks with rank agreement and the health words left clear. A stalled-rank test launches the layer block on rank 0 only: its waits give up after ~10 s, the failed word and rank 0's flag are set, the given-up waits name only mailbox regions of this kernel, and the next launch returns at once.
  • test_runtime.py (host side, no kernels launched; skipped off ROCm like test_kernels.py): off without the flag, every-reason refusal messages, parallel/feature refusals, the PIECEWISE warning, and plan() routing.
  • Serving: --monokernel compiles 64 builds (92 layers, widths 1–16) at startup, captures FULL graphs, and takes specialized steps.

Details

Launches. Per layer: (1) on the GDN layers, the GDN block: residual add + input_layernorm, in_proj_qkvz (MXFP4) and in_proj_ba, the conv update, the gated delta rule on the cached states, RMSNormGated; on the full attention layers vLLM's input norm and attention run instead, up to o_proj's input. (2) The layer block: out_proj (MXFP4) / o_proj (BF16, with the output gate), the TP all-reduce, the residual add + post_attention_layernorm, the router's softmax top-10 and the shared-expert gate, the MXFP4 experts with the fused shared expert (expert 512), the TP all-reduce. Embeddings, final norm, head and sampling stay vLLM's.

x without an LDS tile. At H = 8192 a 16-row bf16 x is 262 KB, more than a CU's LDS, so neither launch holds it. The norm CTAs put their columns' sums of squares and every consumer builds what it reads of x from global memory (in the GDN block at up to 3 rows every CTA computes the rows' rstd itself, cheaper than the hand-off); from 4 (GDN) / 5 (layer) rows, the norm CTAs instead quantize their columns once into a shared MXFP4 image that in_proj_qkvz and the experts copy, as every CTA rebuilding all of x would move 256 × 2 [S, H] bf16 through the L2s each launch.

MoE. At decode widths stock runs AITER's AITER_MXFP4_MXFP4 path with x and silu(g)·u through dynamic MXFP4 (1×32 blocks, RCEIL scales); the layer block does the same, the intermediate quantized where it is produced, and feeds the codes to the scaled fp4 MFMA. From 4 rows the fused shared expert's gate/up runs before the route is known; from 8 rows down starts on the union slots whose gate/up finished in the full rounds, under the last, partial round.

MXFP4 layouts. The GDN projections are read in the layout of whichever AITER FP4 GEMM vLLM picked for the shape (runtime/guards.py, mxfp4_shuffled), quantizing their inputs with the matching rounding (Triton's vs ASM's RCEIL). Both layouts are covered by test_kernels.py. No weights are duplicated.

Refusals. Config (model __init__): an architecture other than Qwen3_5MoeForCausalLM, TP≠8, PP, DP, EP, EPLB, DCP, PCP, DBO, sequence-parallel MoE, LoRA, KV connectors, routed-expert capture, speculative decoding, sleep mode, CPU weight offload, weight updates, GDN prefix caching, other GDN cache dtypes, an unfused shared expert, MRV1, STOCK_TORCH_COMPILE, other shapes, unnormalized top-k weights, layer scale, non-gfx950 or partitioned GPUs. Model (after load): every layer's modules, quant methods, weight layouts and shapes. Caches (every bind): GDN state dtypes, shapes and slot layout. Without --monokernel none of this runs.

Steps. plan() returns a Step for a pure decode of 1–16 rows with int32 GDN state indices, else None (stock forward). Every width is compiled when the executor is built (FlyDSL COMPILE_ONLY), so no JIT on the first step of a width. PIECEWISE steps run stock.

Shared helpers. The launch ABI and health words, the tagged mailbox with its bounded waits, the build keys, the GEMV stage, peer memory and FailStop are adapted from the Kimi-K3 decode (#60683, by @hyukjlee, after ROCm/ATOM), as in #63032, and trimmed to what these kernels use. The package imports nothing from other models; the kernel cache key covers every source under its mono/, and the Buildkite step triggers only on this model's directories.

Limitations / follow-ups.

  • Speculative decoding is refused; MTP verify steps are a follow-up.
  • At 4–8 rows the CTAs that run the routed gate/up's last, partial round still own two down row groups each and set the launch's end; moving row groups off them is a follow-up (~3 µs at those widths).
  • Failure: every cross-rank wait is bounded (10 s). A rank that gives up records the wait, flags the output rank and sets its sticky failed word; FailStop reads that word after each step without blocking and the TP ranks agree every 16 steps, then raise together. There is no fallback to the stock forward after a failure.
  • Startup does not check the FlyDSL version.

Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

@mergify mergify Bot added ci/build new-model Requests to new models qwen Related to Qwen models kimi k3 rocm Related to AMD ROCm labels Oct 9, 2026
@mjkvaak-amd mjkvaak-amd changed the title Rocm/qwen38 mono decode [ROCm][Perf][Qwen3.8] Mono decode layer for Qwen3.8 on MI355X Oct 9, 2026
@mergify

mergify Bot commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @mjkvaak-amd.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Oct 10, 2026
omirosh added a commit to omirosh/sglang that referenced this pull request Oct 12, 2026
Copy gdn.py, layer.py and layout.py from vllm-project/vllm#60833 at
6a1c13b2fc. Keep the bf16 SSM state in K1.

The glue passes the per-width speed flags (tuned) to both kernels and
sizes the scratch with them. The weights stay as SGLang loads them:
bf16 dense layers, an unfused shared expert, and the output gate in the
attention layer. So fse, oq, qq and og stay off.

The K1 pad-row test now runs each case with and without the speed
flags and checks that the outputs are bitwise equal.

Co-authored-by: Cursor <cursoragent@cursor.com>
@maeehart

Copy link
Copy Markdown
Contributor

@mjkvaak-amd, I think that the GDN projections in this PR should be MXFP6 instead of MXFP4. With the online MXFP4 targets for in_proj_qkv, in_proj_z and out_proj, the gsm8k score stays within noise, but the answers get about 30% longer and about three times as many of them run into the token cap before the answer line. MXFP6 E2M3 brings the answers back to the bf16 length, and K2 reads it as fast as MXFP4.

Accuracy. lm_eval gsm8k, 5-shot completions, max_gen_toks=2048, 8 concurrent, all 1,319 questions. amd/Qwen3.8-2.4T-A95B-Quark-MXFP4 TP8 on MI355X, vllm/vllm-openai-rocm:nightly-8cbd5d03006c33185f402249ff2b448efd594986, the shared expert in online MXFP4 in every row. The standard error of the score is about 0.006, so the answer length shows the difference more clearly than the score.

GDN projections Mono decode gsm8k flexible Mean answer length (characters) Answers at the 2048-token cap
bf16 off 0.9492 433 8
online MXFP8 off 0.9560 458 9
online MXFP4 off 0.9424 564 27
online MXFP4 on, 6a1c13b2fc 0.9477 560 21
MXFP6 E2M3 on, K1 and K2 read MXFP6 0.9500 452 9

The MXFP6 row also has the attention qkv_proj and o_proj in MXFP6, so it quantizes more weights than the MXFP4 rows and still keeps the bf16 length.

How to make the MXFP6 weights.

  1. Serve the GDN projections in bf16, so the targets keep only the shared expert: --quantization-config.targets '{"*shared_expert*": "mxfp4"}'. in_proj_ba stays bf16 as before.
  2. After loading, round every bf16 weight of in_proj_qkvz and out_proj (and the attention qkv_proj and o_proj) to its MXFP6 E2M3 value in place, one block of 32 along K at a time. The block scale follows AITER's dynamic_mxfp4_quant with the E2M3 maximum of 7.5. The block maximum is rounded to 3 mantissa bits, and the scale is 2 to the power of its exponent minus 2, so no value in the block is above 7.5 times the scale. Each value then rounds to the nearest E2M3 value, ties to even. An E2M3 value times a power of two fits in bf16 exactly, so prefill, the steps wider than 8 rows and K1 and K2 all multiply the same values.
  3. Pack a copy for the kernels with the OCP E2M3 codes (sign at bit 5, then 2 exponent and 3 mantissa bits) and one e8m0 byte a block. 32 codes are 24 bytes, with code i at bits 6i to 6i+5 in little-endian order, which is the order in which v_cvt_scalef32_pk32_bf16_fp6 reads them. One such instruction turns a lane's 32 codes times 2^(e8m0 - 127) into 32 bf16 values exactly, and they go into the bf16 MFMA with the bf16 activations (W6A16). We store a tile of 16 rows and 128 K as 64 lanes of 24 bytes, with the first 16 bytes of every lane together and the last 8 together, so a lane loads one dwordx4 and one dwordx2. The converted dwords then come out in the order in which K2 reads an MXFP4 tile, so the MFMA loop does not change.

The rounding of step 2:

def round_e2m3(x: torch.Tensor) -> torch.Tensor:
    v = x.float().reshape(-1, x.shape[-1] // 32, 32)
    amax = v.abs().amax(dim=-1, keepdim=True)
    # Adding 2**19 to the fp32 bits of the block maximum rounds its mantissa
    # at the third bit, and a carry moves into the exponent. The mask keeps
    # only the exponent field.
    exp = (((amax.view(torch.int32) + 0x80000) & 0x7F800000) >> 23) - 127 - 2
    # An all-zero block keeps a normal scale, because a scale of zero would
    # turn 0 / 0 into NaN.
    scale = torch.exp2(exp.clamp(-126, 127).float())
    a = v.abs() / scale
    q = torch.where(
        a < 2.0,
        torch.round(a * 8.0) / 8.0,
        torch.where(a < 4.0, torch.round(a * 4.0) / 4.0, torch.round(a * 2.0) / 2.0),
    )
    return (torch.sign(v) * q.clamp(max=7.5) * scale).reshape(x.shape).to(x.dtype)

Cost. In our build on top of 6a1c13b2fc, K2 with an MXFP6 out_proj or o_proj takes 34.7 µs a launch at 2 rows, as fast as with MXFP4. K1 with an MXFP6 in_proj_qkvz takes 14.41 µs at 2 rows, against 13.37 µs with MXFP4 and 28.54 µs with bf16, so about 0.07 ms a decode step over the 69 GDN layers. The bf16 weights stay for prefill and for the steps wider than 8 rows, and the packed copies of in_proj_qkvz, out_proj and o_proj add 3.24 GB a GPU. At --gpu-memory-utilization 0.9 the KV cache goes from 81.44 GiB (7.33M tokens) with the MXFP4 targets to 72.57 GiB (6.53M tokens). A W6A16 GEMM for the stock path would let the bf16 weights go.

@mjkvaak-amd
mjkvaak-amd force-pushed the rocm/qwen38-mono-decode branch from 3346929 to 1301a7c Compare October 14, 2026 08:14
mjkvaak-amd added a commit to mjkvaak-amd/vllm-project that referenced this pull request Oct 14, 2026
… MTP verify steps

Flash-Next's kernels and host code move onto the Qwen mono base in
vllm-project#60833: the fp4 GEMV, x quantization, o_proj, route and expert stages
from qwen3_5/amd/mono/moe.py (fp4.py is gone), the GDN tasks and the
in_proj_ba stage from gdn.py, the HC down GEMV's split-K reduction from
moe.ksplit_rows, and the refusals, step gate, buffers and weight checks
from mono_base.QwenMonoDecode. The layout and host tests are merged into
tests/models/qwen3_5 and run for both models; the Qwen3.8 mono CI step
runs Flash-Next's kernel test too (40 minutes).

The kernels are unchanged: the ISA of every build variant is
byte-identical to before, except K2 at 4-8 rows, where the shared
expert's prefetch address order reschedules instructions (same
instruction count, VGPRs and no spills).

MTP verify steps: K1's recurrence keeps no per-draft-token state to roll
back rejected drafts, so on a verify step every layer runs K1's HC front
alone, then vLLM's GDN core on the spec-decode metadata (the base's
path) or the QSA core, then K2. The multi-stream state before the final
mixer goes to the drafter's hidden-state buffer, as the stock forward
leaves it.

Signed-off-by: Mikko Tukiainen <Mikko.Tukiainen@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
mjkvaak-amd added a commit to mjkvaak-amd/vllm-project that referenced this pull request Oct 14, 2026
A GDN layer (K1, K2 with the MXFP4 out_proj) and a QSA layer (the HC front,
K2 with the gated o_proj) at six consecutive decode steps of widths 1-8,
captured in one HIP graph and replayed 300 times at TP2: bit-identical to
the eager run every time, on both ranks. vllm-project#60833's race stress for these
kernels.

Signed-off-by: Mikko Tukiainen <Mikko.Tukiainen@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@mergify mergify Bot removed the needs-rebase label Oct 14, 2026
maeehart added a commit to maeehart/vllm that referenced this pull request Oct 14, 2026
At 2 and 3 rows, K2 gives every expert pick its own slot (`up`) instead of
building the union of the picked experts. An expert that two tokens pick
gets two slots, and each token's contributions are summed in top-k order,
so the outputs are those of the union in expert order. At 2 rows, the
fused shared expert's ug also starts before the route lands (`ps`).

K2 also issues some loads in a better order. The comment next to each
change explains why.

- A router CTA issues its router weights after run_oproj. Before, they
  delayed its o_proj, which every norm task waits for.
- A CTA with at most two ug tasks issues both tasks' loads before the
  first task's MFMAs, with no loop that carries the tiles.
- When one batch covers the union, the down rounds run without a
  prefetch, and the second row group's tiles go out behind the INTER
  polls. A prefetch past the last batch reads no memory.

This is the first commit of vllm-project#61271 on the fixed
schedule of vllm-project#60833. The W4A16 option for MXFP4
projections (`wa`) is left out, because the MXFP6 projections of the
next commit replace it and W4A16 does not match the stock decode.

Signed-off-by: Markus Hartikainen <markus.hartikainen@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
vorapolsiloai pushed a commit to vorapolsiloai/sglang that referenced this pull request Oct 15, 2026
…layer on MI355X

Port the vllm-project/vllm#60833 kernels. A decode step of at most 8 rows
runs each layer as K1 (GDN front) and K2 (o_proj/out_proj, in-kernel TP
all-reduces, norm, router, MXFP4 experts, shared expert). The full-attention
layers keep SGLang's fused HIP front through a new attention_core().

Builds on the Kimi-K3 mono framework (k3_mono_flydsl common/ and stages/).
Off by default: SGLANG_QWEN3_5_MONO_DECODE=1.
okorzh-amd added a commit to okorzh-amd/Hyperloom that referenced this pull request Oct 19, 2026
…uccessor

Step 1 took any PR a title or comment called this one's "replacement,
follow-up or reland" as a successor, and the superseded row fired for
any unmerged PR with one, with no Step 9 block behind it. A follow-up
builds on a PR rather than ending it: on vllm-project/vllm#60833, open,
xrefs.tsv lists #61271 "Mono decode follow-up", a draft stacked on
#60833's commits, and the skill read literally answered
"NOT-APPLICABLE -- superseded by #61271" for a live PR.

A successor is now a PR that either PR's title, body or comments call
this one's replacement or reland, the test check-prior-art's SUPERSEDED
class uses; a stacked or follow-up PR is not one. Each successor gets a
successor:<ref> block in Step 9, attacked in both PRs' bodies, and the
superseded row, like the revert row, needs one that survived. The file
stays at its 280-line budget.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mergify

mergify Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @mjkvaak-amd.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Oct 20, 2026
okorzh-amd added a commit to okorzh-amd/Hyperloom that referenced this pull request Oct 20, 2026
…uccessor

Step 1 took any PR a title or comment called this one's "replacement,
follow-up or reland" as a successor, and the superseded row fired for
any unmerged PR with one, with no Step 9 block behind it. A follow-up
builds on a PR rather than ending it: on vllm-project/vllm#60833, open,
xrefs.tsv lists #61271 "Mono decode follow-up", a draft stacked on
#60833's commits, and the skill read literally answered
"NOT-APPLICABLE -- superseded by #61271" for a live PR.

A successor is now a PR that either PR's title, body or comments call
this one's replacement or reland, the test check-prior-art's SUPERSEDED
class uses; a stacked or follow-up PR is not one. Each successor gets a
successor:<ref> block in Step 9, attacked in both PRs' bodies, and the
superseded row, like the revert row, needs one that survived. The file
stays at its 280-line budget.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mjkvaak-amd

Copy link
Copy Markdown
Contributor Author

@mjkvaak-amd, I think that the GDN projections in this PR should be MXFP6 instead of MXFP4. With the online MXFP4 targets for in_proj_qkv, in_proj_z and out_proj, the gsm8k score stays within noise, but the answers get about 30% longer and about three times as many of them run into the token cap before the answer line. MXFP6 E2M3 brings the answers back to the bf16 length, and K2 reads it as fast as MXFP4.

Accuracy. lm_eval gsm8k, 5-shot completions, max_gen_toks=2048, 8 concurrent, all 1,319 questions. amd/Qwen3.8-2.4T-A95B-Quark-MXFP4 TP8 on MI355X, vllm/vllm-openai-rocm:nightly-8cbd5d03006c33185f402249ff2b448efd594986, the shared expert in online MXFP4 in every row. The standard error of the score is about 0.006, so the answer length shows the difference more clearly than the score.

GDN projections Mono decode gsm8k flexible Mean answer length (characters) Answers at the 2048-token cap
bf16 off 0.9492 433 8
online MXFP8 off 0.9560 458 9
online MXFP4 off 0.9424 564 27
online MXFP4 on, 6a1c13b2fc 0.9477 560 21
MXFP6 E2M3 on, K1 and K2 read MXFP6 0.9500 452 9
The MXFP6 row also has the attention qkv_proj and o_proj in MXFP6, so it quantizes more weights than the MXFP4 rows and still keeps the bf16 length.

How to make the MXFP6 weights.

  1. Serve the GDN projections in bf16, so the targets keep only the shared expert: --quantization-config.targets '{"*shared_expert*": "mxfp4"}'. in_proj_ba stays bf16 as before.
  2. After loading, round every bf16 weight of in_proj_qkvz and out_proj (and the attention qkv_proj and o_proj) to its MXFP6 E2M3 value in place, one block of 32 along K at a time. The block scale follows AITER's dynamic_mxfp4_quant with the E2M3 maximum of 7.5. The block maximum is rounded to 3 mantissa bits, and the scale is 2 to the power of its exponent minus 2, so no value in the block is above 7.5 times the scale. Each value then rounds to the nearest E2M3 value, ties to even. An E2M3 value times a power of two fits in bf16 exactly, so prefill, the steps wider than 8 rows and K1 and K2 all multiply the same values.
  3. Pack a copy for the kernels with the OCP E2M3 codes (sign at bit 5, then 2 exponent and 3 mantissa bits) and one e8m0 byte a block. 32 codes are 24 bytes, with code i at bits 6i to 6i+5 in little-endian order, which is the order in which v_cvt_scalef32_pk32_bf16_fp6 reads them. One such instruction turns a lane's 32 codes times 2^(e8m0 - 127) into 32 bf16 values exactly, and they go into the bf16 MFMA with the bf16 activations (W6A16). We store a tile of 16 rows and 128 K as 64 lanes of 24 bytes, with the first 16 bytes of every lane together and the last 8 together, so a lane loads one dwordx4 and one dwordx2. The converted dwords then come out in the order in which K2 reads an MXFP4 tile, so the MFMA loop does not change.

The rounding of step 2:

def round_e2m3(x: torch.Tensor) -> torch.Tensor:
    v = x.float().reshape(-1, x.shape[-1] // 32, 32)
    amax = v.abs().amax(dim=-1, keepdim=True)
    # Adding 2**19 to the fp32 bits of the block maximum rounds its mantissa
    # at the third bit, and a carry moves into the exponent. The mask keeps
    # only the exponent field.
    exp = (((amax.view(torch.int32) + 0x80000) & 0x7F800000) >> 23) - 127 - 2
    # An all-zero block keeps a normal scale, because a scale of zero would
    # turn 0 / 0 into NaN.
    scale = torch.exp2(exp.clamp(-126, 127).float())
    a = v.abs() / scale
    q = torch.where(
        a < 2.0,
        torch.round(a * 8.0) / 8.0,
        torch.where(a < 4.0, torch.round(a * 4.0) / 4.0, torch.round(a * 2.0) / 2.0),
    )
    return (torch.sign(v) * q.clamp(max=7.5) * scale).reshape(x.shape).to(x.dtype)

Cost. In our build on top of 6a1c13b2fc, K2 with an MXFP6 out_proj or o_proj takes 34.7 µs a launch at 2 rows, as fast as with MXFP4. K1 with an MXFP6 in_proj_qkvz takes 14.41 µs at 2 rows, against 13.37 µs with MXFP4 and 28.54 µs with bf16, so about 0.07 ms a decode step over the 69 GDN layers. The bf16 weights stay for prefill and for the steps wider than 8 rows, and the packed copies of in_proj_qkvz, out_proj and o_proj add 3.24 GB a GPU. At --gpu-memory-utilization 0.9 the KV cache goes from 81.44 GiB (7.33M tokens) with the MXFP4 targets to 72.57 GiB (6.53M tokens). A W6A16 GEMM for the stock path would let the bf16 weights go.

Thanks for pointing these up! 💯 The issues with the long MXFP4 thinking chains have been fixed in #62750. Alone the Hadamard rotations from #62750 hurt the throughput, but there's another stacked PR #63272 that returns the performance by fusing the rotations with preceding quant kernels of the MXFP4 GEMM. Please have a look and let me know what you think.

@mjkvaak-amd
mjkvaak-amd force-pushed the rocm/qwen38-mono-decode branch from 1301a7c to 075a85f Compare October 28, 2026 20:56
@mjkvaak-amd mjkvaak-amd changed the title [ROCm][Perf][Qwen3.8] Mono decode layer for Qwen3.8 on MI355X [ROCm][Perf] Qwen3.8-2.4T specialized decode on MI355X (--monokernel) Oct 28, 2026
@mergify mergify Bot removed the needs-rebase label Oct 28, 2026
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Mikko Tukiainen <Mikko.Tukiainen@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@mjkvaak-amd
mjkvaak-amd force-pushed the rocm/qwen38-mono-decode branch from 075a85f to 63036c1 Compare October 28, 2026 21:25
@mergify

mergify Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @mjkvaak-amd.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Oct 29, 2026
HaiShaw pushed a commit to sgl-project/sgl-amd-mono that referenced this pull request Nov 12, 2026
…ne K2 launch (#83)

Part 1 of 5 of the split of #16.

## Motivation

Qwen3.8-2.4T-A95B (`amd/Qwen3.8-2.4T-A95B-Quark-MXFP4`) has 92 decoder
layers. Each layer has attention or Gated DeltaNet (GDN, a linear
attention), then a sparse MoE. The MoE has a router that picks 10 of 512
MXFP4 experts, and one bf16 shared expert. In a decode step of 1 to 8
rows, SGLang runs the layer tail after the attention as many small
launches and two TP all-reduces. At these batch sizes, the launches and
the waits take most of the step time.

This PR runs the tail of each layer as one persistent launch, K2, on
each of the 8 ranks. The kernels are the K2 kernels of
sgl-project/sglang#43516 (at `51b0c592b2`), which adapts them from
vllm-project/vllm#60833. This PR moves them into the plugin (RFC
sgl-project/sglang#43949), with two differences:

- Each wait on another workgroup or GPU has a deadline. A failed wait
stops the launch on every rank, and the server stops without tokens from
that step.
- This PR leaves out the fused shared expert (`fse`) and MXFP4 out_proj
(`oq`) variants. The spec refuses those configurations at startup.

Since sgl-project/sglang#43839, SGLang quantizes the GDN projections to
MXFP4 by default. K2 reads out_proj in bf16, so start the server with
`SGLANG_ROCM_GDN_ONLINE_MXFP4=0`.

Later PRs add the MTP verify steps, K1 (the attention and GDN front of a
layer), the fused shared expert and the MXFP4 GDN projections.

## Modifications

| Area | Change |
| --- | --- |
| `qwen38_a95b_mxfp4/spec.py` | Adds the spec, its forward and its
refusals. The spec covers the decode steps of 1 to 8 rows at TP8 on
gfx950. SGLang runs the input norm and the attention or GDN, up to the
projection input. Then one K2 launch runs the rest of the layer:
out_proj (o_proj on the attention layers) and its all-reduce, the
post-attention norm, the router, the shared and routed experts, and the
MoE all-reduce. Other steps run SGLang's path. |
| `qwen38_a95b_mxfp4/gfx950/` | Adds the K2 kernels (`layer.py`), the
Kimi-K3 helpers that they use (`_k3.py`, by way of
sgl-project/sglang#43481) and the MoE weight layout (`layout.py`). Each
of the 8 build keys (widths 1 to 8) builds one kernel. A server builds
only the widths of its CUDA graphs. With the test flags, these are 1, 2,
4 and 8. |
| `qwen38_a95b_mxfp4/check.py` | Adds check mode
(`SGLANG_AMD_MONO_CHECK=1`). Check mode runs SGLang's own layer code
next to each K2 launch and compares the outputs. The spec's `NUMERICS`
threshold is `k2.out.cosine` ≥ 0.99. |
| `sgl_amd_mono/__init__.py` | Registers the spec in `SPECS`. |
| Tests | Adds the model's tests in `test/unit`, `test/kernels` and
`test/e2e` (results below). |
| `README.md`, `NOTICE` | `README.md` adds the model row, the design
text, the `SGLANG_ROCM_GDN_ONLINE_MXFP4=0` requirement and a usage
command. `NOTICE` names the origins of the adapted code. |

**Refusals.** The server stops at startup, with the reason in the log,
for each of these configurations:

- the fused shared expert (`--enforce-shared-experts-fusion`);
- the MXFP4 GDN projections (`SGLANG_ROCM_GDN_ONLINE_MXFP4` not `0`);
- speculative decoding, such as MTP (the spec does not accept the
feature, so SGLang's monokernel guard refuses it);
- a model config that does not match Qwen3.8 in `HF_CONFIG` (the hidden
size, the GDN heads, the experts, the top-k, the expert sizes or SiLU);
- a config without `norm_topk_prob`;
- more layers than the mailbox tags hold;
- a layer whose norm, router, shared expert, o_proj or out_proj is not
bf16;
- routed experts that are not MXFP4 in AITER's shuffled layout;
- an image without FlyDSL.

The kernels read SGLang's weights in place and make no weight copies.
Each rank allocates one peer buffer, and one scratch buffer for each
width.

K2 does not replace SGLang's attention, GDN, embedding, final norm or
logits processor. This PR does not change `common/` or the other models.

## Accuracy Tests

All the results below use this setup:

| Item | Value |
| --- | --- |
| GPUs | 8× MI355X (gfx950), one node |
| TP | 8 |
| Image |
`rocm/sgl-dev:v0.5.22-rocm10-mi35x-20261028@sha256:b51edf074b567263afb19be9108756645ee00d0604374b6e4c5006caccaa17fe`
|
| SGLang | `5bb7ecc2c247db05e3de41d21f36aed7aa1a2dfb`, which has
sgl-project/sglang#43839, with no unmerged SGLang PR |
| AITER, FlyDSL | `285579b6211b0da30f35b91a890c29c823597cd1`, 0.4.0 |
| Model revision | `13dc9676cdd9e2374c3efb50389d3392b975acae` |
| Server flags | `test/e2e/qwen38_a95b_mxfp4_server.py`. The
monokernel-on servers add `--monokernel-backend amd`. |
| Branch commit | `b0c3818` (tag `q38v3-tested/1-k2`). The head
`3daea98` is rebased onto `main` at `b10f3b7`. Its model code and tests
differ from that commit only in the text of a comment, a docstring and a
skip message. The test runs used the old base `0aa9749`, before #76
changed `PeerBuffer.close()`. |

The port test (`check_port.py`) compares K2 with the original kernels of
sgl-project/sglang#43516 at `51b0c592b2`. That PR is not merged, so the
test run mounts its two kernel folders (`k3_mono_flydsl` and
`qwen3_5_mono_flydsl`) read-only into SGLang's tree in the container. No
SGLang code imports them. Only the port test does.

### GSM8K, monokernels on and off

The test uses lm_eval 0.4.13 with `local-chat-completions` and the chat
template. It runs all 1,319 questions, 5-shot, at temperature 0, with
`max_gen_toks` 32768. It sends 8 concurrent requests, so the decode
steps take the K2 path. Each arm starts a fresh server. Each question
has the same `prompt_hash` in both arms.

| Filter | Monokernels off | Monokernels on | Right only off | Right
only on | McNemar's p (exact) |
| --- | --- | --- | --- | --- | --- |
| flexible-extract | 1291 (0.9788) | 1289 (0.9773) | 10 | 8 | 0.815 |
| strict-match | 1141 (0.8650) | 1143 (0.8666) | 103 | 105 | 0.945 |

The arms differ by 2 questions on each filter, and McNemar's test finds
no accuracy difference. Strict-match scores lower in both arms because
of the answer format. In the off arm, all 151 questions that pass
flexible-extract and fail strict-match have no `#### N` line. Their
answers end in other forms, such as `**Answer: 45**`.

### GPQA Diamond and SWE-bench Pro, monokernels on and off

These runs use `3daea98` in one image, with the flags of the GSM8K and
speed servers plus `--default-chat-template-kwargs '{"reasoning_effort":
"xhigh"}'`. Claude Code sends the effort `high`, which the chat template
refuses. With this flag, the template renders its default effort, xhigh,
for every request. The SWE-bench servers leave out
`--disable-radix-cache`, because the turns of a task share long
prefixes. Each arm starts a fresh server on its own node with 8× MI355X.

#### GPQA Diamond

The test uses sgl-eval 0.1.3 on all 198 questions, with 4 samples of
each question. It samples at temperature 1.0, top-p 0.95, top-k 20 and
min-p 0, with 131072 max tokens, and it sends 8 concurrent requests.

| | Monokernels off | Monokernels on | Model card |
| --- | --- | --- | --- |
| avg@4, with the 95% interval over the questions | 93.43 ± 3.20 | 92.68
± 3.24 | 92.6 |

Over the 198 paired questions, on minus off is -0.76 points, with a 95%
interval of ±1.10. Five questions score higher with the monokernels on,
and nine with them off. The sign test gives p 0.424 and finds no
difference. The model card value is the score in the Qwen3.8-Max column
of
[Qwen/Qwen3.8-2.4T-A95B](https://huggingface.co/Qwen/Qwen3.8-2.4T-A95B),
and it is inside the 95% interval of both arms. With the monokernels
off, 3 of the 792 samples stop at the 131072-token cap, and 5 do with
them on.

#### SWE-bench Pro

The test runs 50 tasks of [SWE-bench Pro
V2](https://github.com/scaleapi/SWE-bench_Pro-os/tree/66f92766bba642462d4bbe5479e83f91f9211862/v2),
Scale's validated set of 642 tasks. The 50 tasks come from 11 repos.
`random.Random(20261105).sample(sorted(ids), 50)` picks them from the
642 task names in `v2/tasks`. Harbor 0.23.0 runs Claude Code with the
`locked_claude_code` agent of V2, 8 tasks at a time. Each task gets 50
minutes, network access only to the model server, and no WebFetch or
WebSearch. Each turn can have 32768 output tokens and 31999 thinking
tokens. The server samples with the defaults of the checkpoint
(temperature 1.0, top-p 0.95, top-k 20). Each decode batch in the server
logs has 1 to 8 running requests, so the decode steps take the K2 path.
After the run, `patch_replay` grades each patch again on a fresh task
image, and the table gives that grade.

| | Monokernels off | Monokernels on |
| --- | --- | --- |
| Resolved | 46 of 50 (92%) | 48 of 50 (96%) |
| Agent timeouts | 3 | 0 |
| Median agent time per task | 18.2 min | 15.1 min |

Over the 50 paired tasks, 3 pass only with the monokernels on and 1 only
with them off. McNemar's exact test gives p 0.625 and finds no
difference. One of the 3 is an infrastructure failure in the off arm.
There, `docker compose` failed in the environment setup, and the agent
did not start. In the task that passes only with the monokernels off,
the `claude` process of the on arm was killed ("Killed") and left no
patch.

Fifty tasks find only a large change. The 95% interval of the resolved
rate is ±7.6 points with the monokernels off and ±5.5 points with them
on. We did not run the other 592 tasks of V2.

### Kernel checks (`test/kernels`, 8 GPUs, TP8)

| Check | Result |
| --- | --- |
| The MoE weight layout (the w13 and w2 tiles and the scales) | Equal to
SGLang's layout |
| Compile every build key (widths 1 to 8) on every rank | All 8 keys
compile |
| Batch invariance on random TP8 shards. A row's `out` and `res_out` do
not change with the other rows or the width. | 57 cases pass, and the
ranks agree |
| Graph replay of widths 8, 1, 3, 8, 2, 5 in one graph on one peer
buffer | 200 replays are bit for bit equal to eager mode, and the ranks
agree |
| Fault: rank 3 stalls at its first wait | Rank 3 sets its deadline
flag, the flag reaches every rank, and the launch returns in 0.52 s |
| Port test: `out` and `res_out` against the sgl-project/sglang#43516
kernels, at widths 1 to 8 on every rank | Bit for bit equal |

The kernel tests do not compare K2 with SGLang's ops. Check mode in the
e2e test does that, with the real weights.

### Three runs of all tests

| Run | unit | kernels | e2e | GSM8K (200, the e2e test) | Highest mean
KL (2, 3, 7, 8 rows) | Rows that check mode skipped | Lowest
`k2.out.cosine` |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | 12 passed | 5 passed | 4 passed | 0.970 | 0.0118 | 1 of 3956 |
0.999897 |
| 2 | 12 passed | 5 passed | 4 passed | 0.975 | 0.0179 | 0 of 3956 |
0.998428 |
| 3 | 12 passed | 5 passed | 4 passed | 0.975 | 0.0107 | 0 of 3956 |
0.999738 |

Each run gave the kernel check results above. `k2.res_out.cosine` is 1
in all 4416 checks (3 runs of 1472).

The e2e tests are:

- GSM8K: 200 questions at 12 concurrent requests, with thinking, at
temperature 1.0. The batch grows past 8 rows as it fills, and drops
below 8 rows as it drains.
- Prefill match: at 2, 3, 7 and 8 rows, the KL divergence of the decode
logprobs against prefill is below 0.06.
- Check mode: each K2 layer output is within `NUMERICS`.
- Fault injection: after 100 steps, rank 1 stalls at its first wait. At
the deadline, every rank stops, and the server returns no tokens from
that step.

The check-mode test runs without CUDA graphs, at 1, 3 and 8 rows, with
1472 checks in each run. In rare rows, K2 and SGLang pick a different
10th expert, because two experts score within one bf16 step. Check mode
skips those rows for the layer output and counts them. The test fails if
it skips more than 1% of the rows.

The tests do not cover TP4 or MI350X, because the spec serves only TP8
on MI355X.

## Speed Tests and Profiling

The test uses `sglang.bench_serve` with random prompts of 8192 input and
1024 output tokens (range ratio 0.8). Each arm starts a fresh server, in
the order off, on, on, off. The GSM8K and speed servers use the flags
and environment of `test/e2e/qwen38_a95b_mxfp4_server.py`, plus
`--tool-call-parser qwen3_coder` and `--watchdog-timeout 12000`. On each
rank, the monokernel-on servers log "Qwen3.8 monokernel: 4 K2 launches
(widths [1, 2, 4, 8])".

| Concurrency | Off median TPOT (ms), rounds 1 and 2 | On median TPOT
(ms), rounds 1 and 2 | Per-user decode tok/s, off and on | Speedup |
Spread between rounds |
| --- | --- | --- | --- | --- | --- |
| 1 | 11.06, 11.04 | 7.46, 7.46 | 90.5, 134.0 | 1.48× | 0.2% |
| 2 | 11.61, 11.58 | 8.69, 8.69 | 86.2, 115.1 | 1.33× | 0.3% |
| 4 | 13.79, 13.76 | 10.41, 10.41 | 72.6, 96.1 | 1.32× | 0.2% |
| 8 | 17.38, 17.34 | 14.91, 14.89 | 57.6, 67.1 | 1.17× | 0.2% |
| 16 (past the largest width) | 23.39, 23.35 | 23.34, 23.39 | 42.8, 42.8
| 1.00× | 0.2% |

- The speedup uses the mean of the two rounds.
- The spread is the largest difference between the two rounds of one
arm.
- At concurrency 16, most steps have more than 8 rows and run SGLang's
path, so the speed does not change.
- No concurrency is slower with the monokernels on.

**Registers.** The table comes from a compile-only build of the 8 K2
build keys at `7c038cb` (the head before the rebase, with the same
kernel code) for gfx950. Each key has a `stop=True` build (the spec) and
a `stop=False` build (the tests). The numbers come from the AMDGPU
metadata in FlyDSL's ISA dump. Where the two builds differ, a cell gives
`stop=True / stop=False`.

| Width | VGPR | VGPR spill | SGPR spill | LDS (bytes) | Scratch (bytes)
|
| --- | --- | --- | --- | --- | --- |
| 1 | 203 | 0 | 22 / 123 | 27616 | 0 |
| 2 | 203 / 205 | 0 | 65 / 26 | 46176 | 0 |
| 3 | 209 | 0 | 23 / 27 | 62656 | 0 |
| 4 | 213 | 0 | 26 / 29 | 79136 | 0 |
| 5 | 209 | 0 | 22 / 27 | 95616 | 0 |
| 6 | 219 | 0 | 23 / 27 | 112096 | 0 |
| 7 | 213 | 0 | 23 / 28 | 128576 | 0 |
| 8 | 221 | 0 | 26 | 145056 | 0 |

- No build spills a VGPR or uses scratch. The SGPR spills go to VGPR
lanes.
- No build uses AGPRs. Each build has 106 SGPRs.
- LDS grows with the width, to 145,056 of gfx950's 163,840 bytes at 8
rows.

**Kernels before and after.** The table compares one decode step at
batch 1 in two TP-0 profiles: monokernels off and monokernels on. Both
servers ran the head `3daea98` on one node with 8× MI355X. The prompt
has 8192 tokens, and the output has 100 tokens. The counts come from the
`step[DECODE bs=1]` slices on the GPU stream. All 89 decode steps after
the first 10 have the same counts. Each time is the mean of these 89
steps.

### Kernel Replacement (layer tail, decode bs=1)
| Part of the layer | Before: SGLang kernel | Calls per step | Time per
call (µs) | After |
| --- | --- | --- | --- | --- |
| out_proj (69 GDN layers) or o_proj (23 attention layers) |
`_gemm_a16_w16_kernel` (`SPLITK_BLOCK_SIZE_2048`) | 92 | 8.0 | One
`qwen38_mono_k2_s1_a0` launch for each layer: 92 calls per step, 39.8 µs
each |
| The all-reduce after out_proj | `aiter::ar_ll_gfx9` | 92 | 7.6 | In K2
|
| The post-attention norm | `_gemma_fused_add_rmsnorm_kernel` | 92 | 4.3
| In K2 |
| The shared expert gate_up | `aiter::wv_splitk_small_fp16_bf16_kernel`
| 92 | 8.0 | In K2 |
| The shared expert SiLU and mul |
`sgl_hip::activation::act_and_mul_kernel` | 92 | 4.3 | In K2 |
| The shared expert down | `_gemm_a16_w16_kernel`
(`SPLITK_BLOCK_SIZE_256`) | 92 | 4.1 | In K2 |
| The router | `aiter::wv_splitk_small_fp16_bf16_kernel` | 92 | 8.0 | In
K2 |
| The top-k | `vllm::moe::topkGatingSoftmax` | 92 | 9.8 | In K2 |
| The routed experts |
`aiter::fmoe_bf16_pertokenMXfp4_g1u1_flat_vs_silu_16x32` | 92 | 19.2 |
In K2 |
| The shared expert gate, sigmoid and add |
`_fused_gate_sigmoid_mul_add_kernel` | 92 | 4.2 | In K2 |
| The MoE all-reduce | `aiter::ar_ll_gfx9` | 92 | 6.8 | In K2 |
| The step epoch of the K2 waits | None | | | One
`CUDAFunctorOnSelf_add<int>` per step (`self.epoch[:1].add_(1)` in
`spec.py`) |

| Before | Time (us) | After | Time (us) |
| --- | ---: | --- | ---: |
| `_gemm_a16_w16_kernel` (SPLITK_BLOCK_SIZE_2048, out_proj / o_proj,
×92) | 8.0 | `qwen38_mono_k2_s1_a0` (×92) | 39.8 |
| `aiter::ar_ll_gfx9` (all-reduce after out_proj, ×92) | 7.6 | (in K2) |
-- |
| `_gemma_fused_add_rmsnorm_kernel` (×92) | 4.3 | (in K2) | -- |
| `aiter::wv_splitk_small_fp16_bf16_kernel` (shared expert gate_up, ×92)
| 8.0 | (in K2) | -- |
| `sgl_hip::activation::act_and_mul_kernel` (×92) | 4.3 | (in K2) | -- |
| `_gemm_a16_w16_kernel` (SPLITK_BLOCK_SIZE_256, shared expert down,
×92) | 4.1 | (in K2) | -- |
| `aiter::wv_splitk_small_fp16_bf16_kernel` (router, ×92) | 8.0 | (in
K2) | -- |
| `vllm::moe::topkGatingSoftmax` (×92) | 9.8 | (in K2) | -- |
| `aiter::fmoe_bf16_pertokenMXfp4_g1u1_flat_vs_silu_16x32` (×92) | 19.2
| (in K2) | -- |
| `_fused_gate_sigmoid_mul_add_kernel` (×92) | 4.2 | (in K2) | -- |
| `aiter::ar_ll_gfx9` (MoE all-reduce, ×92) | 6.8 | (in K2) | -- |
| | | `CUDAFunctorOnSelf_add<int>` (×1 per step) | -- |
| **Total** | **84.3** | **Total** | **39.8 (52.8% reduce)** |

- The 11 kernels of a layer tail take 84.3 µs together. K2 takes 39.8
µs.
- The step falls from 1669 to 750 kernels, and from 12.14 to 8.11 ms of
kernel time.
- SGLang still runs the embedding and its all-reduce, the input norms,
the GDN or attention, the final norm, the LM head and the sampling.
- Each screenshot selects one `step[DECODE bs=1]` slice and shows only
its kernels (filter `Category = kernel`).

Before (monokernels off), one decode step:
<img width="1928" height="1172" alt="image"
src="https://github.com/user-attachments/assets/977c336a-1228-4c3a-b711-e49b525828ce"
/>


After (monokernels on), one decode step: 
<img width="1928" height="1172" alt="image"
src="https://github.com/user-attachments/assets/ccac7f6b-a326-4b25-b354-cf741bc899f8"
/>

E2E: 12.14 ms => 8.103 ms,33.3% reduce


## Checklist

- [x] `pre-commit run --all-files` passes.
- [x] Tests under `test/` cover the change.
- [x] The SGLang commit or PR this was tested against is named above.
- [x] Accuracy and speed results are attached when kernel or forward
code changes.
- [ ] A new model adds one line to `.github/CODEOWNERS` that names its
owner. (N/A: `main` already has `**/*qwen38_a95b_mxfp4* @omirosh
@yichiche @chuyeh-amd`.)

## Review and Merge Process

1. The `lint` and `Check PR title` checks must pass.
2. Ask a code owner from `.github/CODEOWNERS` to review.
3. Merge with "Squash and merge": the PR title becomes the commit title
on `main`.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

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

Labels

ci/build k3 kimi needs-rebase new-model Requests to new models qwen Related to Qwen models rocm Related to AMD ROCm

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

2 participants