Repository navigation
[ROCm][Perf][MiniMax-M3.1] Pick the NVFP4 sparse decode splits per XCD, not per CU - #65481
Fangzhou-Ai wants to merge 7 commits into
Conversation
…D, not per CU The split count of the XCD-aware grid came from a cost model that spreads the workgroups over all 256 CUs, but the grid places a request's rows and splits on one XCD (32 CUs). At 5 requests it picked 6 splits (15.7 us per layer) where 4 splits take 11.9 us. Measured on MI355X (top-k 16, 8 rows per request, 30K-400K contexts, 1-24 requests): 4 splits is best up to 8 requests, 2 splits for 9-16 and 4 for 17-24. Other shapes keep the cost model. Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
I need some roofline analysis to proceed with this PR |
|
/ci run |
|
✅ Triggered Buildkite CI #99751 for commit |
If outputs differ let's run A/B gsm8k with and without this change (reported end-to-end sanity check is shallow). |
|
GSM8K A/B: no accuracy change Full GSM8K test set (n=1319), side by side on
Per question (strict): 19 correct only with the PR, 21 correct only on main (McNemar exact p = 0.87). The 0.16 pp gap is well inside the sampling noise (SE about 0.65 pp per arm). Only 100 of 1319 outputs are textually identical, as expected: a different split count changes the fp32 softmax-merge order (about 1e-3 relative on bf16 outputs, see the Correctness section), which shifts greedy decoding for some questions, but the changes split evenly in both directions. Sanity prompt ("What is 17*23?") returns 391 on both. AI assistance: the runs were driven with Claude Code; I reviewed the numbers. 🤖 Generated with Claude Code |
|
/ci run |
|
✅ Triggered Buildkite CI #100111 for commit |
There was a problem hiding this comment.
Actually I just noticed this test file doesn't actually run anywhere, can we add it to MiniMax-M3 Attention Kernels?
There was a problem hiding this comment.
Done in 61d5ade: the file is now in the :amd: (MI355 DPX) MiniMax-M3 Attention Kernels step (source_file_dependencies and the pytest command in the mirror.amd block of .buildkite/test_areas/kernels.yaml). I did not add it to the B200 command: the module skips itself off ROCm / gfx950, so it could only ever run on the MI355 lane. The repo's "new tests are wired into Buildkite CI" pre-commit check passes. The file is 125 tests, about 1 minute on MI355X.
PR Pundit review(Testing PR Pundit Code Review agent. Not all comments may be relevant. This is a one-time automated pass: please address what is useful, no need to request a re-review. Please reply to each point, whether it applies or not: we harvest these replies, and both kinds are enormously helpful for tuning the agent. Reach out to @peymanr for suggestions.)
1. 🟠 Should fix: Decode at TP=1/2 may over-split: this PR keys
|
…run the sparse decode test in AMD CI _xcd_splits is tuned for one KV head per rank (TP4). The XCD grid also puts a request's KV heads on one XCD, so at TP2 / TP1 the table over-splits: at 4 KV heads 24 requests take 55.5 us against 42.8 us with the cost model, and at 2 KV heads 16 requests 21.3 vs 18.3 us. Return None unless num_kv_heads == 1. Assert the chosen split count through the kernel entry point (including padded requests and 1/2/4 KV heads), and add the test file to the MI355 MiniMax-M3 Attention Kernels step, where it was not running. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com>
|
Thanks, both findings are valid; fixed in 61d5ade. 1.
(Not a uniform loss, so the numbers above are the worst cases: at 2 KV heads the default is equal or faster up to 9 requests, by up to 20% at 1-2 requests, and equal or slower from 12; at 4 KV heads it is 28% faster at 1 request, within 3% of the old model at 2-6 and 12, 4.5% faster at 9, and slower at 8, 16, 17 and 24. I did not tune those shapes.) So the table is right at one KV head per rank (TP4, what it was measured on), and over-splits at 2 and 4 (TP2, TP1). 2. Tests (valid). Added |
|
@olka-amd SPEED-Bench A/B on acceptance; the GSM8K A/B is the earlier comment above (0.9393 vs 0.9409 strict, McNemar p = 0.87). Setup. SPEED-Bench Qualitative split (880 prompts, 11 categories, built with the NeMo-Skills
Both passes combined: acceptance length 3.196 (this PR) vs 3.199 (main). The difference between arms (0.003) is smaller than the pass-to-pass spread inside one arm (0.021 for this PR, 0.005 for main), and every position agrees to within 0.3 pp, so I see no effect on acceptance. This measures acceptance only; the two arms ran on different GPU sets (0-3 and 4-7), so I am not using it for speed. The decode-step effect is in the PR description. One caveat on the numbers I had before: the GSM8K server logs showed mean acceptance length 4.63 (PR) vs 4.71 (main). That run was not paired (the generated texts differ between arms, 1219 of 1319 answers are not identical) and had no repeat, so I would not read it as an acceptance change; the controlled run above is the better measurement. AI assistance: the runs were driven with Claude Code. 🤖 Generated with Claude Code |
|
/ci run |
|
✅ Triggered Buildkite CI #100347 for commit |
The MI355 DPX CI runner reports fewer than 8 XCDs per device, so _xcd_count returns 0 (the XCD-aware grid stays off by design) and test_default_splits_are_chosen_per_xcd failed its hard `xcds == 8` assert in all 24 cases. Skip that test unless the device has the 8-XCD topology the table was tuned on, and keep coverage on partitioned devices with a default-path test of the plain grid and cost model (xcds == 0) plus a hardware-independent test of when the XCD grid is enabled (8 XCDs on gfx950 only). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com>
|
The That runner is a DPX-partitioned MI355X (
Checked locally on an SPX MI355X (8 XCDs): the per-XCD test runs and passes, as do the new tests. With the HIP XCD query forced to 4, the per-XCD test skips and the fallback tests pass. |
|
/ci run |
|
✅ Triggered Buildkite CI #100393 for commit |
Purpose
The NVFP4 sparse decode kernel of MiniMax-M3.1 (
ops/nvfp4_sparse_decode, ROCm gfx950, XCD-aware grid) picks its number of splits withdecode_splits, a cost model that spreads the work over all CUs of the device (256 on MI355X). The XCD-aware grid does not do that: it places a request's rows and all of its splits on one XCD (32 CUs). The model therefore mispredicts: at 5 concurrent requests (DSpark 7: 8 rows per request) it picks 6 splits (thes6variant, 15.4-15.7 us per layer) where 4 splits take 11.9 us, and at 1-4 requests it picks 8 splits (12.4-13.3 us) where 4 splits take 11.2-12.4 us.s6appears only at exactly 5 requests, which is a common decode batch in the AgentX c16 workload (about 15% of request decode time).This PR adds
_xcd_splits, used only when the heuristic would otherwise run (explicitsplits=and thesplits == 1large-grid path are unchanged), for the shape it was measured on (top-k 16, 8 rows per request = DSpark 7); every other shape keepsdecode_splits:Not covered by any open PR that I could find (the open MiniMax PRs that touch sparse decode code are the NVIDIA MSA ones and #56451, which changes DSpark verification, not the split choice).
Measurements (MI355X, gfx950; 4 ranks,
amd/MiniMax-M3.1-Flash-Quark-MXFP4, TP4, DSpark 7, NVFP4 KV and indexer)Kernel level (sweep of splits x XCD mode, N = 1..64 requests, 30K / 150K / 400K contexts, per-layer us): the kernel time does not depend on the context length; one split costs ~21 us (16 blocks serial, about 0.8 us per block plus ~8 us fixed). Best split counts: 4 splits up to 8 requests (11.2-12.4 us), 2 splits for 9-16 requests (15.3-16 us), 4 splits for 17-24 requests (21 us). The old cost model gave 8 splits at 1-4 requests (12.4-13.3 us) and 6 at 5 requests (15.7 us). Disabling the XCD-aware mode would be 0.3-0.6 us better only at 9-10 requests, so the mode is kept.
Per-layer kernel time re-measured on the rebased head (graph replay, 8 rows per request, 150K context, L2 flushed; "old" forces
decode_splits, "new" is the default path of this PR). Relative difference between old and new outputs is 7e-4 to 8e-4 where the split count differs and exactly 0 where it does not:N=6 and above pick the same split count as before; the 0.1-0.6 us differences there are run-to-run noise.
Decode step, this change alone (p50 of the gap between streamed chunks with N concurrent 150K-token streams, 3 passes, DBO off,
VLLM_ROCM_USE_SKINNY_GEMM=0; the same-code rows below measure server-to-server noise, about +-0.6%):The gain is -1.1% to -1.8% at 3-6 requests (about 0.15-0.25 ms per decode step); everything else is within the noise band. I therefore claim only the kernel-level change and this small step-level gain at 3-6 requests.
Correctness
tests/kernels/attention/test_minimax_m3_nvfp4_sparse_decode.py.tests/kernels/attention/test_minimax_m3_nvfp4_sparse_decode.pyon the rebased head: 101 passed, 0 failed on MI355X (80 existing cases + 21 new: 1_xcd_splitstable test and 20 default-splits-vs-unsplit cases; vs an fp64 reference < 5e-3). The tests ran against an overlay of the installed tree with this PR'shost.pyand test file (main'sminimax_m3/amdcode needs a newermodel_executor/models/utils.pythan the machine's installed build); this branch differs from main only in those two files.Limitations
AI assistance: this change was developed with Claude Code; the measurements above were run on MI355X (gfx950).
🤖 Generated with Claude Code