Repository navigation
[Fix] moe: Renormalize per-rank shared-slot routing over routed experts only - #44904
Merged
Merged
Conversation
ch-wan
requested review from
BBuf,
DarkSharpness,
Edwardf0t1,
Fridge003,
HaiShaw,
HydraQYH,
Ying1123,
celve,
ispobock,
merrymercy and
yuan-luo
as code owners
October 17, 2026 14:12
This was referenced Oct 17, 2026
ch-wan
force-pushed
the
cheng/hot-fix/moe-prefolded-routed-scaling
branch
from
October 19, 2026 23:52
cdf052b to
8f95be3
Compare
ch-wan
requested review from
JustinTong0323,
fzyzcjy,
hnyls2002,
mickqian and
yhyang201
as code owners
October 19, 2026 23:52
Base automatically changed from
cheng/hot-fix/moe-prefolded-routed-scaling
to
main
October 19, 2026 23:54
ch-wan
force-pushed
the
cheng/hot-fix/per-rank-shared-slot-topk
branch
from
October 19, 2026 23:56
beb15ea to
39bdf12
Compare
1 of 2 tasks
2 of 5 tasks
detain
pushed a commit
to detain/sglang
that referenced
this pull request
Oct 29, 2026
…project#44901) cherry-pick -x 3a059de. PORT ORDER: landed FIRST in this family because its merged mainline chronology makes it the strict precondition of sgl-project#44904 (main merges: sgl-project#44901 16:54 -> sgl-project#44904 16:56 -> sgl-project#44912 16:57 -> sgl-project#44913 16:59; sgl-project#44904's diff preimage literally consumes fused_shared_expert_weight / is_routed_scaling_factor_fused_in_topk introduced here, and sgl-project#44913's preimage deletes the helpers sgl-project#44912 introduces -- the only cherry-pickable order is the mainline one; later commits in this family build on earlier ones). Conflict resolutions (2): * models/deepseek_v2.py: the pick's hunk anchors on `class MergedMoEOutput(MoEOutput)` as CONTEXT, but that class is upstream sgl-project#44657 content this fork never took (review-record skip family), so the region conflicted empty-vs-context. Kept ONLY sgl-project#44901's own addition: _scale_routed_and_add_shared (verified byte-identical to the upstream post-image function text) plus all nine routed_scaling_factor_fused_in_topk sites (two __init__ sets, the aiter shared-scaling compensation block, two non-CUDA post-MoE-multiply guards, and the forward_deepep/state-based forward replacements). Nothing in this tree references MergedMoEOutput; did not smuggle it in. * test/registered/unit/layers/moe/test_mega_moe_decode_capacity.py: modify/delete conflict (the file was created by sgl-project#44657, absent here; sgl-project#44901 merely touched 1 line inside it). Kept it deleted. Fidelity: 7/12 python files are BYTE-IDENTICAL to the upstream post-image (topk.py, hash_topk.py, waterfill.py, moe_ep_setup.py, glm4_moe.py, glm4_moe_lite.py, vl_routing.py). mega_moe/mega_moe_flydsl/bailing_moe_v3/ dots3 landed inside files carrying nemotron-era (sgl-project#44518) upstream drift we do not have; their fold changes were diff-reviewed line by line (old `should_fuse_routed_scaling_factor_in_topk or _use_aiter` predicates replaced by the new config-level predicate; residual old-form reads match upstream's own post-image -- legitimately kept). WaterfillBalancer's routed_scaling_factor kwarg became shared_weight (one production constructor, moe_ep_setup, updated by the pick; no other call sites in the tree). Battery (CVD=9) vs base 0df18e9: test_fused_shared_expert_scaling.py 6P -> 12P/25sub (upstream's +343-line fold matrix, green); test_hash_topk.py 3P -> 4P; test_jit_grouped_topk 10S -> 12S (CUDA-gated adds); every pin held (standard_dispatcher_runner_backend 12P, smallm 28P/66sub, b12x 67P/46sub, kernels_namespace 76P, model_overrides 111P/1S/179sub, modelopt suites exact). Pre-existing classes unchanged (trtllm-gen 8F, fused_gate 46F, round-robin 37F, triton-fused-moe 96F, mem_pool_ep 1F, mega_moe_deepgemm 1F). ruff --select=F401,F821,F823,UP037 + ruff format clean on all 18 files. Fold-predicate e2e on real NVFP4 weights stays GPU-window debt (the standalone CPU contract probe follows this family as its own commit). (cherry picked from commit 3a059de) Co-authored-by: port-moe-contracts subagent (cherry picked from commit c4511d515077ea018200eeccfff256cc25ff18e5)
detain
pushed a commit
to detain/sglang
that referenced
this pull request
Oct 29, 2026
…ts only (sgl-project#44904) cherry-pick -x 48ea486. Landed second of the renorm pair: mainline makes sgl-project#44904 build ON TOP of sgl-project#44901 (its diff preimage contains fused_shared_ expert_weight/is_routed_scaling_factor_fused_in_topk), so the family follows the mainline order (see sgl-project#44901's note here). Wrong-output class: before this, the fused per-rank shared-slot paths asked the gate for top_k while num_fused_shared_experts markers also went through, renormalizing over one routed expert too many (routed sum 0.85-0.97 on CUDA). Conflict resolution (1): test/registered/kernels/ops/moe/test_moe_fused_gate.py carries pre-sgl-project#44690 docstring drift here (this fork never took sgl-project#44518's nemotron rework of that file's header); took the upstream post-image's new one-line docstring, kept this fork's file otherwise. The _reference_gate renorm-order fix, the renorm x apply_scale parametrization, and the production-impl comparison changes all landed. Fidelity: topk.py, hash_topk.py, state_capturer/routed_experts.py, test_fused_append_remap_per_rank_shared_slots.py and test_fused_shared_expert_scaling.py are BYTE-IDENTICAL to the upstream post-image; _router_triton_kernel (kernels/ops/moe/moe_fused_gate.py) verified byte-identical function body against upstream (the file's OTHER regions carry this fork's jit-loader drift and were not touched); fused_moe_triton_kernels.py hunks are docstring-only. Battery (CVD=9) vs sgl-project#44901 commit: test_fused_shared_expert_scaling 12P -> 13P/25sub; waterfill_eplb 4P -> 5P; scattered_a2a 3P -> 5P/2sub (the new routed-width capture case); hash_topk 4P held; append 6S -> 9S, padded 10S -> 11S (CUDA-gated adds); moe_fused_gate 46F -> 52F = the file's pre-existing no-CUDA class +6 new parametrized members (collects clean; needs hardware); kernels_namespace 76P (inventory guard, nothing added/removed); every other pin exact. ruff + format clean on all 11 files. Remap-on-CUDA numerics (torch.cat width change + padded-row remask) remain GPU-window debt, pinned at contract level by the fold-predicate probe commit that follows. (cherry picked from commit 48ea486) Co-authored-by: port-moe-contracts subagent (cherry picked from commit 0432a8fce0b4919b4a85fc6d3e724c6de30189be)
detain
added a commit
to detain/sglang
that referenced
this pull request
Oct 29, 2026
…lass NVFP4 route Standalone CPU contract test pinning WHAT WE NOW BELIEVE after the two fold fixes (sgl-project#44901 + sgl-project#44904) land on our production FP4 arm -- ModelOptNvFp4FusedMoEMethod on a FLASHINFER_CUTLASS runner, the backend auto resolves to on SM110/SM120 (and the value entry 230's fork patch publishes for it): * FusedMoE._fuses_routed_scaling_factor_in_topk answers True for NVFP4 on every non-Marlin runner (and False on Marlin) -- the predicate list sgl-project#44901's shared-slot fix keys off; no NVFP4 class-level fuse_routed_scaling_factor_in_topk flag exists, so the verdict comes from the runner arm. * topk.is_routed_scaling_factor_fused_in_topk (sgl-project#44901) agrees via apply_routed_scaling_factor_on_output. * topk.fused_shared_expert_weight (sgl-project#44901) therefore writes the fused shared column 1.0 -- not 1/routed_scaling_factor -- exactly where sgl-project#44904 moved the kernel/append fill to AFTER routed renormalization. 8 passed here (import/config level only, register_cpu_ci base-a-test-cpu). Red proof of newness: is_routed_scaling_factor_fused_in_topk and fused_shared_expert_weight do not exist at the port base (0df18e9), so the import alone fails pre-pick -- this file cannot pass without sgl-project#44901/sgl-project#44904 landed. GPU-WINDOW DEBT, explicitly NOT covered here: the end-to-end double-scale numeric check (generate against a single-scale reference on 4xSM120 with the real checkpoint; sgl-project#44901's routed-sum 0.85-0.97 symptom and the shared-contribution 1.0x property only OBSERVABLE on hardware). Test-side of the same property: kernels/ops/moe/test_moe_fused_gate.py's new renorm x apply_scale parametrizations are CUDA-gated (52F here, the file's pre-existing device class at base was 46F -- same class, +6 new). Co-authored-by: port-moe-contracts subagent (cherry picked from commit 22ae8e7e609fce945cdc85d3f985a253cfba1aa2)
detain
pushed a commit
to detain/sglang
that referenced
this pull request
Oct 29, 2026
…gl-project#44912) cherry-pick -x 964ab2b. Applies CLEANLY (zero conflicts): the fork never edited topk.py/hash_topk.py since merge sgl-project#4, and sgl-project#44901/sgl-project#44904's post-images (landed in the two preceding commits) exactly reconstruct upstream's preimage at the touched lines. Semantics: TopKConfig.num_fused_shared_experts (the count of fused shared experts) splits into num_shared_topk_slots (the trailing columns the ROUTER writes -- zero when Waterfill appends the shared expert after routing), and TopK.__init__ now derives top_k for the config as routed-per-token + slots instead of mutating the caller's numbers. New helpers waterfill_ routes_shared_experts / shared_topk_slots live in moe/utils.py; the enable_waterfill branch stops silently redefining top_k (waterfill now forces STANDARD output format explicitly). Entry-247 interaction preserved: b12x's _keeps_global_expert_ids and the slot-count vocabulary this family touched before were not in sgl-project#44912's hunk regions; moe/utils.py post-image differs from upstream ONLY by the 7 b12x enum/predicate lines already in the tree (git diff vs 964ab2b: +7/-0), so the split landed upstream-identical and test_b12x_w4a16_route stayed 67P/46sub unchanged. test_manual NPU file (test_npu_topk.py) took the 1-line rename too (hardware_ backend/npu/moe/topk.py likewise); NPU paths are inert here. Battery (CVD=9) vs sgl-project#44904: waterfill_eplb 5P -> 6P/2sub (upstream's new waterfill slot case); everything else byte-stable (scaling 13P/25sub, hash 4P, append 9S, padded 11S, jit 12S, smallm 28P/66sub, dispatcher 12P, b12x 67P/46sub, kernels_namespace 76P, trtllm-gen 8F class). ruff + format clean. NUM_SHARED_TOPK_SLOT wiring at runtime (per-rank append widths on real dispatch) stays GPU-window debt, same as the renorm pair above. (cherry picked from commit 964ab2b) Co-authored-by: port-moe-contracts subagent (cherry picked from commit ad00ea48739c10c0345f572bed8d8aad818c0169)
detain
added a commit
to detain/sglang
that referenced
this pull request
Oct 30, 2026
…ies 28,165,166,186,194,206,296,297,299,300
PER-MARKER TRUTH (census: B5 markers in this set now ZERO):
kernels/aot topk.cu (28): VERIFIED-ABSORBED -- upstream's in-range
legacy_radix_topk::select rewrite gates the threshold-bin population
against the stash BEFORE staging (coarse_population >= StashEntries ->
full-row byte-per-pass refinement), structurally fixing the overflow
the fork patched after-the-fact; jit twin keeps its s_prefix_bins.
kernels/ops/attention/utils.py (194): VERIFIED-ABSORBED -- the function
re-sited to ops/attention/kv_indices.py (spec_kv_index_token_blocks with
the 32768 gate) and every consumer imports it at that module; the saved
utils re-export would be a dead hunk (zero importers).
elementwise/elementwise.py (166): VERIFIED-PRESENT -- the batch-invariant
num_warps clamp rides upstream's guard_or_false refactor ('or
is_batch_invariant_mode_enabled()' intact).
kvcache/__init__.py (186): VERIFIED-PRESENT -- the store_cache_fp8
registry entry is the merge commit's own graft (entry 310), imports +
capabilities in place.
moe/hash_topk.py + moe/topk.py (296/297/299/300): the merged upstream
canonicals of sgl-project#44901/sgl-project#44904/sgl-project#44912/sgl-project#44913 are IN RANGE (all four
MERGED commits proven ancestors of 2a6c204); every fork symbol
(fused_shared_expert_weight, is_routed_scaling_factor_fused_in_topk,
num_shared_topk_slots x54, has_per_rank_fused_shared_slots,
SharedExpertsConfig wiring) verified present. Markers stale.
moe/flashinfer_trtllm.py (165): SUPERSEDED-BY-EQUIVALENT -- upstream's
own in-range rewrite of the deferred block covers the routed triple
(do_finalize=not defer + _make_deferred_finalize_output) fail-LOUDER
(valid_deferred_input raise); the fork's sgl-project#6077 ignored-autotune-buffer
targeted the OLD block shape and is deliberately NOT restored (GPU
perf-only, defer+routed is not our production arm).
moe/mega_moe.py (296): deep_gemm transform_sf redirect RE-PLANTED to the
guarded wrapper (import-safety contract; sibling fix in dsv4 below).
fused_moe_triton/layer.py (165/206/239): _built_runner_backend hunk
ABSORBED by sgl-project#44993 (config carries the built runner); the entry-206
supports_routed_deferred_finalize property RE-PLANTED (upstream's
packed_nvfp4_routed_deferred covers TRTLLM only, NOT trtllm_routed --
our runner arm), with the sgl-project#44993 2-arg fold predicate; the debug-log OR
re-plant paired.
models/deepseek_v2.py (296/297/300/236): fold chain verified; the
entry-206 format-ternary consumer arm RE-PLANTED (BYPASSED ->
supports_deferred_finalize, else supports_routed_deferred_finalize --
restoring what sgl-project#44993's rewrite dropped).
Battery ACTUALS (CVD=9, prod venv, PYTHONPATH=python, import sglang OK):
fused_shared_expert_scaling + mega_moe_deepgemm_api + moe_runner_
extensions + topk_routing_inputs + model_overrides + kernels_namespace:
GREEN (see B5-4 commit for full 43-file battery actuals)
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.
This PR is part of a stack (oldest at bottom):
Motivation
With fused shared experts in per-rank slots (
--enforce-shared-experts-fusionwith a DeepEP-class ormegamoea2a backend), every MoE layer renormalized its routed weights over one routed expert too many on CUDA, so the routed experts it kept summed to about 0.85-0.97 instead of 1.0.How it happened:
select_expertsgives the router no shared slot (num_fused_shared_experts=0), because_post_process_topk_idsappends the shared column afterwards. On aiter it also gives the router the routed count,top_k - num_fused_shared_experts. Off aiter every router call kepttop_k(num_routed_topk if _use_aiter else top_k), which counts the shared slot.top_krouted experts, one more than the model'snum_experts_per_tok, and renormalized their weights to sum to 1.remap_topk_for_per_rank_shared_slotsthen overwrote the last column with the home-rank shared expert.sum(top K) / sum(top K+1), per token, in every layer. The CUDA routers return their top-k sorted by score, so the kept experts are the right ones; only their weights shrink.#36515 (non-grouped routers) and #40943 (grouped routers) gave the router 0 shared slots on this path. That fixed aiter, where the router already gets the routed count, and left CUDA in the state above. Before them, CUDA gave the router
top_kwith the shared slot, which selects the routed count and renormalizes over it.Kept routed weight sum per token, one H200, 64 tokens of random logits, EP8, rank 3, one shared expert:
HashTopK(DeepSeek-V4 hash layers), torch and fused kernelIn every row the routed experts equal routing without fusion (64/64 rows) before and after; the shared column is the home-rank slot with weight
1/routed_scaling_factor.Affected: CUDA, and other platforms without aiter, with shared-experts fusion on a per-rank backend (
deepep,mooncake,mori,pplx,megamoe); for the DeepSeek family that takes--enforce-shared-experts-fusion. The original per-rank normalization bug does not affect aiter, Waterfill, the global fused slot (no a2a backend), runs without fusion,HashTopKand the DeepSeek-V4.1 vision routing, whose routers already select the routed experts alone.Two related problems on the same path are fixed here too:
renormalize.fused_topk_deepseekalways normalizes the selected weights, and its ninth parameter islaunch_with_pdl, which sglang's wrapper calledrenormalize. With routed-only sizing the per-rank top-8 path now fits that router (top-9 did not), so arenormalize=Falseconfiguration would have been normalized. Configurations without fusion or with the global slot already were, within that router's shapes. Grouped sigmoid routing with a bias, 256 experts in 8 groups, top-8,renormalize=False, zero logits (each raw weight is sigmoid(0) = 0.5): before this PR 0.125 per expert without fusion and 0.5 with a per-rank slot; with this PR's first version 0.125 for both; now 0.5 for both.-1to-1 - num_fused_shared_expertsand gave it a live home-rank shared slot, so padded rows did not leave top-k as-1.HashTopKand the vision routing already mask after their append.The
renormalize=Falseguard also exposes the Triton global shared-slot path. That path previously wrotesum(selected routed scores) / rsfbefore normalization, so disabling normalization made the shared output depend on the routed-score sum (4x for eight zero-logit sigmoid selections). It now writes a shared weight of 1 when rsf is folded, otherwise 1/rsf, after processing the routed weights. The dynamic-bias torch route receives the corresponding repair from #44901.Modifications
select_expertsdecides once, for every router, what the router is asked for: where_post_process_topk_idsappends the shared columns (aiter, per-rank slots), the routed count with no shared slot; otherwisetop_kwith the shared slots, as before. The values are unchanged on aiter, on the global slot and without fusion.remap_topk_for_per_rank_shared_slotstakes the routed columns and appends the shared ones, the contractfused_append_remap_shared_experts_deepepalready has on aiter. Routers that still write shared placeholders (the per-token-bias router, the DeepSeek-V4.1 vision routing,HashTopK) have them dropped before the append. On CUDA the EPLB remap splits off shared columns only when the router wrote them, and padded rows are masked again after the append.biased_grouped_topk_gputakes the FlashInfer branch only when renormalizing; otherwise the Triton router, which honorsrenormalize, serves the call. The wrapper's last parameter is renamedlaunch_with_pdl, and the global shared columns that branch appends takefused_shared_expert_weight(...).RoutedExpertsCapturer.capturepads routed-only ids (the device width minus the fused shared experts) to its device width and raises on any other width; the host side already keeps onlynum_experts_per_tokcolumns. The per-rank path now captures routed-only ids, and without the pad--enable-return-routed-expertswould fail with a shape mismatch.Tests
On one H200 (driver 595.71.05, torch 2.14.1+cu130, flashinfer 0.7.2, triton 3.8.0), for this branch (
03d13ebf8b), its base #44901 (bd4de12cfa) and this PR's previous head merged with that base:test/registered/unit/layers/moe/test_fused_shared_expert_scaling.py::test_per_rank_slot_routes_like_no_fusion(CPU):select_expertswith a per-rank slot keeps the routed experts and weights of routing without fusion, summing to 1. Fails on the base, passes here.test/registered/moe/test_fused_append_remap_per_rank_shared_slots.py(CUDA):TestSelectExpertsPerRankSharedSlots::test_routes_like_no_fusion: the same property through five CUDA routers (the DeepSeek-V3 grouped router, the Triton grouped fallback, torch grouped routing, the DeepSeek-V4 sqrtsoftplus router, softmax). Fails on the base for all five, passes here.test_unnormalized_weights_stay_raw: withrenormalize=Falsethe routed weights equal the raw sigmoid scores of the reference selection, with and without a per-rank slot. Fails on the previous head (both cases) and on the base (without fusion), passes here.test/registered/moe/test_topk_padded_region.py::TestSelectExpertsPerRankSharedSlotPadMask(CUDA): padded rows leave the per-rank path fully-1, for a router masked after routing and one that masks itself. Fails on the base and on the previous head, passes here.test/registered/unit/eplb/test_waterfill_eplb.py::test_eplb_remaps_every_column_of_routed_only_per_rank_ids(CPU): EPLB remaps every routed column of routed-only per-rank ids, and the recorder sees them. Fails on the base, passes here.test/registered/unit/state_capturer/test_routed_experts_scattered_a2a.py::TestCaptureRoutedIdsOnly(CPU): routed-only ids land in the routed columns, with and without the DeepEP gather; any other width raises. Fails on the base (shape RuntimeError) and, for the rejection case, on the previous head; passes here.test_fused_shared_expert_scaling.pyand the kernel/eager equivalence case pass routed-only columns.test/registered/unit/layers/moe/,unit/models/test_deepseek_*.py,unit/models/test_shared_experts_fusion_gates.py,unit/eplb/,unit/state_capturer/,moe/test_hash_topk.py) plus the GPU routing tests (moe/test_topk_padded_region.py,moe/test_topk_renormalize_degenerate.py,kernels/ops/moe/test_jit_grouped_topk.py,kernels/ops/moe/test_moe_fused_gate.py,kernels/ops/moe/test_round_robin_ep_balance.py,moe/test_fused_append_remap_per_rank_shared_slots.py): 55 files, each in its own process, CPU-registered ones with the GPUs hidden. All pass on the base and on this branch.select_experts,HashTopKandvision_topkwith the real kernels, on the base, the previous head and this branch.Additional validation at
4676743e28(one H200, same library versions):renormalize=Falseregression fails against the previous production code for both folding modes (4x shared coefficient) and passes after the kernel repair.Owed
--moe-a2a-backend deepep --enforce-shared-experts-fusion, or DeepSeek-V4 on MegaMoE (SM100). Not run.CI fixture follow-up at
beb15ea999The ROCm 10 MI35x job failed at
faaf4bb4e6becausetest_deepseek_v4_amd_tp4.py::_moe_stubbypassesDeepseekV2MoE.__init__and omittedrouted_scaling_factor_fused_in_topk. The stub now supplies that construction state. The other five test files in that partition passed. This fixes the test fixture; production executable behavior is unchanged.On one H200, the original fixture reproduces the same AttributeError with the AITER epilogue flags forced; after repair, all four normal/dual-stream and shared-TP1 combinations pass through the actual forward methods. This checks the Python fixture/epilogue contract, not native ROCm collectives or graph replay. The complete AMD TP4 file still needs CI validation.
The kernel comparison now covers normalization and explicit folding independently; the complete standalone router test file passes all 52 cases on one H200. Its stale normalization explanation and CUDA JIT parity claim are corrected. The CUDA counterpart retains its existing unnormalized shared-weight behavior; caller search found tests and benchmarks, with no serving caller.
TopKConfig documents its total ordinary output width and the global AITER absolute shared coefficient versus the standard-path multiplier. Eager/fused subtest labels include the folding mode. Duplicate ordinary model-context scopes and a derived routed-count property remain optional follow-ups. Applicable pre-commit hooks pass. No native AMD rerun or model accuracy result is claimed for this follow-up.
Landing cleanup (2026-10-19)
Current head:
39bdf12a49, rebased onto main after #44901 merged.Trimmed repetitive comments and docstrings. Configured cleanup checks passed before landing. Rebase resolution keeps main's removal of obsolete CUDA-JIT comparison prose and preserves routed-only append behavior, including empty output widths. Earlier test results above retain their original revisions; no new accelerator or full-model run is claimed for this cleanup/rebase.
The broader routing matrix moves to opt-in Extra coverage in #44913; the focused CPU and append/remap regressions remain on the normal path.
CI States
Latest PR Test (Base): ❌ Run #39344592460
Latest PR Test (Extra): 🚫 Run #39344592427
Latest PR Test (AMD ROCm 10): ❌ Run #39344592456