Skip to content

[MoE] Unify DeepGEMM MegaMoE experts behind one feature-matrix layer; add NVFP4 - #61538

Merged
zyongye merged 7 commits into
vllm-project:mainfrom
zyongye:refactor/deep-gemm-mega-moe-experts
Oct 15, 2026
Merged

zyongye merged 7 commits into
vllm-project:mainfrom
zyongye:refactor/deep-gemm-mega-moe-experts

Conversation

@zyongye

@zyongye zyongye commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Overview

Folds the three DeepGEMM MegaMoE expert implementations into one DeepGemmMegaMoEExperts layer. The three are DeepSeek V4/V4.1's DeepseekV4MegaMoEExperts, Kimi K3's KimiK3MegaMoEExperts, and the DeepGemmMegaMoEBackend pair under both. The new layer is driven by a feature matrix: routed precision × activation × fusable shared-expert precision. The PR also adds NVFP4 routed experts (DeepGEMM nvfp4_mega_moe) as a new row. It keeps the procedure from #60096: weights load straight into the DeepGEMM layout, with no post-load transform.

Draft: the end-to-end eval has not been run yet (see Validation).

Claims

  • One layer. Every DeepGEMM MegaMoE caller uses forward(hidden_states, topk_weights, topk_ids, shared_experts=None).

  • Fusion decided at construction. Fusion of the shared expert is decided once, from the shared MLP's weights, and never changes at runtime.

  • Supported matrix. It covers every routed precision and activation that the pinned DeepGEMM exposes:

    routed kernel dispatch fusable shared activations
    mxfp4 fp8_fp4_mega_moe (fp8xfp4) MXFP8 MXFP8 swiglu, situ
    mxfp8 fp8_fp4_mega_moe (fp8xfp8) MXFP8 MXFP8 swiglu, situ
    nvfp4 nvfp4_mega_moe NVFP4, one scale per token MXFP8, BF16 swiglu
    bf16 bf16_mega_moe BF16 BF16 swiglu

    swiglu takes optional clamp, alpha and beta. situ (Kimi K3) takes alpha and beta.

  • Existing models unchanged. DeepSeek V4/V4.1 and Kimi K3 still build the MXFP4 row and pass the same kernel arguments as before. The mxfp8, nvfp4 and bf16 rows have no model caller yet.

  • Size. Code under vllm/ is +360 lines net. The refactor commit is −120, the NVFP4 commit is +472, and the two follow-up commits are −15 and +23.

Validation

GB200 (SM100), vendored DeepGEMM at the current pin:

.venv/bin/python -m pytest tests/kernels/moe/test_deep_gemm_mega_moe.py \
    tests/models/test_deepseek_v4_mega_moe.py \
    tests/model_executor/test_deep_gemm_warmup.py
# 138 passed in 67s

Kernel numerics. The real kernel is compared against an fp32 reference that runs on the same quantized weights and activations. The reference covers dispatch quantization, the activation, the L2 activation quantization and the shared expert. The metric is DeepGEMM's calc_diff; the test requires < 1e-3 and cosine > 0.99. The table shows the worst case over the activations each format supports:

routed unfused shared fused MXFP8 shared fused BF16 shared
mxfp4 2.9e-5 1.8e-4 –
mxfp8 3.1e-5 1.8e-4 –
nvfp4 1.8e-6 1.6e-4 1.4e-6
bf16 2.1e-6 – 1.7e-6

Negative controls. Each of these deliberate breaks made the expected cases fail:

break cases that failed
wrong weight layout, activation or shared input (MX) the affected cases
drop NVFP4 per-token x_scales, l1_alpha or l2_alpha all 9 NVFP4 cases
drop the shared-expert input all 6 fused cases
wrong shared BLOCK_M the 3 fused-MXFP8 cases

