[Kernel] Wait for the PDL dependency before loading the router bias (Triton + radix) - #38290
Merged
Jiminator merged 2 commits intoSep 7, 2026
Merged
Conversation
Jiminator
marked this pull request as ready for review
September 7, 2026 09:47
Jiminator
requested review from
BBuf,
DarkSharpness,
HaiShaw,
HydraQYH,
celve and
yuan-luo
as code owners
September 7, 2026 09:47
Jiminator
force-pushed
the
fix/moe-fused-gate-gdc-wait-before-bias
branch
from
September 7, 2026 09:52
d37b5d6 to
01db521
Compare
Jiminator
requested review from
Edwardf0t1,
FlamingoPg,
Fridge003,
JustinTong0323,
Qiaolin-Yu,
Ying1123,
alphabetc1,
ch-wan,
hanming-lu,
hnyls2002,
huangtingwei9988,
hzh0425,
ispobock,
merrymercy,
mickqian,
xiezhq-hermann,
yctseng0211,
yhyang201 and
yizhang2077
as code owners
September 7, 2026 09:52
Jiminator
requested review from
ByronHsu,
Duyi-Wang,
ShangmingCai,
hebiao064,
mmangkad,
sogalin,
wisclmy0611 and
zijiexia
as code owners
September 7, 2026 09:52
Jiminator
requested review from
Edwardf0t1,
Fridge003,
Ying1123,
ch-wan,
ispobock and
merrymercy
as code owners
September 7, 2026 09:52
The Triton router kernel prefetched `bias` before `gdc_wait()` and loaded `scores` after it. The bias is not an immutable input: `fused_topk` passes a buffer written by the immediately preceding kernel (a dtype cast of the correction bias today; a fresh `torch.zeros` before #36811). Under programmatic dependent launch the router can start once that kernel's blocks exit, before its stores are visible, so the pre-wait load could read the buffer's previous contents. On DGX Spark (GB10) with Qwen3.8-Flash-Next NVFP4 + NEXTN this surfaced as batch-wide NaN routing weights and an output collapse to token 0 (#37111). Move the wait above the bias load so every dependent load sits after it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Same ordering defect as in the Triton router: route_radix_block prefetched the bias before PDLWaitPrimary(), assuming a frozen weight, but the public moe_fused_gate dispatches covered sigmoid inputs here with a bias that can be a fresh fp32 cast or a fresh torch.zeros. Move the wait above the load. Also shorten the Triton kernel comment to state only what is established. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Jiminator
force-pushed
the
fix/moe-fused-gate-gdc-wait-before-bias
branch
from
September 7, 2026 09:55
01db521 to
7e2a26d
Compare
Jiminator
added a commit
that referenced
this pull request
Sep 7, 2026
…-set GSM8K scores The four low-latency (NEXTN) DGX Spark cells collapsed sporadically to token 0 on the 9b2aee2 build: the Triton router kernel loaded its zero-filled bias before its PDL wait and could read stale NaN bytes. qwen4-main-squashed now carries #36811 (no zero-bias allocation) and #38290 (wait before the bias load), and dev-qwen38-next-local is rebuilt from 4ccff14. Full GSM8K (1,319, chat API, greedy, 8,192-token budget) on that image: 97.0 / 97.1 / 97.0 / 97.1 for RDXA 2x, RDXA 1x, NVDA 2x, NVDA 1x, all answers finished, no collapsed batches. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Merged
5 tasks
Jiminator
pushed a commit
that referenced
this pull request
Sep 7, 2026
…dev-qwen38-next-local image (4ccff14) The image now carries the two MoE router fixes for the NEXTN "!" collapse seen on GB10 (#36811 cherry-picked as c8457de, #38290 as 4ccff14). Each RTX cell was booted from the rebuilt image exactly as the command generator emits it and re-measured on 2026-09-07: - Full GSM8K, chat protocol (thinking off, 8,192-token budget, the card figure): RDXA 96.9% / 96.9%, NVDA 97.3% / 97.0%; every answer finished. - Full GSM8K via sglang.test.run_eval (5-shot, thinking on): RDXA 97.7% / 97.8%, NVDA 97.4% / 97.7%; MTP accept length 3.16 / 3.15. - 1024-in/256-out random bench re-run; throughput rows updated (input plus output tok/s per GPU). No error or NaN in any server log. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5B2WK8ciABmJ8pMBtGcgN
Jiminator
pushed a commit
that referenced
this pull request
Sep 7, 2026
…re-verification Follow-up to b4312c6, which updated only the benchmark rows: the RTX PRO 6000 notes accordion and the two cell comment blocks now cite the rebuilt image (4ccff14, #36811 and #38290 router fixes) and its numbers: full GSM8K chat 96.9 / 96.9 (RDXA) and 97.3 / 97.0 (NVDA), run_eval 97.72 / 97.79 and 97.41 / 97.72, 1024/256 bench 6.0 / 11.4 ms TPOT at 1 request, 685 / 561 output tok/s at 16, 909 at 64 without MTP. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5B2WK8ciABmJ8pMBtGcgN
3 tasks
joev-harmonic
added a commit
to joev-harmonic/sglang
that referenced
this pull request
Sep 10, 2026
Port merged upstream PR sgl-project#38290 onto the internal Qwen3.8 serving stack.
joev-harmonic
added a commit
to joev-harmonic/sglang
that referenced
this pull request
Sep 11, 2026
Port merged upstream PR sgl-project#38290 onto the internal Qwen3.8 serving stack.
5 tasks
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.
Targets
qwen4-main-squashed, on top of #38308 (the cherry-pick of #36811, merged).Motivation
Two PDL-launched routing kernels load their
biasbefore the programmatic-dependent-launch wait and loadscoresafter it:python/sglang/kernels/ops/moe/moe_fused_gate.py::_router_triton_kernel(Triton,launch_pdl=Trueon compute capability >= 9)python/sglang/kernels/jit/csrc/moe/route_radix.cuh::route_radix_block(the radix fast path thatmoe_fused_gatedispatches covered sigmoid inputs to; also reused byroute_quant_fused.cuh)Under PDL a grid may start once the preceding kernel's blocks have exited, before that kernel's stores are visible; the wait (
griddepcontrol.wait) is what establishes completion and visibility. A load placed before it must not depend on prior work. Both kernels assumed the bias is a frozen weight, but the callers do not guarantee that:fused_topk's sigmoid path passescorrection_bias.to(torch.float32)(a fresh tensor when the parameter is fp16/bf16) or a freshtorch.zeroswhen there is no correction bias (topk.py:1047-1054), the grouped and ungrouped wrappers cast as well, and before #36811 the softmax path passed a freshtorch.zeroson every call. The compiled sm_121 Triton kernel confirms the pre-fix order: fourLDG.E.128bias loads precedeACQBULK, the score loads follow.This is the mechanism behind #37111 (Qwen3.8-Flash-Next NVFP4 with NEXTN on DGX Spark, output collapsing to repeated
!), confirmed by capturing the values the kernel consumed during an organic collapse. With an instrumented copy of the unmodified kernel that additionally stores, per row, the bias it loaded before the wait, the scores it loaded after the wait, and a volatile re-read of the bias after the wait, an onset step (32 rows, 23 with NaN top-k weights) showed for every bad row: the bias loaded before the wait contained NaN (256 of 512 elements in most rows; in others the first 64 were zero and the next 64 to 128 NaN), the bias re-read after the wait was all zero, and the scores loaded matched the settled logits exactly. Every clean row had seen an all-zero bias before the wait. Upstream of the router the block input and the gate logits were finite; downstream, the expert kernel produced NaN in exactly those rows and the batch collapsed. #36811 already removed the fresh-zero trigger from plain softmax routing; dynamic-bias paths still need correctly ordered loads.Modifications
Move the wait above the bias load in both kernels so every dependent load sits after it. No change to routing semantics, dispatch, launch geometry, or the outgoing
gdc_launch_dependents(); theHAS_BIAS=Falseregister-zero path is unaffected.Accuracy / performance
On 1x DGX Spark (GB10), RadixArk/Qwen3.8-Flash-Next-NVFP4, NEXTN 3/1/4, radix cache off, 8 running requests, GSM8K 1319 (chat, greedy, 8192 max tokens) plus a churn client, on the pre-#36811 build
9b2aee2283where the collapse reproduced within 2 to 25 minutes in most runs: with only the Triton reorder applied (PDL on, zero-bias still allocated) the full set completed at 0.967 with 0 invalid answers, 0 NaN events, and 0 routing mismatches against a reference top-k recomputed after the kernel on the same stream. Two workarounds that also remove the dependency (PDL off for the kernel;SGLANG_OPT_USE_JIT_KERNEL_FUSED_TOPK=0) each completed the same set clean (0.970 / 0.971). Author-reported; the radix change was not exercised by these runs (softmax routing does not dispatch to radix).Cost: the bias load no longer overlaps the tail of the previous kernel, so a program pays that load's latency after the wait instead of hiding it. Per launch, not per row (rows run concurrently); zero on the softmax path with #36811 since no bias is loaded. Not measured here.
Follow-ups (not in this PR)
ld.globalin the PTX comes aftergriddepcontrol.wait.moe_fused_gate.pygrouped routing removes all tied group maxima when computing the second maximum (vals2 = tl.where(vals >= top1, ...)), so tied top-1 values yield the wrong group score. Pre-existing, unrelated to this change.kernels/jit/include/sgl_kernel/utils.cuh:170-172describes the PDL trigger as covering writes issued before it; it is a launch hint, and consumers must wait.Checklist
test/registered/kernels/ops/moe/test_moe_fused_gate.py,test/registered/kernels/ops/kimi_k3/test_compute.py)CI States
Latest PR Test (Base): ❌ Run #34108740533
Latest PR Test (Extra): ❌ Run #34108740503
Latest PR Test (AMD ROCm 7.2): ❌ Run #34108740687