Skip to content

Commit ffd2e75

Browse files
committed
Add Blackwell MXFP8 TMA GEMV
1 parent a1b9281 commit ffd2e75

3 files changed

Lines changed: 586 additions & 0 deletions

File tree

test/test_mxfp8_tma.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import pytest
2+
import torch
3+
4+
5+
if not torch.cuda.is_available():
6+
pytest.skip("CUDA not available", allow_module_level=True)
7+
if torch.cuda.get_device_capability() not in {(10, 0), (10, 3)}:
8+
pytest.skip("MXFP8 TMA GEMV requires SM100 or SM103", allow_module_level=True)
9+
10+
try:
11+
from transformer_nuggets.cute import mxfp8_tma_gemv
12+
except ImportError:
13+
pytest.skip("CuTe DSL not available", allow_module_level=True)
14+
15+
16+
def quantize_mxfp8(value: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
17+
"""Quantize rows into E4M3 values with raw E8M0 block scales."""
18+
blocks = value.float().reshape(value.shape[0], -1, 32)
19+
exponent = torch.ceil(torch.log2(blocks.abs().amax(dim=-1) / 448.0)).clamp(-126, 127)
20+
scale = torch.exp2(exponent).unsqueeze(-1)
21+
quantized = (blocks / scale).clamp(-448, 448).to(torch.float8_e4m3fn)
22+
return quantized.reshape_as(value), (exponent + 127).to(torch.uint8)
23+
24+
25+
def dequantize_mxfp8(value: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
26+
"""Dequantize raw MXFP8 storage to float32."""
27+
expanded_scale = scale.view(torch.float8_e8m0fnu).float().repeat_interleave(32, dim=1)
28+
return value.float() * expanded_scale
29+
30+
31+
@pytest.mark.parametrize(
32+
("k", "block_n", "num_stages"),
33+
[(2048, 4, 2), (4096, 8, 3)],
34+
)
35+
def test_mxfp8_tma_gemv_matches_reference(k, block_n, num_stages):
36+
"""Match independently dequantized float32 matmul."""
37+
torch.manual_seed(k)
38+
q_input, input_scale = quantize_mxfp8(torch.randn((1, k), dtype=torch.bfloat16, device="cuda"))
39+
weight, weight_scale = quantize_mxfp8(
40+
torch.randn((128, k), dtype=torch.bfloat16, device="cuda")
41+
)
42+
expected = (
43+
dequantize_mxfp8(q_input, input_scale) @ dequantize_mxfp8(weight, weight_scale).T
44+
).bfloat16()
45+
output = torch.empty_like(expected)
46+
47+
actual = mxfp8_tma_gemv(
48+
q_input,
49+
weight,
50+
input_scale,
51+
weight_scale,
52+
block_n=block_n,
53+
num_stages=num_stages,
54+
output=output,
55+
)
56+
torch.cuda.synchronize()
57+
58+
assert actual is output
59+
torch.testing.assert_close(actual, expected, atol=1.0, rtol=0.05)
60+
61+
62+
def test_mxfp8_tma_gemv_cuda_graph_replay():
63+
"""Replay into caller-owned output without hidden allocation or copies."""
64+
k = 2048
65+
q_input, input_scale = quantize_mxfp8(torch.randn((1, k), dtype=torch.bfloat16, device="cuda"))
66+
weight, weight_scale = quantize_mxfp8(
67+
torch.randn((128, k), dtype=torch.bfloat16, device="cuda")
68+
)
69+
output = torch.empty((1, 128), dtype=torch.bfloat16, device="cuda")
70+
mxfp8_tma_gemv(
71+
q_input,
72+
weight,
73+
input_scale,
74+
weight_scale,
75+
block_n=4,
76+
output=output,
77+
)
78+
graph = torch.cuda.CUDAGraph()
79+
80+
with torch.cuda.graph(graph):
81+
mxfp8_tma_gemv(
82+
q_input,
83+
weight,
84+
input_scale,
85+
weight_scale,
86+
block_n=4,
87+
output=output,
88+
)
89+
graph.replay()
90+
torch.cuda.synchronize()
91+
92+
expected = (
93+
dequantize_mxfp8(q_input, input_scale) @ dequantize_mxfp8(weight, weight_scale).T
94+
).bfloat16()
95+
torch.testing.assert_close(output, expected, atol=1.0, rtol=0.05)
96+
97+
98+
def test_mxfp8_tma_gemv_combines_cancelling_scales():
99+
"""Avoid an infinite intermediate when E8M0 scale exponents cancel."""
100+
k = 2048
101+
q_input = torch.ones((1, k), dtype=torch.float8_e4m3fn, device="cuda")
102+
weight = torch.ones((128, k), dtype=torch.float8_e4m3fn, device="cuda")
103+
input_scale = torch.full((1, k // 32), 254, dtype=torch.uint8, device="cuda")
104+
weight_scale = torch.zeros((128, k // 32), dtype=torch.uint8, device="cuda")
105+
106+
actual = mxfp8_tma_gemv(
107+
q_input,
108+
weight,
109+
input_scale,
110+
weight_scale,
111+
block_n=4,
112+
)
113+
torch.cuda.synchronize()
114+
115+
torch.testing.assert_close(actual, torch.full_like(actual, k), rtol=0, atol=0)
116+
117+
118+
def test_mxfp8_tma_gemv_preserves_nan_scale():
119+
"""Decode the reserved E8M0 byte as NaN rather than infinity."""
120+
k = 2048
121+
q_input = torch.ones((1, k), dtype=torch.float8_e4m3fn, device="cuda")
122+
weight = torch.ones((128, k), dtype=torch.float8_e4m3fn, device="cuda")
123+
input_scale = torch.full((1, k // 32), 127, dtype=torch.uint8, device="cuda")
124+
weight_scale = torch.full((128, k // 32), 127, dtype=torch.uint8, device="cuda")
125+
input_scale[:, 0] = 0xFF
126+
127+
actual = mxfp8_tma_gemv(
128+
q_input,
129+
weight,
130+
input_scale,
131+
weight_scale,
132+
block_n=4,
133+
)
134+
torch.cuda.synchronize()
135+
136+
assert torch.isnan(actual).all()

transformer_nuggets/cute/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
from transformer_nuggets.cute import profiler
1414

1515

16+
_MXFP8_TMA_EXPORTS = {
17+
"Mxfp8TmaGemv",
18+
"get_mxfp8_tma_gemv",
19+
"mxfp8_tma_gemv",
20+
}
21+
1622
_SYMMETRIC_MEMORY_EXPORTS = {
1723
"compile_symmetric_memory_all_reduce",
1824
"init_torchrun_process_group",
@@ -23,6 +29,10 @@
2329

2430

2531
def __getattr__(name):
32+
if name in _MXFP8_TMA_EXPORTS:
33+
from transformer_nuggets.cute import mxfp8_tma
34+
35+
return getattr(mxfp8_tma, name)
2636
if name in _SYMMETRIC_MEMORY_EXPORTS:
2737
from transformer_nuggets.cute import symmetric_memory
2838

0 commit comments

Comments
 (0)