You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[OPUS] [FlyDSL] [JIT] [Feature] Build the MoE and MegaMoE AOT caches by replaying the runtime on CPU - #6362
Folds in #6310 (opus fake signatures, now closed). It is kept here despite the one-backend rule because the replay below is the only code that runs those ops under FakeTensorMode, and it cannot land without the fix.
Summary
Replaces the FlyDSL MoE AOT scripts that restated the runtime's kernel selection (aiter/aot/flydsl/moe.py, mxfp4_moe.py, fhmoe.py, grouped_moe.py) with a CPU-only replay of the runtime itself. For every tuned row, moe.py builds what a caller passes fused_moe as FakeTensors and calls _fused_moe_impl (_fhmoe_impl for shared-expert rows) with COMPILE_ONLY=1, so every FlyDSL kernel on the runtime's path is compiled into the cache instead of launched. That includes the gfx1250 grouped table, which _fused_moe_impl hands to FlyDSL's grouped GEMM.
MegaMoE gets the same treatment: mega_moe.py builds the runtime's MegaMoEV2 for each rank of each deployment profile in the same dry run and calls its own preload_aot_bundles(), the method serving uses to load a profile's kernels.
Running the runtime this way also surfaced two bugs outside the AOT scripts, fixed here: the fakes of two opus a8w4 MoE ops lacked parameters, and merging tuned-config tables wrote integers as N.0, which the gfx1250 grouped MoE runtime cannot parse.
Which archs get built follows the build: each row compiles for the arch its gfx column names, and with GPU_ARCHS naming targets, as setup.py's wheel build sets it, only rows of those archs are replayed. Each arch replays in a process of its own that aiter's runtime arch queries answer for that arch, explicitly (AITER_AOT_REPLAY=1), never for the machine it runs on.
Against a full build of the old MoE scripts, the runtime needs 249 kernels they never compiled, which JIT on the first inference call. Most are the heuristic kernels the runtime switches to when a tuned config cannot take an expert bias; others have compile-time parameters that follow the actual M.
grouped_moe.py compiled only the auxiliary kernels around the gfx1250 grouped GEMM. Since the runtime moved that GEMM to the TDM batched kernel, the GEMM itself compiled on first use, and most of the auxiliary variants it did compile were for the removed non-TDM path.
The old mega_moe.py compiled only the mask_topk_ids=False combine kernels, so forward(mask_invalid_slots=True) compiles online, and fails under FLYDSL_RUNTIME_RUN_ONLY=1.
AOT must run without a GPU.
Changes
aiter/aot/flydsl/dry_run.py (new): context manager for the replay. It sets COMPILE_ONLY=1, pins the target arch, CU count and optionally the device name (MegaMoE's combine geometry is tuned per GPU model), enters FakeTensorMode, and stubs or forbids the GPU runtime (CUDA init, streams, events and device queries, the FlyDSL execution engine). It also turns off the runtime's rebuild of a C++ module whose device code lacks the target arch: only host code runs there, and the per-arch replays share one build.
aiter/aot/flydsl/moe.py (rewritten, 1186 to 726 lines):
Parses the tuned FMoE, FHMoE and gfx1250 grouped tables with strings only, so the parent process stays on the light AITER_AOT_IMPORT import.
Enumerates per row what the row does not fix. On the caller side: expert bias for Swiglu + MXFP4 rows, the GPT-OSS path, as fused_moe enables it (both stages, or stage 1 only), both gate/up layouts for MXMOE stage 1, SiTUv2 betas (unset, 1.0, model defaults), num_local_tokens (passed by callers behind an EP dispatch), and for FHMoE rows with MXFP8 routed weights both values of clamp_shared ([FlyDSL] [CI] Add HY4 MXFP8 heterogeneous MoE fusion #6050), which the runtime compiles into stage 1. For M: both values get_padded_M maps to the row (token // 2 + 1 and token).
Where a variant's two token counts request different FlyDSL kernels, identified by FlyDSL's own cache key, it bisects the bucket down to every M at which they change, because a row can switch more than once in its bucket: for 7168x768, E384, topk 6 at token 2048, the gfx1250 stage-1 quant uses K split 7, then 4, then the compact quant. These replays are reported as [BISECT].
Replays gfx1250 grouped rows with the gate/up-interleaved weights and n32k4 scales gfx1250 callers pass. Grouped rows tuned for EP only (topk −1, or ep_fused) are skipped: the runtime picks them only under a caller's expert mask or MegaMoE's stage-2 scatter, and the table records neither the global expert count nor the real topk.
Replays each job. Caller-side combinations the runtime itself rejects (NotImplementedError) are reported as [SKIP]. A row's default caller must be served: token counts at which the runtime refuses it while serving it at others (an M-dependent limit, such as the 32-bit route-reduce workspace that op_tests/test_moe_2stage.py also skips) are reported as [REFUSED]. A row refused at every token count, or any other error, fails the build.
Replays the archs GPU_ARCHS names, intersected with the tables' gfx column (every arch with rows when it is unset or native), so CI's gfx942;gfx950 wheel build skips the gfx1250 rows.
Runs one process per arch, started with GPU_ARCHS naming it and AITER_AOT_REPLAY=1 and with the GPUs hidden, since aiter fixes arch-dependent values such as dtypes.fp8 at import. Each process checks at entry that it was started that way and that dtypes.fp8 is its arch's type. aiter's C++ core module, which has host code only, is built once before they start: not every toolchain builds every arch (ROCm 7.2.4 cannot build gfx1250's cluster launch).
Schedules the jobs:
A worker replays up to 12 jobs of one row in turn, so the kernels they share compile once instead of concurrent variants waiting on each other's compile locks.
Equal-sized batches are shuffled stably, so rows of one model do not start together.
--check compiles nothing and fails on any kernel the replay needs but the cache lacks (FLYDSL_RUNTIME_RUN_ONLY=1).
aiter/aot/flydsl/mega_moe.py (rewritten, 590 to 305 lines): one job per rank of each deployment profile (DeepSeek-V4-Pro r0/r32/r64, Kimi-K3; MTPR 8192/16384/32768) and quant (A8W4, and A4W4 since [FlyDSL] [MegaMoE] A4W4 mega_moe operator #4980). Each job builds MegaMoEV2 with FakeTensor weights and calls preload_aot_bundles() and quantize(), which the preload leaves out but fixed-slot forward and the per-stage benchmarks use. mori shmem, through which MegaMoEV2 allocates symmetric buffers and exchanges peer pointers, hands out FakeTensors and fake addresses; a build host without mori gets a stand-in module. One job covers both stages, so the old script's 384 jobs become 192. The module keeps only the profiles; CLI options are unchanged. Like the MoE replay, it runs in a process of its own, started with GPU_ARCHS=gfx950 and AITER_AOT_REPLAY=1, and it is skipped when GPU_ARCHS names targets without gfx950.
aiter/aot/flydsl/common.py: run_aot runs the MoE and MegaMoE replays first, in processes of their own (replay_env), then the pooled kinds. OpKind.MXFP4_MOE and OpKind.GROUPED_MOE are removed.
aiter/jit/core.py: update_config_files reads the tables it merges as text, so the merged file reproduces every cell as written. Parsed, a column with a blank cell turned float and its integers were written back as N.0: 6 of the 15 merged tables had such cells, and since [Config] [Tuning] Add tuned grouped MoE config for Qwen3.5 FP4 on gfx1250 #6090 added a second grouped table, the gfx1250 grouped MoE runtime raised ValueError on every row of the merged one. Shapes are still compared on numeric values, so 256 and 256.0 stay one shape, and the fastest duplicate is still picked on a numeric us. Since [JIT] Check single-file tuned configs for duplicate shapes #6365 that check lives in _raise_on_duplicate_shapes, shared with the single-file path, which now reads its table as text too.
Runtime-side support:
aiter/jit/utils/torch_guard.py: run_host_scalar_ops_for_real() lets scalar-returning custom ops run their implementation under FakeTensorMode, since their fakes carry no value. Off by default.
aiter/jit/utils/chip_info.py: in an AOT replay process (AITER_AOT_REPLAY=1, set only by replay_env), get_gfx_runtime() returns the one arch GPU_ARCHS names and does not probe the device, so a visible GPU of another arch cannot leak in. Elsewhere it behaves as on main, which never lets GPU_ARCHS override it.
aiter/utility/dtypes.py: since [FlyDSL] [JIT] Fix Windows build arguments and optional imports #6589, get_dtype_fp8() returns uint8 when torch sees no device. An AOT replay process, which hides the GPUs, now takes its arch's fp8 type from get_gfx_runtime() instead; every other process keeps main's behavior. Without it, every fp8 path of the replays took the wrong branch: fp8 activations were quantized to fp4, MXFP8 rows found no input recipe, and gfx1250 grouped rows compiled other kernels than the runtime requests without any error. op_tests/test_dtype_parser.py (main's tests unchanged) and op_tests/test_build_targets_architecture.py cover the flag.
aiter/fhmoe.py: fhmoe_ is split into a plain _fhmoe_impl and the decorated op (same signature, including [FlyDSL] [CI] Add HY4 MXFP8 heterogeneous MoE fusion #6050's clamp_shared), so the replay can call the implementation under FakeTensorMode.
aiter/ops/opus/moe_stage1_a8w4.py, aiter/ops/opus/moe_stage2_a8w4.py: the fakes registered for _opus_moe_stage1_a8w4_fwd_raw and _opus_moe_stage2_a8w4_decode_fwd_raw lacked the ops' scalar parameters (topk, block_m, kernelName, inter_dim_pad, activation, swiglu_limit, situ_beta, situ_linear_beta and token_num, topk, block_m, kernel_id, inter_dim_pad). torch_compile_guard calls the fake with every argument the op takes, so any call under FakeTensorMode raised TypeError. The fakes still return out.
op_tests/test_fhmoe.py: the tests of the removed script's internals are replaced by five tests of the replay: row parsing (DSV4 i384 and HY4 MXFP8 rows, clamp_shared), import under AITER_AOT_IMPORT=1, padding columns, the caller choices handed to _fhmoe_impl, and, on gfx950, replaying HY4 rows into a scratch cache and loading their kernels under FLYDSL_RUNTIME_RUN_ONLY=1.
aiter/aot/flydsl/README.md: documents how both replays work and --check.
Runtime behavior is unchanged apart from the config merge: the torch_guard switch is off outside the replay, the chip_info and dtypes changes only apply under AITER_AOT_REPLAY=1, and eager execution never calls the opus fakes. Merged tables now carry their sources' cells unchanged; no value differs from before, and the gfx1250 grouped lookup that raised now parses.
Performance (AOT build time, 256-CPU host, 96 workers)
MoE kinds, measured before the last merge of main:
Build
Old scripts (pooled run_aot path)
This PR
From scratch
170 s for MoE + MXFP4 MoE (3713 jobs, 2595 artifacts), plus 10 s for grouped MoE
166 s (22350 jobs plus 1588 bisection replays, 2353 artifacts)
Rebuild, cache already complete
not measured
30 s
--check of a complete cache
n/a
44 s
Run-to-run variation is about ±6%. Without the scheduling above (one forked worker per job, archs one after another), the from-scratch build took 537 s. That build was bound by the parent's fork rate and by concurrent variants of a row waiting on each other's compile locks. The artifact count follows the runtime: it fell from 2751 to 2353 when #6376 limited FlyDSL expert bias to Swiglu rows, rose to 3463 (22698 jobs) after #6460 reverted that and #6405 added the glm53 fp4 table, and is 2406 (22992 jobs) since #6478 limits it to Swiglu again.
On the current head with #6700 (FlyDSL 0.4.0, AITER_MAX_JOBS=160), the MoE build takes 124.5 s from scratch for every arch (9440 jobs plus 660 bisection replays; 8948 of the jobs remain under CI's GPU_ARCHS=gfx942;gfx950). Before 2f85842 limited expert bias to Swiglu rows, it took 182 s at AITER_MAX_JOBS=96 (22992 jobs plus 1696 bisection replays). MegaMoE, A8W4 and A4W4, takes 415 s (192 jobs).
The wheel build's run_aot now runs MegaMoE as a phase of its own instead of inside the shared pool (the 180 s below).
MegaMoE, same host and workers, A8W4 only (measured before #4980 added A4W4, which builds about as much again):
Build
Old script
This PR
From scratch
222 s (192 jobs, 3018 artifacts)
180 s (96 jobs, 3162 artifacts)
Testing
Unless marked as measured on the current head with #6700, the numbers below were measured on 61fa528 or earlier, before 2f85842 limited expert bias to Swiglu rows.
Full replay on the current head with [JIT] Replay biased interleaved MoE callers again and fix #6362's CI failures #6700 (main through [MHA] Fix -DONLY_FAV3=0 quoting that breaks the backward JIT build #6616, FlyDSL 0.4.0; all tuned FMoE, FHMoE and gfx1250 grouped rows; gfx942, gfx950, gfx1250): 1551 rows, 9440 jobs and 660 bisection replays (gfx1250 422, gfx950 238), 0 failures, 2191 artifacts, and --check of that cache passes. Caller-side combinations the runtime itself rejects are reported as [SKIP] (153, all on gfx950): no safe MXFP4 backend at inter_dim 384/640/1152, which includes MiniMax-M3 a4w4 with interleaved weights and a bias, and the 32-bit route-reduce limit. One token count is [REFUSED] (see Known gaps). Without [JIT] Replay biased interleaved MoE callers again and fix #6362's CI failures #6700, head 2f85842 replays 9116 jobs into 2156 artifacts and lacks 35 the runtime requests: GPT-OSS a8w4's interleaved, biased kernels on gfx950 and the biased gfx1250 callers'.
Target archs: with GPU_ARCHS=gfx942;gfx950, as CI's wheel build sets it, the replay skips the 1428 gfx1250 jobs and leaves exactly the gfx942 and gfx950 artifacts of the full replay (84 and 2057, same keys); with GPU_ARCHS unset, all 2406. MegaMoE leaves the same 6236 artifacts as before and is skipped under GPU_ARCHS=gfx942.
Without the arch reaching the replay processes (each inheriting CI's GPU_ARCHS=gfx942;gfx950, so dtypes.fp8 is uint8 since [FlyDSL] [JIT] Fix Windows build arguments and optional imports #6589), the gfx950 replay fails 530 jobs, misses 503 of its 2057 artifacts and builds 294 wrong ones; the gfx1250 replay reports no failure while 185 of its 265 artifacts are missing, all 130 fp8 TDM grouped GEMMs among them. gfx942 is unaffected only because none of its rows has fp8 activations. With the flag, a gfx942 replay process on this gfx950 host gets float8_e4m3fnuz and gfx942 from get_gfx_runtime() even with a GPU visible.
Artifact count across the merge: replaying the pre-merge head and the current head with the same FlyDSL 0.4.0 gives 3463 and 2406 artifacts. The difference is [HIP] [CK] [MoE] Fix missing SwiGLU non-split no-bias CK-Tile MXFP4 dispatch (follow-up to #6376 revert) #6478's limit of FlyDSL expert bias to Swiglu: all 405 MXMOE gemm2 kernels that disappear are bias variants whose bias-free twin is still built, the FlyDSL a8w4/a4w4 GEMMs drop their bias specialization (per kernel name, exactly half the artifacts where the bias variant applied), and a bias=both replay of a Silu row requests one extra stage-1 kernel before the merge and none after. The only new kernel names come from tables main added.
Wheel-build emulation: the AOT step run the way CI's setup.py bdist_wheel runs it (aiter/jit/core.py loaded a second time as jit.core, aiter imported with AITER_AOT_IMPORT=1, every JIT .so removed first, GPU_ARCHS=gfx942;gfx950). Before the fix, CI's wheel build failed on both replays: the gfx950 MoE replay found module_moe_mxfp4_aux built for gfx942 only, by the gfx942 replay running alongside, and died; all 96 MegaMoE jobs raised KeyError: 'module_aiter_core'. Now the MoE replay (all three archs), MegaMoE (96/96) and the full run_aot (GEMM 6787, CONV 133, CHUNK_GDN_H 288, FMHA_FP8 92) pass, and the two replays leave exactly the artifacts of the standalone builds (5515 = 2353 + 3162). A later CI run failed again when the gfx1250 replay happened to build aiter's C++ core module first: gfx1250 builds turn on cluster launch, which the HIP of ROCm 7.2.4 does not declare. That module has host code only and is now built once before the replays start; in the emulation, only that step builds it. On the current head the emulation replays gfx942 and gfx950 only, as GPU_ARCHS names, with 0 failures. CI's prebuild wheel job passed on the head before the GPU_ARCHS filter, which still replayed gfx1250.
Dense M check: replaying every bucket at about 16 sampled M (every M up to token 64) under FLYDSL_RUNTIME_RUN_ONLY=1 finds no missing kernel: 132834 jobs on gfx950, 16716 on gfx942, 11160 on gfx1250. Before the bisection, 138 of the gfx1250 probes missed the stage-1 quant kernel of an intermediate K split; gfx942/gfx950 had no such case, and bisection compiles nothing new there.
The scheduled build leaves exactly the same artifact set as one worker per job, and --check passes on it.
Opus fakes: called under FakeTensorMode with every argument the ops take, both raise TypeError on main (takes 10 positional arguments but 18 were given, and 9 of 14 for stage 2) and return out here. Without the fix, 43 replay jobs fail.
MoE artifacts vs. a full build of the old scripts (same FlyDSL 0.3.4.1, on the base before the merges):
2502 shared artifacts, all byte-identical ELF.
93 only in the old build, none reachable by the runtime:
78 are bias variants of tuned configs that the runtime discards when a bias is present; this is confirmed per row from the runtime's own log.
13 are rows with token=3, which get_padded_M never returns.
2 are CK-Tile epilogues for rows that either have no split-K or are tagged flydsl_fallback, which the runtime drops at load.
249 only in this build: kernels the runtime uses that the old scripts missed.
[FlyDSL] fix(flydsl): support kmk3 ep moe & opt for ep moe gemm2 #5804's use_valid_token_count kernels: of the 442 the old mxfp4_moe.py builds for it, 215 are in the replay's cache, byte-identical. The other 227 are never requested by the runtime: bias variants of Silu/SiTUv2 rows, inline-sort rows whose tuned config the runtime drops under num_local_tokens (it then uses heuristic FlyDSL kernels, which the replay builds and the old script did not), and a token=3 row.
gfx1250 grouped artifacts vs. grouped_moe.py (its own table, 117 rows): 21 shared, byte-identical. 162 only in this build, including 112 TDM grouped GEMM kernels the old script never built. 149 only in the old build: 109 auxiliary variants for the removed non-TDM path, and 40 built with topk −1 for the EP-only rows, which no call matches. No gfx1250 GPU was available, so this path is validated on CPU only.
Kimi-K3 TP8 grouped table ([FlyDSL] Kimi-K3 TP8 gfx1250 grouped-MoE tuning (experts split 8 ways) #6768, inter_dim 384): [FlyDSL] Kimi-K3 TP8 gfx1250 grouped-MoE tuning (experts split 8 ways) #6768 made grouped_moe.py skip the stage-2 activation quant, whose layout needs inter_dim % 256 == 0. The replay needs no such rule: the runtime launches that quant only when gemm1 cannot requantize its own output (a gemm1 bias, or AITER_FLYDSL_DISABLE_GEMM1_REQUANT=1), and SiTUv2 callers pass no bias. The table's 15 rows replay as 168 jobs and 78 bisection replays, 0 failures, into 31 artifacts with no inter_dim-384 quant kernel; every gemm1 requantizes in its epilogue. With AITER_FLYDSL_DISABLE_GEMM1_REQUANT=1, every job fails on the assertion [FlyDSL] Kimi-K3 TP8 gfx1250 grouped-MoE tuning (experts split 8 ways) #6768 worked around, as the runtime itself would. Of the 11 artifacts main's grouped_moe.py builds for the table, 8 are in the replay's cache, byte-identical, and 3 are never requested: K-split route quant for wmma_rep 2 and 8, whose rows serve only 1025+ tokens, where the runtime turns K-split off, and a dense route-quant-scatter kernel the runtime no longer calls. The replay adds the table's 23 TDM GEMMs.
Config merge: regenerating all 15 merged tables changes no value, and each now has exactly as many N.0 cells as its sources. A duplicate written as 256 in one table and 256.0 in another is still caught and resolved to the faster row. op_tests/tuning_tests/test_config_shape_collision.py, test_tuner_infra.py and test_fast_tuning.py pass (72 tests).
kimik3_a16w4 has no cases because the test skips SiTUv2 a16w4. Re-run after the merges with the full default build: glm5_mxfp8 (32) and dsv3_fp4 (89) on idle GPUs, kimik2_i4 and dsv4_fp8fp4 (59 and 85 cases; the test skipped the rest because other jobs left too little free GPU memory for its reference) all pass with no cache miss.
AOT vs. on-GPU JIT: glm5_mxfp8 and dsv3_fp4 run again with an empty cache and the AOT check off. Every kernel the runtime JIT-compiles on the GPU (41 and 83) is byte-identical to the replay's. logits_diff varies from run to run either way: two runs on the same AOT cache differ in more cases than AOT vs. JIT.
MegaMoE full build: on the current head (FlyDSL 0.4.0), A8W4 and A4W4 each pass all 96 jobs.
MegaMoE A4W4 artifacts vs. the old script on current main (same FlyDSL 0.3.4.1): all 3218 old artifacts are present; 3082 are byte-identical and the other 136 differ only in the same v_add3_u32 vs. v_add_u32 + v_sub_u32 selection (2-7 asm lines). The 144 only in this build are again the mask_topk_ids=True combine kernels.
MegaMoE artifacts vs. a full build of the old script (same FlyDSL 0.3.4.1):
All 3018 old artifacts are present; 2921 are byte-identical.
The other 97 differ only in how one address sum is selected (v_add3_u32 vs. v_add_u32 + v_sub_u32, 2-7 asm lines). Building the old script twice gives 109 such differences, so this is FlyDSL/LLVM run-to-run variation, not the replay.
144 only in this build: the mask_topk_ids=True combine kernels (combine kernels 144 -> 288).
MegaMoE on 8x MI355X under FLYDSL_RUNTIME_RUN_ONLY=1, with the cache built by this PR: op_tests/multigpu_tests/test_mega_moe_v2.py --max-tok-per-rank 8192 --bs-list 1,128,1024,8192 --pad-tail-rows 4 passes for v4_pro and kimi_k3_route, A8W4 and A4W4, on the previous head (main through [FlyDSL] fix(comm-fused-moe): key winners by GPU model #6541, FlyDSL 0.3.4.1; not yet re-run on this one) (relL2 0.059 and 0.057 for A8W4 against rtol 0.10, 0.212 and 0.219 for A4W4 against rtol 0.25; CUDA-graph replay unchanged, masked rows zero). With the old script's cache, the --pad-tail-rows run fails with no usable AOT cache for combine_launch.
The MegaMoE replay also runs on a host without mori, including twice in one process.
op_tests/test_fhmoe.py, the AOT tests plus the clamp_shared and op-schema tests, on gfx950: 12 passed, including the run-only load of replayed HY4 kernels.
op_tests/test_flydsl_moe_aux.py (script, gfx950): every case matches exactly, including gather-reduce with f32, bf16 and f16 route weights. op_tests/test_dtype_parser.py and op_tests/test_build_targets_architecture.py pass with GPU_ARCHS unset, gfx950 and gfx942;gfx950; under the earlier GPU_ARCHS fallback, main's test_linux_runtime_errors_are_not_fallbacks failed with GPU_ARCHS=gfx950.
black and ruff are clean on the changed files.
To reproduce:
python -m aiter.aot.flydsl.moe # build, CPU only
python -m aiter.aot.flydsl.moe --check # verify an existing cache without compiling
python -m aiter.aot.flydsl.mega_moe # MegaMoE profiles, CPU only
Known gaps (follow-up)
EP-only rows of the gfx1250 grouped table are not replayed: 30 of its 153 rows (7168x3072 with 96 local experts, a8w4 and a4w4, two of them ep_fused; 7168x2048 with 64 and with 65 local experts). The runtime picks them only when the caller passes an expert_mask (it then looks up topk −1) or uses MegaMoE's stage-2 scatter (ep_fused), and the table records neither the global expert count nor the real topk the kernels are specialized on. Covering them needs an EP deployment profile per shape (global experts, EP size, topk), like mega_moe.py's profiles, plus a Stage2ScatterContext for the two ep_fused rows. Until then, those kernels compile on first use. The old grouped_moe.py did not cover them either: it built them with topk −1, which no call matches.
With the old merge, the two ep_fused rows also come out of the merged table as 1.0, which the runtime's string match never selects; the merge fix here keeps them 1.
Token counts above a row's tuned token are not replayed: get_padded_M serves M up to 131071 with the row tuned at 32768 and every M from 131072 up with the row tuned at 131072, while the replay covers M from token // 2 + 1 to token. Probing the extra range under FLYDSL_RUNTIME_RUN_ONLY=1 finds nothing missing for the 32768 rows or on gfx1250, but on gfx950, 42 of 4020 probes miss a stage-1 or stage-2 kernel at M >= 262144, on all three shapes tuned at 131072 (for a8w4 7168x3072 with 48 local experts, from M = 262144). Those kernels compile on first use; the old scripts did not cover them either. Covering them means replaying each tier's upper end and bisecting up to a chosen M cap.
The Qwen3 8x2400B fp4 row tuned at token 32768 cannot be served on main: its reduce-mode stage 2 needs a 5.4 GB route-reduce workspace, beyond the 32-bit buffer addressing [FlyDSL] [CI] Add HY4 MXFP8 heterogeneous MoE fusion #6050 guards, so the runtime raises NotImplementedError for every M from 26215 to 131071 that maps to the row, and test_moe_2stage.py skips it. The replay reports that token count as [REFUSED] and builds the row's kernels from the token counts it does serve. The fix belongs in the table (retuning that token count), not in the replay.
CI's wheel no longer ships gfx1250 MoE kernels: its GPU_ARCHS is gfx942;gfx950, and listing gfx1250 there would also build the C++ modules for gfx1250, which the HIP of ROCm 7.2.4 cannot (hipLaunchAttributeClusterDimension is undeclared). The old scripts, and this PR before this change, built them regardless of GPU_ARCHS. They build on their own with GPU_ARCHS=gfx1250 python -m aiter.aot.flydsl.moe (README §1): an installed wheel already ships the host-only C++ core module that build imports, and on ROCm 7.2.4 from source, GPU_ARCHS=gfx950 python -c "import aiter" builds it first. From a fresh JIT directory that sequence replays the 1428 gfx1250 jobs and leaves the same 265 artifacts as the full replay (counts measured before 2f85842).
gfx1250 is validated on CPU only so far; a run on a gfx1250 machine is pending: a strict cache-hit sweep of every grouped row (op_tests/flydsl_tests/test_flydsl_grouped_gemm.py --scenario csv, which runs under FLYDSL_RUNTIME_RUN_ONLY=1 by default), token counts inside the buckets the bisection covers (--scenario verify --tokens ...), and a byte comparison of the kernels the runtime JIT-compiles there (--no-check-aot-cache) against the AOT cache.
Documentation
aiter/aot/flydsl/README.md updated
Dependencies
No new third-party dependencies
Breaking Changes
python -m aiter.aot.flydsl.grouped_moe and the aiter.aot.flydsl.fhmoe module are removed; python -m aiter.aot.flydsl.moe covers their rows. Nothing in the repo or CI calls them. mxfp4_moe.py stays, outside the AOT build.
OpKind.MXFP4_MOE and OpKind.GROUPED_MOE are removed.
python -m aiter.aot.flydsl.moe keeps --csv and adds --row-tokens-only and --check.
When GPU_ARCHS names targets, the MoE and MegaMoE AOT (standalone and in run_aot) build only those archs; unset or native, every arch the tables have, as before.
Merged tuned-config tables (<tmpdir>/aiter_configs/*.csv) now keep each cell as written in its source, e.g. 64 rather than 64.0.
_gen_opus_moe_stage1_a8w4_fake_tensors and
_gen_opus_moe_stage2_a8w4_decode_fake_tensors lacked the scalar parameters
of the ops they are registered for, so any call under FakeTensorMode
raised TypeError (10 of 18 and 9 of 14 positional arguments accepted).
Signed-off-by: zhimding <zhimding@amd.com>
The MoE AOT scripts (moe.py, mxfp4_moe.py, fhmoe.py) restated the
runtime's kernel selection and launch parameters, so every kernel or
wrapper change needed a matching AOT change, and drift between the two left
kernels the runtime uses uncompiled.
moe.py now builds what a caller passes fused_moe for each tuned row as
FakeTensors and calls the runtime's own _fused_moe_impl (_fhmoe_impl for
shared-expert rows) inside dry_run, which sets COMPILE_ONLY=1 and forbids
the GPU runtime; every FlyDSL launch on that path is compiled into the cache
instead of run. The AOT side only enumerates what a row does not fix:
expert bias, gate/up layout and SiTUv2 betas on the caller side, and the M
values get_padded_M maps to the row. Each target arch replays in its own
process with the GPUs hidden, and --check verifies a cache without
compiling.
Runtime-side support:
- torch_guard: scalar-returning custom ops can run their implementation
under FakeTensorMode, whose fakes carry no value (off by default).
- chip_info: with no GPU to detect, a single explicit GPU_ARCHS stands in
for the live GPU.
- fhmoe: fhmoe_ is split into a plain _fhmoe_impl and the decorated op.
mxfp4_moe.py and fhmoe.py are removed; moe.py replays their rows.
Signed-off-by: zhimding <zhimding@amd.com>
A from-scratch build forked one worker per job (9903) from the full-import
arch process, so it was bound by the parent's fork rate, and the variants of
one row ran concurrently and waited on each other's compile locks. A worker
now replays up to 12 jobs of one row in turn, equal-sized batches are
shuffled so that rows of one model do not start together, and the arch
processes run side by side on a split worker budget.
Fresh full build at 96 workers: 537 s -> 187 s; rebuild with a complete
cache: 157 s -> 21 s. The compiled artifact set is unchanged.
Signed-off-by: zhimding <zhimding@amd.com>
zhiding512
changed the title
[FlyDSL] [JIT] [Feature] Build the MoE AOT cache by replaying fused_moe on CPU
[OPUS] [FlyDSL] [JIT] [Feature] Build the MoE AOT cache by replaying fused_moe on CPU
Oct 10, 2026
mega_moe.py restated MegaMoEV2's buffer geometry, placeholder tensors,
Stage2 variant expansion and combine geometry lookup, so MegaMoE kernel
changes needed matching AOT changes. It had also drifted: it compiled
only the mask_topk_ids=False combine kernels, so a caller passing
mask_invalid_slots=True compiles online.
Each job now builds MegaMoEV2 for one rank of a deployment profile
inside dry_run, with FakeTensor weights and a mori shmem that hands out
FakeTensors and fake peer addresses, and calls its preload_aot_bundles()
and quantize(). The module keeps only the deployment profiles, and one
job covers both stages, so 192 jobs become 96.
dry_run takes an optional device name, since the combine launch
geometry is tuned per GPU model, and stands in for torch.cuda Stream
and Event.
Signed-off-by: zhimding <zhimding@amd.com>
zhiding512
changed the title
[OPUS] [FlyDSL] [JIT] [Feature] Build the MoE AOT cache by replaying fused_moe on CPU
[OPUS] [FlyDSL] [JIT] [Feature] Build the MoE and MegaMoE AOT caches by replaying the runtime on CPU
Oct 12, 2026
Merging read each table with type inference, so a column with a blank
cell turned float and every integer in it was written back as N.0:
6 of the 15 merged tables carry such cells, among them tuned_fmoe and
tuned_grouped_fmoe. The gfx1250 grouped MoE runtime parses integers
from that text, so since #6090 added a second grouped table it raises
ValueError on every row of the merged one.
Read the tables as text so the merge reproduces each cell. Shapes are
still compared on numeric values, so 256 and 256.0 stay one shape, and
the fastest duplicate is still picked on a numeric us.
Signed-off-by: zhimding <zhimding@amd.com>
grouped_moe.py compiled only the auxiliary kernels around the grouped
GEMM: the runtime has since moved the GEMM to the TDM batched kernel,
which got no AOT coverage and compiled on first use, and most of the
auxiliary variants it did compile were for the removed non-TDM path.
On gfx1250 _fused_moe_impl hands the grouped table's rows to FlyDSL's
grouped GEMM, so moe.py now replays those rows as well, with the
gate/up-interleaved weights and n32k4 scales gfx1250 callers pass.
Rows tuned for EP only (topk -1, or ep_fused) are skipped: the runtime
picks them only under a caller's expert mask or MegaMoE's stage-2
scatter, and the table records neither the global expert count nor the
real topk. The two-stage host query is no longer preloaded when an
arch replays only grouped rows, since that path never makes it.
grouped_moe.py and OpKind.GROUPED_MOE are removed.
Signed-off-by: zhimding <zhimding@amd.com>
A row serves a bucket of M, and the replay compiled the bucket's two
ends, which covers any choice that switches once. The gfx1250 stage-1
quant's K split switches several times in one bucket (7168x768, E384,
topk 6, token 2048: K split 7, then 4, then the compact quant), so M in
the middle asked for a kernel the cache did not have.
Where a variant's two ends request different FlyDSL kernels, identified
by FlyDSL's own cache key, the worker now bisects the bucket to every M
at which they change and replays it. The default build replays 775
more M (573 on gfx1250, 202 on gfx950) and compiles nothing new on
gfx942/gfx950; a densely sampled RUN_ONLY replay of every bucket now
finds no missing kernel on any of the three archs.
Signed-off-by: zhimding <zhimding@amd.com>
mxfp4_moe.py stays deleted. #5804 taught it the use_valid_token_count
GEMM2 variant, which the runtime selects when the caller passes
num_local_tokens; the next commit has the replay enumerate that caller
choice instead.
Signed-off-by: zhimding <zhimding@amd.com>
Callers behind an EP dispatch pass the received token count as
num_local_tokens. Since #5804 the runtime then bounds the layout-v2
GEMM2 atomics by it, a compile-time choice the old mxfp4_moe.py had to
be taught by hand; on inline-sort rows the runtime instead drops the
tuned config for heuristic FlyDSL kernels that no AOT compiled.
Each row is now replayed with num_local_tokens too. Of the old script's
use_valid_token_count kernels, the replay has every one the runtime
reaches; the 227 it lacks come from bias variants of Silu/SiTUv2 rows
(the runtime enables bias for Swiglu only since #6376), from inline-sort
rows the runtime abandons under num_local_tokens, and from a token=3
row that get_padded_M never selects.
Signed-off-by: zhimding <zhimding@amd.com>
A replay process imports aiter for the arch it replays, so its
dtypes.fp8 matches the table's fp8 flavour. The pytest process imports
aiter for its host instead: on gfx942 dtypes.fp8 is float8_e4m3fnuz,
_caller_inputs finds no recipe for the HY4 float8_e4m3fn row, and both
cases of test_fhmoe_aot_replay_forwards_caller_choices failed on CI's
MI300X shard. Pin the alias as _mock_gfx950_fhmoe_metadata does.
Signed-off-by: zhimding <zhimding@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Reading tuned tables as text keeps every cell as written, but
_raise_on_duplicate_shapes still sorted on `comparable`, where a column
with one unparseable written cell stays text. One us written as nan, on
any row, made the lowest-us pick a text sort: "10.5" sorts before
"9.5", so the slower duplicate was kept and the faster row deleted from
its source table, where main, parsing nan as NaN, keeps 9.5. Coerce us
on its own.
op_tests/tuning_tests/test_config_shape_collision.py also pins the two
behaviours the text merge exists for: integer cells of a column with a
blank stay integers in the merged table, and 256 and 256.0 stay one
shape.
Signed-off-by: zhimding <zhimding@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Built the FlyDSL AOT cache from empty on both sides and compared the resulting artifacts.
Setup: main @ c2a682572, this branch @ 3563493b1, same box, FlyDSL 0.4.0, ROCm 7.2.4, GPU_ARCHS unset (all archs), separate FLYDSL_RUNTIME_CACHE_DIR per side.
Modules run — main: moe + mxfp4_moe + grouped_moe + mega_moe; this branch: moe + mega_moe
(matching what run_aot dispatches on each side).
Both sides build clean
kernels
result
mainmoe / mxfp4_moe / grouped_moe / mega_moe
2710 / 1783 / 356 / 384
all 0 failed
this branch, replay
gfx942 1218, gfx950 7576, gfx1250 492
all 0 failed
Plus 153 [SKIP] caller-side variants and 1 [REFUSED] token count on gfx950
(the 32-bit route-reduce workspace limit), and 660 [BISECT] probes, 0 failed.
Artifact diff
main
this branch
artifacts
9507 (166 MB)
8427 (155 MB)
common
7972
7972
only on this side
1535
455
Split by module (MegaMoE isolated into its own cache dirs to attribute it exactly):
main
branch
common
main-only
branch-only
MoE family
3415
2191
1944
1471
247
MegaMoE
6092
6236
6028
64
208
Which cache does the runtime actually want
--check replays the runtime and fails on anything the cache lacks, so it answers this directly:
So the asymmetry is real: main's extra 1535 artifacts are largely combinations no caller reaches,
while mainmisses 1312 that are reached.
The cleanest instance is cache-key drift rather than a missing compile — launch_topids_to_rows:
main: launch_topids_to_rows_193a5a5fbae511fbef597d5fc98e9e98
branch: launch_topids_to_rows_dbe460ae9a7818cb0e35d0afd200bbd7 <- what the runtime looks up
grouped_moe.py:252 does build it, but under a manager_key no caller ever requests. Every
gfx1250 grouped MoE row therefore JITs online today, while the AOT log reports 356 ok, 0 failed.
Four kernel names are absent from main's cache entirely — launch_gemm_a8w4_tdm (94), launch_token_multidest (17), launch_compact_quant (9), launch_scatter_preshuffle (5) —
and MegaMoE combine_launch coverage doubles (144 -> 288).
Two caveats
The replay needs up-to-date C++ modules. It executes aiter's real host code, so stale
prebuilt aiter/jit/*.so break it where the old enumeration never noticed. On my checkout all
9440 jobs failed with AttributeError: type object 'aiter.jit.module_aiter_core.ActivationType' has no attribute 'Relu2',
and the _load_host_helpers prebuild failed with module 'aiter.jit.module_moe_mxfp4_aux' has no attribute '_mxfp4_moe_sort_internal_is_supported',
until I deleted the stale .so and let them rebuild. Both symbols exist in csrc
(aiter_enum.h:15, rocm_ops.hpp:2871). Not a bug in this PR, but it is a new environment
requirement worth a line in the README next to the gfx1250 note.
--check only proves coverage of what the replay enumerates. The caller-side dimensions are
fixed (bias, gate/up layout, SiTUv2 betas, num_local_tokens, clamp_shared); a real caller
outside that set would be missed here too. The difference is that such a gap now shows up in --check, whereas main's key drift was silent. I have no independent evidence that all 1471 main-only artifacts are dead — only that the replay never requests them.
…-moe-dryrun-20261009
aiter/aot/flydsl/grouped_moe.py stays deleted: moe.py replays the grouped
table through the runtime. #6768 taught it to skip the stage-2 quant prep
for an inter_dim the quant layout rejects (Kimi-K3 TP8, 384); the runtime
launches that prep only when gemm1 cannot requantize (a gemm1 bias, or
AITER_FLYDSL_DISABLE_GEMM1_REQUANT=1), and the replay passes SiTUv2 rows no
bias, so it never reaches the prep either.
Co-authored-by: Cursor <cursoragent@cursor.com>
#6768 added it so the grouped-MoE AOT script could skip the stage-2
quant prep that inter_dim 384 (Kimi-K3 at TP8) cannot build. This branch
deletes that script: moe.py replays the grouped rows through the
runtime, which launches the prep only when gemm1 cannot requantize its
own output (a gemm1 bias, or AITER_FLYDSL_DISABLE_GEMM1_REQUANT=1).
Kimi-K3's SiTUv2 rows take no bias, so their replay requests no stage-2
quant kernel, and nothing else calls the helper.
Signed-off-by: zhimding <zhimding@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Include FlyDSL implementations and No-input replay recipes
aiter/aot/flydsl/moe.py:151
This filter excludes registered whole-graph FlyDSL names such as impl__flydsl_gfx942__16_16_16_False:default:0, which fused_moe.py resolves through fused_moe_registry. It rejects all 27 FlyDSL rows in qwen38_flash_next_bf16_tuned_fmoe.csv, leaving that table with zero replay jobs, so --csv <that table> --check succeeds without checking those kernels. Include these implementations and add their QuantType.No input recipe in _caller_inputs, with a regression test; accepting the prefix alone would hit the unsupported-recipe branch.
Resolves the conflict in aiter/aot/flydsl/moe.py.
Upstream ROCm#6362 ("Build the MoE and MegaMoE AOT caches by replaying the
runtime on CPU") removed the _precompile_to_cache helper that this branch
patched to pass a real int32 topk_ids placeholder. The replacement replay
path already builds "topk_ids": empty(m, topk, dtype=dtypes.i32) for every
job, so the dropped-route guard specialization is precompiled without any
extra change here. Taking upstream's file drops a hunk that no longer has
anything to apply to and keeps the AOT cache key correct.
All other changes are carried over unchanged.
The branch was 250 commits behind main and conflicted with it, so CI ran main's workflow
(which now runs pytest on tests/) against a tree without tests/ and failed before the op
tests. aiter/aot/flydsl/moe.py takes main's version: #6362 replaced the per-kernel AOT list,
in which this branch skipped the _g942 stage-1 names, with a replay of fused_moe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#6362, which this branch was stacked on, landed squashed as fc0a013.
Every file except the 12 of the replay commit is therefore taken as main
has it, which also leaves the PR's diff against main at those 12.
Conflicts:
- aiter/aot/flydsl/gemm.py: #6060 taught the hand-written AOT the gfx950
1x32 BMM (mxscale_bmm_groups_of_block) and to read a blank or 0
w_scale_block cell as the runtime's loaders do
(mxscale_w_scale_block_cell). The replay calls the public entries, which
take the scale block from the caller's w_scale, so none of that code
returns. The cell reading does: _blocks() reads a cell through
mxscale_w_scale_block_cell, 1x32 for the mxfp8 table and 128x128 for the
others, as their loaders default. A 0 cell made it raise before.
Main also brought #6060's BMM changes and #6946's launcher cache, which
this branch had not been validated against. Ten rows from the tables they
touch (gfx950 1x32, 32x32 and 128x128 through the 2D and batched entries,
gfx950 mxfp8, gfx1250 batched and a8w8 bpreshuffle) replay into a cache
that --check accepts, and on gfx950 the real calls of those rows and of
the sizes between them (9 calls) all run from it under
FLYDSL_RUNTIME_RUN_ONLY=1.
Co-authored-by: Cursor <cursoragent@cursor.com>
IzacharyI
added a commit
to IzacharyI/aiter
that referenced
this pull request
Oct 29, 2026
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>
#6362, which this branch was stacked on, landed squashed as fc0a013.
Every file except the 12 of the replay commit is therefore taken as main
has it, which also leaves the PR's diff against main at those 12.
Conflicts:
- aiter/aot/flydsl/gemm.py: #6060 taught the hand-written AOT the gfx950
1x32 BMM (mxscale_bmm_groups_of_block) and to read a blank or 0
w_scale_block cell as the runtime's loaders do
(mxscale_w_scale_block_cell). The replay calls the public entries, which
take the scale block from the caller's w_scale, so none of that code
returns. The cell reading does: _blocks() reads a cell through
mxscale_w_scale_block_cell, 1x32 for the mxfp8 table and 128x128 for the
others, as their loaders default. A 0 cell made it raise before.
Main also brought #6060's BMM changes and #6946's launcher cache, which
this branch had not been validated against. Ten rows from the tables they
touch (gfx950 1x32, 32x32 and 128x128 through the 2D and batched entries,
gfx950 mxfp8, gfx1250 batched and a8w8 bpreshuffle) replay into a cache
that --check accepts, and on gfx950 the real calls of those rows and of
the sizes between them (9 calls) all run from it under
FLYDSL_RUNTIME_RUN_ONLY=1.
Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the FlyDSL MoE AOT scripts that restated the runtime's kernel selection (
aiter/aot/flydsl/moe.py,mxfp4_moe.py,fhmoe.py,grouped_moe.py) with a CPU-only replay of the runtime itself. For every tuned row,moe.pybuilds what a caller passesfused_moeas FakeTensors and calls_fused_moe_impl(_fhmoe_implfor shared-expert rows) withCOMPILE_ONLY=1, so every FlyDSL kernel on the runtime's path is compiled into the cache instead of launched. That includes the gfx1250 grouped table, which_fused_moe_implhands to FlyDSL's grouped GEMM.MegaMoE gets the same treatment:
mega_moe.pybuilds the runtime'sMegaMoEV2for each rank of each deployment profile in the same dry run and calls its ownpreload_aot_bundles(), the method serving uses to load a profile's kernels.Running the runtime this way also surfaced two bugs outside the AOT scripts, fixed here: the fakes of two opus a8w4 MoE ops lacked parameters, and merging tuned-config tables wrote integers as
N.0, which the gfx1250 grouped MoE runtime cannot parse.Which archs get built follows the build: each row compiles for the arch its
gfxcolumn names, and withGPU_ARCHSnaming targets, assetup.py's wheel build sets it, only rows of those archs are replayed. Each arch replays in a process of its own that aiter's runtime arch queries answer for that arch, explicitly (AITER_AOT_REPLAY=1), never for the machine it runs on.Motivation
mxfp4_moe.pyits new GEMM2 compile parameter. With the replay, [FlyDSL] [MegaMoE] A4W4 mega_moe operator #4980 needs only"a4w4"added to the profile list.grouped_moe.pycompiled only the auxiliary kernels around the gfx1250 grouped GEMM. Since the runtime moved that GEMM to the TDM batched kernel, the GEMM itself compiled on first use, and most of the auxiliary variants it did compile were for the removed non-TDM path.mega_moe.pycompiled only themask_topk_ids=Falsecombine kernels, soforward(mask_invalid_slots=True)compiles online, and fails underFLYDSL_RUNTIME_RUN_ONLY=1.Changes
aiter/aot/flydsl/dry_run.py(new): context manager for the replay. It setsCOMPILE_ONLY=1, pins the target arch, CU count and optionally the device name (MegaMoE's combine geometry is tuned per GPU model), enters FakeTensorMode, and stubs or forbids the GPU runtime (CUDA init, streams, events and device queries, the FlyDSL execution engine). It also turns off the runtime's rebuild of a C++ module whose device code lacks the target arch: only host code runs there, and the per-arch replays share one build.aiter/aot/flydsl/moe.py(rewritten, 1186 to 726 lines):AITER_AOT_IMPORTimport.fused_moeenables it (both stages, or stage 1 only), both gate/up layouts for MXMOE stage 1, SiTUv2 betas (unset, 1.0, model defaults),num_local_tokens(passed by callers behind an EP dispatch), and for FHMoE rows with MXFP8 routed weights both values ofclamp_shared([FlyDSL] [CI] Add HY4 MXFP8 heterogeneous MoE fusion #6050), which the runtime compiles into stage 1. For M: both valuesget_padded_Mmaps to the row (token // 2 + 1andtoken).[BISECT].topk−1, orep_fused) are skipped: the runtime picks them only under a caller's expert mask or MegaMoE's stage-2 scatter, and the table records neither the global expert count nor the real topk.NotImplementedError) are reported as[SKIP]. A row's default caller must be served: token counts at which the runtime refuses it while serving it at others (an M-dependent limit, such as the 32-bit route-reduce workspace thatop_tests/test_moe_2stage.pyalso skips) are reported as[REFUSED]. A row refused at every token count, or any other error, fails the build.GPU_ARCHSnames, intersected with the tables'gfxcolumn (every arch with rows when it is unset ornative), so CI'sgfx942;gfx950wheel build skips the gfx1250 rows.GPU_ARCHSnaming it andAITER_AOT_REPLAY=1and with the GPUs hidden, since aiter fixes arch-dependent values such asdtypes.fp8at import. Each process checks at entry that it was started that way and thatdtypes.fp8is its arch's type. aiter's C++ core module, which has host code only, is built once before they start: not every toolchain builds every arch (ROCm 7.2.4 cannot build gfx1250's cluster launch).AITER_MAX_JOBSworker budget ([HIP] [OPUS] [FlyDSL] Simplify AITER compiler worker fan-out to smooth-out regressions #5164's shared policy) by job count.--checkcompiles nothing and fails on any kernel the replay needs but the cache lacks (FLYDSL_RUNTIME_RUN_ONLY=1).aiter/aot/flydsl/mega_moe.py(rewritten, 590 to 305 lines): one job per rank of each deployment profile (DeepSeek-V4-Pro r0/r32/r64, Kimi-K3; MTPR 8192/16384/32768) and quant (A8W4, and A4W4 since [FlyDSL] [MegaMoE] A4W4 mega_moe operator #4980). Each job buildsMegaMoEV2with FakeTensor weights and callspreload_aot_bundles()andquantize(), which the preload leaves out but fixed-slotforwardand the per-stage benchmarks use. mori shmem, through whichMegaMoEV2allocates symmetric buffers and exchanges peer pointers, hands out FakeTensors and fake addresses; a build host without mori gets a stand-in module. One job covers both stages, so the old script's 384 jobs become 192. The module keeps only the profiles; CLI options are unchanged. Like the MoE replay, it runs in a process of its own, started withGPU_ARCHS=gfx950andAITER_AOT_REPLAY=1, and it is skipped whenGPU_ARCHSnames targets without gfx950.aiter/aot/flydsl/common.py:run_aotruns the MoE and MegaMoE replays first, in processes of their own (replay_env), then the pooled kinds.OpKind.MXFP4_MOEandOpKind.GROUPED_MOEare removed.aiter/aot/flydsl/fhmoe.pyandgrouped_moe.py; the replay covers their rows.quant_layout_supported()inmoe_fused_route_quant_scatter.py, which [FlyDSL] Kimi-K3 TP8 gfx1250 grouped-MoE tuning (experts split 8 ways) #6768 added forgrouped_moe.pyalone, goes with it.mxfp4_moe.pyleaves the AOT build too, but stays: the MXFP4 tuner's--fast-scan([HIP] [CK] [FlyDSL] Accelerate MXFP4 FMoE AOT tuning ~9.5x on gfx950 #6405) precompiles its candidates with it.aiter/jit/core.py:update_config_filesreads the tables it merges as text, so the merged file reproduces every cell as written. Parsed, a column with a blank cell turned float and its integers were written back asN.0: 6 of the 15 merged tables had such cells, and since [Config] [Tuning] Add tuned grouped MoE config for Qwen3.5 FP4 on gfx1250 #6090 added a second grouped table, the gfx1250 grouped MoE runtime raisedValueErroron every row of the merged one. Shapes are still compared on numeric values, so256and256.0stay one shape, and the fastest duplicate is still picked on a numericus. Since [JIT] Check single-file tuned configs for duplicate shapes #6365 that check lives in_raise_on_duplicate_shapes, shared with the single-file path, which now reads its table as text too.aiter/jit/utils/torch_guard.py:run_host_scalar_ops_for_real()lets scalar-returning custom ops run their implementation under FakeTensorMode, since their fakes carry no value. Off by default.aiter/jit/utils/chip_info.py: in an AOT replay process (AITER_AOT_REPLAY=1, set only byreplay_env),get_gfx_runtime()returns the one archGPU_ARCHSnames and does not probe the device, so a visible GPU of another arch cannot leak in. Elsewhere it behaves as onmain, which never letsGPU_ARCHSoverride it.aiter/utility/dtypes.py: since [FlyDSL] [JIT] Fix Windows build arguments and optional imports #6589,get_dtype_fp8()returnsuint8when torch sees no device. An AOT replay process, which hides the GPUs, now takes its arch's fp8 type fromget_gfx_runtime()instead; every other process keepsmain's behavior. Without it, every fp8 path of the replays took the wrong branch: fp8 activations were quantized to fp4, MXFP8 rows found no input recipe, and gfx1250 grouped rows compiled other kernels than the runtime requests without any error.op_tests/test_dtype_parser.py(main's tests unchanged) andop_tests/test_build_targets_architecture.pycover the flag.aiter/fhmoe.py:fhmoe_is split into a plain_fhmoe_impland the decorated op (same signature, including [FlyDSL] [CI] Add HY4 MXFP8 heterogeneous MoE fusion #6050'sclamp_shared), so the replay can call the implementation under FakeTensorMode.aiter/ops/opus/moe_stage1_a8w4.py,aiter/ops/opus/moe_stage2_a8w4.py: the fakes registered for_opus_moe_stage1_a8w4_fwd_rawand_opus_moe_stage2_a8w4_decode_fwd_rawlacked the ops' scalar parameters (topk, block_m, kernelName, inter_dim_pad, activation, swiglu_limit, situ_beta, situ_linear_betaandtoken_num, topk, block_m, kernel_id, inter_dim_pad).torch_compile_guardcalls the fake with every argument the op takes, so any call under FakeTensorMode raisedTypeError. The fakes still returnout.op_tests/test_fhmoe.py: the tests of the removed script's internals are replaced by five tests of the replay: row parsing (DSV4 i384 and HY4 MXFP8 rows,clamp_shared), import underAITER_AOT_IMPORT=1, padding columns, the caller choices handed to_fhmoe_impl, and, on gfx950, replaying HY4 rows into a scratch cache and loading their kernels underFLYDSL_RUNTIME_RUN_ONLY=1.aiter/aot/flydsl/README.md: documents how both replays work and--check.Runtime behavior is unchanged apart from the config merge: the torch_guard switch is off outside the replay, the chip_info and dtypes changes only apply under
AITER_AOT_REPLAY=1, and eager execution never calls the opus fakes. Merged tables now carry their sources' cells unchanged; no value differs from before, and the gfx1250 grouped lookup that raised now parses.Performance (AOT build time, 256-CPU host, 96 workers)
MoE kinds, measured before the last merge of
main:run_aotpath)--checkof a complete cacheRun-to-run variation is about ±6%. Without the scheduling above (one forked worker per job, archs one after another), the from-scratch build took 537 s. That build was bound by the parent's fork rate and by concurrent variants of a row waiting on each other's compile locks. The artifact count follows the runtime: it fell from 2751 to 2353 when #6376 limited FlyDSL expert bias to Swiglu rows, rose to 3463 (22698 jobs) after #6460 reverted that and #6405 added the glm53 fp4 table, and is 2406 (22992 jobs) since #6478 limits it to Swiglu again.
On the current head with #6700 (FlyDSL 0.4.0,
AITER_MAX_JOBS=160), the MoE build takes 124.5 s from scratch for every arch (9440 jobs plus 660 bisection replays; 8948 of the jobs remain under CI'sGPU_ARCHS=gfx942;gfx950). Before 2f85842 limited expert bias to Swiglu rows, it took 182 s atAITER_MAX_JOBS=96(22992 jobs plus 1696 bisection replays). MegaMoE, A8W4 and A4W4, takes 415 s (192 jobs).The wheel build's
run_aotnow runs MegaMoE as a phase of its own instead of inside the shared pool (the 180 s below).MegaMoE, same host and workers, A8W4 only (measured before #4980 added A4W4, which builds about as much again):
Testing
Unless marked as measured on the current head with #6700, the numbers below were measured on 61fa528 or earlier, before 2f85842 limited expert bias to Swiglu rows.
Full replay on the current head with [JIT] Replay biased interleaved MoE callers again and fix #6362's CI failures #6700 (
mainthrough [MHA] Fix -DONLY_FAV3=0 quoting that breaks the backward JIT build #6616, FlyDSL 0.4.0; all tuned FMoE, FHMoE and gfx1250 grouped rows; gfx942, gfx950, gfx1250): 1551 rows, 9440 jobs and 660 bisection replays (gfx1250 422, gfx950 238), 0 failures, 2191 artifacts, and--checkof that cache passes. Caller-side combinations the runtime itself rejects are reported as[SKIP](153, all on gfx950): no safe MXFP4 backend at inter_dim 384/640/1152, which includes MiniMax-M3 a4w4 with interleaved weights and a bias, and the 32-bit route-reduce limit. One token count is[REFUSED](see Known gaps). Without [JIT] Replay biased interleaved MoE callers again and fix #6362's CI failures #6700, head 2f85842 replays 9116 jobs into 2156 artifacts and lacks 35 the runtime requests: GPT-OSS a8w4's interleaved, biased kernels on gfx950 and the biased gfx1250 callers'.Target archs: with
GPU_ARCHS=gfx942;gfx950, as CI's wheel build sets it, the replay skips the 1428 gfx1250 jobs and leaves exactly the gfx942 and gfx950 artifacts of the full replay (84 and 2057, same keys); withGPU_ARCHSunset, all 2406. MegaMoE leaves the same 6236 artifacts as before and is skipped underGPU_ARCHS=gfx942.Without the arch reaching the replay processes (each inheriting CI's
GPU_ARCHS=gfx942;gfx950, sodtypes.fp8isuint8since [FlyDSL] [JIT] Fix Windows build arguments and optional imports #6589), the gfx950 replay fails 530 jobs, misses 503 of its 2057 artifacts and builds 294 wrong ones; the gfx1250 replay reports no failure while 185 of its 265 artifacts are missing, all 130 fp8 TDM grouped GEMMs among them. gfx942 is unaffected only because none of its rows has fp8 activations. With the flag, a gfx942 replay process on this gfx950 host getsfloat8_e4m3fnuzand gfx942 fromget_gfx_runtime()even with a GPU visible.Artifact count across the merge: replaying the pre-merge head and the current head with the same FlyDSL 0.4.0 gives 3463 and 2406 artifacts. The difference is [HIP] [CK] [MoE] Fix missing SwiGLU non-split no-bias CK-Tile MXFP4 dispatch (follow-up to #6376 revert) #6478's limit of FlyDSL expert bias to Swiglu: all 405 MXMOE gemm2 kernels that disappear are bias variants whose bias-free twin is still built, the FlyDSL a8w4/a4w4 GEMMs drop their bias specialization (per kernel name, exactly half the artifacts where the bias variant applied), and a
bias=bothreplay of a Silu row requests one extra stage-1 kernel before the merge and none after. The only new kernel names come from tablesmainadded.Wheel-build emulation: the AOT step run the way CI's
setup.py bdist_wheelruns it (aiter/jit/core.pyloaded a second time asjit.core, aiter imported withAITER_AOT_IMPORT=1, every JIT.soremoved first,GPU_ARCHS=gfx942;gfx950). Before the fix, CI's wheel build failed on both replays: the gfx950 MoE replay foundmodule_moe_mxfp4_auxbuilt for gfx942 only, by the gfx942 replay running alongside, and died; all 96 MegaMoE jobs raisedKeyError: 'module_aiter_core'. Now the MoE replay (all three archs), MegaMoE (96/96) and the fullrun_aot(GEMM 6787, CONV 133, CHUNK_GDN_H 288, FMHA_FP8 92) pass, and the two replays leave exactly the artifacts of the standalone builds (5515 = 2353 + 3162). A later CI run failed again when the gfx1250 replay happened to build aiter's C++ core module first: gfx1250 builds turn on cluster launch, which the HIP of ROCm 7.2.4 does not declare. That module has host code only and is now built once before the replays start; in the emulation, only that step builds it. On the current head the emulation replays gfx942 and gfx950 only, asGPU_ARCHSnames, with 0 failures. CI's prebuild wheel job passed on the head before theGPU_ARCHSfilter, which still replayed gfx1250.Dense M check: replaying every bucket at about 16 sampled M (every M up to token 64) under
FLYDSL_RUNTIME_RUN_ONLY=1finds no missing kernel: 132834 jobs on gfx950, 16716 on gfx942, 11160 on gfx1250. Before the bisection, 138 of the gfx1250 probes missed the stage-1 quant kernel of an intermediate K split; gfx942/gfx950 had no such case, and bisection compiles nothing new there.The scheduled build leaves exactly the same artifact set as one worker per job, and
--checkpasses on it.Opus fakes: called under
FakeTensorModewith every argument the ops take, both raiseTypeErroronmain(takes 10 positional arguments but 18 were given, and 9 of 14 for stage 2) and returnouthere. Without the fix, 43 replay jobs fail.MoE artifacts vs. a full build of the old scripts (same FlyDSL 0.3.4.1, on the base before the merges):
get_padded_Mnever returns.flydsl_fallback, which the runtime drops at load.[FlyDSL] fix(flydsl): support kmk3 ep moe & opt for ep moe gemm2 #5804's
use_valid_token_countkernels: of the 442 the oldmxfp4_moe.pybuilds for it, 215 are in the replay's cache, byte-identical. The other 227 are never requested by the runtime: bias variants of Silu/SiTUv2 rows, inline-sort rows whose tuned config the runtime drops undernum_local_tokens(it then uses heuristic FlyDSL kernels, which the replay builds and the old script did not), and a token=3 row.gfx1250 grouped artifacts vs.
grouped_moe.py(its own table, 117 rows): 21 shared, byte-identical. 162 only in this build, including 112 TDM grouped GEMM kernels the old script never built. 149 only in the old build: 109 auxiliary variants for the removed non-TDM path, and 40 built with topk −1 for the EP-only rows, which no call matches. No gfx1250 GPU was available, so this path is validated on CPU only.Kimi-K3 TP8 grouped table ([FlyDSL] Kimi-K3 TP8 gfx1250 grouped-MoE tuning (experts split 8 ways) #6768, inter_dim 384): [FlyDSL] Kimi-K3 TP8 gfx1250 grouped-MoE tuning (experts split 8 ways) #6768 made
grouped_moe.pyskip the stage-2 activation quant, whose layout needs inter_dim % 256 == 0. The replay needs no such rule: the runtime launches that quant only when gemm1 cannot requantize its own output (a gemm1 bias, orAITER_FLYDSL_DISABLE_GEMM1_REQUANT=1), and SiTUv2 callers pass no bias. The table's 15 rows replay as 168 jobs and 78 bisection replays, 0 failures, into 31 artifacts with no inter_dim-384 quant kernel; every gemm1 requantizes in its epilogue. WithAITER_FLYDSL_DISABLE_GEMM1_REQUANT=1, every job fails on the assertion [FlyDSL] Kimi-K3 TP8 gfx1250 grouped-MoE tuning (experts split 8 ways) #6768 worked around, as the runtime itself would. Of the 11 artifactsmain'sgrouped_moe.pybuilds for the table, 8 are in the replay's cache, byte-identical, and 3 are never requested: K-split route quant for wmma_rep 2 and 8, whose rows serve only 1025+ tokens, where the runtime turns K-split off, and a dense route-quant-scatter kernel the runtime no longer calls. The replay adds the table's 23 TDM GEMMs.Config merge: regenerating all 15 merged tables changes no value, and each now has exactly as many
N.0cells as its sources. A duplicate written as256in one table and256.0in another is still caught and resolved to the faster row.op_tests/tuning_tests/test_config_shape_collision.py,test_tuner_infra.pyandtest_fast_tuning.pypass (72 tests).Strict cache-hit test on MI355X (gfx950):
op_tests/test_moe_2stage.py --no-legacyunderfail_on_aot_cache_miss, with a cache built by this PR. On the current head (mainthrough [MHA] Fix -DONLY_FAV3=0 quoting that breaks the backward JIT build #6616, FlyDSL 0.4.0), every model CSV with FlyDSL rows on gfx950 passes: 24 CSVs, 1198 cases, no cache miss. Every CSV has the same case count as on the previous head (mainthrough [FlyDSL] fix(comm-fused-moe): key winners by GPU model #6541, FlyDSL 0.3.4.1, 1199 cases) except qwen3_8_2400b_fp4, where the test now skips the token-32768 row itself. These runs predate 2f85842. On the current head with [JIT] Replay biased interleaved MoE callers again and fix #6362's CI failures #6700, the 8gptoss_fp8fp4cases (interleaved weights with stage-1 and stage-2 bias) pass with no cache miss against a cache built from that table; without [JIT] Replay biased interleaved MoE callers again and fix #6362's CI failures #6700 they fail as CI's MI35X shard 7 did. On the base before the merges, 662 cases across 12 model CSVs all passed with no cache miss:kimik3_a16w4 has no cases because the test skips SiTUv2 a16w4. Re-run after the merges with the full default build: glm5_mxfp8 (32) and dsv3_fp4 (89) on idle GPUs, kimik2_i4 and dsv4_fp8fp4 (59 and 85 cases; the test skipped the rest because other jobs left too little free GPU memory for its reference) all pass with no cache miss.
AOT vs. on-GPU JIT: glm5_mxfp8 and dsv3_fp4 run again with an empty cache and the AOT check off. Every kernel the runtime JIT-compiles on the GPU (41 and 83) is byte-identical to the replay's.
logits_diffvaries from run to run either way: two runs on the same AOT cache differ in more cases than AOT vs. JIT.MegaMoE full build: on the current head (FlyDSL 0.4.0), A8W4 and A4W4 each pass all 96 jobs.
MegaMoE A4W4 artifacts vs. the old script on current
main(same FlyDSL 0.3.4.1): all 3218 old artifacts are present; 3082 are byte-identical and the other 136 differ only in the samev_add3_u32vs.v_add_u32+v_sub_u32selection (2-7 asm lines). The 144 only in this build are again themask_topk_ids=Truecombine kernels.MegaMoE artifacts vs. a full build of the old script (same FlyDSL 0.3.4.1):
v_add3_u32vs.v_add_u32+v_sub_u32, 2-7 asm lines). Building the old script twice gives 109 such differences, so this is FlyDSL/LLVM run-to-run variation, not the replay.mask_topk_ids=Truecombine kernels (combine kernels 144 -> 288).MegaMoE on 8x MI355X under
FLYDSL_RUNTIME_RUN_ONLY=1, with the cache built by this PR:op_tests/multigpu_tests/test_mega_moe_v2.py --max-tok-per-rank 8192 --bs-list 1,128,1024,8192 --pad-tail-rows 4passes forv4_proandkimi_k3_route, A8W4 and A4W4, on the previous head (mainthrough [FlyDSL] fix(comm-fused-moe): key winners by GPU model #6541, FlyDSL 0.3.4.1; not yet re-run on this one) (relL2 0.059 and 0.057 for A8W4 against rtol 0.10, 0.212 and 0.219 for A4W4 against rtol 0.25; CUDA-graph replay unchanged, masked rows zero). With the old script's cache, the--pad-tail-rowsrun fails withno usable AOT cache for combine_launch.The MegaMoE replay also runs on a host without mori, including twice in one process.
op_tests/test_fhmoe.py, the AOT tests plus theclamp_sharedand op-schema tests, on gfx950: 12 passed, including the run-only load of replayed HY4 kernels.op_tests/test_flydsl_moe_aux.py(script, gfx950): every case matches exactly, including gather-reduce with f32, bf16 and f16 route weights.op_tests/test_dtype_parser.pyandop_tests/test_build_targets_architecture.pypass withGPU_ARCHSunset,gfx950andgfx942;gfx950; under the earlier GPU_ARCHS fallback, main'stest_linux_runtime_errors_are_not_fallbacksfailed withGPU_ARCHS=gfx950.black and ruff are clean on the changed files.
To reproduce:
Known gaps (follow-up)
ep_fused; 7168x2048 with 64 and with 65 local experts). The runtime picks them only when the caller passes anexpert_mask(it then looks up topk −1) or uses MegaMoE's stage-2 scatter (ep_fused), and the table records neither the global expert count nor the real topk the kernels are specialized on. Covering them needs an EP deployment profile per shape (global experts, EP size, topk), likemega_moe.py's profiles, plus aStage2ScatterContextfor the twoep_fusedrows. Until then, those kernels compile on first use. The oldgrouped_moe.pydid not cover them either: it built them with topk −1, which no call matches.ep_fusedrows also come out of the merged table as1.0, which the runtime's string match never selects; the merge fix here keeps them1.get_padded_Mserves M up to 131071 with the row tuned at 32768 and every M from 131072 up with the row tuned at 131072, while the replay covers M fromtoken // 2 + 1totoken. Probing the extra range underFLYDSL_RUNTIME_RUN_ONLY=1finds nothing missing for the 32768 rows or on gfx1250, but on gfx950, 42 of 4020 probes miss a stage-1 or stage-2 kernel at M >= 262144, on all three shapes tuned at 131072 (for a8w4 7168x3072 with 48 local experts, from M = 262144). Those kernels compile on first use; the old scripts did not cover them either. Covering them means replaying each tier's upper end and bisecting up to a chosen M cap.main: its reduce-mode stage 2 needs a 5.4 GB route-reduce workspace, beyond the 32-bit buffer addressing [FlyDSL] [CI] Add HY4 MXFP8 heterogeneous MoE fusion #6050 guards, so the runtime raisesNotImplementedErrorfor every M from 26215 to 131071 that maps to the row, andtest_moe_2stage.pyskips it. The replay reports that token count as[REFUSED]and builds the row's kernels from the token counts it does serve. The fix belongs in the table (retuning that token count), not in the replay.GPU_ARCHSisgfx942;gfx950, and listing gfx1250 there would also build the C++ modules for gfx1250, which the HIP of ROCm 7.2.4 cannot (hipLaunchAttributeClusterDimensionis undeclared). The old scripts, and this PR before this change, built them regardless ofGPU_ARCHS. They build on their own withGPU_ARCHS=gfx1250 python -m aiter.aot.flydsl.moe(README §1): an installed wheel already ships the host-only C++ core module that build imports, and on ROCm 7.2.4 from source,GPU_ARCHS=gfx950 python -c "import aiter"builds it first. From a fresh JIT directory that sequence replays the 1428 gfx1250 jobs and leaves the same 265 artifacts as the full replay (counts measured before 2f85842).op_tests/flydsl_tests/test_flydsl_grouped_gemm.py --scenario csv, which runs underFLYDSL_RUNTIME_RUN_ONLY=1by default), token counts inside the buckets the bisection covers (--scenario verify --tokens ...), and a byte comparison of the kernels the runtime JIT-compiles there (--no-check-aot-cache) against the AOT cache.Documentation
aiter/aot/flydsl/README.mdupdatedDependencies
Breaking Changes
python -m aiter.aot.flydsl.grouped_moeand theaiter.aot.flydsl.fhmoemodule are removed;python -m aiter.aot.flydsl.moecovers their rows. Nothing in the repo or CI calls them.mxfp4_moe.pystays, outside the AOT build.OpKind.MXFP4_MOEandOpKind.GROUPED_MOEare removed.python -m aiter.aot.flydsl.moekeeps--csvand adds--row-tokens-onlyand--check.GPU_ARCHSnames targets, the MoE and MegaMoE AOT (standalone and inrun_aot) build only those archs; unset ornative, every arch the tables have, as before.<tmpdir>/aiter_configs/*.csv) now keep each cell as written in its source, e.g.64rather than64.0.