Skip to content

[MoE] Consolidate ungrouped + grouped gate/topk onto one Triton router (#26771) — faster than AOT on B200/H100/H200, at parity with flashinfer - #29771

Merged
BBuf merged 6 commits into
sgl-project:mainfrom
BBuf:moe-gate-consolidation-phase1
Jul 3, 2026
Merged

BBuf merged 6 commits into
sgl-project:mainfrom
BBuf:moe-gate-consolidation-phase1

Conversation

@BBuf

@BBuf BBuf commented Jun 30, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

Part of the fused MoE gate/topk kernel consolidation (#26771). SGLang has many per-model gate/topk kernels (grouped_topk.cuh, AOT topk_sigmoid/topk_softmax, the CUDA-JIT moe_fused_gate, flashinfer fused_topk_deepseek, …). #25835 introduced one Triton moe_fused_gate router and made it the default for the ungrouped sqrtsoftplus path.

This PR converges the remaining paths onto that single router, in three phases:

  • Phase 1 — ungrouped: route the CUDA softmax/sigmoid topk and the ungrouped grouped_topk (num_expert_group==1) branch through the Triton router, and optimize it to beat the hand-tuned AOT kernels on B200 + H100 + H200.
  • Phase 2 — cleanup: delete the kernel the consolidation makes dead.
  • Phase 3 — grouped: add DeepSeek-V3 grouped routing (n_group/topk_group, noaux_tc) to the same router and wire an opt-in dispatch, validated bit-exact e2e and at parity with flashinfer.

Modifications

Triton router (jit_kernel/moe_fused_gate.py):

  • Add softmax scoring (softmax-prob weight, bias kept, optional tanh softcap) alongside sigmoid/sqrtsoftplus.
  • Accept fp32/fp16/bf16 scores (kernel upcasts on load) — bf16 router logits no longer need a host-side fp32 upcast.
  • Phase 3: grouped routing (constexpr N_GROUP>1): per-group score = sum of the top-2 biased values, keep TOPK_GROUP groups (lowest group id wins ties), mask dropped groups, then the existing top-k runs. N_GROUP==1 compiles the block out (ungrouped path unchanged). More general than the AOT grouped kernel — no experts-per-group ≤ 32 cap.
  • Perf: row-pack the launch (num_warps=1, BLOCK_M = max(1, min(4, 256//BLOCK_N))). The old single-warp-per-row grid=(M,) was occupancy-bound at small N on Hopper and lost ~25% to AOT; packing a few rows per program only when N is small fixes it (swept BLOCK_M × num_warps on H100/B200/H200 — this heuristic is the sweep-optimal point; larger tiles / more warps regress via register pressure).

Dispatch (srt/layers/moe/topk.py):

  • Phase 1: route the CUDA fused_topk softmax/sigmoid branches and the biased_grouped_topk_gpu num_expert_group==1 branch through the Triton router (gated by SGLANG_OPT_USE_JIT_KERNEL_FUSED_TOPK, default on, _is_cuda). AOT stays the default on non-CUDA backends and the env-off fallback.
  • Phase 3: new opt-in SGLANG_OPT_USE_JIT_KERNEL_GROUPED_TOPK (default off) routes the DeepSeek-V3 grouped path (num_expert_group>1) through the Triton router. Off by default so flashinfer fused_topk_deepseek stays the tuned production default; the flag is a consolidation escape hatch (validated bit-exact + at parity, below), not a perf flip.

Cleanup (Phase 2): delete the now-dead jit grouped_topk (kernel + wrapper + test) — its only dispatch site now routes to the Triton router.

Accuracy Tests

  • test/registered/jit/test_moe_fused_gate.py: 1063 passed on GPU (sigmoid/sqrtsoftplus/softmax × experts × topk × shared-experts × renorm × scale, fp32+bf16), including:
    • softmax/sigmoid parity vs AOT topk_softmax/topk_sigmoid + a torch reference (Phase 1);
    • grouped parity vs the definitional biased_grouped_topk_impl for DeepSeek-V3 shapes (256/8/4/8, 128/8/4/6, 256/4/2/8), fp32+bf16, ≤1e-3 (Phase 3 kernel);
    • grouped dispatch parity: biased_grouped_topk_gpu with the flag on (Triton) vs off (flashinfer/AOT default), with and without a fused shared expert (Phase 3 wiring).
  • Grouped kernel matches the AOT moe_fused_gate in fp32; bf16 differs only on borderline experts because AOT scores in bf16 while the reference + this router upcast to fp32.

Model accuracy — end-to-end (Phase 3)

Three levels of accuracy evidence for the grouped path:

1. Full-model downstream eval — GSM8K + MMLU on DeepSeek-V3-0324 (671B fp8, the flagship sigmoid-grouped model this dispatch targets; n_group=8, topk_group=4, 256 experts). Served with sglang.launch_server --tp 8 on 8×B200; the two runs use the same build, differing only in the SGLANG_OPT_USE_JIT_KERNEL_GROUPED_TOPK env var (flag verified live in the server process). sglang.test.run_eval:

eval default (flashinfer fused_topk_deepseek) Triton grouped (flag on) Δ
GSM8K (1319, greedy) 95.74% 95.89% +0.15% (2/1319)
MMLU (3000-example sample) 86.47% 86.47% 0.00%

MMLU is identical (2594/3000 both). GSM8K differs by 2 questions — the grouped Triton router and flashinfer diverge only on borderline-expert selection (fp32 vs bf16 scoring rounding), which occasionally flips a mid-CoT token; the effect is noise-level and here Triton scores marginally higher. No accuracy regression.

2. Kernel-level numerical precision (test_moe_fused_gate.py, on GPU): the grouped Triton router vs the definitional biased_grouped_topk_impl — routed-expert selection exact and weights max abs diff ≤ 1e-7 in fp32 (DeepSeek-V3 256/8/4/8, 128/8/4/6, 256/4/2/8). In bf16 selection is identical except on borderline experts, and only because the AOT kernel scores in bf16 while the reference + this router upcast to fp32. Grouped-dispatch parity (flag on vs off, ±fused shared expert) matches to ≤ 1e-3.

3. Bit-exact end-to-end output — DeepSeek-V3.2-5layer-bf16 (real DSA + grouped MoE: 256 experts, n_group=8, topk_group=4, sigmoid), greedy generation on fixed prompts, 1×H200:

dispatch grouped-MoE invocations output token hash
default (flashinfer fused_topk_deepseek) — 5518026aa2a27596
SGLANG_OPT_USE_JIT_KERNEL_GROUPED_TOPK=1 (Triton) 52 (prefill+decode) 5518026aa2a27596

Every generated token is bit-identical across the two dispatches (a stderr marker confirmed the Triton grouped branch fired 52× across prefill + decode, ruling out a silent no-op).

Speed Tests and Profiling

CUDA-graph, kernel-only replay (production runs the gate under graphs).

Phase 1 — ungrouped, Triton vs AOT (Triton ≥ AOT on every shape on B200 + H100; H200 5/6, see note):

B200 (Blackwell):

scoring M N topk AOT µs Triton µs speedup
softmax 4096 128 8 10.21 6.24 1.64x
softmax 8192 256 8 19.05 12.38 1.54x
softmax 8192 512 8 24.44 18.53 1.32x
sigmoid 4096 128 8 10.35 6.23 1.66x
sigmoid 8192 256 8 18.74 12.36 1.52x
sigmoid 8192 512 8 141.82 18.51 7.66x

H100 (Hopper):

scoring M N topk AOT µs Triton µs speedup
softmax 4096 128 8 6.31 6.34 1.00x
softmax 8192 256 8 16.22 13.94 1.16x
softmax 8192 512 8 21.61 20.66 1.05x
sigmoid 4096 128 8 6.51 6.50 1.00x
sigmoid 8192 256 8 17.80 13.99 1.27x
sigmoid 8192 512 8 128.97 21.19 6.09x

H200 (Hopper):

scoring M N topk AOT µs Triton µs speedup
softmax 4096 128 8 6.19 6.41 0.96x*
softmax 8192 256 8 16.07 13.85 1.16x
softmax 8192 512 8 21.61 20.68 1.05x
sigmoid 4096 128 8 6.43 6.41 1.00x
sigmoid 8192 256 8 17.73 14.07 1.26x
sigmoid 8192 512 8 128.65 21.19 6.07x

* The single 0.96x is a bench artifact, not a kernel regression: the Triton wrapper allocates its two output tensors inside the captured graph while the AOT bench pre-allocates them. Kernel-only at that shape the Triton router is 1.13x (BLOCK_M=2, num_warps=1 — the heuristic's choice, confirmed sweep-optimal on H200). Production allocates identically on both paths.

Phase 3 — grouped, Triton vs the real defaults (DeepSeek-V3 256/8/4/8, H200; flashinfer fused_topk_deepseek is the current production default):

M N n_group topk_group topk flashinfer µs AOT µs Triton µs vs-flashinfer vs-AOT
1024 256 8 4 8 5.86 12.21 5.93 0.99x 2.06x
4096 256 8 4 8 13.40 14.83 12.74 1.05x 1.16x
8192 256 8 4 8 23.03 20.88 22.36 1.03x 0.93x

Grouped Triton is at parity with flashinfer (0.99–1.05x) and generally beats AOT — i.e. no perf loss vs the actual production default. Combined with the bit-exact e2e result, this is why the grouped dispatch is wired as an opt-in flag rather than deferred to a follow-up.

Follow-ups (#26771, separate PRs)

  • Default-enable the grouped Triton path once it has run through flagship (full DeepSeek-V3) e2e accuracy at scale.
  • Retire the AOT .cu once XPU/HIP/MUSA/CPU migrate; MUSA decision.

Checklist

  • pre-commit (ruff/isort/black/codespell + registered-test validator) pass.
  • Unit tests (1063 on GPU: ungrouped + grouped kernel + grouped dispatch).
  • End-to-end model accuracy (DeepSeek-V3-0324 GSM8K 95.74%→95.89%, MMLU 86.47%→86.47%; + DeepSeek-V3.2 bit-identical tokens, above).
  • Docs.
  • Accuracy + speed benchmarks (B200 + H100 + H200, above).
  • Code style.

🤖 Generated with Claude Code


CI States

Latest PR Test (Base): ❌ Run #28581806799
Latest PR Test (Extra): ❌ Run #28581806410

… router

Part of the fused MoE gate/topk kernel consolidation (sgl-project#26771): converge the
many per-model ungrouped gate kernels onto the single Triton `moe_fused_gate`
router introduced in sgl-project#25835.

- Extend the Triton router to support `softmax` scoring (softmax-prob weight,
  optional tanh softcapping, bias kept) alongside `sigmoid`/`sqrtsoftplus`.
- Accept fp32/fp16/bf16 `scores` (the kernel already upcasts on load), so bf16
  router logits no longer need a host-side fp32 upcast.
- Route the CUDA `fused_topk` softmax/sigmoid paths through the Triton router,
  gated by `SGLANG_OPT_USE_JIT_KERNEL_FUSED_TOPK` (default on, `_is_cuda` only).
  AOT `topk_softmax`/`topk_sigmoid` remain the default for non-CUDA backends
  and the env-off fallback, so this is additive (no kernel deleted yet).

Validated on B200: full test_moe_fused_gate.py (1055 cases) passes, including
new softmax/sigmoid parity vs AOT sgl_kernel.topk_softmax/topk_sigmoid and a
torch reference, fp32+bf16. CUDA-graph (kernel-only) benchmark: Triton is
1.14-1.51x faster than the AOT kernels on the softmax/sigmoid shapes (up to
6.9x where AOT topk_sigmoid degrades at N=512).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@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!

@BBuf BBuf added run-ci CI: run the baseline test suite on this PR run-ci-extra CI: also run the extra suite (requires run-ci) labels Jun 30, 2026
@BBuf

BBuf commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

/tag-and-rerun-ci

…Triton router

Fold the second ungrouped CUDA sigmoid dispatch point (biased_grouped_topk_gpu's
num_expert_group==1 / topk_group==1 branch, previously the jit grouped_topk.cuh
kernel) onto the same unified Triton moe_fused_gate router. Validated on B200:
matches biased_grouped_topk_impl at fp32-epsilon (fp32+bf16, renorm + scaling).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BBuf

BBuf commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

Added a second commit folding the biased_grouped_topk_gpu ungrouped (num_expert_group==1/topk_group==1) sigmoid path onto the same Triton router (previously the jit grouped_topk.cuh kernel). Validated on B200: matches biased_grouped_topk_impl at fp32-epsilon across fp32+bf16, renorm + scaling. Now both ungrouped CUDA gate dispatch points go through the unified router.

BBuf and others added 2 commits July 1, 2026 15:30
The jit grouped_topk (grouped_topk.cuh + wrapper + test) was only reachable via
the biased_grouped_topk_gpu num_expert_group==1 dispatch branch, which now routes
to the unified Triton moe_fused_gate router. With that call site gone the kernel
has no remaining importer, so remove the kernel, its Python wrapper, and its test
(the ungrouped-sigmoid-vs-biased_grouped_topk_impl coverage is retained by
test_moe_fused_gate.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r too

The single-warp-per-row launch (grid=(M,)) was occupancy-bound at small N on
Hopper (H100), losing ~25% to the hand-tuned AOT topk_softmax/topk_sigmoid.
Tile a few rows per program (num_warps=1, BLOCK_M = max(1, min(4, 256//BLOCK_N)))
so small-N launches stay occupancy-bound while the per-row top-k stays on cheap
warp shuffles. Swept BLOCK_M x num_warps on H100/B200 — larger tiles / more warps
regress (register pressure); this config wins across shapes.

CUDA-graph kernel-only (softmax/sigmoid, topk=8):
  B200: 1.32-7.66x vs AOT (was 1.19-6.91x)
  H100: 1.00-6.09x vs AOT (was 0.74-6.27x -> no more small-N regression)
Correctness unchanged: test_moe_fused_gate.py 1055 passed on B200 + H100 parity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BBuf BBuf changed the title [MoE] Route ungrouped softmax/sigmoid topk through the unified Triton router [MoE] Consolidate ungrouped gate/topk onto one Triton router (softmax/sigmoid/sqrtsoftplus), faster than the AOT kernels on B200 + H100 Jul 1, 2026
@BBuf

BBuf commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

/tag-and-rerun-ci

Extend the Triton router with grouped top-k (constexpr N_GROUP>1): per-group
score = sum of the top-2 biased values, keep TOPK_GROUP groups (lowest group id
wins ties), mask dropped groups' experts, then the existing top-k runs. Weight is
still the bias-free activated score. N_GROUP==1 skips the block (ungrouped path
unchanged). More general than the AOT grouped kernel (no experts/group<=32 cap).

Validated on B200: grouped parity vs biased_grouped_topk_impl (fp32+bf16, incl.
DeepSeek-V3 256/8/4/8) passes at <=1e-7; matches the AOT grouped moe_fused_gate
in fp32 (bf16 differs only on borderline experts because the AOT scores in bf16
while the reference + this kernel upcast to fp32). CUDA-graph kernel-only bench
vs the AOT grouped kernel: 1.22-1.75x on DeepSeek-V3 shapes.

NOTE: this only adds the capability; the DeepSeek-V3 grouped dispatch is NOT
switched to Triton here — that needs on-model e2e accuracy validation first
(the bf16 borderline-selection difference vs the current AOT path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BBuf

BBuf commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

/rerun-failed-ci

…uter

Add SGLANG_OPT_USE_JIT_KERNEL_GROUPED_TOPK (EnvBool, default False). When set,
biased_grouped_topk_gpu routes DeepSeek-V3 grouped routing (num_expert_group>1)
through the unified Triton moe_fused_gate instead of the flashinfer/AOT grouped
kernels, completing the issue sgl-project#26771 consolidation for the grouped path.

Off by default: flashinfer fused_topk_deepseek stays the production default. The
Triton grouped path is validated bit-exact against it on DeepSeek-V3.2-5layer-bf16
e2e (52 grouped-MoE invocations across prefill+decode, identical output hash) and
benchmarks at parity on H200 (0.99-1.05x vs flashinfer), so this is a
consolidation escape hatch, not a perf flip. It also lifts the AOT experts/group
<= 32 cap.

Adds test_grouped_dispatch_flag_matches_default asserting the flag-on Triton path
matches the flag-off default path for DeepSeek-V3 shapes (with and without a fused
shared expert).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BBuf BBuf changed the title [MoE] Consolidate ungrouped gate/topk onto one Triton router (softmax/sigmoid/sqrtsoftplus), faster than the AOT kernels on B200 + H100 [MoE] Consolidate ungrouped + grouped gate/topk onto one Triton router (#26771) — faster than AOT on B200/H100/H200, at parity with flashinfer Jul 2, 2026
@BBuf
BBuf merged commit a2d7eb3 into sgl-project:main Jul 3, 2026
425 of 555 checks passed
BBuf added a commit to BBuf/sglang that referenced this pull request Jul 3, 2026
…nels

Follow-up to the gate/topk consolidation (sgl-project#26771, on top of sgl-project#29771): retire the
two remaining AOT gate kernels now that the unified Triton router covers their
CUDA paths.

(1) moe_fused_gate (moe_fused_gate.cu): its only live CUDA caller was the grouped
    `experts_per_group <= 32` fallback in biased_grouped_topk_gpu. Reroute that
    fallback to the Triton router (which has no MAX_VPT=32 cap and handles any
    experts-per-group), so flashinfer stays the grouped default and Triton is the
    fallback/opt-in.

(2) kimi_k2_moe_fused_gate (kimi_k2_moe_fused_gate.cu): already dead on sgl-project#29771 --
    Phase 1 routes the 384/256-expert single-group sigmoid path (Kimi-K2, MiMo-V2
    Flash) through the Triton router; only a register_fake stub remained. Removed.
    The experimental-LoRA bf16 fast path uses a separate JIT kernel (trtllm_lora_temp),
    which is untouched.

Deletes both .cu (+ the MUSA moe_fused_gate .cu, since sglang's MUSA path uses the
external `mate` package, not this sgl_kernel op -- no runtime impact), their CUDA
and MUSA pybind registrations, header decls, CMake/setup_musa build entries, the
sgl_kernel Python wrappers + exports, and their unit tests + benchmarks. ROCm never
registered these ops, so it is unaffected. Net -2454 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ch-wan added a commit that referenced this pull request Jul 4, 2026
The unified Triton router (PR #29771, issue #26771) is on by default for
ungrouped softmax/sigmoid MoE (SGLANG_OPT_USE_JIT_KERNEL_FUSED_TOPK=True), so
qwen3-moe now routes topk through moe_fused_gate instead of the AOT topk_softmax
kernel.

On an all-NaN gating row (garbage padding positions during EAGLE3 + DP-attention
decode CUDA graph capture) max_val is NaN, so `cur == max_val` is false on every
lane and the top-k tie-break falls through to the `N + 1` sentinel. That
out-of-range expert id (129 for 128 experts) overflows the moe_align cumsum
buffer (size num_experts + 2) and faults count_and_sort_expert_tokens_kernel
with CUDBG_EXCEPTION_WARP_ILLEGAL_ADDRESS. The same root cause also faults
fused_moe_kernel / DeepEP dispatch and propagates NaN into the sampler logits in
other MoE + spec/EP tests, all during graph capture / decode.

Fix, entirely within the degenerate-row path (normal rows bit-identical):
- Map NaN -> a finite floor before ranking, so degenerate rows select in-range
  and *distinct* experts 0..K-1 (the finite floor, unlike -inf, lets the
  winner-removal advance).
- Zero NaN weights so degenerate rows don't feed NaN into topk_weights (the
  weights are computed from the raw scores before the ranking sanitize).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chronostasys pushed a commit to MindLab-Research/sglang that referenced this pull request Aug 24, 2026
sgl-project#26771) — faster than AOT on B200/H100/H200, at parity with flashinfer (sgl-project#29771)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jit-kernel run-ci CI: run the baseline test suite on this PR run-ci-extra CI: also run the extra suite (requires run-ci)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants