Repository navigation
[ROCm][Perf][MiniMax-M3.1] Emit the o_proj MXFP8 input from the NVFP4 sparse decode kernel - #65508
Fangzhou-Ai wants to merge 4 commits into
Conversation
…e decode Every layer's decode step ran a separate `_kernel` launch (the MXFP8 activation quant, K = 2048 per TP4 rank) between the NVFP4 sparse decode attention and the o_proj GEMM. Add an `fp8_out` variant of the FlyDSL sparse decode kernel whose final store writes E4M3 values and row-major E8M0 scales instead of bf16. A lane of that store already owns a whole 32-dim MX block (one head, dims 32g..32g+31), so the quantization needs no cross-lane exchange, and with split-K it runs on the merged O in the last workgroup. It quantizes the same bf16-rounded values the bf16 store would write, with the E8M0 scale computed by integer exponent arithmetic, so the result is bit-identical to `mxfp8_e4m3_quantize` of the kernel's bf16 output. The attention layer passes the result to o_proj as a QuantizedActivation for decode-only batches (eager or full CUDA graph); mixed batches, piecewise graphs and other consumers keep the bf16 output. Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Narrow the optional output buffers in the sparse decode host wrapper, avoid redefining attn_output, and stop reusing md for two types. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com>
|
/ci run |
|
✅ Triggered Buildkite CI #99723 for commit |
ChuanLi1101
left a comment
There was a problem hiding this comment.
Looks correct on the fusion shape: quantizing after the split-K merge, one MX block per lane, and matching mxfp8_e4m3_quantize of the bf16-rounded store is the right contract for o_proj.
Two asks:
- Please confirm
_emits_o_proj_mxfp8stays decode-only (prefill / mixed steps still take the bf16 → separate quant path), and that a graph-captured decode step cannot flip the cached_o_proj_mxfp8decision mid-capture. - The bit-identical tests cover splits and heads — please also note (or test) the multi-XCD / multi-request path that #65481 retunes, so the fp8_out store is checked under the split counts we actually serve.
…est XCD split counts Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com>
|
Thanks for the review. On your two asks: 1. Decode-only and graph capture. 2. Multi-request / multi-XCD splits. Added |
|
Thanks, this addresses both points. Please add the focused dispatch test as well: decode-only should use the MXFP8 hand-off, while mixed and PIECEWISE batches keep the BF16 path. |
Purpose
On ROCm, every MiniMax-M3.1 decode layer runs the NVFP4 sparse decode attention, then a separate
_kernellaunch (the MXFP8 activation quant of its bf16 output, K = 2048 per TP4 rank), then theo_projGEMM. In the profile of a real decode step (rank-0 profile windows at N=4 and N=16) that quant is 60 launches per step, ~4.4-4.6 us each (profiled, inflated ~1.6x).This PR adds an
fp8_outvariant of the FlyDSL sparse decode kernel (m3_nvfp4_sparse_decode_*_q8) whose final store writes E4M3 values and row-major E8M0 scales instead of bf16, and hands them too_projas aQuantizedActivation(same mechanism as #63637 forqkv_projand the FFN fusion in #65478).How it works
n, dims32g..32g+31, i.e. exactly one 32-element MX block, so the quantization needs no cross-lane exchange. With split-K (splits > 1) the last workgroup of a row merges the partials and stores O; the quant runs there on the merged O, so every 32-block sees the final values.pk_bf16), computes the block amax on their integer bit patterns and the E8M0 byte asclamp(exponent_field - 8 + (mantissa > 0.75), 0, 254)(448 = 1.75 x 2^8, so this isceil(log2(amax/448)) + 127exactly, no log2/division), scales by the exact power of two2^(127-sb)and converts withv_cvt_pk_fp8_f32(RNE). Hence the output is bit-identical tomxfp8_e4m3_quantizeof the same kernel's bf16 output (not "more accurate": it is the same values, unlike the swiglu fusion in [ROCm][Perf][MiniMax-M3.1] Fuse the FFN-side MXFP8 activation quant into the post-attention norm and swiglu #65478)._mxfp8_e4m3_quantize_torchon 20,000 random bf16 blocks over ~2^60 dynamic range plus edge blocks (zeros, subnormals, 447/448/449, the 1.75 boundary and the value just above it): 0 mismatches. The whole kernel compiles for gfx950 for splits 1/4/8 (FLYDSLcompile-only on CPU). Bit-exactness on MI355X: see Tests.fp8_outis a compile-time variant; the bf16 kernel and all other callers are unchanged. The attention layer uses it only for decode-only batches (num_prefills == 0) in eager or full-CUDA-graph mode and wheno_projcan take MXFP8 input (_can_fuse_input_norm_mxfp8); mixed batches, piecewise graphs and non-NVFP4 impls keep the bf16 path and the linear's own quant.Files
ops/nvfp4_sparse_decode/kernel.py(epilogue),host.py(new optionalout_q/out_scale;outputmay beNonethen),sparse_attention_nvfp4.py(forward_decode_mxfp8),model.py(MiniMaxM3SparseAttentionNVFP4 branch +_run_attentionkeyword outputs),tests/kernels/attention/test_minimax_m3_nvfp4_sparse_decode.py.host.pyis also touched by the open split-heuristic PR #65481 (different hunks: it patches cleanly with offsets).Tests (MI355X, gfx950)
New in
test_minimax_m3_nvfp4_sparse_decode.py: (1) N = 1,2,3,5,8,12,16,24 requests x 4/8 rows x 30K/150K/500K context at the default split choice, (2) every split count 1/2/4/8 x XCD on/off x 1/4 KV heads x 1/4/8 rows with padding requests and a wide magnitude range. Both assert bitwise equality of values and scales withmxfp8_e4m3_quantizeof the bf16 kernel output and that rows past the batch are untouched. Result: the 96 new tests pass, and the whole file passes (176 passed), i.e. the existing decode tests are unchanged. Thei8scale store and the split-merge path were correct on the first run.Measurements
Profile trace