Input staging. The Triton staging kernels match DeepGEMM's reference quantizers (per_token_cast_to_fp8, per_token_cast_to_nvfp4) bit for bit. This covers values, scales, shared-expert inputs, unrouted padding and the zero-token case.

Not yet done:

  • End-to-end DeepSeek-V4.1 gsm8k A/B against main: TP4, --enable-expert-parallel, --kernel-config '{"moe_backend":"deep_gemm_mega_moe"}'. The main baseline is 92.65% flexible / 92.57% strict.
  • End-to-end Kimi K3 with moe_backend=deep_gemm_mega_moe.
  • Only single-rank kernel numerics have been run; there is no multi-rank EP numerics test.

Details

Feature matrix.

  • Each MegaMoEWeightFormat row declares five things: the MMA type, the kernel entry point, the routed weight spec (dtype, packing, scale group, scale dtype), the dispatch quant key, and the fusable shared-expert specs.
  • The layer validates its MegaMoEActivation against the row at construction.
  • A new precision is a new row plus whatever staging it needs. It needs no new expert class.

Shared-expert fusion.

  • The layer fuses the shared expert when two conditions hold. First, the shared MLP's weight dtype, scale dtype and shape match one of the row's shared specs. Second, the installed DeepGEMM exposes the shared-expert buffer and kernel API.
  • Otherwise forward adds shared_experts(hidden_states) once.
  • A fused layer raises if it is still handed a shared module.

Input staging.

  • prepare_megamoe.py moved from vllm/models/deepseek_v4/nvidia/ops/ to fused_moe/. The old location caused an import cycle through vllm.models.deepseek_v4.
  • It gained a BF16 passthrough and an NVFP4 kernel.
  • Shared Triton helpers were factored out of the MX kernel, which is still bit-exact.

