Skip to content

[SM90] Add FP8 MegaMoE support - #383

Open
AichenF wants to merge 21 commits into
deepseek-ai:nv_devfrom
AichenF:megamoe_sm90_fp8_dev
Open

[SM90] Add FP8 MegaMoE support#383
AichenF wants to merge 21 commits into
deepseek-ai:nv_devfrom
AichenF:megamoe_sm90_fp8_dev

Conversation

@AichenF

@AichenF AichenF commented Jul 16, 2026

Copy link
Copy Markdown

co-author: @jychen21

This PR adds an SM90 FP8 MegaMoE kernel for Hopper GPUs, executing the expert MLP as L1 + SwiGLU followed by L2.

Benchmark Results

Speedup is calculated as DeepEP / MegaMoE - 1.

H200 High Throughput

Model M MegaMoE (us) DeepEP HT (us) Speedup
Flash 8 225.0 348.0 +54.7%
Flash 16 245.9 395.4 +60.8%
Flash 32 253.1 394.4 +55.8%
Flash 64 265.8 402.6 +51.5%
Flash 128 265.1 414.0 +56.2%
Flash 256 293.4 439.3 +49.7%
Flash 512 413.4 496.8 +20.2%
Flash 1024 637.3 705.0 +10.6%
Flash 2048 959.1 1208.0 +26.0%
Flash 4096 1739.0 2238.1 +28.7%
Flash 8192 3269.0 4317.6 +32.1%
Pro 8 636.6 801.8 +25.9%
Pro 16 778.0 946.4 +21.6%
Pro 32 822.9 1244.4 +51.2%
Pro 64 858.1 1022.8 +19.2%
Pro 128 871.6 1042.5 +19.6%
Pro 256 881.8 1085.2 +23.1%
Pro 512 1004.5 1178.7 +17.3%
Pro 1024 1392.7 1617.5 +16.1%
Pro 2048 2405.7 2678.6 +11.3%
Pro 4096 4217.0 4832.9 +14.6%
Pro 8192 7939.0 9207.9 +16.0%

H200 Low Latency

Model M MegaMoE (us) DeepEP LL (us) Speedup
Flash 8 225.0 264.0 +17.3%
Flash 16 245.9 398.4 +62.0%
Flash 32 253.1 405.2 +60.1%
Flash 64 265.8 309.1 +16.3%
Flash 128 265.1 419.7 +58.3%
Pro 8 636.6 712.5 +11.9%
Pro 16 778.0 850.7 +9.3%
Pro 32 822.9 911.3 +10.7%
Pro 64 858.1 976.9 +13.8%
Pro 128 871.6 948.2 +8.8%

H20 High Throughput

Model M MegaMoE (us) DeepEP HT (us) Speedup
Flash 8 273.1 566.7 +107.5%
Flash 16 304.4 619.2 +103.4%
Flash 32 302.0 618.6 +104.8%
Flash 64 340.7 622.6 +82.7%
Flash 128 414.4 638.5 +54.1%
Flash 256 569.5 688.7 +20.9%
Flash 512 922.0 1057.3 +14.7%
Flash 1024 1516.6 1983.4 +30.8%
Flash 2048 2735.1 3419.5 +25.0%
Flash 4096 5116.0 6087.7 +19.0%
Flash 8192 9749.0 11779.2 +20.8%
Pro 8 768.0 1349.2 +75.7%
Pro 16 950.3 1585.8 +66.9%
Pro 32 1026.3 1770.2 +72.5%
Pro 64 1059.9 1787.6 +68.7%
Pro 128 1201.0 1803.9 +50.2%
Pro 256 1639.9 1857.7 +13.3%
Pro 512 2599.0 2898.8 +11.5%
Pro 1024 4036.0 5412.7 +34.1%
Pro 2048 6986.0 8067.9 +15.5%
Pro 4096 12932.0 14614.0 +13.0%
Pro 8192 24777.0 28184.2 +13.8%

H20 Low Latency

Model M MegaMoE (us) DeepEP LL (us) Speedup
Flash 8 273.1 479.7 +75.7%
Flash 16 304.4 528.4 +73.6%
Flash 32 302.0 531.8 +76.1%
Flash 64 340.7 552.2 +62.1%
Flash 128 414.4 552.8 +33.4%
Pro 8 768.0 1265.6 +64.8%
Pro 16 950.3 1493.3 +57.1%
Pro 32 1026.3 1667.2 +62.4%
Pro 64 1059.9 1683.8 +58.9%
Pro 128 1201.0 1714.1 +42.7%

Comment thread csrc/apis/sm90_mega.hpp
// * Hidden dims must be multiples of 128 (per-128 SF + scheduler integer-tiling).
// * `l2_arrival_mask` is uint64, with one bit per L1-output N-block of size 64 in the
// intermediate dim, so `kNumL1BlockNs = intermediate_hidden / 64` must be ≤ 64.
DG_HOST_ASSERT(hidden % 128 == 0 and intermediate_hidden % 128 == 0);

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.

🟡 warning: Validate the combine kernel's actual hidden alignment: For hidden values such as 128 or 384, this check passes, but the generated L2 kernel fails kCombineChunkElems % 256 == 0 during JIT compilation. These values satisfy the documented multiple-of-128 contract, so either handle partial combine vectors or reject them before invoking the compiler.

🤖 v6

Comment thread csrc/apis/sm90_mega.hpp
Comment on lines +197 to +200
// * `l2_arrival_mask` is uint64, with one bit per L1-output N-block of size 64 in the
// intermediate dim, so `kNumL1BlockNs = intermediate_hidden / 64` must be ≤ 64.
DG_HOST_ASSERT(hidden % 128 == 0 and intermediate_hidden % 128 == 0);
DG_HOST_ASSERT(intermediate_hidden / 64 <= 64);

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.

🟡 warning: Remove the stale intermediate-size cap: Any intermediate_hidden > 4096 is rejected based on the stated l2_arrival_mask limit, but the new split SM90 kernel and scheduler never access that mask. Consequently otherwise valid multiples of 128, such as 4224, are rejected despite the README's contract.

🤖 v6

Comment thread csrc/apis/sm90_mega.hpp
Comment on lines +206 to +209
check_sf_layout(l1_weights_sf, intermediate_hidden * 2, hidden, kGranMN, kGranK,
num_experts_per_rank, false, true, torch::kFloat);
check_sf_layout(l2_weights_sf, hidden, intermediate_hidden, kGranMN, kGranK,
num_experts_per_rank, false, true, torch::kFloat);

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.

🟡 warning: Reject transpose-contiguous weight scales: When a weight-SF tensor is transpose-contiguous—especially for square SF shapes such as L1 with hidden == 2 * intermediate_hiddensm90_sfb_check=true accepts it, but this kernel indexes the raw pointer as (E, N, K) with K contiguous. The call then silently applies scales to transposed blocks; require stride(-1) == 1 here or account for the supplied layout.

🤖 v6



def fp8_fp4_mega_moe(y: torch.Tensor,
l1_weights: Tuple[torch.Tensor, torch.Tensor],

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.

🟡 warning: transform_weights_for_mega_moe_sm90 returns (_interleave_weights(l1_fp8), l1_sf), l2_weights. This yields the correct outer 2-tuple ONLY because l2_weights is already the (weight, sf) pair and l1 happens to need no SF/UTCCP transform. It does no dtype/shape/divisibility validation and is fragile/inconsistent with the sibling SM100 transform, which builds both pairs explicitly. Mirror that and validate each pair; otherwise a future caller passing a single tensor gets silently wrong shapes (a lambda/comment-only weight transform is the only thing standing between a working WGMMA run and a garbage/NaN result).

🤖 v4

Comment thread csrc/apis/sm90_mega.hpp
l2_sf_buffer.get_end_ptr());

// Check SF buffer requirements
DG_HOST_ASSERT(hidden % 128 == 0 and intermediate_hidden % 128 == 0);

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.

🟡 warning: Host entry aborts via DG_HOST_ASSERT on arch!=9, recipe!=(128,128,128), activation!='swiglu', use_fp8_dispatch==false, hidden/intermediate%128!=0 and intermediate_hidden/64>64, with no graceful degradation/fallback to the existing DeepEP/BF16 path. It is fully usable only on the high-SM H200/H20 tuning SKU; a smaller/consumer Hopper card or any unsupported-but-legal shape aborts rather than falling back. The benchmark was also run on the tuned SKU, so portability risk is invisible there.

🤖 v4

@@ -0,0 +1,170 @@
#pragma once

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.

🟡 warning: The two-phase per-CTA traversal reconstructs (expert, m_block, n_block) from scratch per pass on the SAME grid/blockIdx.x with phase-specific kNumBlockNs/N-M-major and a RESET per-CTA state (the same CTA therefore does unrelated L1/L2 blocks). This is correct only if L1 and L2 are fully independent GLOBAL-memory producer/consumer passes over a byte-for-byte identical set of M-blocks. The per-expert block count/volume differs between phases (different N tiling), so the two passes must cover the same (expert, m_block) set exactly or a block is dropped/duplicated and the combine reader gets wrong/NaN data. Confirm this coverage equivalence before relying on it (the kernel itself could only be read in part here).

🤖 v4

return requested_num_experts_per_wave;

// The tuned schedules use exact divisors. For a generic, previously unseen
// expert count, round upward to the first divisor so the fallback remains

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.

🔵 suggestion: normalize_num_experts_per_wave_for_mega_moe_sm90 rounds a non-divisor requested experts-per-wave up to the next divisor, but the search loop runs candidate < num_experts_per_rank (strict), so num_experts_per_rank itself is never considered inside the loop; it is only reached via the final fallback return num_experts_per_rank. That happens to be correct here, but the loop bound looks like an off-by-one relative to intent. Consider candidate <= num_experts_per_rank (or documenting that the trailing return covers the top divisor) to make the invariant explicit and avoid a subtle regression if the fallback is ever changed.

🤖 v3

Comment thread csrc/apis/sm90_mega.hpp

