Skip to content

[Fix][AMD] Qwen3.5 MoE: disable global-slot shared-expert fusion under per-rank EP backends (MoRI + dp-attention init crash) - #31793

Merged
HaiShaw merged 2 commits into
sgl-project:mainfrom
yichiche:fix/qwen35-mori-shared-expert-slot-31336
Jul 26, 2026
Merged

HaiShaw merged 2 commits into
sgl-project:mainfrom
yichiche:fix/qwen35-mori-shared-expert-slot-31336

Conversation

@yichiche

@yichiche yichiche commented Jul 20, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

Fixes #31336.

Serving Qwen/Qwen3.5-397B-A17B-FP8 (512 routed + 1 fused shared expert) with --moe-a2a-backend mori --enable-dp-attention on MI355X/ROCm crashes at model init. Every scheduler rank aborts in FusedMoE.__init__:

File ".../srt/layers/moe/fused_moe_triton/layer.py", line 227, ...
    assert (num_experts - num_shared_slots) % self.moe_ep_size == 0
AssertionError

With --enable-dp-attention, moe_ep_size is promoted to tp_size (8). has_per_rank_fused_shared_slots() is True for MoRI (deepep-class), so num_shared_slots = 1 * 8 = 8, and (513 - 8) % 8 = 505 % 8 = 1 != 0.

Root cause (and why the layout matters)

MoRI's dispatcher (token_dispatcher/moriep.py) masks experts with a contiguous per-rank block: expert_mask[ep_rank * num_local_experts : + num_local_experts] = 1. This is valid only when the blocks tile the id space exactly, i.e. num_experts == num_local_experts * ep_size, which requires per-rank shared slots (one shared slot per EP rank). DeepSeek-V2 already builds num_experts this way for per-rank backends.

Qwen's shared-expert fusion, however, is a global-slot design: Qwen2MoeSparseMoeBlock constructs its TopK without num_fused_shared_experts (so the per-rank remap in topk.py never fires) and instead appends the shared expert manually at a single global id (_append_shared_to_topk_output, base id N == num_experts) with a per-token shared_expert_gate. This global-slot-with-gate layout cannot be expressed in MoRI's per-rank contiguous tiling, and it can't reuse DeepSeek's (gate-less) per-rank remap machinery.

The alternative floated in the issue — treating MoRI as "global" (num_shared_slots = 1) — passes the __init__ assertion but then breaks MoRI's contiguous dispatch mask (num_local_experts * ep_size = 65 * 8 = 520 != 513, so rank 7's block runs out of bounds) and silently misroutes experts.

Modifications

python/sglang/srt/models/qwen2_moe.py: when shared-expert fusion is enabled and a per-rank EP shared-slot backend is active (uses_per_rank_fused_shared_slots()) and moe_ep_size > 1, disable the fusion and fall back to the separate (correct) shared-expert MLP path. A one-time warning is logged. moe_ep_size == 1 and non-per-rank backends are unaffected (fusion stays on).

This is the correct-by-construction fix: with fusion off, num_experts = 512 tiles exactly (64 * 8), the shared expert is computed by the dense shared_expert MLP (with its gate) and added after the experts. The fully-fused per-rank shared expert with a per-token gate is a larger enablement task (needs a per-rank remap+append kernel that also applies the gate) and is left as follow-up.

Accuracy Tests

GSM8K sweep on Qwen3.5-397B-A17B-FP8 (MI355X), with this PR applied so the MoRI configs get past the #31336 init crash. Flags added one at a time. Accuracy = correct fraction, Invalid = unparseable/garbage fraction.

┌─────┬───────────────────────────────────────────┬──────────┬─────────┬───────────────┐
│ Run │                Added flag                 │ Accuracy │ Invalid │    Verdict    │
├─────┼───────────────────────────────────────────┼──────────┼─────────┼───────────────┤
│ R0  │ (baseline)                                │    0.925 │   0.005 │ ✓             │
├─────┼───────────────────────────────────────────┼──────────┼─────────┼───────────────┤
│ R1  │ --kv-cache-dtype fp8_e4m3                 │    0.915 │   0.010 │ clean         │
├─────┼───────────────────────────────────────────┼──────────┼─────────┼───────────────┤
│ R2  │ --enable-dp-attention --enable-dp-lm-head │    0.935 │   0.000 │ clean         │
├─────┼───────────────────────────────────────────┼──────────┼─────────┼───────────────┤
│ R3  │ --moe-a2a-backend mori                    │    0.010 │   0.420 │ ← breaks      │
├─────┼───────────────────────────────────────────┼──────────┼─────────┼───────────────┤
│ R4  │ mori + fp8-KV                             │    0.010 │   0.455 │ breaks (mori) │
├─────┼───────────────────────────────────────────┼──────────┼─────────┼───────────────┤
│ R5  │ full (dp-attn + mori + fp8-KV)            │    0.000 │   0.415 │ breaks (mori) │
└─────┴───────────────────────────────────────────┴──────────┴─────────┴───────────────┘

