Skip to content

perf(qwen4-exp): keep 128-aligned MXFP4 expert intermediate unpadded - #2649

Merged
valarLip merged 2 commits into
mainfrom
lingzha/qwen38-moe-pad-align
Oct 26, 2026
Merged

valarLip merged 2 commits into
mainfrom
lingzha/qwen38-moe-pad-align

Conversation

@ZLkanyo009

@ZLkanyo009 ZLkanyo009 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Mxfp4MoEMethod pads expert weights to a multiple of 256, so Qwen3.8-Flash-Next's moe_intermediate_size 640 becomes 768 at TP1: one sixth of every expert is zeros, ~11.7 GB of HBM. Qwen4-Exp now passes pad_align=128 when the per-rank intermediate is already a 128-multiple (TP1: 640 stays 640), as Kimi-K3 and DeepSeek-V4 already do, and keeps the default otherwise. The freed memory goes to the KV cache (+25% blocks), which is what AgentX C16 is bound by.

Depends on #2606 (to run the model with MTP; this PR does not touch MTP code).
Companion AITER PR (tuned MXMOE table keyed on the unpadded inter 640): ROCm/aiter#6708. This PR works without it (inter 640 then uses AITER's heuristic FlyDSL kernels, same speed as 768 with pad); with it the MoE kernels get faster as well.

Motivation

Qwen3.8-Flash-Next (amd/Qwen3.8-Flash-Next-Quark-MXFP4) optimization on MI355X, https://github.com/wuhuikx/LLM-Booster/issues/263 (item 3, MoE tuning).

Changes

  • atom/models/qwen4_exp.py: moe_pad_align(intermediate_size, tp_size) returns 128 when intermediate_size // tp_size is a multiple of 128, else None (the MXFP4 default 256); Qwen4ExpSparseMoeBlock passes it to FusedMoE(pad_align=...). pad_align is only read by the MXFP4 MoE method, so BF16 / FP8 / NVFP4 experts are unaffected.
  • tests/test_qwen4_exp_quantization.py: the choice for TP 1/2/4/8.

No env var, no config knob: 2 files, +20 lines.

Per-rank intermediate for 640: TP1 640 (was 768), TP2 320, TP4 160, TP8 80 → not 128-multiples, so they keep the 256 padding (unchanged behavior; only TP1 was validated). The check is per TP on purpose rather than a flat pad_align=128: TP2 / TP8 would become 384 / 128, shapes with no AITER aux kernel or tuned table, while the Qwen3.8 TP8 aux shape (513, 8192, 256, 11) that AITER main just added (ROCm/aiter#6468) relies on the 256 padding.

Accuracy

GSM8K (lm-eval, real MTP verification, i.e. no forced acceptance; same script as the issue's baseline), TP1, MTP 3:

config strict-match flexible-extract MTP acceptance avg tokens / forward
before (main, inter padded to 768) 0.9780 ± 0.0040 0.9780 0.721 3.163
this PR (inter 640, AITER heuristic kernels) 0.9757 ± 0.0042 0.9757 0.720 3.159
same, validated earlier on a tree that forced pad_align=128 0.9750 ± 0.0043 0.9750 0.721 3.162
this PR after dropping the env var (6ea9fabd merged into main 40db24c2) 0.9757 ± 0.0042 0.9757 0.729 3.187
its control: main 40db24c2, same node and run time 0.9780 ± 0.0040 0.9780 0.720 3.159
this PR + companion AITER table (MXMOE kernels) 0.9757 ± 0.0042 0.9757 0.724 3.173

The shared baseline of the issue measured 0.9757 on main; the differences are run-to-run noise.

Performance

Test config: image rocm/atom-dev:latest (566aa3dc9d5d), ATOM 53e4fb524, AITER 948dd267f, MI355X, TP1, MTP 3, bf16 KV, block 16, prefix caching, --gpu-memory-utilization 0.88.

Memory (server log):

weight-loading peak available for KV KV blocks
before (768) 184.17 GB 58.55 GB 100,826
this PR (640) 172.44 GB 70.28 GB 126,130 (+25%)

AgentX C16 (aiperf --scenario agentx --concurrency 16 --benchmark-duration 3600, semianalysis_cc_traces_weka_062126_256k, golden AL 2.32):

run tput/GPU p90 norm intvty tok/s/user p90 ITL intvty realized prefix hit theoretical hit prompt tokens generated errors/reqs Effective Conc
before (main, 768) 45,111 8.69 17.28 60.7% 95.9% 178,920,730 1,463,928 0/1644 11.24
before, repeat 48,221 8.68 17.05 63.9% 95.9% 190,279,227 1,549,957 0/1799 10.96
ATOM#2649 only (640, AITER heuristic) 64,635 29.13 43.22 83.7% 96.0% 249,262,024 2,087,631 0/2351 7.96
ATOM#2649 + aiter#6708 (640, MXMOE) 69,775 33.93 52.45 85.9% 96.1% 266,589,865 2,247,026 0/2468 7.39
ATOM#2649 + aiter#6708, repeat 68,047 31.25 48.76 84.9% 96.1% 261,268,100 2,206,191 0/2433 7.57

Cache = GPU prefix, TP1, concurrency 16 for all rows. Runs were done two at a time on separate GPUs of one node (the "ATOM#2649 only" run and the second "+ aiter#6708" run ran side by side).

  • The two before runs differ by 7% in tput/GPU: that is the run-to-run noise.
  • ATOM#2649 alone: tput/GPU +38.5%, p90 norm interactivity 3.35× vs the mean of the two before runs. The extra 25% of KV blocks lifts the realized prefix hit from 62% to 84% on these long-prefix agentic traces, so far fewer prompt tokens are recomputed.
  • aiter#6708 on top: vs the side-by-side ATOM-only run, tput/GPU +5.3%, p90 norm interactivity +7.3%, p90 ITL interactivity +12.8% (vs the mean of both runs: +6.6% / +11.9% / +17.1%). The ITL gain (the two before runs differ by only 1.3% there) matches the −10% decode-step kernel time; the tput gain is of the same size as the noise.

How to use

Nothing to set: TP1 Qwen3.8-Flash-Next MXFP4 picks pad_align=128 automatically; other TP sizes keep the MXFP4 default.

Test plan

  • black / ruff check on the changed files (same as pre-checks.yaml).
  • pytest tests/test_qwen4_exp_quantization.py tests/test_envs.py in a ROCm container with torch + aiter: 137 passed (main: 132; the 5 new cases are moe_pad_align). (In the CPU-only CI job this file is skipped by its importorskip("triton") guard.)
  • CPU unit suite as in CI (.github/scripts/run_unit_tests.sh): 3 failed / 9252 passed; the 3 failures (test_graph_marker ×2, test_sparse_indexer_side_effect_op) fail on main too.
  • End-to-end on MI355X after dropping the env var (6ea9fabd merged into main 40db24c2, AITER 508736740 with the ROCm 7.2.4-invalid hipBLASLt rows filtered out of the BF16 GEMM table), TP1, MTP 3, side by side with main on the same node: weight peak 172.44 vs 184.17 GB, available for KV 70.39 vs 58.66 GB (+11.73 GB), KV blocks 126,366 vs 101,062 (+25%); every MXFP4 fused_moe key AITER logs (AITER_LOG_LEVEL=INFO) has inter_dim=640 (main: 768); full GSM8K as above (last two rows).
  • Earlier runs: the first "this PR" GSM8K row and the memory table used this branch's code (+ fix(qwen4_exp): load unscaled Quark MTP weights as BF16 #2606). The AgentX rows and the forced-pad_align=128 GSM8K row were run on a validation tree (ATOM 53e4fb524 + fix(qwen4_exp): load unscaled Quark MTP weights as BF16 #2606 + a two-line patch forcing pad_align=128), which gives the same expert layout as this PR's automatic choice (same weight peak and KV block count).

Limitations / follow-ups

  • TP > 1 keeps the 256 padding: 320 / 160 / 80 are not 128-multiples and those shapes were not validated without padding.
  • Tuned AITER tables keyed on inter_dim=768 for this model would stop matching at TP1; there are none upstream.

The MXFP4 MoE method pads expert weights to a multiple of 256, which turns
Qwen3.8-Flash-Next's moe_intermediate_size 640 into 768 at TP1: one sixth of
every expert is zeros, ~12 GB of HBM that the KV cache could use. Pass
pad_align=128 when the per-rank intermediate is already a 128-multiple (as
Kimi-K3 / DeepSeek-V4 do) and keep the default otherwise.
ATOM_QWEN4_MOE_PAD_ALIGN overrides the choice (e.g. 256 for the old layout).

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏷️ CI Guide

Runs automatically on every eligible PR before approval:

  • ✅ Pre Checkin: Black, Ruff, catalog schema validation, non-GPU unit tests

Heavy model tests:

  • ✅ Run after the PR is approved and Pre Checkin passes
  • ✅ Run immediately when an approval review is submitted
  • ✅ Can be requested before approval with labels
Label Tests
ci:full Run all heavy PR model tests: native ATOM, vLLM, and SGLang
ci:atom Run native ATOM model accuracy tests
ci:vllm Run ATOM vLLM OOT model accuracy tests
ci:sglang Run ATOM SGLang model accuracy tests

Heavy jobs are skipped when the PR is not approved and no matching ci:* label is present.
Add labels via the sidebar or gh pr edit 2649 --add-label <label>

… 128 pad align

The env override is not needed: 128 is used whenever the per-rank expert
intermediate is already a multiple of 128 (TP1: 640), and every other
shard keeps the MXFP4 default of 256.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ZLkanyo009

Copy link
Copy Markdown
Contributor Author

Dropped ATOM_QWEN4_MOE_PAD_ALIGN as suggested (6ea9fab). The PR is now qwen4_exp.py + its test, +20 lines: moe_pad_align() is a single return 128 if (intermediate_size // tp_size) % 128 == 0 else None.

The per-TP check stays instead of a flat pad_align=128: only TP1 (640) is a 128-multiple. TP2 (320) and TP8 (80) keep the MXFP4 default 256 padding, unchanged from main. A flat 128 would turn them into 384 / 128, shapes with no AITER aux kernel or tuned table, and the Qwen3.8 TP8 aux shape (513, 8192, 256, 11) from ROCm/aiter#6468 relies on the 256 padding.

Re-validated on MI355X (this branch merged into main 40db24c2, run side by side with main on the same node, TP1, MTP 3 with real verification):

main this PR
MXFP4 expert inter_dim (AITER fused_moe keys) 768 640
available for KV 58.66 GB 70.39 GB
KV blocks 101,062 126,366 (+25%)
GSM8K strict, full 1319 0.9780 0.9757
MTP acceptance 0.720 0.729

Unit tests: test_qwen4_exp_quantization.py + test_envs.py 137 passed in a ROCm container; the CPU suite has the same 3 failures as main.

@ZLkanyo009

Copy link
Copy Markdown
Contributor Author

@valarLip could you take a look at this one?

Updated today in 6ea9fabd: the env var is gone, and the PR is now qwen4_exp.py plus its test (+20 lines). At TP1 the MXFP4 expert intermediate stays at 640 instead of being padded to 768, which frees ~11.7 GB for KV (+25% KV blocks). TP2 / TP8 keep the MXFP4 default 256 padding, unchanged from main. GSM8K (full 1319, MTP 3) is 0.9757 against 0.9780 for main on the same node. Details are in the comment above.

Pre Checkin is green. The first run's Ruff job failed on a GitHub API 502, and its unit-test job hit the 3 test_atomesh_mooncake_store.py failures. That test draws the master port at random and can collide with the fixed owner port 50152. Both jobs passed on rerun.

@valarLip
valarLip merged commit b968a35 into main Oct 26, 2026
24 of 28 checks passed
@valarLip
valarLip deleted the lingzha/qwen38-moe-pad-align branch October 26, 2026 09:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants