Skip to content

Support --moe-a2a-backend deepep_v2 for MiMoV2 - #39080

Open
whn09 wants to merge 1 commit into
sgl-project:mainfrom
whn09:fix/mimo-v2-deepep-v2-a2a
Open

Support --moe-a2a-backend deepep_v2 for MiMoV2#39080
whn09 wants to merge 1 commit into
sgl-project:mainfrom
whn09:fix/mimo-v2-deepep-v2-a2a

Conversation

@whn09

@whn09 whn09 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Motivation

MiMoV2MoE gates its EP path on three a2a backends and omits deepep_v2:

if (
    get_moe_a2a_backend().is_deepep()
    or get_moe_a2a_backend().is_mooncake()
    or get_moe_a2a_backend().is_ascend_fuseep()
):

so --moe-a2a-backend deepep_v2 on a MiMoV2 model silently builds the non-EP MoE — the
dispatcher is never constructed, self.ep_size stays 1 and _enable_a2a_moe stays False. The
server starts and answers, so nothing looks wrong; the flag is just inert.

deepseek_v2.py and kimi_k3.py already list is_deepep_v2() immediately after
is_deepep() in the same two places. This is the same two-line addition for MiMoV2, plus the
architecture whitelist entry that validate_deepep_v2_model_architecture requires — without
which the launch is rejected outright, which is why both files have to land together.

MiMo-V2.5-Pro is a good fit for the v2 path: 384 experts, top-8, hidden_size=4096, FP8
checkpoint, so it is EP-heavy in exactly the regime deepep_v2 targets.

Modifications

  1. models/mimo_v2.py: or get_moe_a2a_backend().is_deepep_v2() after is_deepep() in both
    the self.ep_size block and the self._enable_a2a_moe assignment. Ordering matches
    deepseek_v2.py.
  2. arg_groups/moe_hook.py: "MiMoV2ForCausalLM" added to
    validate_deepep_v2_model_architecture's validated_architectures, in alphabetical
    position.

Deliberately not claiming MiMoV2FlashForCausalLM. It shares this file and would inherit
the gate change, but I have only validated the Pro checkpoint, and this whitelist means
"someone has actually run it". Flash can be added by whoever measures it.

Accuracy Tests

Numerics against the DeepEP v1 path on the same weights and prompts — v1 is the right
control, since it is the established EP dispatcher for this model and any difference is then
attributable to the dispatcher rather than to EP-vs-not.

XiaomiMiMo/MiMo-V2.5-Pro (FP8), 2 nodes × 8 B200, TP=EP=16, per-token logprobs over a
fixed 12-prompt set, greedy:

result
10 / 12 prompts bit-identical logprobs, v1 vs v2
2 / 12 prompts differ — and they are the only two prompts longer than the v2 arm's prefill chunk, i.e. the two that took a different chunking path, not a different MoE result

Sanity-checked separately that the flag is actually doing something after the patch:
Initialized DeepEP v2 ElasticBuffer: world_size=16 hidden_size=4096 num_topk=8 ... appears
in the log, which it does not on unpatched main.

Speed Tests and Profiling

Same fleet, 2 nodes × 8 B200 per role, PD-disaggregated, one image, deep_gemm runner on
every arm so the dispatcher is the only variable.

Decode, server-side ms/token per request (lower is better), max_running_requests=512:

global batch deepep_v2 none (plain TP)
32 34.08 30.56
64 37.06 39.01
128 39.44 45.81
256 44.24 51.24
512 49.99 62.34 (v2 +24.7%)

The slope is the point: over a 16× batch deepep_v2 grows 1.47×, plain TP 2.04×.

Prefill, input tok/s at matched delivered chunk 16384: 73 836 (deepep_v2) vs 35 541
(none) — 2.08×. This needs SGLANG_DEEPEP_V2_NUM_MAX_DISPATCH_TOKENS_PER_RANK raised
well above its default for the prefill role; at a small value v2 prefill looks about half as
fast, which is a capacity bound rather than a property of the path.

One caveat worth recording, since it is not obvious: with --moe-a2a-backend deepep_v2 the
MiMoV2 config-time override rewrites moe_runner_backend autoflashinfer_trtllm on
sm100, and flashinfer_trtllm has no fused func registered for deepep_v2, so model init
dies with Runner backend MoeRunnerBackend.FLASHINFER_TRTLLM requires a fused func for a2a backend deepep_v2, but none is registered. Either pass --moe-runner-backend deep_gemm
explicitly, or take the one-line guard in the companion PR (linked below). Every number above
is with deep_gemm.

Checklist

Review and Merge Process

  1. Ping Merge Oncalls to start the process. See the PR Merge Process.
  2. Get approvals from CODEOWNERS and other reviewers.
  3. Trigger CI tests with comments or contact authorized users to do so.
  4. After green CI and required approvals, ask Merge Oncalls or people with Write permission to merge the PR.

I cannot add the run-ci label myself, so this needs someone with write access to start CI.


Companions, all independent of each other but a MiMoV2 server needs all of them:


CI States

Latest PR Test (Base): ❌ Run #34590446291
Latest PR Test (Extra): ❌ Run #34590445724
Latest PR Test (AMD ROCm 10): ❌ Run #34590446212

MiMoV2MoE decides twice, by enumeration, whether an EP all-to-all backend is active,
and neither list includes is_deepep_v2(). Under --moe-a2a-backend deepep_v2 both are
therefore False, so _enable_a2a_moe routes the MoE through forward_normal, which ends
in a TP all-reduce on top of the a2a-combine the v2 dispatcher has already done, and
self.ep_size is never assigned. That is silently wrong output, not an error.

deepseek_v2.py and kimi_k3.py list is_deepep_v2() in the same places; this adds the
one clause to each of MiMoV2MoE's two, and adds MiMoV2ForCausalLM to the deepep_v2
architecture whitelist so the path is reachable. The two changes must land together:
the whitelist entry alone would expose the double-reduce above.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant