Repository navigation
[Feat] [AMD] minimax: gfx950 MoE, router GEMV, block-fp8 GEMM and fp8 producer kernels - #46691
Merged
Merged
Conversation
… 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
requested review from
BBuf,
DarkSharpness,
HaiShaw,
HydraQYH,
celve and
yuan-luo
as code owners
October 27, 2026 00:28
This was referenced Oct 27, 2026
Collaborator
Author
|
/tag-and-rerun-ci |
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>
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>
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>
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>
3 of 5 tasks
4 of 5 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
moe/moe_sort_quant.py(one launch for decode batches) andmoe/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.pyhands them to aiter as runtime patches.moe/aiter_moe_addend.pyfolds the shared-expert output into aiter's stage-2 reduction as a runtime patch, applied only when aiter's signature matches.gemm/router_gemv.pyraises 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.pyaccepts 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/aiter_asm_blockscale.pylaunches 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 addsout_zeroed), andjit/utils/hipcc.pylaunches the 2-D grid it needs.smallm_fp8_gfx950gains the M3.1 shared expert's 1536 x 6144 gate_up and exposes its kernel body, whichm31_routerruns in its fused launch.quantization/aiter_fp8_quant_triton.pyholds Triton epilogues that write exactly aiter's per-token and 1x128 fp8 bytes and scales;layernorm/minimax_m3_rmsnorm.pyadds the Gemma RMSNorm (+ residual) producers that use them.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:
m31_router, router GEMV partials): decode +4.2% at 1 request, +1.5% at 8. Bitwise equal to the unfused chain.Not in this PR
moe_sorting_small.sorting_impl_wrapperis ~160 lines; its two one-launch branches could share a helper.m31_routerkeeps module-level kernel caches andglobal _disabled, like itssmallm_moe_gfx950/smallm_fp8_gfx950siblings.Changes to existing kernels
router_gemv,moe_fused_gate,moe_sorting_smallandsmallm_fp8_gfx950keep 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), andmoe_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.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_quantup to 1024 tokens,moe_sort_quant_largeabove (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_gfx950gains (1536, 6144), which main's per-token fp8 linear also dispatches up to 16 rows; it stays within one bf16 ulp of aiter'sgemm_a8w8_bpreshufflethere.Verification
main:test_minimax_m3_rmsnorm32 (the kernel cases),test_aiter_asm_blockscale1 (the launch case),test_aiter_moe_addend3,test_m31_router_gfx9506,test_moe_sort_quant_large3,test_moe_sorting_small7 (192 subtests),test_router_gemv_gfx9504,test_aiter_fp8_quant_triton7, main'stest_smallm_fp8_proj_gfx9507 (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.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-testto run the tests your change touches, or copy this prompt for an agent: