Skip to content

[Feat] [AMD] minimax: gfx950 MoE, router GEMV, block-fp8 GEMM and fp8 producer kernels - #46691

Merged
hnyls2002 merged 18 commits into
sgl-project:mainfrom
kevin-mii:m31-amd/k2-moe-gemm
Nov 1, 2026
Merged

hnyls2002 merged 18 commits into
sgl-project:mainfrom
kevin-mii:m31-amd/k2-moe-gemm

Conversation

@kevin-mii

@kevin-mii kevin-mii commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Part of the MiniMax-M3.1-Flash AMD series that replaces #45936 (closed with its base branch minimax-m3.1-flash): kernel PRs #46690 (attention), #46691 (MoE / GEMM) and #46692 (quick all-reduce), each on main; then #46693 with the M3.1 call sites and #46694 (indexer CP). It follows the DeepSeek-V4.1 AMD layout (#41018-#41021, then #41308).

Summary

  • MoE sorting + quant. moe/moe_sort_quant.py (one launch for decode batches) and moe/moe_sort_quant_large.py (four launches for prefill) sort and MXFP4-quantize the stage-1 activations bit-for-bit like aiter's multi-phase path; moe_sorting_small.py hands them to aiter as runtime patches.
  • Shared-expert fold. moe/aiter_moe_addend.py folds the shared-expert output into aiter's stage-2 reduction as a runtime patch, applied only when aiter's signature matches.
  • Router. gemm/router_gemv.py raises the gfx950 split-K GEMV to 512 rows with a reduce launch past 16 rows, exposes its split-K partials, and fuses them with the shared expert's fp8 SwiGLU in one launch; moe_fused_gate.py accepts those partials and sums them in order. moe/smallm_router_gfx950/m31_router.* is a one-launch decode router for 1-16 tokens, bitwise equal to router GEMV, gate and aiter's sort.
  • GEMM. gemm/aiter_asm_blockscale.py launches aiter's asm block-fp8 split-K GEMM into an output its producer already zeroed (aiter always memsets it first; [HIP] [Feature] a8w8 blockscale asm: out_zeroed skips the split-K output memset ROCm/aiter#6786 adds out_zeroed), and jit/utils/hipcc.py launches the 2-D grid it needs. smallm_fp8_gfx950 gains the M3.1 shared expert's 1536 x 6144 gate_up and exposes its kernel body, which m31_router runs in its fused launch.
  • fp8 producers. quantization/aiter_fp8_quant_triton.py holds Triton epilogues that write exactly aiter's per-token and 1x128 fp8 bytes and scales; layernorm/minimax_m3_rmsnorm.py adds the Gemma RMSNorm (+ residual) producers that use them.
  • Merged with main's Beam FP32-gate path ([Feat] kernels: Beam model support #45254): FP32 gates keep their CUDA configs and one-row tile; BF16 gates stay gfx950-only.
  • The M3.1 call sites (fused routers, fp8 producers, shared-expert fold, pre-zeroed GEMM launch) are in the integration PR.

Serving gain of each kernel

Measured in the integration PR at TP4 on MI350X (NVFP4 KV, DSpark acceptance simulated at 3 tokens per step, 32K-token contexts), each change turned off alone; the share decode TPOT or prefill latency grows without it, mean of two GPU sets:

  • Shared-expert fold into aiter's stage-2 reduction: 32K-token prefill +15.9%, decode +1.9-5.0% at 8-64 requests. Not bit-identical to adding the shared output afterwards (its test bounds the relative error at 1e-2).
  • Gemma RMSNorm producers writing the next GEMM's fp8: decode +8.5% at 1 request, +1.5-2.9% at 8-64.
  • SwiGLU writing the down projection's fp8: decode +4.4% at 1 request, prefill +5.3%.
  • Fused decode routers (m31_router, router GEMV partials): decode +4.2% at 1 request, +1.5% at 8. Bitwise equal to the unfused chain.
  • The MoE sort + quant kernels were not toggled on their own; they are bitwise equal to aiter's multi-phase path.
  • The pre-zeroed asm split-K launch is bit-exact and saves one memset launch per prefill block-fp8 GEMM; end to end it measured within run-to-run noise.

Not in this PR

  • moe_sorting_small.sorting_impl_wrapper is ~160 lines; its two one-launch branches could share a helper.
  • m31_router keeps module-level kernel caches and global _disabled, like its smallm_moe_gfx950 / smallm_fp8_gfx950 siblings.

Changes to existing kernels

router_gemv, moe_fused_gate, moe_sorting_small and smallm_fp8_gfx950 keep their signatures. On CUDA, router_gemv's FP32-gate path runs main's kernel body and tiles, with a split-K scratch now sized for its own tiles (64 KiB at N = 256, against main's 512 KiB), and moe_fused_gate's 2-D path is unchanged; split-K partials never take its SM120 or radix fast paths.

On gfx950, existing callers change when this merges, with no new call site:

  • router_gemv's bf16 tiles change for its callers (MiniMax-M3, Beam, DeepSeek-V4.1 VL routing), which changes the fp32 summation order; the fp64 accuracy and determinism test covers them.
  • main's aiter MoE runner applies moe_sorting_small's patch, which now sorts 128-expert top-4 routing at hidden 6144 in one launch with the stage1 MXFP4 activation quant: moe_sort_quant up to 1024 tokens, moe_sort_quant_large above (aiter's sort again past 349,525 tokens, the large sort's int32 offset limit), both bitwise equal to aiter's multi-phase sort. It no longer emits an mxfp8 quant for separated-SwiGLU MXFP4 weights, which never read one, and it stays off, with an info log, when aiter's sort or quant signatures differ from the ones it wraps.
  • smallm_fp8_gfx950 gains (1536, 6144), which main's per-token fp8 linear also dispatches up to 16 rows; it stays within one bf16 ulp of aiter's gemm_a8w8_bpreshuffle there.

Verification

  • MI350X (same stack), this branch alone on main: test_minimax_m3_rmsnorm 32 (the kernel cases), test_aiter_asm_blockscale 1 (the launch case), test_aiter_moe_addend 3, test_m31_router_gfx950 6, test_moe_sort_quant_large 3, test_moe_sorting_small 7 (192 subtests), test_router_gemv_gfx950 4, test_aiter_fp8_quant_triton 7, main's test_smallm_fp8_proj_gfx950 7 (with the M3.1 shape). Main's FP32-gate path run on gfx950 through the merged kernel: max error 3.9e-7 to 6.2e-7 against fp64 at 1-8 rows (main's bound 2e-6), deterministic. pre-commit clean.
  • Not executed: main's CUDA test_router_gemv.py (needs CUDA), end-to-end serving.

🤖 Generated with Claude Code


CI States

Latest PR Test (Base): ⏳ Run #41356557775
Latest PR Test (Extra): ❌ Run #41356557742
Latest PR Test (AMD ROCm 10): ⏳ Run #41356557741

Tip

Base CI only covers the core cases, and its workload may not exercise your change. Use /rerun-test to run the tests your change touches, or copy this prompt for an agent:

Find the registered tests this PR affects and run them with /rerun-test, following https://github.com/sgl-project/sglang/blob/main/.agents/skills/rerun-test/SKILL.md

Kevin Mi and others added 4 commits October 26, 2026 21:02
… producer kernels

Kernels for MiniMax-M3.1-Flash on MI350X / MI355X with no callers yet: MoE sorting + quant and the aiter MoE addend, the M3.1 small-M router, the split-K router GEMV with fused shared-expert SwiGLU, aiter's asm block-fp8 GEMM, the small-M fp8 projection, and the producers of their fp8 inputs: RMSNorm + quant epilogues and the TP4 decode boundary's fused all-reduce + residual add + RMSNorm + fp8 quant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- hipcc.py: the 2-D grid comment named its caller (aiter_asm_blockscale); it
  now states the accepted grid forms, which is what a reader of launch() needs.
- router_gemv.py: the fused partials + SwiGLU kernel passes partials_ptr as
  out_ptr and counter_ptr and 0 as stride_om, which reads like a bug; say those
  are never read without the fixup. Reword the `assert num_warps == 4` comment
  ("the two kernels' warps, so each keeps its layouts") so it reads as the
  constraint: both kernels run at 4 warps alone, and sharing that count keeps
  their layouts.
- m31_router.py: SHARED_N sat between the `_SHARED` table comment and the
  table it describes; move it above the comment.

No behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- moe_sorting_small module docstring: the fallback sentence ("Sorts this
  module's kernels do not cover go to ..., larger ones that come with that
  quant to ..., else to aiter") had to be parsed twice; state the three
  destinations in order.
- presorted_moe_sort: the tuple layout sat inside the sentence and ran past
  110 columns in an 88-column file; name the hand-off first, then the layout.
- router_gemv_partials_swiglu: "in one launch, bitwise:" did not say bitwise
  what; say "bitwise equal to the two calls", as m31_shared_router does.
- test_build_is_cached_across_loads: the docstring told the incident (every
  rank of every start loaded a fresh build); state the live constraint the
  test guards instead, per the bug-regression docstring rule.

No behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- moe_sorting_small.sorting_impl_wrapper: the presorted hand-off and both
  one-launch MXFP4 sorts each spelled out `plain_sort and expert_mask is None
  and num_local_tokens is None`; bind it once as `unmasked_sort`, so a reader
  sees the three paths share one eligibility. Read `_pending_fp4_input` once
  before the two one-launch branches instead of in each, and name the
  moe_sort_quant input after its MAX_QUANT_TOKENS cap `quant_input` instead
  of overwriting `fp4_input` with None.
- m31_router: `8 if m <= 8 else 16` picked the router tile in three places
  (`_tile` and both `_SHARED` lookups); one `_token_bucket` helper keeps the
  `_TILES` and `_SHARED` keys from drifting apart.

The conditions, their order and the values passed are unchanged: nothing
between the hoisted ContextVar read and its old sites sets it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kevin-mii

Copy link
Copy Markdown
Collaborator Author

/tag-and-rerun-ci

@github-actions github-actions Bot added the run-ci CI: run the baseline test suite on this PR label Oct 27, 2026
Kevin Mi and others added 7 commits October 27, 2026 21:46
Its caller, the integration PR's fused M3.1 decode boundary, measured within
run-to-run noise on and off at TP4 and left; the boundaries keep aiter's
all-reduce and the fp8 norm producer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The integration PR no longer hands the M3.1 decode projections to the
small-M kernel (on and off at TP4 it measured within run-to-run noise), so
their seven shapes and the instances only they used go. The shared expert's
1536 x 6144 gate_up stays: m31_router runs it in its fused launch and picks
its config from the same table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… int32 offsets

moe_sort_quant_large addresses x and moe_buf with int32 offsets but accepted
any batch under 2**24 tokens, so one past 349,525 tokens at hidden 6144 (an
unchunked long prefill) faulted on the GPU. main's aiter runner applies the
sort patch, so the bound protects main's gfx950 MoE path as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SM120 top-22 and K3 radix paths read one [M, N] score tensor; given split-K
partials they would have routed on the first split alone. Partials now always
take the Triton kernel that sums them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(1536, 6144) was covered only by m31_router's fused-against-itself test; it now
runs the same against-aiter and dispatch cases as the Qwen3.5 projections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-fill

gemma_add_rmsnorm_fp8 divided the zero-fill tensor's size by the row count, so
an idle DP-attention rank's empty layer raised ZeroDivisionError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on this PR

The two gemm tests registered the MI35x suite as stage/runner_config while the
rest of the PR spells suite=, the form most MI35x tests use; the suite is the
same. The rmsnorm file's second AMD registration now says why it exists, and the
addend test no longer names a class only the integration PR defines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kevin Mi and others added 2 commits October 29, 2026 00:01
The scratch took the largest split and row count of every table, so CUDA's
FP32-gate path, which runs at most 8 rows and 8 splits, allocated the gfx950 bf16
tiles' 12 x 512 rows (6 MiB at N = 256, against 0.5 MiB on main). It is now
keyed and sized per weight dtype: 64 KiB for the FP32 path, unchanged for bf16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… show

Drop a comment that restated the grid unpacking, split long docstring summaries into one summary line plus a paragraph, use double backticks in docstrings, and make each file's comments one consistent style (sentence or lowercase fragment).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kevin-mii kevin-mii added the bypass-fail-fast CI: a failing job no longer aborts its siblings (lint still gates) label Oct 29, 2026
Picks up sgl-project#47127, which fixes main's test_gdn_prefill_graph that pr-test-finish fails on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kevin-mii and others added 4 commits November 1, 2026 16:27
Graph-padded rows past moe_num_token_non_padded carry -1 ids. The small
one-launch sort counted them into expert 0's histogram slot and scattered
them over its first block; the large sort read prefix[c * E - 1] and wrote
before the sorted buffers. Both now drop such pairs, as aiter's sort does,
and the tests cover padded batches bit-for-bit against aiter (codes of
padded tokens, which no sorted row reads, are left out of the compare).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
router_gemv_partials hands out a view of the device's one split-K scratch, and
both routers keep a self-resetting ticket counter in their workspace, so a
router call on a second stream could overwrite a caller's partials or race the
counter. The scratch is now keyed by stream as well; a graph capture that
skipped warm-up on its own stream reuses the device's existing scratch, as
before. A test runs a router call on a side stream between producing the
partials and reading them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apply_aiter_small_moe_sort_patch and apply_aiter_moe_addend_patch now return
whether they patched aiter. The sort, large-sort, M3.1 router and addend tests
skip with the reason when the patch did not apply, or when aiter lacks FlyDSL
or the MXFP4 stage1 quant, instead of failing on a missing __wrapped__.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hnyls2002
hnyls2002 merged commit e37c1fb into sgl-project:main Nov 1, 2026
143 of 165 checks passed
Yuxingwang-intel pushed a commit to Yuxingwang-intel/sglang that referenced this pull request Nov 6, 2026
… producer kernels (sgl-project#46691)

Co-authored-by: Kevin Mi <kevin.mi@radixark.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bypass-fail-fast CI: a failing job no longer aborts its siblings (lint still gates) jit-kernel quant LLM Quantization run-ci CI: run the baseline test suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants