Repository navigation
Conversation
At decode sizes (M <= 256) the upstream-tested 128- and 256-row tiles compute 16 valid rows per tile at M = 16 and launch fewer workgroups than there are CUs. launch_gemm_a8w8_splitk splits K across grid z: each split writes fp32 partials to a caller workspace and bumps a per-tile arrival counter, and the last workgroup sums the partials in split order and resets the counter. select_gemm_a8w8_decode_config picks 16-row tiles up to M = 64, 64x128 tiles up to M = 256, 128x256 or 256x256 tiles for wide N, and 4-way split-K for deep K at M <= 16. launch_gemm_a8w8 keeps its signature and generated code. Signed-off-by: Jin Pan <jin.pan@amd.com>
- Compute split z's first K-tile as z*(T//S) + z*(T%S)//S instead of z*T//S. For a valid direct call such as K = 2**30 with split_k = 256, z*T overflowed i32, the last split's K-tile count went negative, and its drain read LDS outside the arena. - Apply the per-token/per-channel scales to the sum of the partials instead of to each partial before the workspace store. With large scales, a scaled partial could overflow to +-inf where the unscaled sum cancels, and the reduction returned NaN instead of 0. The reducing workgroup loads the scales itself, so that all its workspace loads stay in flight, and the other splits of a tile skip the loads. - State in launch_gemm_a8w8_splitk's docstring that N % tile_n == 0, as for launch_gemm_a8w8. - Test the selector's M > 64 returns (64x128 and 64x64 tiles), and the scale order with partials that cancel. Signed-off-by: Jin Pan <jin.pan@amd.com>
max_split was only compared with 4, so values 1-3 turned split-K off instead of capping it, and no caller passed it. Add type hints, and a GPU-free test that checks the config each selector row returns, so CI covers the thresholds. Signed-off-by: Jin Pan <jin.pan@amd.com>
The docstring said that an N of 16384 fills the GPU with 128x256 tiles, but at M <= 64 that grid has only 64 workgroups on 256 CUs. State that the thresholds were measured on one 256-CU part instead. Signed-off-by: Jin Pan <jin.pan@amd.com>
Benchmark on a gfx1250 box: the tile choices reproduce, the split-K rows run slower than without splitI measured this PR on a gfx1250 machine (4 × 256-CU parts, the 3 idle ones used; amdgpu 7.1.1, ROCm 10.1; FlyDSL built from main with the pinned LLVM
The pre-PR candidates are the 128×256 (nb2/3/4) and 256×256 (nb4) configs from the base tests. Absolute times differ from yours (the baseline configs run at 0.88–1.30x of your table, the selected small tiles at 1.17–1.47x), so the ratios are what to compare. HIP-graph replay gives the same picture: 75–88 µs for the three deep-K split rows. Where the split-K time goesDevice time in µs, 16×128×128 nb4, split 4 unless noted:
The four rows in the middle are diagnostics only; they are not correct protocols (the workgroup-scope one returned NaN on 16×1536×7168). With the pinned LLVM, the epilogue compiles to: Compared with the workgroup-scope variant, the extra time grows with the workgroup count: +14 µs with 48 workgroups and +71–73 µs with 224, about 0.3 µs each, as if these operations serialize across the device. The compiler is the pinned one, so I suspect the platform (driver or firmware cache configuration) makes them cheap on your machine. Which driver and firmware was your table measured on? Comparison with #971's split-K (same kernel file)#971 writes bf16 partials to a split_k × M × N buffer and sums them in a second kernel; K must divide by tile_k × split_k. I ported #971 (
The selector on shapes outside your table22 shapes not in the table (Llama-3.1 8B, 70B TP8 and 405B TP8; Qwen3-32B and Qwen3-235B TP8; DeepSeek-V3 TP8 q_b_proj, kv_b_proj and dense MLP) × M = 1/16/32/64/128/256, 132 rows, against the best of 15 single-launch configs:
Overall the selector gives 1.53x over the pre-PR configs (geomean over the 126 rows they cover) and runs 1.15x slower than the best config, within 10% of it on 87 of 132 rows. The 16×64 branch holds up on these shapes; the other three don't:
Suggestions
|
|
|
||
| @flyc.jit | ||
| def launch_gemm_a8w8( | ||
| def select_gemm_a8w8_decode_config(M: int, N: int, K: int) -> tuple[int, int, int, int, int, int, int]: |
There was a problem hiding this comment.
Our E2E path AITER already uses offline auto tuning, device/shape keyed tuned CSVs, and a legal heuristic fallback. See https://github.com/ROCm/aiter/blob/main/aiter/ops/gemm_op_a8w8.py#L1198
| ) | ||
| fx.ptr_store(arrived, arrive_lds) | ||
| workgroup_barrier(use_cluster=False) | ||
| if fx.ptr_load(arrive_lds) == split_k - 1: |
There was a problem hiding this comment.
We also already have a cluster-barrier fused split-K epilogue that has performed well in our tests. See https://github.com/ROCm/aiter/blob/main/aiter/ops/flydsl/kernels/splitk_fused_epilogue_gfx1250.py.
In our earlier tests, the last-arriving-workgroup approach was slower. Could you compare the two on the same workloads?
|
|
||
|
|
||
| @flyc.jit | ||
| def launch_gemm_a8w8_splitk( |
There was a problem hiding this comment.
Could these two launchers be consolidated?
|
Thanks for the detailed benchmark. Our table was measured on the box below. The numbers in this comment are from the same box today, with the same driver, firmware, and compiler.
Your rows on this box. Base
The per-workgroup cost you found shows up here too, but it is about 10x smaller. With many workgroups, split 4 also loses on this box:
That is about 0.03 µs per workgroup here, against about 0.3 µs on your box. The selector splits only for M ≤ 16 with at most 64 N tiles, so at most 256 workgroups. At 0.03 µs each, the split still wins here. Could you share the same data for your box? That is |
|
@aoli26 Thanks for the review.
We will decide on the selector, the split-K path, and the two launchers after that comparison. |
|
@aoli26 Here is the comparison on our gfx1250 box (one 256-CU part, the box from the comment above). Both sides get the same A, B, and E8M0 block scales (AITER gets the 16x16-preshuffled B). All variants run in one process and match one fp64 reference. Hot device time, median of 15 interleaved samples, µs. AITER is at Decode, M = 16:
M = 512, where your tuned CSV uses the fused split-K (
So on this box the split-K and the selector of this PR do not beat what AITER already ships. |
|
Closing in favor of AITER's gfx1250 FP8 GEMM and its tuned configs; see the comparison above. Thanks @zhiding512 and @aoli26 for the measurements and the review. |
Summary
For decode shapes (M <= 256), the gfx1250 FP8 GEMM leaves most CUs idle. This PR adds a split-K launcher,
launch_gemm_a8w8_splitk, and a decode config selector,select_gemm_a8w8_decode_config. Over 94 decode rows, the selected configs are 2.23x faster in median than the best upstream-tested config.Motivation
Changes
launch_gemm_a8w8_splitk(..., split_k, i64_ws, i64_counters)splits K across grid z:[z*T//S, (z+1)*T//S)of the T = K / tile_k K-tiles. S does not have to divide T.split_k * ceil(M / tile_m) * ceil(N / tile_n) * tile_m * tile_nfp32 values) and the counters (ceil(M / tile_m) * ceil(N / tile_n)int32 values, zero before the first launch). Launches that can overlap need separate buffers. The docstring states these sizes and the shape requirements.split_k > 1.select_gemm_a8w8_decode_config(M, N, K)returns(tile_m, tile_n, tile_k, m_warp, n_warp, num_buffers, split_k)for M <= 256:ValueErrorfor M > 256, K % 128 != 0, K < 256, and N % 64 != 0.launch_gemm_a8w8keeps its signature. Its kernel gets two unused arguments.Performance (gfx1250, MI450-class, 256 CUs)
Before: the best upstream-tested config per row. After: the selected config. Blockscale, hot device time, median of 15 interleaved samples per side.
Testing
Dependencies
Breaking Changes
None. Callers opt in through the selector and the new launcher.
Related PRs
store_cand relax theN % tile_n == 0line of the docstring.