Repository navigation
[FlyDSL] [CI] MegaMoEV2: fuse Dispatch, GEMM1, GEMM2 and Combine into one kernel - #6881
Conversation
…to one kernel Add two opt-in paths for the EP8 DeepSeek-V4 a8w4 layout on gfx950: - AITER_MEGA_SMALL_M: fuse quant, dispatch, GEMM1, GEMM2 and combine into one MegaMoE kernel; called through forward_small_m(). - AITER_MEGA_LARGE_M: fuse dispatch, GEMM1, GEMM2 and combine into one persistent MegaMoE kernel, with quant as a separate kernel. Signed-off-by: zhengyao <zayao@amd.com>
🏷️ CI GuideRuns automatically on every PR:
Extended tests (opt-in via labels):
One backend per PR: PR title tags & labels: |
Opt-in switches for the two aiter MegaMoEV2 paths (needs ROCm/aiter#6881): - SGLANG_OPT_USE_AITER_MEGA_SMALL_M: steps whose DP-global max token count fits the small-input runtimes call forward_small_m(), one kernel for quant, dispatch, GEMM1, GEMM2 and combine. - SGLANG_OPT_USE_AITER_MEGA_LARGE_M: extend steps with at least SGLANG_OPT_AITER_MEGA_LARGE_M_MIN_TOKENS (default 1024) tokens on some DP rank run the persistent large-input kernel. Each MoE layer gets its own protocol slot on the shared operator, and the choice of path depends only on DP-gathered counts, so every EP rank takes the same path.
…e fused paths Correctness: - The large-input choice no longer depends on a per-rank mask: its combine skips invalid (-1) top-k slots, so all ranks take the same path. The small-input combine skips them too. - The large-input quant kernel runs on the stream passed to forward(). - A small-input rank with no tokens passes a 1-row stand-in to the kernel. - Both paths are gated to SiLU. - Small input serves up to 512 tokens per rank. Skewed routing, decided inside the kernels; uniform routing keeps its code path and speed: - Large input: GEMM1 takes m-tiles in payload completion order when one expert holds many tiles, and GEMM2 picks its claim order and chunk length from the rows this rank received. - Small input: a rank that receives many rows, or has a large expert, reads the expert weights through the cache and sends its GEMM2 rows as FP8 (1x32 E8M0 scales); the other ranks keep streaming weights and BF16 rows, and the combine reads both formats per peer. Tests cover the explicit stream, rank-local mask, SiLU gate, empty ranks, -1 slots and skewed routes. Signed-off-by: zhengyao <zayao@amd.com>
The fused kernels waited on cross-rank flags with mori device helpers, which makes FlyDSL link an external library and skip its disk cache, so they could not be precompiled. They now wait with the comm_ops helpers the main-path kernels use; behavior and timing are unchanged. aot/flydsl/mega_moe.py builds, for every rank of the DeepSeek-V4-Pro EP8 a8w4 profile (48 experts per rank), each config of the small-input runtimes (MTPR 256 and 512) and the large-input kernel (MTPR 8192) with its quant kernel. The kernel configs and specs come from functions the runtimes share, so AOT and runtime keys match; a run-only check (FLYDSL_RUNTIME_RUN_ONLY=1) passes on the main, small and large paths. Signed-off-by: zhengyao <zayao@amd.com>
Resolve the conflict with the MegaMoE AOT rework (ROCm#6362), which builds the cache by replaying MegaMoEV2 itself and calling preload_aot_bundles(): - aiter/aot/flydsl/mega_moe.py and README.md take main's version. The MTPR 8192 jobs build the instance with AITER_MEGA_SMALL_M=1 and AITER_MEGA_LARGE_M=1; the runtime's eligibility checks decide which profiles get the small- and large-input kernels. - MegaMoEV2.preload_aot_bundles() also loads the small- and large-input runtimes the instance built. Each runtime's preload_aot_bundle() runs the same launch code as forward() with a no-dispatch flag: every token bucket's kernel for the small-input runtimes, the quant and persistent kernels for the large-input runtime. - The large-input runtime allocates its token-ready arena through the op's symmetric allocator like its other arenas, so the replay's fake shmem covers it. Signed-off-by: zhengyao <zayao@amd.com>
… binding - Large-input GEMM1: thread 0 broadcast the claimed unit and then has_work through one LDS word that was also the first word of the A tile buffer. A slow wave could read has_work as the unit, or tile data as has_work and skip a tile the rest of its block ran (mismatched barriers). Each value now has its own LDS word outside the tile buffers; the loop keeps its barriers, so the GEMM1 code is unchanged. - forward_small_m() needs max_tokens: a per-rank default could put ranks on different small-input runtimes, which share no protocol state. - The small-input kernel splits its work over 8 XCDs, so it is eligible only on 256-CU (8-XCD) gfx950 parts. - bind_weights() refuses strided weights instead of binding a temporary copy a captured graph would outlive; the constructor keeps its contiguous copies for the instance's lifetime. - bind_weights() and bind_layer() check everything that can refuse (contiguity, the small-input W2 dispatch tables during capture, the layer slot) before changing any binding. - The large-input quant runs on the default stream for a null stream handle: ExternalStream(0) would take a new pool stream. Signed-off-by: zhengyao <zayao@amd.com>
NOT READY FOR HUMAN REVIEWPR Self-ReviewRevision Waiting for the PR author. The readiness report will appear after the self-review answers are complete for this revision. Questions to answer in the PR description
Edit the Missing or invalid answers
Shadow mode reports what would block but keeps the status successful. Set |
LargeMRuntime.bind_layer() switched the layer slot before binding the weights, so a refused binding (strided weights) left the new slot with the previous weights. It now checks the slot first, binds the weights, then switches the slot, like MegaMoEV2.bind_layer(); the edge test covers both refusals on the runtime itself. Signed-off-by: zhengyao <zayao@amd.com>
With one state slot the edge check's "other" slot was the bound one, so it could not catch bind_layer switching the slot before refusing the weights. The test now builds two slots by default and reports the check as RANGE-ONLY when run with one. It fails on the old bind_layer order and passes on the current one. Signed-off-by: zhengyao <zayao@amd.com>
…ight rebinding - SmallMRuntime refuses a device without 256 CUs itself: its kernel splits the work over 8 XCDs, so a 4-XCD part would leave half of it unclaimed even when the runtime is built directly. - SmallMRuntime and LargeMRuntime keep the contiguous copies they make of strided construction weights for their lifetime, as MegaMoEV2 does, so a graph captured with them never outlives them. - The main and large-input Stage1 refuse scales that are not 1-byte E8M0 rows of model_dim / 32 on the instance's device; the kernel reads a whole row per token. - The layer-slot stress gives layer B its own W2 values (twice A) and nonzero routing weights, checks three slots per cycle against the main path on the same binding, and checks that layer B changes the output, so a call on the wrong layer's weights now fails it. Signed-off-by: zhengyao <zayao@amd.com>
The Stage1 checks accepted any 1-byte scales, so float8_e4m3fn, int8 or bool scales were read as E8M0 exponents without an error. The main and large-input Stage1 now accept uint8 or float8_e8m0fnu only; the edge test feeds both a float8_e4m3fn view and expects a refusal. Signed-off-by: zhengyao <zayao@amd.com>
mega_moe@1.0 is the expert-parallel MoE of one call on every rank of an EP group: dispatch of each token to the ranks holding its top-k experts, the two MXFP4 expert GEMMs with the clamped SwiGLU, and the combine back on the token's rank. The reference sums over the group's experts in float64; the baseline is aiter MegaMoEV2's main path, and with one rank aiter's local fused_moe on the same gate-up-interleaved experts (MegaMoEV2 needs mori's group). Three flydsl kernels serve DeepSeek-V4-Pro EP8 a8w4 (7168 / 3072, 48 experts a rank, top-6): aiter's single-kernel small-input path at 1-256 and 320-512 tokens a rank, and its persistent large-input path at 1024-8192. Their host code takes the weights with every call, sets up the mori symmetric buffers once in the compile function, and writes the combine straight into out. The sources follow ROCm/aiter#6881 at a7b7e7441, including its fix of the large-input GEMM1 work broadcast (one LDS word per broadcast value, apart from the tile buffers) and the small-input 256-CU requirement. Signed-off-by: zhengyao <zayao@amd.com>
…ract - The 32-bit launch generations of the fused paths may wrap: every wait that relied on them growing now waits for equality. Small-input plan words and the combine format flag compare their epoch field exactly (spin_until_shifted_eq_i64), GEMM2 readiness is a release store instead of an unsigned max that would stick at 0xffffffff, and the large-input launch handshake and combine generation wait for their exact value (no peer can be a launch of the same slot ahead). - weights.py holds the binding contract: bind_weights() on the operator and both runtimes refuses weights that are strided or differ from the construction shape, dtype (byte views of FP4/E8M0 are equivalent) or device, and computes every view before it changes any binding. - forward() of every path refuses inputs on another device; a rank that faulted on a foreign pointer would leave its peers waiting. - SmallMRuntime built directly checks the full layout MegaMoEV2 gates it to (small_m_eligible). - python -m aiter.aot.flydsl.mega_moe --check replays in run-only mode and fails on any kernel the cache lacks. - Tests: launches across preset epochs just below 2**31 and 2**32 (the previous code hangs there), every rank with zero rows, the null stream, and a refused binding leaving every bound view, table and slot of the operator and its runtimes unchanged. Signed-off-by: zhengyao <zayao@amd.com>
Signed-off-by: zhengyao <zayao@amd.com>
#14) * feat(flydsl): add the mega_moe op with DeepSeek-V4 EP8 MegaMoE kernels mega_moe@1.0 is the expert-parallel MoE of one call on every rank of an EP group: dispatch of each token to the ranks holding its top-k experts, the two MXFP4 expert GEMMs with the clamped SwiGLU, and the combine back on the token's rank. The reference sums over the group's experts in float64; the baseline is aiter MegaMoEV2's main path, and with one rank aiter's local fused_moe on the same gate-up-interleaved experts (MegaMoEV2 needs mori's group). Three flydsl kernels serve DeepSeek-V4-Pro EP8 a8w4 (7168 / 3072, 48 experts a rank, top-6): aiter's single-kernel small-input path at 1-256 and 320-512 tokens a rank, and its persistent large-input path at 1024-8192. Their host code takes the weights with every call, sets up the mori symmetric buffers once in the compile function, and writes the combine straight into out. The sources follow ROCm/aiter#6881 at a7b7e7441, including its fix of the large-input GEMM1 work broadcast (one LDS word per broadcast value, apart from the tile buffers) and the small-input 256-CU requirement. Signed-off-by: zhengyao <zayao@amd.com> * fix(mega_moe): run it as a ranks group and build it with FlyDSL 0.3.2 - Call the op as a ranks group (ranks="ep_size"): group.py sets up mori's symmetric heap over the group, 16 GiB so MegaMoEV2's and the kernel's buffers both fit; the reference reads each expert from its owner rank's args and decodes to float32. The baseline takes out=None. - communication_ops_utils.py emits its ordered loads, stores, fences and atomics as LLVM-dialect ops, since FlyDSL 0.3.2 (the baseline AITER pin) lacks fx.generic_load/generic_store/atomic_*/memory_fence; schemas renamed. - CI: FIXME workaround exposing TheRock's libdrm headers to mori's JIT. --------- Signed-off-by: zhengyao <zayao@amd.com> Co-authored-by: zhenhuang12 <Zhen.Huang@amd.com>
Main's gfx1250-strict wheel builds (#7379, #7434) and MegaMoEV2 (#6881) edited the code this branch rewrites; both sides are kept: - moe.py: an arch's replay keeps the jobs of that arch's family, retargets them to it and leaves out the MXFP4 ones on gfx1250-strict; deployment jobs now pass through that filter as rows do. run_aot and moe.py's main run inside fail_build_on_missing_target_feature(), and run_aot still ends with the strict errata check. - mega_moe.py: the MTPR 8192 instances build the opt-in small- and large-input paths, and --check compiles nothing. FUSED_PATHS_MTPR and FUSED_PATHS_ENV stay module constants: they say how the replay builds an instance, not what a deployment is. - README: main's gdn_fused_prefill entries next to the deployment ones. #6464 moved DeepSeek-V4-Pro's TP8 rows at M 32 and 2048 to MXMOE stage 1, which compiles the clamp in, so the clamp the TP8 deployment passes now picks kernels the deployment builds. The merge changes no job list: the 1195 deployment jobs are 88ab025's, the 14374 tuned-row jobs main's (apart from the empty expert_mask_size this branch adds) and the 192 MegaMoE jobs main's. Co-authored-by: Cursor <cursoragent@cursor.com>
Title: [FlyDSL] [Perf] MegaMoEV2: fuse dispatch, GEMM1, GEMM2 and combine into one kernel
Add two opt-in paths for the EP8 DeepSeek-V4 a8w4 layout on gfx950:
Motivation
DeepSeek-V4-Pro served with
TP8 DP8(attn) EP8on MI355X runs every MoE call through one MegaMoEV2 instance, from a few tokens per rank up to 8192. These two paths speed up the small- and large-input ends of that range; everything else keeps the existing path.Technical Details
mega_moe/envs.py; existing MegaMoEV2 kernels do not change.forward_small_m()when the DP-global max token count fits, and turns the large-input path on withset_large_m_enabled()for extend steps with at least 1024 tokens on some rank.aiter/ops/flydsl/kernels/mega_moe/:mega_moe_small_m.pyandmega_moe_large_m.py(one per path: dispatch, kernel, runtime) andmega_moe_fused_gemm.py(GEMM tiles and configs both use).MegaMoEV2imports them only when a path is enabled; its existing path and arguments do not change.test_mega_moe_v2.py([CI] Skip MegaMoE v2 multi-GPU tests #5003) and runs by hand on 8 GPUs.Test Plan
8 × MI355X, ROCm 7.2.0, image
lmsysorg/sglang-rocm:v0.5.21-rocm720-mi35x-20261007, aiter main 7061cbd, FlyDSL 0.4.0,MORI_SHMEM_HEAP_SIZE=16G.End-to-end setup, the same for every line below unless noted:
--scenario agentx, datasetsemianalysis_cc_traces_weka_062126), 3600 s per run, concurrency 128 / 256 / 384.TP8 DP8(attn) EP8(attention data-parallel on 8 ranks, MoE expert-parallel, 48 experts per rank), MegaMoE a8w4 with MTPR 8192, DSPARK speculative decoding with the InferenceX acceptance simulation (accept length 3.77 on every line), sglang-router cache_aware + dp-aware. Server, router and client flags are those of InferenceX run 37608671586.lmsysorg/sglang-rocm:v0.5.21-rocm720-mi35x-20261007.The two lines on our node differ only in the MoE kernels. They were measured before this branch was rebased onto aiter main and before the skewed-routing changes; the kernel tables above are from the final code. GSM8K uses the same server with real speculative verification instead of the acceptance simulation.
Test Result
V4-Pro EP8 A8W4; H=7168, I=3072, 384 experts, top-k=6; 8 × gfx950 / 256 CU. Latency in µs, CUDA Graph 10 replays × 3 rounds, median of rank-max latency. Speedup = baseline / this PR.
Uniform routing — MTPR=TPR
Uniform routing — MTPR=8192
Skewed routing — speedup vs main, MTPR=8192
hot60: floor(0.6 × TPR) tokens route to experts 0–5;Zipf: exponent 1.3;one_rank: all routes target experts 0–5;boundary: experts 0, 1, 2, 3, 46, 47;rank-balanced-hot: six distinct destination ranks per token, with 60% probability of local expert 0 on each selected rank. Values below 1.00× indicate regressions.End-to-end A/B on our node, MegaMoEV2 (our node) vs this PR. Total token throughput = all input tokens (including prefix-cache hits) plus output tokens, per second per GPU; run 1 / run 2, gain of the means:
Against the published InferenceX results:
InferenceX official → this PR (first run of each point, as plotted):
GSM8K (1319 questions, 5-shot): MegaMoEV2 0.932–0.938, this PR 0.926–0.948.
Submission Checklist