Repository navigation
[FlyDSL] [gfx950] Add an 8-wave mxfp8 GEMM - #6370
yangyuhuiling wants to merge 6 commits into
Conversation
…tep barrier The barrier that ends each main-loop K step admits `2 * N_LDS_STEPS_A + N_LDS_STEPS_B` outstanding global->LDS loads. That is more than the step can have in flight and still be safe: the barrier passes while writes the next step reads have not landed, and the next step consumes stale LDS. The result is a few wrong rows, not a NaN -- rel_l2 lands around 1e-3, close enough to fp8 quantisation noise to go unnoticed. Whether it fires depends on how the waves interleave, so a shape can be clean on one launch and wrong on the next, and larger grids hit it more often. Shapes in the tuned configs are affected. `M=32768 N=7168 K=1792` on the 256x256 tile is wrong on 4 of 4 runs, and `M=3456 N=7168 K=4224` on 128x256 on 1 of 4. Lowering the count to `N_LDS_STEPS_A + N_LDS_STEPS_B - 1` clears it: both shapes above are then correct and bit-identical across runs. A sweep of the 128x256 tile (where the two counts are 4 and 2) shows 4 and 3 corrupting and 2 and 0 clean, so this is the loosest count that is safe there. Waiting on more loads costs throughput, mostly on 128x256 and mostly at small grids where the K loop is short enough for the barrier to matter: tile grid before after 128x256 168 55.6us 65.0us +16.9% 128x256 224 60.6us 67.8us +11.9% 128x256 243 64.9us 68.0us +4.8% 128x256 252 63.7us 67.9us +6.6% 256x256 144 84.7us 87.0us +2.7% 256x256 363 167.8us 177.3us +5.7% 256x256 495 204.9us 204.7us -0.1% 256x256 3584 451.7us 479.0us +6.0% Measured on MI355X, min of per-arm medians over two alternating passes. Recovering this would need a tighter count per tile rather than one formula; correctness comes first here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prologue's `if wave_m == 1: rocdl.s_barrier()` is executed by waves 4-7 only. Waves 0-3 skip it and reach the next barrier instead, so the hardware pairs the two groups one barrier apart and keeps them that way for the rest of the kernel. Documented rather than changed: making it unconditional while keeping the old wait count still corrupts, and with the corrected count both forms are clean, so it is not what this PR is fixing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit read the divergent `if wave_m == 1: s_barrier()` as a bug in itself. It is not: it deliberately puts waves 0-3 one phase ahead, which is the stagger the double-buffered main loop is built around. Making it unconditional destroys that and fixes nothing. What is missing is its counterpart. The stagger is never closed, so the two halves are still a barrier apart when the epilogue starts. gcnasm closes it before the same epilogue (opus_gemm_a2a_lsa quad-subtile template) and so does mori's fused copy of this pipeline, with `if wave_m == 0: rocdl.s_barrier()`. Still a note rather than a change: nothing follows store_c here, so the imbalance cannot be observed today. It stops being unobservable the moment anything is appended to the epilogue -- a `wait_barrier(0)` placed after store_c would rendezvous waves 0-3 with waves 4-7 still one barrier back and before their stores, rather than meaning what it says. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A second gfx950 route for 1x32 ue8m0 operands, beside the 4-wave mxscale_preshuffle kernel. `v_mfma_scale_f32_16x16x128_f8f6f4` takes e8m0 scales as instruction operands, so this needs no dequantisation arithmetic at all -- no second accumulator, no running rescale -- unlike the 128-wide blockscale kernels, whose promote and rescale chain exists precisely because a 128-wide block cannot be expressed that way. The mainloop is gemm_a8w8_8wave's with the scale operands threaded into the four MFMA calls. Two things around it are not, and both are about addresses rather than arithmetic: - A and B are swapped, so a lane's four accumulator values land in four consecutive columns rather than four rows -- 8 contiguous bytes in a row-major C where the unswapped layout can only emit buffer_store_short. Two cross-lane stages take a row to 64 contiguous bytes. - The scales are K-block major and packed four to a dword. A block group's sixteen lanes want sixteen consecutive rows of one K block; K-block major makes that one coalesced access where the quantiser's own [M, K/32] row major makes it sixteen. And a lane's four A tiles differ only by sixteen rows and share the K block, so packing their bytes into one dword lets opsel_b pick between them at no VALU cost. `Mfma16x16x128` gains an optional scaled path; passing neither scale leaves the unscaled call byte-for-byte as it was, so the ptpc kernel is unaffected. Not reachable from the tuned dispatch. The scale layout is incompatible with the mxscale_preshuffle kernel's and converting on the critical path would eat the margin, so callers select this explicitly and shuffle with `shuffle_mxfp8_a_scale` / `shuffle_mxfp8_b_scale`. The weight layout is shared: `shuffle_weight(w, (16, 16))` serves both this and the ptpc 8-wave kernel. Ported from mori's `mori.ops.gemm_ar.kernels_fused`, minus the all-reduce epilogue it exists to serve, and minus the ptpc and blockscale quant paths. Verified bit-identical to mori on four shapes covering both N tiles, including M=64 where the tail block masks its stores. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WIDE_TILE_MIN_GRID came over from mori as a bare constant. Timing both tiles at 13 points on MI355X confirms it picks the faster one everywhere, and shows why a grid threshold rather than an M one: the narrow tile's cost is a step, and the two shapes step at different M but at similar grid sizes -- (120, 140] on one, (128, 160] on the other. 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: |
Black's line joining on four files, and one ruff SIM102: the overflow guard's `if M is not None and N is not None:` wrapping a second `if` is now one condition. The nested form came from the ptpc dispatcher next door, which predates the rule being enforced there. Checked with the versions CI pins -- `ruff==0.16.0` from `.github/workflows/pre-checks.yaml`, not the older one that happened to be installed, which does not flag SIM102 here. `ruff check .` over the repository now passes. Black still reports ten other files, none of them touched by this branch; that is a local black version differing from `psf/black@stable`, and CI's own run on this PR listed only the three files fixed here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
This is duplicate functionality — it's already on main. Also, the 950 interface currently supports non-shuffle scale in the current architecture.The shuffle variant also exists, it's just not enabled. |
Adds a second gfx950 FlyDSL route for 1×32 ue8m0 (mxfp8) operands, beside the 4-wave
mxscale_preshufflekernel. Ported from mori, which vendored this pipeline from aiter's owngemm_a8w8_8wave.pyand added the scaled path.Why it is faster
v_mfma_scale_f32_16x16x128_f8f6f4takes e8m0 scales as instruction operands, so a 32-wide ue8m0 GEMM needs no dequantisation arithmetic at all — no second accumulator, no running rescale, no division. One MFMA consumes a whole K=128 step with its four 32-blocks already dequantised in hardware. That is what separates it from the 128-wide blockscale kernels, whose promote/rescale chain exists precisely because a 128-wide block cannot be expressed that way.The mainloop is
gemm_a8w8_8wave's, with the scale operands threaded into the four MFMA calls. Two things around it are not, and both are about addresses rather than arithmetic:A and B are swapped.
fx.gemm(atom, c, b, a, c)computesB^T A^T = (A B)^T, moving a lane's four accumulator values from four consecutive rows to four consecutive columns — 8 contiguous bytes in a row-major C, where the unswapped layout can only ever emitbuffer_store_short. Two cross-lane stages (permlane16_swap, then ads_bpermutelane transpose) take a row to 64 contiguous bytes. This is gcnasm'smfma_adaptor_swap_aband the store stages from its quad-subtile template.The scales are K-block major and packed four to a dword. A block group's sixteen lanes want sixteen consecutive rows of one K block, so K-block major makes that one coalesced access where the quantiser's own
[M, K/32]row major makes it sixteen — same instruction count, 3.2× the cache accesses, +50 to +67% on the whole GEMM. And a lane's four A tiles differ only by sixteen rows and share the K block, so packing their bytes into one dword letsopsel_bpick between them at no VALU cost (−5 to −7.5%). CK does the same inpreShuffleScaleBuffer_gfx950.Measured
Against
gemm_a8w8_blockscale_bpreshufflewith 2-D e8m0 scales — the route aiter already takes for these operands, which dispatches to the batched kernel at B=1. Identical logical operands; the scale shuffles are weight-prep work and are not timed. MI355X, min of 3 medians per arm, one process per point.Correctness
fx.gemmtakes**kwargs, so a misspelledscale_acompiles and computes an unscaled product.rel_l2≈ 2e-4 throughout. ue8m0 scales are exact powers of two, so applying one is lossless and that residual is fp8 quantisation plus bf16 output rounding — nothing else hides in it.Mfma16x16x128gains an optional scaled path; passing neither scale leaves the unscaled call byte-for-byte as it was, andtest_gemm_a8w8_8wave.pystill passes 6/6.Integration
Deliberately not reachable from the tuned-CSV dispatch. The scale layout is incompatible with the
mxscale_preshufflekernel's, and the two buffers are indistinguishable by shape and dtype — converting on the critical path would eat the margin, and getting them crossed computes garbage rather than raising. So callers select this explicitly viagemm_a8w8_mxfp8_8wave_flydsland shuffle with the two new helpers:The weight layout is shared:
shuffle_weight(w, (16, 16))serves both this and the ptpc 8-wave kernel, verified byte-identical to mori'spreshuffle_b, so a server shuffles once.block_ndefaults topick_block_n(M, N), which gates on the wide grid size rather than on M — the narrow tile exists to widen a launch-starved grid, and the grid depends on N as much as on M. Timing both tiles at 13 points confirms it picks the faster one everywhere; the table behind the constant is in the source.Constraints
BLOCK_M: the grid isceildiv(M, BLOCK_M)and the tail block masks its stores. The op rejects rather than pads, because the A scale is built over the padded M and repairing its packed layout afterwards is not a reshape — the caller has to pad before quantising.BLOCK_Mis fixed at 256: the packed scale puts a lane's four 16-row M tiles in one dword, which is exactly that.block_nand of 32.Not included
mori's
Mxfp8GemvOp(decode, M ≤ 32, its own kernel and tuned table) is a separate follow-up. So is pushing the swap-ab store stages back into the ptpc 8-wave kernel, where they are worth −6.7% bit-exactly; that wants its own bit-exactness check rather than riding along here.🤖 Generated with Claude Code