Skip to content

Commit a3521bc

Browse files
authored
Add wide_stores support for MXFP4 (4wave) GEMM (llvm backend) (#1282)
## Wide epilogue stores for bf16 MXFP4 GEMM via `v_permlane16_swap` In MXFP4 GEMM kernels with bf16 output, the MMA accumulator layout (`F32_16x16x128_F8F6F4`) distributes results across threads in a non-contiguous pattern each thread holds 4 values spanning 4 rows but only 1 column. Without optimization, the epilogue emits 192 scalar `buffer_store_short` (2 bytes each), underutilizing memory bandwidth. This PR adds a `wide_stores` that swaps MFMA operands (B as LHS, A as RHS) and uses `v_permlane16_swap_b32` to coalesce epilogue stores into `buffer_store_dwordx4` (16 bytes each), reducing store count from 192 to 48. ### How it works **Swapped MFMA operands (`wide_stores=True`):** The kernel computes C^T = B × A^T (and stores C) instead of C = A × B^T, making the accumulator's 4-contiguous values align with the output memory's stride-1 dimension. The source/target write syntax maps `Register[N, M]` to `Memory[M, N]` so the output is C [M, N] directly. **`wide_store_coalescing` pass:** An always-on graph pass that identifies eligible bf16 global Write nodes using the source/target dimension remapping pattern (indicating swapped operands). Only tags writes that satisfy all conditions: global address space, bf16 dtype, and source/target syntax. Safe to run unconditionally, non-`wide_stores` kernels are unaffected. **`_write_permlane_pack_to_global` codegen:** At code generation time, tagged writes emit `v_permlane16_swap_b32` to exchange each thread's 4 bf16 values (packed as 2 i32 dwords) with a partner lane 16 positions apart. Both lane halves write identical 8-bf16 vectors to the same address (benign duplicate store), avoiding divergent control flow. The buffer descriptor's `valid_bytes` handles out-of-bounds suppression for dynamic shapes. ### Assembly comparison (shape 1024×3072×8192, block 256×192×256) | Instruction | Baseline | Wide Stores | Change | |---|---|---|---| | `buffer_store_short` | 192 | 0 | eliminated | | `buffer_store_dwordx4` | 0 | 48 | 16B/store | | `v_permlane16_swap` | 0 | 96 | lane exchange | | `v_cvt_pk_bf16_f32` | 192 | 96 | 2x reduction | | Total asm lines | 12,115 | 9,322 | 23% fewer | LLVM now emits `v_cvt_pk_bf16_f32` natively (bf16 softening has been fixed upstream), so the paired f32->bf16 conversion is a single instruction. Compute-bound shapes (large K) show 6-9% gains; small-K shapes show 13-27% gains | Shape (M, N, K) | Baseline TFLOPS | Wide Stores TFLOPS | Speedup | |---|---|---|---| | (315904, 384, 1792) | 1169.51 | 1557.86 | 1.332x | | (20480, 1152, 28928) | 2714.36 | 3015.93 | 1.111x | | (5888, 1920, 3328) | 1582.35 | 2104.81 | 1.330x | | (46336, 2688, 8448) | 2495.16 | 2940.50 | 1.179x | | (512, 3072, 428288) | 590.71 | 648.15 | 1.097x | | (256, 4224, 102656) | 409.79 | 458.95 | 1.120x | | (512, 4224, 55808) | 794.47 | 893.25 | 1.124x | | (4864, 4608, 2560) | 1435.43 | 2123.30 | 1.479x | | (4864, 4608, 5888) | 2083.50 | 2669.39 | 1.281x | | (4608, 4992, 7424) | 2227.52 | 2813.12 | 1.263x | | (2048, 5376, 63232) | 3112.73 | 3405.23 | 1.094x | | (2048, 5760, 6400) | 2230.97 | 2690.92 | 1.206x | | (3584, 6912, 548608) | 3272.81 | 3531.95 | 1.079x | | (4864, 7680, 7936) | 2406.89 | 3036.57 | 1.262x | | (13056, 10368, 15360) | 2936.09 | 3352.67 | 1.142x | | (14848, 12672, 10496) | 2781.61 | 3254.58 | 1.170x | | (1792, 19968, 60928) | 3115.72 | 3392.02 | 1.089x | --------- Signed-off-by: xintin <gaurav.verma@amd.com>
1 parent 5186495 commit a3521bc

9 files changed

Lines changed: 678 additions & 125 deletions

File tree

examples/python/7.1_schedule.py

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,37 @@
1212
"""
1313

1414
import torch
15-
import wave_lang.kernel.lang as tkl
15+
from utils import list_tests, parse_args, run_test
1616

17-
from wave_lang.kernel.wave.compile import wave_compile
18-
from wave_lang.kernel.wave.utils.run_utils import set_default_run_config
19-
from wave_lang.kernel.wave.templates import (
20-
get_tagged_mxfp4_gemm,
21-
get_tagged_mxfp4_gemm_preshuffle_b,
22-
get_tagged_mxfp4_gemm_preshuffle_scales,
23-
get_tagged_mxfp4_gemm_preshuffle_scales_and_B,
17+
import wave_lang.kernel.lang as tkl
18+
from wave_lang.kernel.lang.global_symbols import (
19+
GLOBAL_ADDRESS_SPACE,
20+
SHARED_ADDRESS_SPACE,
2421
)
22+
from wave_lang.kernel.wave.compile import wave_compile
2523
from wave_lang.kernel.wave.schedules import (
26-
get_mxfp4_dbuf_schedule,
27-
get_mxfp4_dbuf_pingpong_schedule,
28-
get_mxfp4_dbuf_mixed_pingpong_schedule,
2924
get_mxfp4_asymmetric_schedule,
25+
get_mxfp4_dbuf_mixed_pingpong_schedule,
3026
get_mxfp4_dbuf_mixed_pingpong_shuffle_schedule,
27+
get_mxfp4_dbuf_pingpong_schedule,
3128
get_mxfp4_dbuf_pingpong_schedule_Bshuffled,
3229
get_mxfp4_dbuf_pingpong_schedule_Bshuffled_lds,
30+
get_mxfp4_dbuf_schedule,
31+
)
32+
from wave_lang.kernel.wave.templates import (
33+
get_tagged_mxfp4_gemm,
34+
get_tagged_mxfp4_gemm_preshuffle_b,
35+
get_tagged_mxfp4_gemm_preshuffle_b_wide_store,
36+
get_tagged_mxfp4_gemm_preshuffle_scales,
37+
get_tagged_mxfp4_gemm_preshuffle_scales_and_B,
3338
)
3439
from wave_lang.kernel.wave.utils.mxfp_utils import (
35-
generate_gemm_afp4wfp4_inputs,
36-
torchScaledGemmMXFP4,
3740
b_preshuffle,
3841
e8m0_shuffle,
42+
generate_gemm_afp4wfp4_inputs,
43+
torchScaledGemmMXFP4,
3944
)
40-
from wave_lang.kernel.lang.global_symbols import (
41-
GLOBAL_ADDRESS_SPACE,
42-
SHARED_ADDRESS_SPACE,
43-
)
44-
from utils import parse_args, list_tests, run_test
45+
from wave_lang.kernel.wave.utils.run_utils import set_default_run_config
4546

4647

4748
def _run_mxfp_gemm(gemm, shape):
@@ -424,6 +425,44 @@ def test_dbuf_4wave_mxfp_dynamic_preshuffle_b_gemm(
424425
print("MXFP GEMM preshuffle-B 4-wave dynamic M, N, K (LLVM backend) test passed!")
425426

426427

428+
def test_dbuf_4wave_mxfp_dynamic_preshuffle_b_gemm_wide_stores(
429+
is_debug=False,
430+
shape=(1024, 3072, 8192),
431+
block=(256, 192, 256),
432+
eliminate_epilogue=False,
433+
):
434+
"""Preshuffle-B MXFP4 GEMM with dynamic M, N, K and wide epilogue stores.
435+
436+
Uses the wide_store variant to swap MFMA operands (B as LHS, A as RHS),
437+
aligning the accumulator's contiguous values with the output's stride-1
438+
dimension. The coalesce_wide_stores pass emits v_permlane16_swap_b32
439+
+ buffer_store_dwordx4 (8 bf16 per store) instead of buffer_store_short.
440+
"""
441+
gemm, options = get_tagged_mxfp4_gemm_preshuffle_b_wide_store(
442+
shape,
443+
block,
444+
wave_shape=(2, 2),
445+
reorder_workgroups=True,
446+
)
447+
dynamic_symbols = [tkl.sym.M, tkl.sym.N, tkl.sym.K]
448+
for sym in dynamic_symbols:
449+
del options.subs[sym]
450+
options.dynamic_symbols = dynamic_symbols
451+
options.use_buffer_ops = True
452+
options.backend = "llvm"
453+
options.wave_runtime = True
454+
options.eliminate_epilogue = eliminate_epilogue
455+
schedule = get_mxfp4_asymmetric_schedule(
456+
eliminate_epilogue=eliminate_epilogue, is_bscale_shuffled=True
457+
)
458+
options.print_ir_after = "all" if is_debug else []
459+
options = set_default_run_config(options)
460+
gemm = wave_compile(options, gemm, schedule)
461+
462+
_run_mxfp_gemm_preshuffle(gemm, shape, all=True, output_dtype=torch.bfloat16)
463+
print("MXFP GEMM preshuffle-B 4-wave dynamic M, N, K (wide stores) test passed!")
464+
465+
427466
def test_dbuf_4wave_mxfp_dynamic_preshuffle_b_gemm_asm(
428467
is_debug=False,
429468
shape=(1024, 1024, 8192),

lit_tests/kernel/wave/mlir_roundtrip_pipeline.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ def mxfp4_gemm_progressive_roundtrip():
304304
"guard_g2s_with_bounds_check",
305305
"schedule_reordering",
306306
"minimize_shared_allocs",
307+
"coalesce_wide_stores",
307308
"add_shared_memory_barriers",
308309
"add_cluster_barriers",
309310
"compute_shared_memory_usage",
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# RUN: python %s | FileCheck %s
2+
3+
"""
4+
Test wide store coalescing for preshuffle-B MXFP4 GEMM with bf16 output.
5+
6+
The wide_store variant kernel swaps MFMA operands (B as LHS, A as RHS)
7+
so the accumulator's 4-contiguous values align with the output's stride-1
8+
dimension. The coalesce_wide_stores pass tags eligible bf16 global
9+
writes, and the codegen emits v_permlane16_swap_b32 to exchange data
10+
between lane pairs 16 apart, producing 8 consecutive bf16 values written
11+
as a single buffer_store_dwordx4.
12+
13+
Key structural invariants verified:
14+
1. Function signature accepts dynamic index arguments for M, N, K.
15+
2. Register type is [N, M] (swapped from standard [M, N]).
16+
3. scaled_mfma has B as LHS and A as RHS (swapped operands).
17+
4. rocdl.permlane16.swap used for lane exchange.
18+
5. vector.store of vector<8xbf16> for wide stores.
19+
6. arith.truncf for f32 -> bf16 conversion.
20+
"""
21+
22+
import wave_lang.kernel.lang as tkl
23+
from wave_lang.kernel.wave.compile import wave_compile
24+
from wave_lang.kernel.wave.constraints import ScaledMMAType
25+
from wave_lang.kernel.wave.schedules import get_mxfp4_asymmetric_schedule
26+
from wave_lang.kernel.wave.templates import (
27+
get_tagged_mxfp4_gemm_preshuffle_b_wide_store,
28+
)
29+
from wave_lang.kernel.wave.utils.general_utils import run_test
30+
31+
32+
@run_test
33+
def test_wide_stores_preshuffle_b_mxfp4():
34+
shape = (1024, 3072, 8192)
35+
block = (256, 192, 256)
36+
kernel, options = get_tagged_mxfp4_gemm_preshuffle_b_wide_store(
37+
shape,
38+
block,
39+
wave_shape=(2, 2),
40+
reorder_workgroups=True,
41+
mfma_variant=ScaledMMAType.F32_16x16x128_F8F6F4,
42+
)
43+
dynamic_symbols = [tkl.sym.M, tkl.sym.N, tkl.sym.K]
44+
for sym in dynamic_symbols:
45+
del options.subs[sym]
46+
options.dynamic_symbols = dynamic_symbols
47+
schedule = get_mxfp4_asymmetric_schedule(is_bscale_shuffled=True)
48+
options.use_buffer_ops = True
49+
options.compile_to_mlir = True
50+
options.device = "hip"
51+
options.target = "gfx950"
52+
result = wave_compile(options, kernel, schedule)
53+
print(result.asm)
54+
55+
# CHECK-LABEL: test_wide_stores_preshuffle_b_mxfp4
56+
57+
# 1. Dynamic index arguments for M, N, K in function signature.
58+
# CHECK: func.func @gemm(%arg0: {{.*}}, %arg1: {{.*}}, %arg2: {{.*}}, %arg3: {{.*}}, %arg4: {{.*}}, %arg5: index, %arg6: index, %arg7: index)
59+
60+
# 2. f32 -> bf16 conversion in the epilogue.
61+
# CHECK: arith.truncf %{{.*}} : vector<4xf32> to vector<4xbf16>
62+
63+
# 3. vector.bitcast from bf16 to i32 for permlane swap.
64+
# CHECK: vector.bitcast %{{.*}} : vector<4xbf16> to vector<2xi32>
65+
66+
# 4. rocdl.permlane16.swap for lane exchange.
67+
# CHECK: rocdl.permlane16.swap
68+
69+
# 5. llvm.extractvalue to get the swapped value.
70+
# CHECK: llvm.extractvalue %{{.*}}[0]
71+
72+
# 6. arith.select to choose between original and swapped values.
73+
# CHECK: arith.select %{{.*}}, %{{.*}}, %{{.*}} : i32
74+
75+
# 7. vector.from_elements to pack 4 i32 into vector<4xi32>.
76+
# CHECK: vector.from_elements %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}} : vector<4xi32>
77+
78+
# 8. vector.bitcast from i32 to bf16 for the wide store.
79+
# CHECK: vector.bitcast %{{.*}} : vector<4xi32> to vector<8xbf16>
80+
81+
# 9. Wide store of 8 bf16 values.
82+
# CHECK: vector.store %{{.*}}, %{{.*}}[%{{.*}}] : memref<{{.*}}xbf16, {{.*}}>, vector<8xbf16>

tests/kernel/wave_gemm_mxfp_test.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from wave_lang.kernel.wave.templates import (
3030
get_tagged_mxfp4_gemm,
3131
get_tagged_mxfp4_gemm_preshuffle_b,
32+
get_tagged_mxfp4_gemm_preshuffle_b_wide_store,
3233
get_tagged_mxfp4_gemm_preshuffle_scales,
3334
get_tagged_mxfp4_gemm_preshuffle_scales_and_B,
3435
)
@@ -1028,6 +1029,62 @@ def testScaledGemmMXFP4PreshuffleBDynamicWaveRuntime(
10281029
torch.testing.assert_close(torch_out, out, check_dtype=False)
10291030

10301031

1032+
@require_e2e
1033+
@require_cdna4
1034+
@pytest.mark.timeout(900)
1035+
@pytest.mark.parametrize(
1036+
"shape",
1037+
[(1024, 3072, 8192)],
1038+
)
1039+
@pytest.mark.parametrize(
1040+
"block_shape,wave_shape",
1041+
[((256, 192, 256), (2, 2))],
1042+
)
1043+
@pytest.mark.parametrize(
1044+
"mfma_variant",
1045+
[ScaledMMAType.F32_16x16x128_F8F6F4],
1046+
)
1047+
def testScaledGemmMXFP4PreshuffleBWideStores(
1048+
shape: tuple[int, int, int],
1049+
block_shape: tuple[int, int, int],
1050+
wave_shape: tuple[int, int],
1051+
mfma_variant: ScaledMMAType,
1052+
):
1053+
"""End-to-end test for MXFP4 GEMM with wide epilogue stores (dwordx4).
1054+
1055+
Uses the wide_store variant to swap MFMA operands and emit
1056+
buffer_store_dwordx4 via v_permlane16_swap_b32 for bf16 output.
1057+
"""
1058+
gemm, options = get_tagged_mxfp4_gemm_preshuffle_b_wide_store(
1059+
shape,
1060+
block_shape,
1061+
wave_shape=wave_shape,
1062+
mfma_variant=mfma_variant,
1063+
reorder_workgroups=True,
1064+
)
1065+
dynamic_symbols = [tkl.sym.M, tkl.sym.N, tkl.sym.K]
1066+
for sym in dynamic_symbols:
1067+
del options.subs[sym]
1068+
options.dynamic_symbols = dynamic_symbols
1069+
schedule = get_mxfp4_asymmetric_schedule(is_bscale_shuffled=True)
1070+
options.use_buffer_ops = True
1071+
options = set_default_run_config(options)
1072+
gemm = wave_compile(options, gemm, schedule)
1073+
1074+
x, w, x_scales, w_scales = generate_gemm_afp4wfp4_inputs(shape)
1075+
torch_out = torchScaledGemmMXFP4(x, w, x_scales, w_scales)
1076+
1077+
w_t = w.T.contiguous()
1078+
w_t_ps = b_preshuffle(w_t)
1079+
x_scales_ps = e8m0_shuffle(x_scales)
1080+
w_scales_ps = e8m0_shuffle(w_scales)
1081+
1082+
out = device_zeros(x.shape[0], w_t_ps.shape[0], dtype=torch.bfloat16)
1083+
gemm(x, x_scales_ps, w_t_ps, w_scales_ps, out)
1084+
1085+
torch.testing.assert_close(torch_out, out, check_dtype=False)
1086+
1087+
10311088
MACROTILES_PRESHUFFLE_8WAVE_PINGPONG = [
10321089
(256, 160, 256),
10331090
(256, 224, 256),

0 commit comments

Comments
 (0)