Skip to content

Commit 70adfec

Browse files
committed
Add tunable MXFP8 consumer warps
1 parent 20fbdc1 commit 70adfec

2 files changed

Lines changed: 54 additions & 18 deletions

File tree

test/test_mxfp8_tma.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ def dequantize_mxfp8(value: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
3636
return value.float() * expanded_scale
3737

3838

39+
@pytest.mark.parametrize("num_compute_warps", [1, 2, 4])
3940
@pytest.mark.parametrize(
4041
("k", "block_n", "num_stages"),
4142
[(2048, 4, 2), (4096, 8, 3)],
4243
)
43-
def test_mxfp8_tma_gemv_matches_reference(k, block_n, num_stages):
44+
def test_mxfp8_tma_gemv_matches_reference(k, block_n, num_stages, num_compute_warps):
4445
"""Match independently dequantized float32 matmul."""
4546
torch.manual_seed(k)
4647
q_input, input_scale = quantize_mxfp8(torch.randn((1, k), dtype=torch.bfloat16, device="cuda"))
@@ -60,14 +61,16 @@ def test_mxfp8_tma_gemv_matches_reference(k, block_n, num_stages):
6061
block_n=block_n,
6162
num_stages=num_stages,
6263
output=output,
64+
num_compute_warps=num_compute_warps,
6365
)
6466
torch.cuda.synchronize()
6567

6668
assert actual is output
6769
torch.testing.assert_close(actual, expected, atol=1.0, rtol=0.05)
6870

6971

70-
def test_mxfp8_tma_gemv_cuda_graph_replay():
72+
@pytest.mark.parametrize("num_compute_warps", [1, 2, 4])
73+
def test_mxfp8_tma_gemv_cuda_graph_replay(num_compute_warps):
7174
"""Replay into caller-owned output without hidden allocation or copies."""
7275
k = 2048
7376
q_input, input_scale = quantize_mxfp8(torch.randn((1, k), dtype=torch.bfloat16, device="cuda"))
@@ -82,6 +85,7 @@ def test_mxfp8_tma_gemv_cuda_graph_replay():
8285
weight_scale,
8386
block_n=4,
8487
output=output,
88+
num_compute_warps=num_compute_warps,
8589
)
8690
graph = torch.cuda.CUDAGraph()
8791

@@ -93,6 +97,7 @@ def test_mxfp8_tma_gemv_cuda_graph_replay():
9397
weight_scale,
9498
block_n=4,
9599
output=output,
100+
num_compute_warps=num_compute_warps,
96101
)
97102
graph.replay()
98103
torch.cuda.synchronize()
@@ -110,7 +115,13 @@ def test_mxfp8_tma_gemv_profiles_labeled_regions():
110115
weight, weight_scale = quantize_mxfp8(
111116
torch.randn((128, k), dtype=torch.bfloat16, device="cuda")
112117
)
113-
op = get_mxfp8_tma_gemv(128, k, 4, enable_profiling=True)
118+
op = get_mxfp8_tma_gemv(
119+
128,
120+
k,
121+
4,
122+
enable_profiling=True,
123+
num_compute_warps=4,
124+
)
114125

115126
with profile_session(
116127
max_events_per_unit=op.max_profile_events_per_cta,
@@ -169,6 +180,8 @@ def test_mxfp8_tma_cli_writes_pftrace(tmp_path):
169180
"2048",
170181
"--block-n",
171182
"4",
183+
"--num-compute-warps",
184+
"4",
172185
"--output",
173186
str(trace_path),
174187
],

