Skip to content

SM90 (Hopper) FP4 MegaMoE fused kernel with swapAB small-batch path - #53

Open
qiushixiaoyu wants to merge 22 commits into
sgl-project:devfrom
qiushixiaoyu:fp4_swapAB
Open

SM90 (Hopper) FP4 MegaMoE fused kernel with swapAB small-batch path#53
qiushixiaoyu wants to merge 22 commits into
sgl-project:devfrom
qiushixiaoyu:fp4_swapAB

Conversation

@qiushixiaoyu

@qiushixiaoyu qiushixiaoyu commented Jun 29, 2026

Copy link
Copy Markdown

This PR adds an FP4-weight MegaMoE fused kernel that:

  • cuts expert-weight memory traffic ~2× vs FP8 by using packed FP4 (E2M1) weights, and
  • adds a swapAB tiling for small batches (weight on the WGMMA M dimension,
    tokens on N), which is more efficient when tokens-per-rank is small.

Changes

  • New SM90 FP4 MegaMoE fused kernel sm90_fp8_fp4_mega_moe_impl
    (deep_gemm.fp8_fp4_mega_moe): FP8 (E4M3) activations × packed FP4 (E2M1)
    expert weights with per-32-K UE8M0 weight scales folded into the FP4→E4M3
    dequant; fused L1 GEMM → SwiGLU → per-token FP8 requant → L2 GEMM → combine.
  • swapAB small-batch path: for small per-rank token counts the grouped GEMM
    runs with A/B swapped, selected by the L1/L2 dispatch ladders.

Accuracy (DeepSeek-V4-Flash, 8×H20, swapAB on)

SGLANG_OPT_USE_DEEPGEMM_MEGA_MOE=1 SGLANG_OPT_FIX_MEGA_MOE_MEMORY=1 SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK=8192 SGLANG_DSV4_FP4_EXPERTS=1 GLOO_SOCKET_IFNAME=eth0 SGLANG_DEFAULT_THINKING=1
sglang serve
--trust-remote-code
--model-path /data00/models/DeepSeek-V4-Flash
--tp 8
--dp-size 8
--enable-dp-attention
--enable-dp-lm-head
--ep-size 8
--cuda-graph-max-bs 128
--chunked-prefill-size 8192
--mem-fraction-static 0.75
--max-running-requests 128
--tool-call-parser deepseekv4
--reasoning-parser deepseek-v4
--host 0.0.0.0
--moe-runner-backend deep_gemm
--moe-a2a-backend deepep
--port 30000

sgl-eval run gpqa
--n-repeats 16 --max-tokens 200000
--temperature 1.0 --top-p 1.0 --thinking
--out-dir /sgl-workspace/logs
--base-url http://localhost:30000/v1
2>&1 | tee /sgl-workspace/logs/gpqa_$(date +%Y%m%d_%H%M%S).console.log

== gpqa ==
198 examples x 16 repeats | 11789.7s | 2895 tok/s | 34.1M tokens

  • pass@1[avg-of-16] = 88.32% +/- 1.35% (SEM 0.34%)
    pass@16 = 96.46%
    majority@16 = 90.15%
    no_answer = 0.00%
    stop_rate = 100.00%
    truncated_rate = 0.00%
    error_rate = 0.00%
Eval FP4 MegaMoE (swapAB)
GSM8K (1319) 0.951 (invalid 0.000)
GPQA-diamond (32, thinking) 0.938

Performance (single-op MegaMoE kernel, 8×H20, bench_kineto)

DeepSeekV4Flash

batch/卡 FP4 µs FP8-LL µs speedup
1 137.4 311.9 2.27×
2 206.2 335.2 1.63×
4 332.1 449.6 1.35×
8 373.4 526.3 1.41×
16 416.5 572.1 1.37×
32 440.3 602.6 1.37×
64 476.3 609.0 1.28×
128 519.4 634.4 1.22×
256 540.5 655.1 1.21×
batch/卡 FP4 µs FP8-normal µs speedup
1 154.7 501.1 3.24×
8 375.1 965.8 2.57×
32 512.7 1165.4 2.27×
64 481.4 1150.1 2.39×
128 518.4 1147.7 2.21×
256 535.8 1197.5 2.24×
512 966.4 1256.8 1.30×
1024 1837.8 2241.3 1.22×
2048 3166.6 3741.3 1.18×
4096 5821.2 6771.4 1.16×
8192 11213.5 12821.1 1.14×

DeepSeekV4Pro