static std::tuple<int64_t, std::function<std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>(const torch::Tensor&)>>
get_symm_buffer_size_for_sm90_mega_moe(
const int& num_ranks, const int& num_experts,

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.

🔵 suggestion: get_symm_buffer_size_for_sm90_mega_moe / fp8_mega_moe hard-assert use_fp8_dispatch and activation == "swiglu" via DG_HOST_ASSERT. Since these are the only supported modes for the SM90 path today this is acceptable, but the public Python signatures still accept use_fp8_dispatch and activation arguments, which invites callers to pass unsupported values and hit a raw host assert. A clearer error message (e.g. explicitly stating SM90 currently only supports fp8 dispatch + swiglu) would improve the failure mode.

🤖 v3


# Skip on non-SM90
if get_arch_major() != 9:
dist_print(f'[SKIP] test_mega_moe_sm90 requires SM90; got SM{get_arch_major()}0',

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.

🔵 suggestion: The test suite correctly skips on non-SM90 devices and uses a PyTorch fp32/bf16 reference with calc_diff < 0.01. Because the reference is chunked (_CHUNK = 32) and not bitwise-identical to the kernel, correctness confidence depends on tolerance tuning. The layered coverage (smoke/heuristic/shape/edges/stress) is good; consider adding an assertion or note that Layer 3 production H200 shapes (Pro m128/m256) require a full node to exercise the BN512/BK256 paths, so a 2-rank default run may silently not cover those heuristic branches.

🤖 v3


// Tensormap construction
// Acts/weights: standard 2D TMA descriptors (FP8 K-major).
// Activation SF: per-128 channel float for L1, per-64 for L2 (MN-major, no swizzle).

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.

🔵 suggestion: The L1-output tensor map comment explains that a BK256 stage is two adjacent BK128 TMA tiles and the box N is capped at 256 (l1_tma_block_n / l2_tma_block_n via std::min(..., 256)). This is a subtle invariant tied to the kernel's stage advancement by 256 while issuing two 128 copies. It is documented here, but given how easy it is to break, a static/host assertion tying block_n<=512 and the 256 box cap together (rather than relying on the selector's legality check) would make the contract self-enforcing at the launch site.

🤖 v3

@ds-review-bot

Copy link
Copy Markdown
Collaborator

🤖 ds-review-bot Code Review

v6

The new SM90 path rejects documented-valid shapes, can fail only during JIT for accepted dimensions, and accepts a scale layout that produces silently incorrect results.

v4

The PR adds an SM90/Hopper FP8 MegaMoE kernel that runs the expert MLP as L1 (gate/up) + SwiGLU followed by a separate L2, connected by a global-memory FP8 activation staging buffer, using the same dispatch/combine contract as the existing SM100 path but with FP8 + block-(128,128) float weight SF + split SwiGLU/quant epilogue + two independent full-grid single-CTA phase passes. Most of the runtime tuning is in the host heuristics/launcher that the benchmark numbers were produced with. The core kernel (~2,400 lines) could only be partially read via git, so the kernel-internal/staging-equivalence risks below should be confirmed by a full read/run before merge.

v3

This PR adds an SM90 (Hopper) FP8xFP8 MegaMoE kernel to DeepGEMM, executing the expert MLP as L1 -> SwiGLU -> L2 while preserving the same overlapped EP dispatch/combine contract used by the existing SM100 FP8xFP4 path. The change is well-structured and cleanly layered end-to-end:

  • Public C++ API: csrc/apis/sm90_mega.hpp exposes fp8_mega_moe, get_symm_buffer_size_for_sm90_mega_moe, and get_token_alignment_for_sm90_mega_moe, registered via register_sm90_apis and wired into csrc/python_api.cpp.
  • Python API: deep_gemm/mega/init.py and deep_gemm/init.py expose fp8_mega_moe, get_symm_buffer_for_sm90_mega_moe, transform_weights_for_mega_moe_sm90, and SM90SymmBuffer.
  • Heuristics: csrc/jit_kernels/heuristics/sm90_mega_moe.hpp implements an H200/H20-tuned launch-config selector (block tiling, per-phase L1/L2 schedules, swap-AB, BF16-scaled accumulation, direct-L2-scatter, experts-per-wave) with legality checks and a conservative BN128 fallback.
  • JIT runtime: csrc/jit_kernels/impls/sm90_fp8_mega_moe.hpp builds tensormaps and launches separate L1/L2 kernels.
  • CUDA kernel: deep_gemm/include/deep_gemm/impls/sm90_fp8_mega_moe.cuh (~2400 lines) implements the FP8 TMA/WGMMA pipeline with register-resident accumulators, per-row SwiGLU + FP8 (e4m3) quantize, and NVLink dispatch/combine.
  • Scheduler/layout: scheduler/sm90_mega_moe.cuh (per-CTA phase scheduling) and layout/sm90_mega_moe.cuh (warpgroup split-M/split-N layout).
  • Tests/bench: tests/test_mega_moe_sm90.py (5-layer correctness suite with a PyTorch reference) and tests/bench_mega_moe_sm90.py.
  • Docs: README.md SM90 usage section.

SM90-specific design is sound: FP8 e4m3 weights/activations with block-(128,128) float weight SF matching the DeepEP/DeepSeekV4FlashFp8 convention (so SF tensors can be physically shared), per-token/per-128-K activation SF for L1 and per-64-K SF for L2 activations, no TMEM/FP4/cluster-MMA (WGMMA accumulators register-resident, one CTA per work item). The kernel's SwiGLU clamp semantics (silu(min(gate,c)) * clamp(up,-c,c)), weight-application ordering, and per-64-column SF grouping match the test reference. The reported benchmarks show consistent +8% to +107% speedups over DeepEP HT/LL across H200 and H20 for Flash and Pro shapes.

Overall the integration is coherent and correct in its wiring and public surface. Comments below are minor/robustness observations rather than blockers; note that this reviewer could not compile or execute the SM90 kernel (requires Hopper hardware), so correctness relies on the included test suite and the reported benchmarks.

Files reviewed: 11
Issues found: 🟡 7 warning | 🔵 5 suggestion
Inline comments posted: 11
General comments (无法定位到 diff): 1


📍 未定位到 diff 的评论

🔵 suggestion deep_gemm/include/deep_gemm/impls/sm90_fp8_mega_moe.cuh:L116: The NVLink barrier uses a fixed 300s timeout (300ll * 2000000000ll cycles) assuming ~2 GHz. On parts with a substantially different clock64() rate this constant maps to a different wall-clock timeout. This matches the documented legacy trap-only semantics and is only a diagnostic ceiling, so it is not a correctness issue, but a brief comment noting the 2 GHz assumption would help future maintenance. 🤖 v3

@AichenF
AichenF force-pushed the megamoe_sm90_fp8_dev branch from bbe9686 to bc4f33a Compare July 16, 2026 09:02
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.

3 participants