transformer_nuggets/cute/mxfp8_tma.py

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def __init__(
7070
block_n: int,
7171
num_stages: int,
7272
enable_profiling: bool = False,
73+
num_compute_warps: int = 1,
7374
):
7475
super().__init__()
7576
m = 1
@@ -82,6 +83,8 @@ def __init__(
8283
)
8384
if num_stages not in (2, 3):
8485
raise ValueError("num_stages must be 2 or 3")
86+
if num_compute_warps not in (1, 2, 4) or block_n % num_compute_warps != 0:
87+
raise ValueError("num_compute_warps must be 1, 2, or 4 and divide block_n")
8588
if sf_k < num_stages * 32:
8689
raise ValueError("TMA staging requires at least one K tile per stage")
8790
self.m = m
@@ -92,6 +95,8 @@ def __init__(
9295
self.tile_k_u32 = 256
9396
self.num_k_tiles = sf_k // 32
9497
self.enable_profiling = enable_profiling
98+
self.num_compute_warps = num_compute_warps
99+
self.rows_per_warp = block_n // num_compute_warps
95100
self.max_profile_events_per_cta = 2 + 3 * self.num_k_tiles
96101
self.num_profile_units = n // block_n
97102

@@ -166,7 +171,9 @@ def kernel(
166171
producer, consumer = pipeline.PipelineTmaAsync.create(
167172
num_stages=self.num_stages,
168173
producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
169-
consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread, self.m),
174+
consumer_group=pipeline.CooperativeGroup(
175+
pipeline.Agent.Thread, self.num_compute_warps
176+
),
170177
tx_count=(self.block_n + self.m) * self.tile_k_u32 * 4,
171178
barrier_storage=barriers,
172179
tidx=lane,
@@ -218,7 +225,7 @@ def kernel(
218225
)
219226
stage.commit()
220227

221-
acc = [cutlass.Float32(0.0) for _ in range(self.block_n)]
228+
acc = [cutlass.Float32(0.0) for _ in range(self.rows_per_warp)]
222229
for k_tile in cutlass.range_constexpr(self.num_k_tiles):
223230
if warp == TMA_PRODUCER_WARP and k_tile < self.num_k_tiles - 1:
224231
with profile_region(
@@ -269,15 +276,15 @@ def kernel(
269276
cute.copy(
270277
smem_atom,
271278
cute.make_tensor(
272-
sX.iterator + cute.assume(sX.layout((warp, col_a, full.index)), divby=4),
279+
sX.iterator + cute.assume(sX.layout((0, col_a, full.index)), divby=4),
273280
chunk_layout,
274281
),
275282
cute.make_tensor(x_frag.iterator, chunk_layout),
276283
)
277284
cute.copy(
278285
smem_atom,
279286
cute.make_tensor(
280-
sX.iterator + cute.assume(sX.layout((warp, col_b, full.index)), divby=4),
287+
sX.iterator + cute.assume(sX.layout((0, col_b, full.index)), divby=4),
281288
chunk_layout,
282289
),
283290
cute.make_tensor(x_frag.iterator + 4, chunk_layout),
@@ -291,12 +298,13 @@ def kernel(
291298
input_scale = cute.make_rmem_tensor(1, cutlass.Uint8)
292299
cute.copy(
293300
input_scale_atom,
294-
cute.make_tensor(mSFX.iterator + mSFX.layout((warp, scale_k)), scale_layout),
301+
cute.make_tensor(mSFX.iterator + mSFX.layout((0, scale_k)), scale_layout),
295302
input_scale,
296303
)
297304
sx = input_scale[0]
298305

299-
for row in cutlass.range_constexpr(self.block_n):
306+
for local_row in cutlass.range_constexpr(self.rows_per_warp):
307+
row = warp * self.rows_per_warp + local_row
300308
w_frag = cute.make_rmem_tensor((1, 8), cutlass.Uint32)
301309
cute.copy(
302310
smem_atom,
@@ -334,7 +342,7 @@ def kernel(
334342
product = (x_values * w_values).reduce(
335343
cute.ReductionOp.ADD, cutlass.Float32(0.0), (None, 1)
336344
)
337-
acc[row] += (product[0] + product[1]) * combined_e8m0_to_f32(
345+
acc[local_row] += (product[0] + product[1]) * combined_e8m0_to_f32(
338346
sx, weight_scale[0]
339347
)
340348
cute.arch.fence_view_async_shared()
@@ -352,11 +360,12 @@ def kernel(
352360
):
353361
if warp == TMA_PRODUCER_WARP:
354362
producer.tail()
355-
for row in cutlass.range_constexpr(self.block_n):
356-
acc[row] = cute.arch.warp_reduction(acc[row], operator.add)
363+
for local_row in cutlass.range_constexpr(self.rows_per_warp):
364+
acc[local_row] = cute.arch.warp_reduction(acc[local_row], operator.add)
357365
if lane == 0:
358-
for row in cutlass.range_constexpr(self.block_n):
359-
mO[warp, n0 + row] = acc[row].to(cutlass.BFloat16)
366+
for local_row in cutlass.range_constexpr(self.rows_per_warp):
367+
row = warp * self.rows_per_warp + local_row
368+
mO[0, n0 + row] = acc[local_row].to(cutlass.BFloat16)
360369

361370
@cute.jit
362371
def __call__(
@@ -399,23 +408,24 @@ def __call__(
399408
_name_prefix=name,
400409
).launch(
401410
grid=[self.n // self.block_n, 1, 1],
402-
block=[self.m * 32, 1, 1],
411+
block=[self.num_compute_warps * 32, 1, 1],
403412
stream=stream,
404413
)
405414

406415
def get_key(self) -> str:
407416
"""Return the static kernel specialization key."""
408417
return (
409418
f"{self.n}_{self.sf_k}_{self.block_n}_{self.num_stages}"
410-
f"_profile={self.enable_profiling}"
419+
f"_cw={self.num_compute_warps}_profile={self.enable_profiling}"
411420
)
412421

413422
def get_name(self) -> str:
414423
"""Return the compiled kernel name."""
415424
profile_suffix = "_profiled" if self.enable_profiling else ""
416425
return (
417426
f"mxfp8_tma_gemv_n{self.n}_k{self.sf_k * 32}"
418-
f"_bn{self.block_n}_s{self.num_stages}{profile_suffix}"
427+
f"_bn{self.block_n}_s{self.num_stages}_cw{self.num_compute_warps}"
428+
f"{profile_suffix}"
419429
)
420430

421431
def interface(
@@ -536,9 +546,17 @@ def get_mxfp8_tma_gemv(
536546
block_n: int,
537547
num_stages: int = 2,
538548
enable_profiling: bool = False,
549+
num_compute_warps: int = 1,
539550
) -> Mxfp8TmaGemv:
540551
"""Return a cached MXFP8 TMA GEMV specialization."""
541-
return Mxfp8TmaGemv(n, k, block_n, num_stages, enable_profiling)
552+
return Mxfp8TmaGemv(
553+
n,
554+
k,
555+
block_n,
556+
num_stages,
557+
enable_profiling,
558+
num_compute_warps,
559+
)
542560

543561

544562
def mxfp8_tma_gemv(
@@ -552,6 +570,7 @@ def mxfp8_tma_gemv(
552570
output: torch.Tensor | None = None,
553571
enable_profiling: bool = False,
554572
profile_buffer: torch.Tensor | None = None,
573+
num_compute_warps: int = 1,
555574
) -> torch.Tensor:
556575
"""Compute raw-layout MXFP8 GEMV on prequantized inputs.
557576
@@ -565,6 +584,7 @@ def mxfp8_tma_gemv(
565584
output: Optional caller-owned contiguous ``[1, N]`` BF16 output.
566585
enable_profiling: Compile a separate specialization with labeled region timing.
567586
profile_buffer: Buffer from ``profile_session`` for the profiled specialization.
587+
num_compute_warps: Consumer warps sharing each CTA's output-row tile.
568588
569589
Returns:
570590
The provided or newly allocated output tensor.
@@ -577,6 +597,7 @@ def mxfp8_tma_gemv(
577597
block_n,
578598
num_stages,
579599
enable_profiling,
600+
num_compute_warps,
580601
).interface(
581602
q_input,
582603
weight,
@@ -606,6 +627,7 @@ def profile_mxfp8_tma(
606627
k: int = 8192,
607628
block_n: int = 4,
608629
num_stages: int = 2,
630+
num_compute_warps: int = 1,
609631
output: Path = Path("mxfp8_tma.pftrace"),
610632
seed: int = 0,
611633
warmups: int = 1,
@@ -631,6 +653,7 @@ def profile_mxfp8_tma(
631653
block_n,
632654
num_stages,
633655
enable_profiling=True,
656+
num_compute_warps=num_compute_warps,
634657
)
635658
output.parent.mkdir(parents=True, exist_ok=True)
636659

0 commit comments

Comments
 (0)