batch/卡 FP4 µs FP8-LL µs speedup
1 373.0 556.4 1.49×
2 539.6 742.1 1.38×
4 827.7 1085.5 1.31×
8 1213.5 1513.2 1.25×
16 1485.2 1781.2 1.20×
32 1482.5 1861.6 1.26×
64 1521.6 1870.0 1.23×
128 1711.8 1898.0 1.11×
192 1900.8 1919.5 1.01×
256 1805.0 1949.8 1.08×
batch/卡 FP4 µs FP8-normal µs speedup
1 384.7 969.5 2.52×
8 1210.0 2948.8 2.44×
32 1486.5 3560.3 2.40×
64 1539.8 3542.3 2.30×
128 1734.8 3596.0 2.07×
256 1804.8 3629.3 2.01×
512 3408.9 3726.4 1.09×
1024 5056.5 5527.3 1.09×
2048 8733.5 9310.6 1.07×
4096 15052.6 15987.0 1.06×
8192 28918.0 30540.9 1.06×

yinding and others added 6 commits July 19, 2026 10:42
Add the SM90 FP8xFP4 MegaMoE runtime, kernel path, Python API, Hopper correctness and benchmark coverage, tuned runtime decode heuristics, swapAB support, synchronization/spill fixes, and the SM90 MegaMoE alignment export.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The FP4 path had ~49 tuned conditions (17 shape x e-band API-default
bands, 9+12 wave-table rules, 11 stage-cap rules) fitted point-by-point
on benchmark batches; on real shapes most bands collapsed to constants
or slivers. Retire them:

- API defaults: constants + one decode/prefill split + a single swapAB
  threshold (measured crossover on H20: swapAB wins at e<=12, ties at
  e~16, loses beyond -> bound 16). wide_load_decode on for all decode.
- Wave scheduling: delegate to the FP8 rule
  (get_num_experts_per_wave_for_mega_moe_sm90).
- Stage cap: removed; always use as many pipeline stages as SMEM allows.
- CPU heuristic mirrors in the test synced to the same closed forms.

