Repository navigation
[ROCm][Perf][DSv4.1] TP1 MXFP8 GEMM tiles and a pre-quantized wo_b input for DEP8 on MI355X #63821
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
1ae3465
621c4e5
825d04d
6392134
98c7dcb
8902eb3
3d2b5cd
fb01225
82daaac
1d99585
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -456,7 +456,7 @@ def _packed(bm, bn, bk, k_pack, warps, stages, nonkdim, waves, fused=1) -> _Conf | |
|
|
||
|
|
||
| # (N, K) -> [(max M, config), ...] for the DeepSeek-V4.1-Flash MXFP8 | ||
| # linears at TP2/TP4, tuned on MI355X under HIP graphs. A tier serves | ||
| # linears at TP1/TP2/TP4, tuned on MI355X under HIP graphs. A tier serves | ||
| # every M up to its bound; larger M reuse the last tier. | ||
| _TUNED: dict[tuple[int, int], list[tuple[int, _Config]]] = { | ||
| # shared expert gate_up, TP4 | ||
|
|
@@ -550,6 +550,20 @@ def _packed(bm, bn, bk, k_pack, warps, stages, nonkdim, waves, fused=1) -> _Conf | |
| (4096, _tiled(128, 128, 128, 1, True, 4, 2, 32, 0)), | ||
| (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): [ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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.)
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
| (8, _packed(8, 16, 256, 2, 2, 3, 16, 0)), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. description needs perf and sccuracy check for each kernel you are adding on a microbench level
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
| (32, _packed(32, 32, 256, 1, 2, 3, 16, 0)), | ||
| (64, _tiled(64, 64, 512, 1, False, 4, 2, 32, 0, 2)), | ||
| (96, _tiled(32, 64, 512, 1, True, 8, 2, 16, 2)), | ||
| (128, _tiled(64, 64, 256, 1, False, 4, 2, 32, 2, 2)), | ||
| (192, _tiled(64, 64, 512, 1, True, 4, 2, 32, 2)), | ||
| (448, _tiled(64, 64, 256, 1, True, 4, 2, 32, 2)), | ||
| (896, _tiled(128, 128, 256, 1, True, 4, 2, 16, 0)), | ||
| (1792, _tiled(128, 128, 128, 1, True, 4, 2, 32, 2)), | ||
| (2560, _tiled(128, 128, 256, 1, True, 8, 2, 32, 0)), | ||
| (8192, _tiled(128, 128, 128, 1, True, 4, 2, 32, 2)), | ||
| ], | ||
| # shared expert down, TP4 | ||
| (5120, 576): [ | ||
| (8, _packed(8, 32, 256, 2, 2, 2, 16, 1)), | ||
|
|
@@ -607,6 +621,20 @@ def _packed(bm, bn, bk, k_pack, warps, stages, nonkdim, waves, fused=1) -> _Conf | |
| (4096, _tiled(128, 128, 128, 1, True, 8, 2, 32, 1)), | ||
| (8192, _tiled(128, 128, 128, 1, True, 2, 2, 32, 1)), | ||
| ], | ||
| # shared expert down, TP1 (DP attention), tuned at the CUDA graph sizes 48-4096 | ||
| (5120, 2304): [ | ||
| (8, _packed(8, 16, 256, 2, 2, 3, 16, 0)), | ||
| (32, _packed(32, 32, 256, 1, 2, 3, 16, 0)), | ||
| (64, _tiled(64, 64, 256, 1, True, 8, 3, 16, 0, 4)), | ||
| (96, _tiled(32, 64, 512, 1, True, 8, 3, 16, 2)), | ||
| (192, _tiled(64, 64, 512, 1, True, 4, 2, 32, 2)), | ||
| (288, _tiled(64, 64, 256, 1, True, 4, 2, 32, 2)), | ||
| (384, _tiled(128, 64, 256, 1, True, 8, 2, 32, 0)), | ||
| (768, _tiled(128, 128, 256, 1, True, 8, 2, 32, 2)), | ||
| (2048, _tiled(128, 128, 128, 1, True, 4, 2, 32, 2)), | ||
| (3072, _tiled(128, 256, 128, 1, True, 8, 3, 32, 2)), | ||
| (8192, _tiled(128, 128, 128, 1, True, 4, 2, 32, 0)), | ||
| ], | ||
| # wo_b, TP2 | ||
| (5120, 4096): [ | ||
| (4, _packed(4, 32, 512, 4, 2, 2, 16, 0)), | ||
|
|
@@ -629,6 +657,23 @@ def _packed(bm, bn, bk, k_pack, warps, stages, nonkdim, waves, fused=1) -> _Conf | |
| (4096, _tiled(128, 128, 128, 1, True, 8, 2, 16, 1)), | ||
| (8192, _tiled(128, 128, 128, 1, True, 4, 2, 32, 0)), | ||
| ], | ||
| # wo_b, TP1 (DP attention), tuned at the CUDA graph sizes 48-4096 | ||
| (5120, 8192): [ | ||
| (8, _packed(8, 16, 256, 2, 2, 3, 16, 0)), | ||
| (32, _packed(32, 32, 256, 1, 2, 3, 16, 0)), | ||
| (48, _tiled(64, 64, 256, 1, True, 4, 2, 16, 2, 4)), | ||
| (64, _tiled(64, 64, 512, 1, True, 4, 2, 32, 2, 2)), | ||
| (96, _tiled(32, 64, 512, 1, True, 8, 2, 16, 0)), | ||
| (128, _tiled(64, 64, 256, 1, True, 4, 2, 32, 0, 2)), | ||
| (192, _tiled(64, 64, 512, 1, True, 4, 2, 16, 0)), | ||
| (384, _tiled(128, 128, 256, 2, True, 4, 2, 16, 0)), | ||
| (768, _tiled(128, 128, 256, 1, True, 4, 2, 16, 0)), | ||
| (1024, _tiled(128, 128, 256, 2, True, 4, 2, 16, 0)), | ||
| (1536, _tiled(128, 128, 256, 1, True, 8, 2, 32, 0)), | ||
| (2048, _tiled(128, 128, 256, 1, True, 8, 2, 32, 2)), | ||
| (3072, _tiled(128, 128, 128, 1, True, 4, 2, 16, 0)), | ||
| (8192, _tiled(128, 128, 256, 1, True, 8, 2, 32, 2)), | ||
| ], | ||
| # wq_b, TP4 | ||
| (8192, 1280): [ | ||
| (1, _packed(8, 32, 256, 2, 4, 3, 16, 1)), | ||
|
|
@@ -663,6 +708,20 @@ def _packed(bm, bn, bk, k_pack, warps, stages, nonkdim, waves, fused=1) -> _Conf | |
| (2048, _tiled(128, 128, 128, 1, True, 4, 2, 32, 0)), | ||
| (8192, _tiled(128, 256, 128, 1, True, 2, 2, 32, 0)), | ||
| ], | ||
| # wq_b, TP1 (DP attention), tuned at the CUDA graph sizes 48-4096 | ||
| (32768, 1280): [ | ||
| (8, _packed(8, 16, 256, 2, 2, 3, 16, 0)), | ||
| (32, _packed(32, 32, 256, 1, 2, 3, 16, 0)), | ||
| (56, _tiled(64, 64, 256, 1, True, 4, 2, 32, 2)), | ||
| (64, _tiled(64, 64, 256, 1, True, 4, 2, 16, 2)), | ||
| (128, _tiled(128, 128, 256, 1, False, 8, 2, 32, 0)), | ||
| (160, _tiled(128, 128, 128, 1, True, 8, 2, 32, 0)), | ||
| (224, _tiled(128, 128, 128, 1, True, 4, 2, 32, 2)), | ||
| (288, _tiled(128, 128, 128, 1, True, 4, 2, 32, 0)), | ||
| (448, _tiled(128, 128, 128, 1, True, 8, 2, 32, 0)), | ||
| (1280, _tiled(128, 128, 128, 1, True, 4, 2, 32, 0)), | ||
| (8192, _tiled(128, 128, 128, 1, True, 4, 2, 32, 2)), | ||
| ], | ||
| } | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,9 @@ | |
| ) | ||
| 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 ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
| rocm_mxfp8_block32_gemm, | ||
| ) | ||
| from vllm.model_executor.layers.fusion.quant_activation import QuantizedActivation | ||
| from vllm.model_executor.layers.quantization.utils.quant_utils import kMxfp8Dynamic | ||
| from vllm.model_executor.layers.rocm_paged_mxfp4_indexer import ( | ||
|
|
@@ -57,6 +60,7 @@ | |
| rocm_inverse_rope_mxfp8_rows, | ||
| rocm_inverse_rope_rows_, | ||
| rocm_mxfp8_wo_a_bmm, | ||
| rocm_mxfp8_wo_a_bmm_mxfp8, | ||
| rocm_sparse_attn_decode, | ||
| rocm_sparse_attn_prefill, | ||
| ) | ||
|
|
@@ -931,6 +935,21 @@ def _o_proj( | |
| self, attn_out: torch.Tensor | QuantizedActivation, positions: torch.Tensor | ||
| ) -> torch.Tensor: | ||
| if isinstance(attn_out, QuantizedActivation): | ||
| if self._wo_b_takes_wo_a_mxfp8(): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 98c7dcb. |
||
| q, q_scale = rocm_mxfp8_wo_a_bmm_mxfp8( | ||
| attn_out.data, | ||
| attn_out.scale, | ||
| self.wo_a, | ||
| self.n_local_groups, | ||
| self.o_lora_rank, | ||
| ) | ||
| return rocm_mxfp8_block32_gemm( | ||
| q, | ||
| q_scale, | ||
| self.wo_b.weight, | ||
| self.wo_b.weight_scale, | ||
| attn_out.orig_dtype, | ||
| ) | ||
| z = rocm_mxfp8_wo_a_bmm( | ||
| attn_out.data, | ||
| attn_out.scale, | ||
|
|
@@ -953,6 +972,38 @@ def _o_proj( | |
| ) | ||
| return self._wo_b_after_wo_a(z.flatten(1)) | ||
|
|
||
| def _wo_b_takes_wo_a_mxfp8(self) -> bool: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
| """Whether wo_b's linear runs exactly a block32 MXFP8 GEMM on the | ||
| MXFP8-quantized wo_a output, so wo_a can hand that over directly: | ||
| 32x32 block-scaled e4m3 weight, no bias, no tensor-parallel reduce.""" | ||
| if self._wo_b_scale is not None: | ||
| return False | ||
| from vllm.model_executor.kernels.linear.mxfp8.rocm_native import ( | ||
| RocmDotScaledMxfp8LinearKernel, | ||
| ) | ||
|
|
||
| # The kernel owns wo_b's weight layout: a kernel that relays out an | ||
| # e4m3 [N, K] weight with uint8 [N/32, K/32] scales passes every | ||
| # dtype and shape check below. | ||
| kernel = getattr(getattr(self.wo_b, "quant_method", None), "kernel", None) | ||
| w = getattr(self.wo_b, "weight", None) | ||
| ws = getattr(self.wo_b, "weight_scale", None) | ||
| return ( | ||
| type(kernel) is RocmDotScaledMxfp8LinearKernel | ||
| and w is not None | ||
| and ws is not None | ||
| and w.dtype == torch.float8_e4m3fn | ||
| and ws.dtype == torch.uint8 | ||
| and w.dim() == 2 | ||
| and ws.dim() == 2 | ||
| and ws.shape[0] != w.shape[0] | ||
| and getattr(self.wo_b, "bias", None) is None | ||
| and ( | ||
| not getattr(self.wo_b, "reduce_results", False) | ||
| or getattr(self.wo_b, "tp_size", 1) == 1 | ||
| ) | ||
| ) | ||
|
|
||
| def _wo_b_after_wo_a(self, zf: torch.Tensor) -> torch.Tensor: | ||
| if self._wo_b_scale is not None and zf.dim() == 2: | ||
| return self._bpre_attn_gemm( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 621c4e5. The MI355 MLA Kernels mirror now runs the wo_a tests and the new gate test from this file.