Before:
After:
Graph-replay microbenchmark (a small benchmark script; 200 back-to-back launches per graph, median of 15 replays; 8 rows per request, 16 heads per rank, MI355X, 1 GPU), us per launch. "bf16 + quant" is the current path (decode kernel then
mxfp8_e4m3_quantize). Context 150K:30K and 500K contexts give the same numbers within 0.1 us (saved 1.79-2.11 us at 30K, 1.81-2.04 us at 500K). The separate quant costs about 2.2 us in the graph; the epilogue adds 0.2-0.5 us to the attention kernel, so the net saving is 1.8-2.1 us per layer.
End to end (MI355X, TP4, DSpark 7 drafts, 150K context, DBO off,
VLLM_ROCM_USE_SKINNY_GEMM=0)Arms on the same installed tree, 5 repeats each: S0 = a tree that already contains the FFN quant fusions of #65478 and the routed-scale fold (so the baseline already has the other launch removals), S2 = S0 + this PR; S0 was also run again at the end to check drift (S0 start and end agree within 0.14 ms at every N).
Profile window (rank 0, 60 steps, all streams), N=4 and N=16:
_kernel.kd(the MXFP8 activation quant) 82 -> 22 per decode step, total kernels per step 1399 -> 1339 (N=4) and 1456 -> 1396 (N=16): exactly the 60 launches this PR removes; nothing else changes.Decode step p50 (ms), median of 5 repeats (noise of this benchmark about +-0.1-0.2 ms):
The step-level change is small: negative at 7 of 9 batch sizes, mean -0.4% (about 0.05 ms), average -0.8% at N >= 12, but it is not separable from the noise at any single N. I therefore claim the exact launch removal and the per-launch kernel saving above, not a specific end-to-end gain. AgentX replay and GSM8K were not run (the epilogue is bit-identical to quantizing the bf16 output in the unit tests).
Limitations
gfx950 FlyDSL decode only; decode-only batches; K = 2048 per rank assumes 16 query heads per KV head with TP4 (generalizes to any heads x 128 since each block is within one head).
AI assistance: this change was developed with Claude Code; the tests and benchmarks above were run on MI355X (gfx950).
🤖 Generated with Claude Code