Verified on 8xH20 EP8: accuracy suites pass (diff <= 0.0006, tol 0.05);
GLM5.2 flat-or-better (b2 -12%, b96 -9%, prefill bit-identical);
DSV4-Flash within noise; DSV4-Pro avg ~+2%, worst b1 +8.6%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The decode config (BLOCK_M=64) and prefill config (BLOCK_M=128) flipped
at a hardcoded e>=64. Measured on 8xH20 both modes across the boundary:
decode wins for e in [64, 80) (its first m-block is exactly full while
prefill's 128-row block runs half empty, -13% at GLM5.2 b256); prefill
wins from e=80 up (decode's second m-block is mostly empty, +2~9%).

Parameterize the four boundary checks (block config auto_split_mn,
split-N band, 2-WG thread band, API prefill band) through a single
DG_SM90_FP4_PREFILL_E knob (default 80) plus a PREFILL_E mirror
constant in the test.

GLM5.2 b256: 1277 -> 1123 us; vs FP8 low-latency 0.98x -> 1.11x (cliff
fixed, b245->b320 now monotone). DSV4-Flash e=72 also improves -1.8%.
Accuracy: forced-decode reference checks at e=64/96/128 pass
(diff ~ 6e-4, tol 0.1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
yinding and others added 16 commits July 19, 2026 10:50
Follow up deepseek-ai#63, which dropped the UE8M0 (power-of-two) activation scale on
the SM90 FP8 path. The same reasoning applies to the FP4 path: SM90 WGMMA
has no hardware block-scale operand (the SF is a plain FFMA in the
epilogue), so rounding the scale to a power of two bought nothing and only
cost precision. The L2 SF pool is already fp32 (`l2_acts_sf` is kFloat32),
so this is byte/layout neutral.

Only the activation scale changes. The FP4 weight SFB stays UE8M0 (DSV4
external format) and is still decoded through the LUT path.

Also clamp amax before the reciprocal: padded rows have amax==0, and
448/0=inf would produce 0*inf=NaN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fast-math SiTU now computes the gate/up caps with __tanhf and the
sigmoid via the 0.5 * (1 + tanh(x / 2)) identity instead of __expf +
reciprocal. Non-fast-math and SwiGLU paths are unchanged. Verified
diff=0.0000 on 3 Kimi scenarios and 2 DSV4Flash regressions; 4096
batch/rank latency improves 23.454 -> 22.830 ms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GEMM1/GEMM2 FP32 activation scales were issued as one 1D TMA per
K-group (4 for the SiTU 32/32 path). Extend the SF descriptor with a
K-group box dimension and fetch all groups of a stage in one 2D TMA,
cutting TMA instructions and barrier arrivals. expect_tx bytes are
unchanged. Verified diff=0.0000 on Kimi and DSV4Flash scenarios; 4096
batch/rank latency improves to ~22.78 ms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The official Kimi-K3 per-32 activation-scale recipe costs ~15% at large
batch on SM90: each K32 WGMMA must fully drain before its FP32 promotion,
and the drain cannot be pipelined within the 640-thread register pool
(ping-pong banks spill). Controlled experiments (doc 15.6-15.11) show the
entire FP4-vs-DeepEP-normal gap comes from this, so SiTU now defaults to
the legacy 128/64 recipe via get_act_sf_grans_for_mega_moe_sm90_fp4;
DG_SM90_FP4_SITU_ACT_GRAN_128_64=0 switches back to per-32 for accuracy
comparison.

With 128/64 the prefill bundle is healthy again, but Kimi routing density
(e = batch/7) wastes 43%/24% of BLOCK_M=128 rows at batch 1024/2048 where
forced decode wins 20%/10%, while e=585 (batch 4096) favors prefill --
hence the SiTU e=512 boundary (per-32 keeps 2048).

Sweep 1-8192: FP4 now beats DeepEP normal at every point (1.76-1.87x for
batch 1-256, 1.26x/1.13x at 1024/2048, 1.015x at 4096/8192). Kimi 3
scenarios, per-32 switchback, and DSV4 regressions all diff=0.0000.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The swapAB decode path was gated on 'not use_situ' because its data
layout only covers 128/64 activation scales, which SiTU's original per-32
recipe violated. Now that SiTU defaults to 128/64, gate on the granularity
instead, and unify the L1 epilogue activation: the swapAB path previously
hard-coded clamp+silu and now shares the same gated_activation lambda as
the regular epilogue (bit-identical for SwiGLU, adds SiTU support).

DG_SM90_FP4_SWAP_AB_MAX_E overrides the e<16 crossover for tuning; A/B on
Kimi (896 experts, top-k 16) confirms the default 16 stands -- e=36.6/73
lose 26%/13% on the swapped path, matching the original swiglu tuning.

Kimi decode batches 1-64 (e<16) gain 13-15%: batch 1 now 403us vs DeepEP
normal 801us (1.99x), batch 4/16 reach 2.19x. Kimi 3 scenarios (swapAB
specialization exercised), and DSV4 2 scenarios all diff=0.0000.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The valley around batch 512 on Kimi (896 experts, top-k 16, e = batch/7)
is a tile-quantization band, with its true bottom at batch 640 (0.978x vs
DeepEP normal). For e in (80, 112] a BLOCK_M=128 tile pads M no worse
than two BLOCK_M=64 tiles while halving per-expert B decodes, and this is
exactly the band where B-decode reuse is lowest: prefill config measures
+4.1%/+2.5% at batch 640/768. Near e~128 routing spread makes single
128-tiles overflow into a second one, so decode wins big again (batch 896
decode is 15% faster), and batch 512 itself (e=73) ties -- it sits at the
ceil(73/64)*64 = 128-row tile-math floor.

Encode this as a non-monotonic is_fp4_sm90_prefill_band predicate shared
by the block-config heuristics and the API feature defaults so the
BLOCK_M choice and the early_b_decode/ss_nsplit/wide_load_decode bundle
always switch together. DG_SM90_FP4_SITU_MIDBAND_LO/HI override the band.

After: 512-1024 all >= 1.018x (640: 0.978->1.022, 768: 1.002->1.026);
512/896/1024 unchanged. Kimi 3 scenarios and DSV4 2 scenarios diff=0.0000.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Boundary A/B at batch 512/544/576/608 (e=73.1/77.7/82.3/86.9) locates the
decode->prefill crossover between e=73 and e=78: prefill already wins
+2.6% at e=77.7 and its latency is nearly flat across the band (one
128-tile per expert regardless of batch), while decode climbs as the
share of experts with <=64 tokens vanishes. The previous lo=80 left
batch 544 on the wrong side. Batch 512 (e=73.1) still favors decode by
0.8% -- the single-64-tile expert share (~16%) outweighs halved B
decodes there, so it stays outside the band.

Kimi 3 accuracy scenarios diff=0.0000.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Boundary scan on Kimi SiTU 128/64 at batch 128/160/192/224
(e=18.3/22.9/27.4/32.0): swapAB still wins +3.1% at e=18.3 and loses
-6..-18% from e~23, so the crossover sits in (18.3, 22.9). The old
bound 16 came from the original swiglu tuning and left batch 128 on
the wrong side. Kimi 3 accuracy scenarios diff=0.0000.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the hand-tuned swapAB/decode/prefill boundaries with a per-expert
expected-cost argmin. With X ~ Poisson(e) tokens per expert, in units of
one regular-mainloop row:
  R(M) = E[ceil(X/M)]*M, T(M) = E[ceil(X/M)]
  cost_decode  = R(64) + 6.29*T(64)
  cost_prefill = (1-0.003)*R(128) + 6.29*T(128)
  cost_swap    = 1.335*R(8) + 40.6*P(X>0)
Constants fitted offline (scratchpad fit_cost_model.py) on 21 same-shape
A/B measurements; all 21 classify correctly and the model reproduces every
hand-tuned boundary (swap<19 vs 20, prefill (76,115.5] vs (76,112],
large-batch prefill from 578 vs 512). It additionally predicted prefill
wins inside the former decode band -- confirmed +3.8%/+2.8% at batch
1540/2450 (e=220/350), which the hand bands were leaving on the table.
The row/decode cost ratio is shape-independent to first order, so the
boundaries transfer to other expert shapes / EP degrees without rescans.

The block-config heuristic and the API feature defaults share the same
argmin so BLOCK_M and the feature bundle never mix. Scope: SiTU 128/64
only; swiglu and per-32 keep their hand-tuned paths.
DG_SM90_FP4_COST_MODEL=0 falls back to the hand-tuned bands;
DG_SM90_FP4_SWAP_AB=0 still disables swapAB inside the model.

Regression: Kimi 3 + DSV4 2 scenarios diff=0.0000; nine batch points
(128..4096) all land on their measured-optimal config within 0.4%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DG_SM90_FP4_MATH_WG_DECODE, DG_SM90_FP4_EARLY_B_DECODE and
DG_SM90_FP4_STAGE_CAP expose the decode-participation, early-B-decode
and pipeline-depth knobs for experiments; all default to current
behavior. Small-batch tile-loop probing used them to rule out decode
throughput (math-WG decode is 15-40% WORSE), decode/TMA overlap (early
B-decode is neutral) and pipeline depth (4 vs 5 stages is neutral) as
the source of the ~600ns/K-stage steady-state (doc 15.17).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Linear1/Linear2 branch and the swapAB n_swap dispatch chain (4-5
compares + generic-lambda selection) executed on every K-stage of the
math warpgroup -- the warp whose per-stage service time sets the whole
pipeline's steady beat (~525ns, doc 15.17). Both decisions are tile
constants: wrap the K-stage loop in a run_k_stages<kIsL1, kNSwap>
template and dispatch once per tile instead. Each instantiation is a
branch-free specialized loop body, which also unlocks compiler loop
optimizations across stages.

Small-batch gains (Kimi SiTU 128/64, 30-rep us_max): batch 1
409->399us (2.01x vs DeepEP normal, first time above 2x), batch 16
1627->1595us, batch 64 1812->1752us, batch 128 2184->2048us (+6%,
the deeper N24 dispatch chain saved the most). batch 512/4096
unchanged. Kimi 3 + DSV4 2 accuracy scenarios diff=0.0000.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DG_SM90_FP4_EXPERTS_PER_WAVE overrides the wave size for sensitivity
probing; default behavior unchanged. The 512+ investigation swept it
({112,56,28,14}: +-1% flat) alongside stage caps (flat), BLOCK_N=256
(rejected on paper: B-decode volume is N-tiling-invariant and n256
accums blow registers) and L2-arrival refinement (measured wait is only
1-2% of kernel time). Phase-aligned batches (multiples of 896, e
divisible by 128) confirm the mid-range valleys are tile-phase effects
recoverable at the deployment layer, and at aligned large batches the
FP4 kernel reaches 80% of FP8 peak vs GLM's 79% -- the remaining
speedup gap vs GLM is normal-baseline strength, not kernel efficiency
(doc 15.22).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--profile-breakdown now also emits the five normal-baseline stages
(dispatch / L1 GEMM / act+quant / L2 GEMM / combine). At batch 7168
(phase-aligned) this pins the comparison the fused kernel must answer
to: normal pure GEMM = 29.36ms = 87.1% of FP8 peak vs fused = 32.0ms =
80%, i.e. the fused GEMM runs ~7 points below DeepEP normal's grouped
GEMM and eats two thirds of the 3.5ms hideable-communication dividend.
DG_SM90_FP4_WIDE_LOAD_DECODE probes wide-load decode outside the decode
band: flat at 4096/7168, so the historical band gating stands. Doc 15.23.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the five probes whose experiments concluded with no benefit:
DG_SM90_FP4_MATH_WG_DECODE (15-40% worse), DG_SM90_FP4_EARLY_B_DECODE
(neutral), DG_SM90_FP4_STAGE_CAP (depth-insensitive),
DG_SM90_FP4_EXPERTS_PER_WAVE (flat) and DG_SM90_FP4_WIDE_LOAD_DECODE
(flat outside the decode band). Behavior is identical to the probed
defaults; conclusions live in docs 15.17/15.22/15.23. The functional
switches (recipe/cost-model/swapAB kill-switch and the hand-tuned
fallback parameters) stay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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