Skip to content

Commit f08f0cc

Browse files
authored
Implement mxfp4 split-k gemm (#958)
The core things added are split-k gemm, and it is tested for (1) generation of the `buffer_atomic_pk_add_bf16` instruction that we wanted to use, and (2) for gemm correctness. Overview of changes unrelated to wave_asm: - `remove_global_indexing` in `general_utils.py`: Zeroes out tiling constraint starts (e.g. `K_SPLIT_OFF`) alongside workgroup IDs before dimension scaling, so that the subtraction of the start offset doesn't mix scaled and unscaled units (K vs K/32 for MXFP4 scales). - Fixing spurious bounds on split-K tiling that prevented scale vector merging: TilingConstraint.get_index_bound was conservatively generating bounds for the split-K case because sympy could not prove that ceiling(Min(K, f(wg)) / tile) * tile <= K. These bounds prevented merge_contiguous_reads from combining scalar scale reads into vector<4xi8> loads (it skips reads that already have bounds). Add _work_may_exceed_dim() to structurally detect the aligned split-k pattern and prove no overshoot, avoiding the spurious bound. (This was necessary to get scale_preshuffle to have 4x vector loads when combined with split-k.) --------- Signed-off-by: William G Hatch <william@hatch.uno>
1 parent a69ed25 commit f08f0cc

23 files changed

Lines changed: 2013 additions & 267 deletions

examples/python/7.1_schedule.py

Lines changed: 131 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44
Double-buffered MXFP4 GEMM with 4-wave and 8-wave configurations.
55
Uses get_tagged_mxfp4_gemm (templates) + get_mxfp4_dbuf_schedule (schedules).
66
7+
The --splitk N flag enables split-K with N splits for supported tests.
8+
79
Usage:
810
python 7.1_schedule.py --test test_dbuf_4wave_mxfp_gemm
9-
python 7.1_schedule.py --test test_dbuf_8wave_mxfp_gemm
10-
python 7.1_schedule.py --test test_dbuf_8wave_mxfp_gemm --debug
11+
python 7.1_schedule.py --test test_dbuf_4wave_mxfp_gemm --splitk 2
12+
python 7.1_schedule.py --test test_dbuf_8wave_pingpong_mxfp_gemm
13+
python 7.1_schedule.py --test test_dbuf_8wave_pingpong_mxfp_gemm --splitk 2
14+
python 7.1_schedule.py --test test_splitk_preshuffle_scales_gemm_cpp
1115
python 7.1_schedule.py --list_tests
1216
"""
1317

@@ -20,6 +24,7 @@
2024
SHARED_ADDRESS_SPACE,
2125
)
2226
from wave_lang.kernel.wave.compile import wave_compile
27+
from wave_lang.kernel.wave.constraints import ScaledMMAType
2328
from wave_lang.kernel.wave.schedules import (
2429
get_mxfp4_asymmetric_schedule,
2530
get_mxfp4_dbuf_mixed_pingpong_schedule,
@@ -35,6 +40,9 @@
3540
get_tagged_mxfp4_gemm_preshuffle_b_wide_store,
3641
get_tagged_mxfp4_gemm_preshuffle_scales,
3742
get_tagged_mxfp4_gemm_preshuffle_scales_and_B,
43+
get_tagged_splitk_mxfp4_gemm,
44+
get_tagged_splitk_mxfp4_gemm_preshuffle_b,
45+
get_tagged_splitk_mxfp4_gemm_preshuffle_scales,
3846
)
3947
from wave_lang.kernel.wave.utils.mxfp_utils import (
4048
b_preshuffle,
@@ -61,7 +69,14 @@ def _run_mxfp_gemm(gemm, shape):
6169

6270

6371
def _run_mxfp_gemm_preshuffle(
64-
gemm, shape, all=False, only_scale=False, only_b=False, output_dtype=torch.float32
72+
gemm,
73+
shape,
74+
all=False,
75+
only_scale=False,
76+
only_b=False,
77+
output_dtype=torch.float32,
78+
atol=None,
79+
rtol=None,
6580
):
6681
"""Run compiled GEMM kernel with preshuffled B and B_scale, verify against reference.
6782
@@ -90,8 +105,13 @@ def _run_mxfp_gemm_preshuffle(
90105

91106
gemm(x, x_scales_ps, w_t_ps, w_scales_ps, out)
92107

108+
tol_kwargs = {}
109+
if atol is not None:
110+
tol_kwargs["atol"] = atol
111+
if rtol is not None:
112+
tol_kwargs["rtol"] = rtol
93113
torch.testing.assert_close(
94-
torch_out, out.cpu(), check_dtype=False, check_device=False
114+
torch_out, out.cpu(), check_dtype=False, check_device=False, **tol_kwargs
95115
)
96116

97117

@@ -137,10 +157,17 @@ def _get_8wave_shape_from_block(block):
137157

138158

139159
def test_dbuf_4wave_mxfp_gemm(
140-
is_debug=False, shape=(1024, 1024, 8192), block=(256, 256, 256)
160+
is_debug=False, shape=(1024, 1024, 8192), block=(256, 256, 256), splitk=None
141161
):
142162
"""Double-buffered MXFP4 GEMM, 4 waves, no stagger."""
143-
gemm, options = get_tagged_mxfp4_gemm(shape, block, wave_shape=(2, 2))
163+
if splitk and block == (256, 256, 256):
164+
block = (128, 128, 256)
165+
if splitk:
166+
gemm, options = get_tagged_splitk_mxfp4_gemm(
167+
shape, num_splits=splitk, block_shape=block, wave_shape=(2, 2)
168+
)
169+
else:
170+
gemm, options = get_tagged_mxfp4_gemm(shape, block, wave_shape=(2, 2))
144171
schedule = get_mxfp4_dbuf_schedule(use_stagger=False)
145172

146173
options.print_ir_after = "all" if is_debug else []
@@ -150,11 +177,16 @@ def test_dbuf_4wave_mxfp_gemm(
150177
gemm = wave_compile(options, gemm, schedule)
151178

152179
_run_mxfp_gemm(gemm, shape)
153-
print("MXFP GEMM double-buffer 4-wave test passed!")
180+
sk = f" split-K({splitk})" if splitk else ""
181+
print(f"MXFP GEMM double-buffer 4-wave{sk} test passed!")
154182

155183

156184
def test_dbuf_8wave_pingpong_mxfp_gemm(
157-
is_debug=False, shape=(1024, 1024, 8192), block=(256, 256, 256), dynamic=False
185+
is_debug=False,
186+
shape=(1024, 1024, 8192),
187+
block=(256, 256, 256),
188+
dynamic=False,
189+
splitk=None,
158190
):
159191
"""Double-buffered MXFP4 GEMM, 8 waves, ping-pong with stagger.
160192
A&B scales are preshuffled and read from global memory directly to VGPRs.
@@ -163,10 +195,17 @@ def test_dbuf_8wave_pingpong_mxfp_gemm(
163195
Note: for dynamic mode, keep block MxN at or below 128x256 or 256x128
164196
to avoid exceeding shared-memory limits.
165197
"""
198+
if splitk and block == (256, 256, 256):
199+
block = (128, 256, 256)
166200
wave_shape = _get_8wave_shape_from_block(block)
167-
gemm, options = get_tagged_mxfp4_gemm_preshuffle_scales(
168-
shape, block, wave_shape=wave_shape
169-
)
201+
if splitk:
202+
gemm, options = get_tagged_splitk_mxfp4_gemm_preshuffle_scales(
203+
shape, num_splits=splitk, block_shape=block, wave_shape=wave_shape
204+
)
205+
else:
206+
gemm, options = get_tagged_mxfp4_gemm_preshuffle_scales(
207+
shape, block, wave_shape=wave_shape
208+
)
170209
options.specialize = True
171210
options.use_buffer_ops = True
172211
options.minimize_shared_allocs = True
@@ -185,19 +224,28 @@ def test_dbuf_8wave_pingpong_mxfp_gemm(
185224

186225
_run_mxfp_gemm_preshuffle(gemm, shape, only_scale=True)
187226
mode = "dynamic" if dynamic else "static"
227+
sk = f", split-K({splitk})" if splitk else ""
188228
print(
189-
f"MXFP GEMM double-buffer 8-wave ping pong with scale shuffling ({mode}) test passed!"
229+
f"MXFP GEMM double-buffer 8-wave ping pong with scale shuffling ({mode}{sk}) test passed!"
190230
)
191231

192232

193233
def test_dbuf_8wave_pingpong_mxfp_gemm_Bshuffle(
194-
is_debug=False, shape=(1024, 1024, 8192), block=(256, 256, 256), dynamic=False
234+
is_debug=False,
235+
shape=(1024, 1024, 8192),
236+
block=(256, 256, 256),
237+
dynamic=False,
238+
splitk=None,
195239
):
196240
"""Double-buffered MXFP4 GEMM, 8 waves, ping-pong with stagger.
197241
A&B scales are preshuffled and read from global memory directly to VGPRs.
198242
Same for B data. However, prefetching shuffled B directly to VGPR consumes too many VGPRs and causes spilling.
199243
A is read from global memory directly to LDS.
200244
"""
245+
if splitk:
246+
raise NotImplementedError(
247+
"split-K is not yet supported with B-shuffled ping-pong schedule"
248+
)
201249
wave_shape = _get_8wave_shape_from_block(block)
202250
gemm, options = get_tagged_mxfp4_gemm_preshuffle_scales_and_B(
203251
shape, block, wave_shape=wave_shape
@@ -390,7 +438,7 @@ def test_dbuf_8wave_pingpong_mxfp_gemm_Bshuffle_lds_transposed(
390438

391439

392440
def test_dbuf_8wave_mixed_pingpong_mxfp_gemm(
393-
is_debug=False, shape=(1024, 1024, 8192), block=(256, 256, 256)
441+
is_debug=False, shape=(1024, 1024, 8192), block=(256, 256, 256), splitk=None
394442
):
395443
"""Double-buffered MXFP4 GEMM, 8 waves, with stagger.
396444
@@ -411,6 +459,10 @@ def test_dbuf_8wave_mixed_pingpong_mxfp_gemm(
411459
barrier releases, effectively hiding the second barrier's latency behind
412460
the early loads and compute.
413461
"""
462+
if splitk:
463+
raise NotImplementedError(
464+
"split-K is not yet supported with the mixed ping-pong schedule"
465+
)
414466
gemm, options = get_tagged_mxfp4_gemm(shape, block, wave_shape=(4, 2))
415467
options.specialize = True
416468
options.use_buffer_ops = True
@@ -426,15 +478,18 @@ def test_dbuf_8wave_mixed_pingpong_mxfp_gemm(
426478

427479

428480
def test_dbuf_8wave_mixed_pingpong_shuffle_mxfp_gemm(
429-
is_debug=False, shape=(16384, 16384, 16384), block=(256, 256, 256)
481+
is_debug=False, shape=(16384, 16384, 16384), block=(256, 256, 256), splitk=None
430482
):
431483
"""Like :func:`test_dbuf_8wave_mixed_pingpong_mxfp_gemm` but with A_scale & B_scale
432484
preshuffled and prefetched to VGPRs.
433485
434486
Note: preshuffling B and loading it directly to VGPRs combined with prefetching
435487
consumes too many VGPRs and causes spilling.
436488
"""
437-
489+
if splitk:
490+
raise NotImplementedError(
491+
"split-K is not yet supported with the mixed ping-pong shuffle schedule"
492+
)
438493
gemm, options = get_tagged_mxfp4_gemm_preshuffle_scales(
439494
shape, block, wave_shape=(4, 2)
440495
)
@@ -453,9 +508,13 @@ def test_dbuf_8wave_mixed_pingpong_shuffle_mxfp_gemm(
453508

454509

455510
def test_dbuf_4wave_mxfp_asymmetric_gemm(
456-
is_debug=False, shape=(1024, 1024, 8192), block=(256, 256, 256)
511+
is_debug=False, shape=(1024, 1024, 8192), block=(256, 256, 256), splitk=None
457512
):
458513
"""Asymmetric-prefetch MXFP4 GEMM: A through LDS (2x prefetch), B direct from global."""
514+
if splitk:
515+
raise NotImplementedError(
516+
"split-K is not yet supported with the asymmetric schedule"
517+
)
459518
gemm, options = get_tagged_mxfp4_gemm(
460519
shape, block, wave_shape=(1, 4), b_address_space=GLOBAL_ADDRESS_SPACE
461520
)
@@ -481,9 +540,17 @@ def test_dbuf_4wave_mxfp_preshuffle_b_gemm(
481540
shape=(1024, 1024, 8192),
482541
block=(128, 256, 256),
483542
eliminate_epilogue=True,
543+
splitk=None,
484544
):
485545
"""Asymmetric MXFP4 GEMM with preshuffled B data and B scales."""
486-
gemm, options = get_tagged_mxfp4_gemm_preshuffle_b(shape, block, wave_shape=(1, 4))
546+
if splitk:
547+
gemm, options = get_tagged_splitk_mxfp4_gemm_preshuffle_b(
548+
shape, num_splits=splitk, block_shape=block, wave_shape=(1, 4)
549+
)
550+
else:
551+
gemm, options = get_tagged_mxfp4_gemm_preshuffle_b(
552+
shape, block, wave_shape=(1, 4)
553+
)
487554
options.minimize_shared_allocs = True
488555
options.linearize_shared_access = True
489556
options.use_buffer_ops = True
@@ -497,13 +564,18 @@ def test_dbuf_4wave_mxfp_preshuffle_b_gemm(
497564
gemm = wave_compile(options, gemm, schedule)
498565

499566
_run_mxfp_gemm_preshuffle(gemm, shape, all=True)
500-
print("MXFP GEMM preshuffle-B 4-wave test passed!")
567+
sk = f" split-K({splitk})" if splitk else ""
568+
print(f"MXFP GEMM preshuffle-B 4-wave{sk} test passed!")
501569

502570

503571
def test_dbuf_4wave_mxfp_asymmetric_gemm_cpp(
504-
is_debug=False, shape=(1024, 1024, 8192), block=(128, 256, 256)
572+
is_debug=False, shape=(1024, 1024, 8192), block=(128, 256, 256), splitk=None
505573
):
506574
"""Asymmetric MXFP4 GEMM using C++ WaveASM backend (no preshuffle)."""
575+
if splitk:
576+
raise NotImplementedError(
577+
"split-K is not yet supported with the asymmetric C++ backend"
578+
)
507579
gemm, options = get_tagged_mxfp4_gemm(
508580
shape, block, wave_shape=(1, 4), b_address_space=GLOBAL_ADDRESS_SPACE
509581
)
@@ -524,8 +596,14 @@ def test_dbuf_4wave_mxfp_preshuffle_b_gemm_cpp(
524596
shape=(512, 1024, 8192), # 4*T0, 4*T1, 8192
525597
block=(128, 256, 256),
526598
eliminate_epilogue=True,
599+
splitk=None,
527600
):
528601
"""Preshuffle-B MXFP4 GEMM using C++ WaveASM backend."""
602+
if splitk:
603+
raise NotImplementedError(
604+
"split-K with WaveASM backend hits register alignment errors in "
605+
"the assembler; use test_dbuf_4wave_mxfp_preshuffle_b_gemm instead"
606+
)
529607
gemm, options = get_tagged_mxfp4_gemm_preshuffle_b(shape, block, wave_shape=(1, 4))
530608
options.backend = "asm"
531609
options.use_buffer_ops = True
@@ -551,8 +629,13 @@ def test_dbuf_4wave_mxfp_dynamic_preshuffle_b_gemm(
551629
shape=(1024, 1024, 8192),
552630
block=(128, 256, 256),
553631
eliminate_epilogue=True,
632+
splitk=None,
554633
):
555634
"""Preshuffle-B MXFP4 GEMM with dynamic M, N, K."""
635+
if splitk:
636+
raise NotImplementedError(
637+
"split-K is not yet supported with the dynamic preshuffle-B schedule"
638+
)
556639
gemm, options = get_tagged_mxfp4_gemm_preshuffle_b(shape, block, wave_shape=(1, 4))
557640
# Make M, N, K dynamic so the compiler does not specialize on problem size.
558641
dynamic_symbols = [tkl.sym.M, tkl.sym.N, tkl.sym.K]
@@ -647,6 +730,33 @@ def test_dbuf_4wave_mxfp_dynamic_preshuffle_b_gemm_asm(
647730
)
648731

649732

733+
def test_splitk_preshuffle_scales_gemm_cpp(
734+
is_debug=False,
735+
shape=(1024, 1024, 8192),
736+
block=(128, 128, 256),
737+
splitk=None,
738+
):
739+
"""Split-K MXFP4 GEMM using C++ WaveASM backend (preshuffled scales)."""
740+
num_splits = splitk if splitk else 2
741+
splitk_fn, options = get_tagged_splitk_mxfp4_gemm_preshuffle_scales(
742+
shape,
743+
num_splits=num_splits,
744+
mfma_variant=ScaledMMAType.F32_16x16x128_F8F6F4,
745+
block_shape=block,
746+
wave_shape=(2, 2),
747+
output_type=tkl.f32,
748+
)
749+
options.backend = "asm"
750+
options.wave_runtime = True
751+
options.use_wave_asm_backend = True
752+
options.print_ir_after = "all" if is_debug else []
753+
options = set_default_run_config(options)
754+
gemm = wave_compile(options, splitk_fn)
755+
756+
_run_mxfp_gemm_preshuffle(gemm, shape, only_scale=True)
757+
print("Split-K MXFP4 GEMM (preshuffled scales, WaveASM backend) test passed!")
758+
759+
650760
if __name__ == "__main__":
651761
args = parse_args()
652762

@@ -667,5 +777,6 @@ def test_dbuf_4wave_mxfp_dynamic_preshuffle_b_gemm_asm(
667777
args.shape,
668778
args.block,
669779
args.eliminate_epilogue,
780+
args.splitk,
670781
)
671782
exit(0 if success else 1)

examples/python/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ def parse_args():
3636
default=None,
3737
help="Enable epilogue elimination (true/false)",
3838
)
39+
parser.add_argument(
40+
"--splitk",
41+
type=int,
42+
default=None,
43+
metavar="NUM_SPLITS",
44+
help="Enable split-K with the given number of splits (e.g. --splitk 2)",
45+
)
3946

4047
args = parser.parse_args()
4148

@@ -64,6 +71,7 @@ def run_test(
6471
shape=None,
6572
block=None,
6673
eliminate_epilogue=None,
74+
splitk=None,
6775
):
6876
"""Run a test function multiple times."""
6977
if test_name not in module_globals:
@@ -78,6 +86,8 @@ def run_test(
7886
kwargs["block"] = block
7987
if eliminate_epilogue is not None:
8088
kwargs["eliminate_epilogue"] = eliminate_epilogue
89+
if splitk is not None:
90+
kwargs["splitk"] = splitk
8191

8292
for i in range(repeat):
8393
try:

lit_tests/kernel/wave/attention/decode_attention.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ def test_gqa_flash_decoding():
339339
# CHECK-LABEL: func.func @phase_0
340340
# CHECK: scf.for
341341
# CHECK: amdgpu.lds_barrier
342-
# CHECK-COUNT-16: vector.maskedload
342+
# CHECK-COUNT-16: vector.load
343343
# CHECK-COUNT-16: vector.store
344344
# CHECK: amdgpu.lds_barrier
345345
# CHECK-COUNT-4: amdgpu.mfma

0 commit comments

Comments
 (0)