Summary
While optimizing kernels/pa_decode_tile.py's QK matmul (raw mfma_f32_16x16x32_fp8_fp8 intrinsics, M=64/N=16/K=128 fp8), I compared it against the equivalent computation expressed via the high-level fx.gemm + make_tiled_mma + make_tiled_copy_A/B/C API. Two issues showed up:
fx.gemm uses more VGPR than the equivalent raw MFMA sequence for the same M/N/K/dtype (confirmed, modest but real: +2 VGPR / +8% in an isolated microbenchmark).
fx.gemm's fragment/copy path fails to compile at all when paired with fx.rocdl.BufferCopy{32,64,128}b (the AMD buffer-descriptor copy atoms recommended in the docs for global memory access) for this fp8 M=64/N=16/K=128 shape — only fx.UniversalCopy32b (plain-pointer copy) worked.
Repro
Attached: gemm_vgpr_compare.py (self-contained, no other repo files needed beyond flydsl/torch).
FLYDSL_RUNTIME_ENABLE_CACHE=0 python3 gemm_vgpr_compare.py raw # raw MFMA version
FLYDSL_RUNTIME_ENABLE_CACHE=0 python3 gemm_vgpr_compare.py gemm # fx.gemm version
Both kernels do the same M=64,N=16,K=128 fp8 mfma_f32_16x16x32_fp8_fp8 GEMM with a 4-warp (NWARP,1,1) tiled_mma split (matching pa_decode_tile.py's own QK tiling).
Issue 1 — VGPR overhead
.amdhsa_next_free_vgpr from the dumped final ISA (FLYDSL_DUMP_IR=1):
| Version |
VGPR |
| raw MFMA intrinsics |
24 |
fx.gemm (with fx.UniversalCopy32b, since BufferCopy* doesn't compile — see below) |
26 |
Reproducible across repeated runs. The gap is small in this toy kernel (no other loop-carried state), but in the real pa_decode_tile.py context, VGPR is the kernel's binding occupancy constraint (waves/SIMD = 512 // vgpr), so even a modest per-operand overhead compounds across a kernel with many such GEMM calls and loop-carried fragments — worth knowing about when choosing between the two APIs for register-pressure-sensitive kernels.
Issue 2 — BufferCopy* + fx.gemm fragments fail to compile
Swapping fx.UniversalCopy32b() for fx.rocdl.BufferCopy128b() in the gemm variant (same script, same shapes) hits a C++ assertion:
python3: include/flydsl/Dialect/Fly/Utils/LayoutUtils.h:791: std::pair<_FIter, _FIter> mlir::fly::detail::complementImpl(...): Assertion `(R == 1 || filteredStride.isStatic()) && "stride must be static for complement"' failed.
Switching to fx.rocdl.BufferCopy64b() (matching the fragment's 8-element/lane granularity) avoids the assertion but hits an MLIR legalization failure instead:
error: failed to legalize operation 'fly.copy_atom_call_ssa' that was explicitly marked illegal:
%80 = "fly.copy_atom_call_ssa"(%55, %78) <{operandSegmentSizes = array<i32: 1, 1, 0, 0>}> :
(!fly.copy_atom<!fly_rocdl.cdna3.buffer_copy<64>, 8>, !fly.memref<f8E4M3FNUZ, global, 8:1>) -> vector<8xi8>
fx.rocdl.BufferCopy32b() produces the same class of legalization failure (just with vector<4xi8> / buffer_copy<32>).
Only fx.UniversalCopy32b() (plain llvm.load-based copy, no buffer-resource descriptor) compiles successfully for this fp8 shape. Given the docs (docs/cute_layout_algebra_guide.md §6.2) present BufferCopy* as the recommended AMD-specific path for efficient global memory access, and examples/03-tiledMma.py's own working example uses BufferCopy32b successfully for fp32 — this looks like a gap specific to fp8 element types (1-byte) combined with make_tiled_copy_A/B + thr_mma.make_fragment_A/B for this particular MNK/warp-tiling combination, rather than BufferCopy* being broadly broken.
Environment
- gfx942 (MI300X-class)
- Repro'd via
FLYDSL_DUMP_IR=1 FLYDSL_RUNTIME_ENABLE_CACHE=0
Context
Found while working on pa_decode_tile.py (a hand-tuned reference PA-decode kernel that intentionally uses raw MFMA intrinsics instead of fx.gemm for its QK/PV matmuls, for exactly this kind of register-pressure reason — this issue is filed to check whether the raw-intrinsic-over-fx.gemm choice there is also masking an underlying fx.gemm/BufferCopy compile gap worth fixing independently).
Summary
While optimizing
kernels/pa_decode_tile.py's QK matmul (rawmfma_f32_16x16x32_fp8_fp8intrinsics, M=64/N=16/K=128 fp8), I compared it against the equivalent computation expressed via the high-levelfx.gemm+make_tiled_mma+make_tiled_copy_A/B/CAPI. Two issues showed up:fx.gemmuses more VGPR than the equivalent raw MFMA sequence for the same M/N/K/dtype (confirmed, modest but real: +2 VGPR / +8% in an isolated microbenchmark).fx.gemm's fragment/copy path fails to compile at all when paired withfx.rocdl.BufferCopy{32,64,128}b(the AMD buffer-descriptor copy atoms recommended in the docs for global memory access) for this fp8 M=64/N=16/K=128 shape — onlyfx.UniversalCopy32b(plain-pointer copy) worked.Repro
Attached:
gemm_vgpr_compare.py(self-contained, no other repo files needed beyondflydsl/torch).Both kernels do the same M=64,N=16,K=128 fp8
mfma_f32_16x16x32_fp8_fp8GEMM with a 4-warp(NWARP,1,1)tiled_mma split (matchingpa_decode_tile.py's own QK tiling).Issue 1 — VGPR overhead
.amdhsa_next_free_vgprfrom the dumped final ISA (FLYDSL_DUMP_IR=1):fx.gemm(withfx.UniversalCopy32b, sinceBufferCopy*doesn't compile — see below)Reproducible across repeated runs. The gap is small in this toy kernel (no other loop-carried state), but in the real
pa_decode_tile.pycontext, VGPR is the kernel's binding occupancy constraint (waves/SIMD =512 // vgpr), so even a modest per-operand overhead compounds across a kernel with many such GEMM calls and loop-carried fragments — worth knowing about when choosing between the two APIs for register-pressure-sensitive kernels.Issue 2 —
BufferCopy*+fx.gemmfragments fail to compileSwapping
fx.UniversalCopy32b()forfx.rocdl.BufferCopy128b()in thegemmvariant (same script, same shapes) hits a C++ assertion:Switching to
fx.rocdl.BufferCopy64b()(matching the fragment's 8-element/lane granularity) avoids the assertion but hits an MLIR legalization failure instead:fx.rocdl.BufferCopy32b()produces the same class of legalization failure (just withvector<4xi8>/buffer_copy<32>).Only
fx.UniversalCopy32b()(plainllvm.load-based copy, no buffer-resource descriptor) compiles successfully for this fp8 shape. Given the docs (docs/cute_layout_algebra_guide.md§6.2) presentBufferCopy*as the recommended AMD-specific path for efficient global memory access, andexamples/03-tiledMma.py's own working example usesBufferCopy32bsuccessfully for fp32 — this looks like a gap specific tofp8element types (1-byte) combined withmake_tiled_copy_A/B+thr_mma.make_fragment_A/Bfor this particular MNK/warp-tiling combination, rather thanBufferCopy*being broadly broken.Environment
FLYDSL_DUMP_IR=1 FLYDSL_RUNTIME_ENABLE_CACHE=0Context
Found while working on
pa_decode_tile.py(a hand-tuned reference PA-decode kernel that intentionally uses raw MFMA intrinsics instead offx.gemmfor its QK/PV matmuls, for exactly this kind of register-pressure reason — this issue is filed to check whether the raw-intrinsic-over-fx.gemm choice there is also masking an underlyingfx.gemm/BufferCopycompile gap worth fixing independently).