NVFP4.

  • Dispatch quantizes hidden states with one NVFP4 scale per token, computed at runtime. The checkpoint's w1/w3 input_scale are therefore unused. A model that wires NVFP4 must skip them.
  • The kernel takes a single scalar L2 activation scale. The layer uses the largest w2 input_scale across all experts. Every rank reads every expert's scale tensors during loading, so all ranks agree and EPLB expert moves stay valid.
  • l2_alpha (the per-expert w2 global scale × the L2 activation scale) is computed inside the staging kernel. This avoids an extra launch and stays correct after EPLB moves or a weight reload.
  • The kernel takes one l1_alpha per expert, so the loader raises if w1 and w3 weight_scale_2 differ (relative tolerance 1e-5, as in ModelOpt's allclose check). It compares the two shards of the same load, not the live slot, so a reload that changes the scale does not trip it. ModelOpt's and Quark's NVFP4 MoE only warn here; they also use one scale.
  • Meta tensors, which reload dry runs pass, skip this host-side scale state.
  • The L2 activation scale is a host scalar in DeepGEMM's API, so CUDA graphs capture its value. A weight reload that changes the w2 input scales needs a graph recapture. In eager mode the layer recomputes the max on reload. Passing it as a device tensor needs a DeepGEMM change and a pin bump, so it is left for a follow-up.

Behaviour changes for existing models:

  • DeepSeek V4/V4.1 now also run Kimi K3's one-time first-launch barrier: a device sync plus a gloo barrier on the EP CPU group. It runs on the first forward, which is the eager profile run before graph capture.
  • get_deep_gemm_mega_moe_backend() and the backend classes are removed. Their docstring named them as the override point for plugins and SM90. The extension point is now a new MegaMoEWeightFormat row, plus check_deep_gemm_mega_moe_supported.

CI. The new test file and the moved sources are added to the B200 "DeepSeek V4 Kernels" step. Before this PR the SM100 numerics ran only from tests/models/test_deepseek_v4_mega_moe.py. The staging-kernel tests are gated to SM100, where those kernels run. The L4 "MoE Kernels" shards also collect tests/kernels/moe/, and on SM89 Triton casts fp32 to fp8 through fp16 (cvt.rz.f16.f32). That rounds the NVFP4 block scales twice, so the bit-exact test failed there. The loader and fusion-selection tests in the file still run on L4.

Unchanged: the XPU DeepseekV4MegaMoEExperts, which uses a different kernel.

Related PRs:


Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

AI assistance (Claude Code) was used for this change. The submitter reviews every changed line and runs the tests before marking it ready.

🤖 Generated with Claude Code

zyongye and others added 3 commits October 14, 2026 00:58
…matrix

Replace DeepseekV4MegaMoEExperts, KimiK3MegaMoEExperts and the
DeepGemmMegaMoEBackend/DeepGemmSm100MegaMoEBackend pair with a single
DeepGemmMegaMoEExperts layer. Routed precisions are rows of a declarative
feature matrix (MegaMoEWeightFormat): MXFP4 (fp8xfp4) and MXFP8 (fp8xfp8)
on fp8_fp4_mega_moe with MXFP8 dispatch and MXFP8 shared experts, BF16
(bf16xbf16) on bf16_mega_moe with BF16 shared experts. The activation
(MegaMoEActivation: swiglu with clamp/alpha/beta, or situ) is fixed at
construction and checked against the format. nvfp4_mega_moe is not wired.

Weights keep the vllm-project#60096 procedure: parameters are created in the kernel
layout and each checkpoint shard is written straight into it. Whether
the shared expert is fused is decided once at construction from the
shared MLP's dtype, scales and shape; forward() takes hidden_states,
topk_weights, topk_ids and an optional unfused shared expert.

Move the input-staging kernel to fused_moe/prepare_megamoe.py (avoids an
import cycle through vllm.models.deepseek_v4) and add a BF16 passthrough.
Move the layer-level tests to tests/kernels/moe/test_deep_gemm_mega_moe.py
and add numerics against a dequantized reference for every format,
activation and shared-expert fusion.

AI assistance was used for this change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
…egaMoE

Wire DeepGEMM's nvfp4_mega_moe into DeepGemmMegaMoEExperts as a new
feature-matrix row: NVFP4 routed experts (E2M1 + E4M3 per 16 + FP32
global scale) fusing an MXFP8 or BF16 shared expert, swiglu only.

- MegaMoEWeightFormat.shared is now a tuple of fusable shared precisions;
  the layer picks the one matching the shared MLP at init (shared_spec).
- NVFP4 weights load straight into the kernel layout like MX; the global
  scales are per-expert params (w13_weight_scale_2 is the kernel l1_alpha).
- Dispatch uses per-token NVFP4 scales (kNvfp4DynamicToken), so w1/w3
  input_scale are unused. The L2 activation scale is the max w2
  input_scale over all experts (every rank loads all scale tensors), and
  l2_alpha is folded in the staging kernel so EPLB moves stay consistent.
- New Triton prepare_megamoe_nvfp4_inputs stages NVFP4 x/x_sf/x_scales,
  the MXFP8 or BF16 shared input and routing in one launch; shared Triton
  helpers are factored out of the MX staging kernel.
- get_block_m() picks the format's BLOCK_M heuristic; warmup uses it.

Tests: tests/kernels/moe/test_deep_gemm_mega_moe.py,
tests/models/test_deepseek_v4_mega_moe.py and
tests/model_executor/test_deep_gemm_warmup.py: 141 passed on GB200. NVFP4
staging is bitwise exact vs DeepGEMM's reference quantizers; NVFP4 kernel
numerics match a dequantized reference (calc_diff 1.4e-6..1.6e-4), and
dropping x_scales, l1_alpha, l2_alpha, the shared input, or using a wrong
BLOCK_M fails the expected cases.

AI assistance was used for this change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
- Run tests/kernels/moe/test_deep_gemm_mega_moe.py in the B200 DeepSeek
  V4 Kernels step and trigger it on the moved MegaMoE sources; the SM100
  numerics otherwise only run where the kernels are skipped.
- Keep each logical expert's w2 input scale and take their max, so a
  weight reload can lower the NVFP4 L2 activation scale.
- Make weight_format a required argument; tighten docstrings.
- Tests: parametrize the reference numerics over supported
  format/activation pairs only, readable ids, reciprocal-multiply
  x_scales reference, drop duplicated lines.

AI assistance was used for this change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
@mergify mergify Bot added ci/build deepseek Related to DeepSeek models kimi k3 DSv4 labels Oct 14, 2026
@hao-aaron

Copy link
Copy Markdown
Collaborator

Checked this against weight reload (layerwise today, and the reload plan in #59502 / #60931). The MXFP4 path DeepSeek V4/V4.1 and Kimi K3 use loads the same way as after #60096: shards are written straight into the kernel layout through the param's loader, and the shared MLP is still dropped in __init__. No reload concerns there.

Two small requested fixes on the new NVFP4 row:

1. Skip the w2_input_scale side-state update for meta tensors (deep_gemm_mega_moe.py:483-484).
Some reload machinery runs load_weights with meta stand-ins (right names and shapes, no data) to learn which loaders get called:

  • sharded RDT's plan bake;
  • the reload verifier's load-coverage dry run;
  • the init-time trace in the planned reload design.

self._w2_input_scales[expert_id] = loaded_weight.max() copies a meta value into a real CPU tensor, which raises, and that aborts the whole dry run. The side state itself is fine for real loads. Adding and not loaded_weight.is_meta to that branch is enough.

2. Check that w1 and w3 weight_scale_2 match.
The kernel takes one L1 alpha per expert, but the loader keeps whichever shard's weight_scale_2 arrives last. A checkpoint or trainer update where they diverge would silently produce wrong alphas. Suggestion: when the second shard's value lands on a slot the first already wrote (nonzero), compare them and raise on mismatch.

Known issue, out of scope for this PR: l2_activation_scale is passed to the kernels as a Python float. CUDA graphs bake it in at capture, so in graph mode a reload that changes the w2 input scales keeps using the old value. The fix is to pass it as a device tensor:

  • DeepGEMM's nvfp4_mega_moe takes a pointer instead of a float (kernel, launcher and op schema in vllm-project/DeepGEMM);
  • the staging kernel loads it from the same buffer;
  • the layer refills that buffer in place on reload.

That needs a DeepGEMM change and a pin bump, so it's better as a follow-up.

This review/comment was written by an AI agent (Claude Code) on my behalf.

… weight_scale_2

Reload dry runs call the weight loaders with meta stand-ins. Writing a meta
w2_input_scale into the host-side L2 scale state raised and aborted the run,
so meta tensors now skip that state.

The kernel takes one L1 alpha per expert, so the loader now raises if w1 and
w3 weight_scale_2 of the same load differ. It pairs the two shards of a load
instead of comparing with the live slot, so a reload that changes the scale
does not trip it.

AI assistance was used for this change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
@zyongye

zyongye commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

Thanks for checking this against reload. Both requested fixes are in 603db01.

1. Meta tensors. weight_loader now skips the host-side scale state when loaded_weight.is_meta. A new test loads meta stand-ins for w2_input_scale and both w13_weight_scale_2 shards. Without the guard it fails with Cannot copy out of meta tensor.

2. w1/w3 weight_scale_2. The loader now raises when they differ, with a relative tolerance of 1e-5 (the same as ModelOpt's allclose check). One change from the suggestion: it pairs the two shards of the same load instead of comparing with the slot. On a reload that changes the scale, the slot still holds the old value when the first new shard lands, so a slot comparison would raise on a valid reload. The existing reload test loads two checkpoints with different scales in shuffled shard order, and it still passes. ModelOpt's and Quark's NVFP4 MoE paths only warn on this mismatch; this one raises, as you suggested.

CUDA-graph L2 scale. Agreed, this is left for a follow-up together with the DeepGEMM change and pin bump. The PR description notes it.

This reply was written by an AI agent (Claude Code) on behalf of @zyongye.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@zyongye

zyongye commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #94889 for commit 5550180721d0.

@zyongye

zyongye commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

/ci retry

@github-actions

Copy link
Copy Markdown

✅ Queued 6 failed job(s) for retry in Buildkite CI #94889.

The staging kernels only feed the SM100 MegaMoE launch, but the tests ran in
the L4 MoE shards after moving to tests/kernels/moe. Below SM90 Triton casts
fp32 to fp8 through fp16 (cvt.rz.f16.f32), which double-rounds the NVFP4 E4M3
block scales and broke the bit-exact comparison. The B200 DeepSeek V4 Kernels
step still runs them.

AI assistance was used for this change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
@zyongye

zyongye commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #94976 for commit d83c1498aa29.

ℹ️ This PR is 26 commits behind upstream main. Merged upstream main into this branch as d83c1498aa29 before starting CI. Pull the branch before pushing more changes.

@zyongye
zyongye enabled auto-merge (squash) October 15, 2026 02:57
@github-actions github-actions Bot added the ready ONLY add when PR is ready to merge/full CI is needed label Oct 15, 2026
@zyongye
zyongye disabled auto-merge October 15, 2026 02:57
@zyongye
zyongye enabled auto-merge (squash) October 15, 2026 03:27
@zyongye

zyongye commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

/ci retry

@github-actions

Copy link
Copy Markdown

✅ Queued 1 failed job(s) for retry in Buildkite CI #94976.

@zyongye

zyongye commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

/ci retry

@github-actions

Copy link
Copy Markdown

✅ Queued 3 failed job(s) for retry in Buildkite CI #94976.

@zyongye
zyongye merged commit 4d58fa2 into vllm-project:main Oct 15, 2026
284 checks passed
andakai added a commit to jeejeelee/vllm that referenced this pull request Oct 18, 2026
vllm-project#61538 moved prepare_megamoe from vllm.models.deepseek_v4.nvidia.ops to
vllm.model_executor.layers.fused_moe. step5-preview picked up that move in
its 2026-10-16 main merge, so Step5MegaMoE raises ModuleNotFoundError on
its first forward and engine startup fails under
--moe-backend deep_gemm_mega_moe. The function signature is unchanged.

Signed-off-by: Dakai An <dakaian108@gmail.com>
michael-gpu pushed a commit to practical-inference/vllm-ampere-extended that referenced this pull request Oct 20, 2026
… add NVFP4 (vllm-project#61538)

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kevin H. Luu <khluu000@gmail.com>
gcanlin added a commit to gcanlin/vllm that referenced this pull request Oct 20, 2026
Port the GLM-5.3-Flash FP8 MegaMoE onto the unified DeepGEMM MegaMoE
experts from vllm-project#61538 / vllm-project#60096, which replaced DeepseekV4MegaMoEExperts and
the post-load MegaMoE backend transform:

- Glm5NextMegaMoEExperts now subclasses DeepGemmMegaMoEExperts with the
  MEGA_MOE_MXFP8 (fp8xfp8) format. E4M3 shards load straight into the
  kernel layout; the float32 128x128 block scales are staged and every
  freshly loaded shard is requantized in place to UE8M0 1x32 scales after
  load_weights (requant_block_fp8_to_ue8m0 moves to the GLM adapter).
- The block-FP8 shared expert is fused the same way: the experts own its
  weights and the separate shared MLP is dropped.
- The SwiGLU clamp moves to MegaMoEActivation; the deep_gemm_mega_moe.py and
  DeepSeek V4 conflicts take main's version.

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

Labels

ci/build deepseek Related to DeepSeek models DSv4 k3 kimi ready ONLY add when PR is ready to merge/full CI is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants