Repository navigation
[FlyDSL] [Perf] [gfx950] Swap A/B in the mxfp8 8-wave epilogue for 64-bit stores - #6605
yangyuhuiling wants to merge 5 commits into
Conversation
…ores
The epilogue wrote one bf16 at a time. Computing B^T A^T instead of A B
transposes every accumulator, so a lane ends up holding four consecutive
columns rather than four rows -- eight contiguous bytes of a row-major C,
which one buffer_store_dwordx2 covers. Two cross-lane stages widen that
further: permlane16_swap pairs the two N-tiles a lane holds into 16 bytes
and 64-byte rows, and a ds_bpermute lane transpose puts adjacent lanes on
one row so a 16-lane group covers 4 rows instead of 16.
The swap is a thin wrapper: the inner atom is built with the tile counts
exchanged, so its asserts and idx() line up on their own, and the scales
and opsels follow their operands.
Measured on 8x MI355X, same checkout, min of three medians of 30:
shape before after
1024x5120x2048 66.8 62.1 -7.1%
2048x5120x2048 69.1 64.1 -7.2%
4096x5120x2048 93.7 85.9 -8.3%
8192x5120x2048 124.2 113.9 -8.3%
16384x5120x2048 188.8 170.4 -9.7%
8192x7168x2048 153.5 139.3 -9.3%
16384x7168x2048 248.2 220.0 -11.4%
8192x8192x1280 128.7 115.6 -10.2%
Also narrow the K rule. K % 256 was required unconditionally, but only
ScalePreshuffledS2R needs it: it addresses scale words by K-pair
(k1 = k // 2). ScaleRawLds stages a chunk of K steps at a time and its
dma() already tolerates a partial last chunk, so the row-major path needs
only whole 128-deep tiles. That admits K like 1792 and 4224 -- odd
multiples of BLOCK_K the kernel previously rejected.
The preshuffled branch keeps its own assert. Nothing in the tree can
reach it today (shuffle_scale_w4 is named in comments but never defined,
and the runner table registers only the row-major layout), but leaving
the check means a future host-side shuffle gets an error rather than a
silently wrong result.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🏷️ CI GuideRuns automatically on every PR:
Extended tests (opt-in via labels):
One backend per PR: PR title tags & labels: |
There was a problem hiding this comment.
this test file need to integrate into op_tests/*py(related op test file).
Review preferred not adding a test file per change. The existing op_tests/test_gemm_a8w8_mxfp8_bpreshuffle.py already reaches this kernel through every tuned gfx950 row that selects it: 145 shapes, 59 of them with N not a multiple of 256, all with zero mismatches on this branch. The K relaxation is not reachable through the dispatch until a row is tuned with K % 256 == 128; it was checked by calling the kernel directly, as described in the PR. Co-Authored-By: Claude <noreply@anthropic.com>
…wapab-store # Conflicts: # aiter/ops/flydsl/kernels/mxfp8_gemm_8wave.py
ScaleRawLds passed each chunk's offset through soffset, which the raw-buffer range check does not cover. The last chunk therefore read past the end of the scale tensor instead of reading zero: the last row's bytes past K on the row-major form, and whole columns past K/128 on the 1x128 column-major A. The bytes were never consumed, but the read could leave the allocation. The narrower K rule makes a partial last chunk more common. The offset now goes in the per-lane offset, which the check covers. The immediate offset is not an option: on an LDS-direct load it also moves the LDS destination. Same results on all scale forms; timing within noise (0-2%, 6 alternating passes, 15 cases incl. K=640 and the 128x128 form). Co-Authored-By: Claude <noreply@anthropic.com>
Builds on #6407's 8-wave mxfp8 kernel. Three changes, all to that kernel: a wider C store, a narrower K rule, and keeping the scale staging inside the scale tensors (which the K rule leans on).
1. A/B-swapped epilogue (2.7-8.5%)
StoreBf16wrote one bf16 at a time: a lane holds four consecutive rows of one column, which cannot be coalesced. ComputingB^T A^Tinstead ofA Btransposes every accumulator, so a lane holds four consecutive columns — eight contiguous bytes of a row-major C, onebuffer_store_dwordx2.Two cross-lane stages widen it further:
permlane16_swappairs the two N-tiles a lane holds into 16 bytes, giving 64-byte rows;ds_bpermutelane transpose puts adjacent lanes on one row, so a 16-lane group covers 4 rows instead of 16.The swap itself is a thin wrapper — the inner atom is constructed with the tile counts exchanged, so its asserts and
idx()line up without further adjustment, and scales and opsels follow their operands.Prior art. The first two stages are not new to FlyDSL: its MXFP4 4-wave GEMM already feeds the MFMA
(B, A)to getC^T = B^T A^Tand pairs N-tiles withpermlane16_swapinto 128-bit C stores (kernels/gemm/fp4_gemm_4wave.py,StoreCFp4at L565-L607, from ROCm/FlyDSL#990). This PR brings the same epilogue to the mxfp8 8-wave kernel, where the swap goes through the scaledMxMfma(scales and opsels swapped with their operands), and adds theds_bpermutelane transpose on top. Neither the mxfp8 nor the fp8 8-wave kernel, in aiter or in FlyDSL, has a wide C store today.Measured on MI355X after merging current main (FlyDSL 0.4.0, as
requirements.txtnow pins), median of 40 launches after warmup,HIP_VISIBLE_DEVICES=0, min over three passes with the arms alternating (main, PR / PR, main / main, PR). Times in µs:The run is gated on an idle machine: it starts only after five consecutive samples show every GPU under 5%, and is discarded if anything is running when it finishes. Worst pass-to-pass spread within an arm was 4.0% (main) and 2.9% (this PR), so the small-M entries are near the noise and the large-M ones are not. An earlier run on FlyDSL 0.3.4.1, before the merge, gave -2.7% to -9.1% on the same shapes.
2. Narrower K rule
K % 256was required unconditionally. OnlyScalePreshuffledS2Rneeds it — it addresses scale words by K-pair (k1 = k // 2).ScaleRawLdsstages a chunk of K steps at a time and never consumes the bytes of the last chunk that lie past K, so the row-major path needs only whole 128-deep tiles. That those bytes are read safely is section 3.That admits odd multiples of
BLOCK_Ksuch as K = 640, 1152, 4224, which the kernel rejected before.The preshuffled branch keeps its own assert. Nothing in the tree reaches that path today (
shuffle_scale_w4is named in comments but never defined, and_RUNNERSregisters only("gfx950", "1x32", "row", "row")), but keeping the check means a future host-side shuffle gets an error rather than a silently wrong result.3. Scale chunk reads stay inside the scale tensors
ScaleRawLds.dma()passed each chunk's offset throughsoffset. The raw-buffer range check covers the VGPR and immediate offsets but notsoffset, so the last chunk read past the end of the scale tensor rather than reading zero: the last row's bytes past K on the row-major form, and whole columns past K/128 on the 1x128 column-major A. Never consumed, but the read could leave the allocation. This is already on main whenever K is not a multiple of 512 (e.g. K=1792); section 2 makes it more common.The offset now goes in the per-lane VGPR offset, which the check covers. The immediate offset is not an option: on an LDS-direct load it also moves the LDS destination. Same review point as on the mori port of this reader (ROCm/mori#806).
Cost, 6 alternating passes before/after on 15 cases (the 8 shapes above, K = 640/1152/4224, and three 128x128-form shapes): every case within -1.4% to +1.7%, against a 2-8% pass-to-pass spread on that run, so no measurable difference. All cases element-exact against the reference.
Tests
No new test file; the existing one covers the epilogue.
Epilogue.
op_tests/test_gemm_a8w8_mxfp8_bpreshuffle.pyover the tuned gfx950 CSV: 555 rows, of which 145 dispatch to this kernel, 59 of those with N not a multiple of 256 (a lane's four columns straddle the tile edge). All 145 report 0 mismatches.K relaxation. No tuned row has K % 256 == 128, so dispatch cannot reach the new K values yet. I called the kernel directly instead:
Each shape ran 6 launches with the output NaN-poisoned beforehand, so a skipped store shows up instead of being masked, and was compared element-wise against the torch reference at 5% relative tolerance. 0 elements off in every case. On main all of these are rejected by the K check. ue8m0 scales are exact powers of two, so the only error against the reference is fp8 quantisation plus bf16 rounding — a misplaced scale moves whole rows rather than hiding in tolerance.
After merging main, which brought #6603's 32x32 and 128x128 scale forms into the same reader, the same direct check across all three forms (
w_scale_block= 1x32, 32x32, 128x128), K = 640 to 7296 on and off a multiple of 256, M = 320 and N = 5152 included: 24 cases, 4 launches each, 0 elements off. The dispatch test above gives the same result after the merge (145 of 555 rows on this kernel, all err 0). Re-run after the latest merge (#6380) on FlyDSL 0.4.0: the 24 direct cases, the 555-row dispatch test andtest_gemm_group32_interface.py(159 passed, 10 skipped) all pass.Not covered
gfx950 only, FlyDSL 0.4.0 (also checked on 0.3.4.1 before the merge). No config is re-tuned: the kernel name and tile are unchanged, so existing tuned rows keep selecting it. The preshuffled scale path is unreachable and therefore untested end to end.
🤖 Generated with Claude Code