Reading:

  • This PR's scope (init crash) is resolved: the MoRI + dp-attention configs now load and run instead of aborting in FusedMoE.__init__.
  • No regression on the clean paths: baseline (R0), fp8-KV (R1), and --enable-dp-attention alone (R2, 0.935) are all healthy. R2 is the path this PR most directly enables and it is clean.
  • A separate MoRI issue remains: every run that adds --moe-a2a-backend mori (R3/R4/R5) collapses to ~0.01 accuracy with a high invalid rate, independent of dp-attention and of fp8-KV. That is a MoRI all-to-all correctness problem, out of scope for this init-crash fix and tracked separately (likely part of the ROCm-MoRI enablement in [bug] ROCm DI MoRI for non-standard attention (such as DeepSeekv4, GLM5 & Qwen3.5) doesnt work #21886). This PR is a prerequisite that lets that issue be reached and debugged at all.

Assertion-level validation (mocked MoRI, ep_size=8): pre-fix num_experts=513, num_shared_slots=8 → assertion fails; with fix, fusion disabled → num_experts=512, num_shared_slots=0 → assertion passes and MoRI's contiguous mask tiles [0, 512) exactly. ep_size==1 and non-per-rank backends keep fusion (no behavior change).

Checklist


CI States

Latest PR Test (Base): ✅ Run #29797490050
Latest PR Test (Extra): ❌ Run #29797489930

…r per-rank EP backends

Qwen shared-expert fusion appends the shared expert at a single global slot
(_append_shared_to_topk_output, base id N == num_experts) with a per-token
shared_expert_gate. That global-slot layout is incompatible with per-rank EP
shared-slot backends (DeepEP/Mooncake/MoRI/MegaMoE) once moe_ep_size > 1, which
tile the expert id space into contiguous per-rank blocks
(num_experts == num_local_experts * ep_size) and thus require per-rank shared
slots.

Mixing the two trips FusedMoE's (num_experts - num_shared_slots) % ep_size == 0
assertion at init for Qwen3.5-397B-A17B-FP8 with --moe-a2a-backend mori
--enable-dp-attention (513 experts, 8 shared slots: 505 % 8 != 0), and even past
the assertion would misroute experts. Fall back to the separate shared-expert
MLP there, which is correct.

Fixes sgl-project#31336

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request disables Qwen shared-expert fusion when using per-rank EP shared-slot backends (such as DeepEP or MoRI) with a moe_ep_size greater than 1, falling back to a separate shared-expert MLP to prevent expert misrouting and assertion failures. The reviewer suggested storing get_parallel().moe_ep_size in a local variable to improve readability and avoid duplicate calls.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread python/sglang/srt/models/qwen2_moe.py Resolved
@yichiche yichiche added the run-ci CI: run the baseline test suite on this PR label Jul 21, 2026

@kkHuang-amd kkHuang-amd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please resolve the gemini comment.

LGTM for others

@yichiche

yichiche commented Jul 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

Please resolve the gemini comment.

LGTM for others

Thanks KK. Gemini reply is asking for a small refactor, I'd prefer to keep the current form: the if guard reads clearly with get_parallel().moe_ep_size > 1 inline, and repeating it in the log makes the logged value explicit without an extra local. This runs once at init, so the duplicate call is negligible.
It's a minor stylistic choice rather than a functional or safety concern, so I'll leave it as-is.

@HaiShaw HaiShaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please comment on the reason why issue happens on AMD, code change is generic to cover NV path? If not, please add comment for clarification.

@HaiShaw

HaiShaw commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

@amd-bot ci-status

@amd-bot

amd-bot commented Jul 21, 2026

Copy link
Copy Markdown

@HaiShaw

CI Status for PR #31793

Merge verdict: Not green — do not merge on CI signal yet. Zero executed failures are attributable to this PR, but AMD PR CI is incomplete (stage-b/stage-c were fast-fail skipped because wait-for-stage-a-amd timed out — a runner-queue backlog, not this PR) and none of the completed CI actually exercises this PR's changed code path (an AMD MoRI + dp-attention–specific guard). Green here proves nothing about the fix.

Caution

This PR's changed code is not exercised by any completed PR-CI test, and AMD CI is incomplete. The fix only fires under uses_per_rank_fused_shared_slots() (DeepEP/MoRI) + moe_ep_size > 1 — an AMD/EP path. The AMD run's wait-for-stage-a-amd timed out at 07:05 UTC because the AMD stage-a runner was still queued and didn't start until 09:11 UTC, so all AMD stage-b/stage-c jobs were skipped. NVIDIA/CPU jobs that passed do not reach the MoRI per-rank shared-slot path. Author should re-run AMD PR CI once runners free up, and ideally reproduce the original crash config (Qwen3.5-397B-A17B-FP8, --moe-a2a-backend mori --enable-dp-attention on MI355X) to confirm the fix.

Changed files: python/sglang/srt/models/qwen2_moe.py (+15/-0) — new import resolves (uses_per_rank_fused_shared_slots at python/sglang/srt/layers/moe/utils.py:386).

Executed CI failure attribution: AMD: 1 executed failure (0 related) + downstream skipped · Others: 2 executed failures (0 related) · plus 4 opt-in "extra" gate/finish cascades (expected). NVIDIA base-c-test-4-gpu-h100 (2 shards) still pending — not counted as passed.

AMD Executed Failures

Job Test File Test Function Error Related? Why
wait-for-stage-a-amd N/A N/A Timeout waiting for stage-a-amd jobs (0/1 completed, 240 attempts) 🟢 Infra/runner-queue backlog: stage-a-test-1-gpu-small-amd succeeded but only started 09:11 UTC, 2h after the gate's 07:05 timeout. Cascades to pr-test-amd-finish and skips all AMD stage-b/c.

Other Executed Failures

Job Test File Test Function Error Related? Why
build-test (Arm64) test/registered/cpu/test_rope.py import-time ImportError: cannot import name 'apply_rotary_pos_emb_native_eager' (log L1381) 🟢 CPU rope test; the installed wheel lacks a symbol added by #30868. Unrelated to a MoE shared-expert change.
build-test (xeon-gnr, base-b-test-cpu) test/registered/cpu/test_rope.py import-time same ImportError (log L2183) 🟢 Same CPU-arch packaging mismatch on PR Test (Xeon). Not the PR's code.

The remaining 4 red checks are opt-in "extra" workflow gates, not real failures: call-gate / pr-gate + pr-test-amd-extra-finish and call-gate / pr-gate + pr-test-extra-finish all exit with Missing required label 'run-ci-extra' — expected until the PR is opted into the extra suites.

Details / what to do before merge


Generated by amd-bot using Claude Code CLI

@yichiche

yichiche commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Please comment on the reason why issue happens on AMD, code change is generic to cover NV path? If not, please add comment for clarification.

@HaiShaw This is a Qwen model issue, not device-specific, so it also hits NVIDIA: Qwen's global-slot shared-expert fusion is incompatible with per-rank EP backends (DeepEP/Mooncake/MoRI) at ep_size > 1. The fix detects that combo and falls back to the separate shared-expert MLP on both AMD and NVIDIA.

@HaiShaw

HaiShaw commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

@yichiche Can you upload the config that triggers issue on NV? Which can help to clarify the mixture of MoRI description and generic code changes above.

@HaiShaw
HaiShaw merged commit 833e1bc into sgl-project:main Jul 26, 2026
138 of 146 checks passed
Atituiset pushed a commit to Atituiset/sglang that referenced this pull request Sep 10, 2026
…r per-rank EP backends (MoRI + dp-attention init crash) (sgl-project#31793)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-ci CI: run the baseline test suite on this PR

Projects

None yet

4 participants