Skip to content

Commit 6bbb29e

Browse files
committed
Support blocked MXFP8 scales
1 parent 1cfc69e commit 6bbb29e

5 files changed

Lines changed: 274 additions & 38 deletions

File tree

test/test_mxfp8_tma.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010

1111
try:
1212
from transformer_nuggets.cute import (
13+
BlockScaleLayout,
1314
GridScheduler,
1415
MXFP8_TMA_PROFILE_TAGS,
1516
get_mxfp8_tma_gemv,
1617
mxfp8_tma_gemv,
18+
mxfp8_tma_scaled_mm,
1719
select_mxfp8_tma_compute_warps,
1820
)
1921
from transformer_nuggets.cute.mxfp8_tma import app
@@ -42,6 +44,27 @@ def test_select_mxfp8_tma_compute_warps(k, block_n, expected_sm100):
4244
assert select_mxfp8_tma_compute_warps(k, block_n) == expected
4345

4446

47+
def swizzle_mxfp8_scales(scales: torch.Tensor) -> torch.Tensor:
48+
"""Convert natural E8M0 scales to canonical SWIZZLE_32_4_4 storage."""
49+
rows, cols = scales.shape
50+
padded_rows = ((rows + 127) // 128) * 128
51+
padded_cols = ((cols + 3) // 4) * 4
52+
padded = torch.zeros(
53+
(padded_rows, padded_cols),
54+
dtype=scales.dtype,
55+
device=scales.device,
56+
)
57+
padded[:rows, :cols] = scales
58+
return (
59+
padded.view(padded_rows // 128, 128, padded_cols // 4, 4)
60+
.permute(0, 2, 1, 3)
61+
.reshape(-1, 4, 32, 4)
62+
.transpose(1, 2)
63+
.reshape(-1)
64+
.contiguous()
65+
)
66+
67+
4568
def dequantize_mxfp8(value: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
4669
"""Dequantize raw MXFP8 storage to float32."""
4770
expanded_scale = scale.view(torch.float8_e8m0fnu).float().repeat_interleave(32, dim=1)
@@ -81,6 +104,65 @@ def test_mxfp8_tma_gemv_matches_reference(k, block_n, num_stages, num_compute_wa
81104
torch.testing.assert_close(actual, expected, atol=1.0, rtol=0.05)
82105

83106

107+
@pytest.mark.parametrize(("block_n", "num_compute_warps"), [(8, 4), (6, 2)])
108+
def test_mxfp8_tma_swizzled_scales_match_raw(block_n, num_compute_warps):
109+
"""Read canonical blocked E8M0 scales across row-layout boundaries."""
110+
n, k = 264, 2048
111+
q_input, input_scale = quantize_mxfp8(torch.randn((1, k), dtype=torch.bfloat16, device="cuda"))
112+
weight, weight_scale = quantize_mxfp8(torch.randn((n, k), dtype=torch.bfloat16, device="cuda"))
113+
expected = mxfp8_tma_gemv(
114+
q_input,
115+
weight,
116+
input_scale,
117+
weight_scale,
118+
block_n=block_n,
119+
num_compute_warps=num_compute_warps,
120+
)
121+
actual = mxfp8_tma_gemv(
122+
q_input,
123+
weight,
124+
swizzle_mxfp8_scales(input_scale),
125+
swizzle_mxfp8_scales(weight_scale),
126+
block_n=block_n,
127+
num_compute_warps=num_compute_warps,
128+
block_scale_layout=BlockScaleLayout.SWIZZLE_32_4_4,
129+
)
130+
torch.cuda.synchronize()
131+
torch.testing.assert_close(actual, expected, atol=1.0, rtol=0.05)
132+
133+
134+
def test_mxfp8_tma_scaled_mm_adapter_matches_scaled_mm():
135+
"""Match scaled_mm's blocked E8M0 scale and transposed-weight contract."""
136+
from torch.nn.functional import ScalingType, SwizzleType, scaled_mm
137+
138+
n, k = 128, 2048
139+
q_input, input_scale = quantize_mxfp8(torch.randn((1, k), dtype=torch.bfloat16, device="cuda"))
140+
weight, weight_scale = quantize_mxfp8(torch.randn((n, k), dtype=torch.bfloat16, device="cuda"))
141+
input_scale = swizzle_mxfp8_scales(input_scale).view(torch.float8_e8m0fnu)
142+
weight_scale = swizzle_mxfp8_scales(weight_scale).view(torch.float8_e8m0fnu)
143+
actual = mxfp8_tma_scaled_mm(
144+
q_input,
145+
weight.t(),
146+
input_scale,
147+
weight_scale,
148+
block_n=8,
149+
num_compute_warps=4,
150+
)
151+
expected = scaled_mm(
152+
q_input,
153+
weight.t(),
154+
scale_a=[input_scale],
155+
scale_recipe_a=[ScalingType.BlockWise1x32],
156+
swizzle_a=[SwizzleType.SWIZZLE_32_4_4],
157+
scale_b=[weight_scale],
158+
scale_recipe_b=[ScalingType.BlockWise1x32],
159+
swizzle_b=[SwizzleType.SWIZZLE_32_4_4],
160+
output_dtype=torch.bfloat16,
161+
)
162+
torch.cuda.synchronize()
163+
torch.testing.assert_close(actual, expected, atol=1.0, rtol=0.05)
164+
165+
84166
def test_mxfp8_tma_gemv_persistent_grid_matches_reference():
85167
"""Reuse a bounded physical CTA grid across all logical output tiles."""
86168
k = 2048

transformer_nuggets/cute/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
_BLOCKSCALED_TMA_EXPORTS = {
1717
"DEFAULT_PERSISTENT_CTAS_PER_SM",
18+
"BlockScaleLayout",
1819
"GridScheduler",
1920
"ProfileTag",
2021
}
@@ -24,6 +25,7 @@
2425
"Mxfp8TmaGemv",
2526
"get_mxfp8_tma_gemv",
2627
"mxfp8_tma_gemv",
28+
"mxfp8_tma_scaled_mm",
2729
"select_mxfp8_tma_compute_warps",
2830
}
2931

transformer_nuggets/cute/blockscaled_tma.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@
88
import cutlass.pipeline as pipeline
99
from cuda.bindings import driver as cuda
1010
from cutlass.cute.nvgpu import cpasync, tcgen05
11+
import cutlass.utils.blockscaled_layout as blockscaled_utils
1112

1213
from transformer_nuggets.cute.base import CuteOp
1314
from transformer_nuggets.cute.profiler.ops import profile_region
1415

1516

17+
class BlockScaleLayout(str, Enum):
18+
"""Describe caller-owned block-scale storage."""
19+
20+
RAW = "raw"
21+
SWIZZLE_32_4_4 = "swizzle_32_4_4"
22+
23+
1624
class GridScheduler(str, Enum):
1725
"""Map logical output tiles onto the launched CTA grid."""
1826

@@ -127,6 +135,14 @@ def __init__(
127135
self.max_profile_events_per_cta = 2 + 3 * self.num_k_tiles
128136
self.num_profile_units = self.num_tiles
129137

138+
@cute.jit
139+
def make_blocked_scale_layout(self, scale_vector_size: cutlass.Constexpr):
140+
"""Map logical matrix coordinates onto canonical blocked scale storage."""
141+
return blockscaled_utils.tile_atom_to_shape_SF(
142+
(((self.n + 127) // 128) * 128, self.k, 1),
143+
scale_vector_size,
144+
)
145+
130146
def x_smem_layout(self):
131147
"""Return the staged layout for one physical input tile."""
132148
return cute.make_ordered_layout(

0 commit comments

Comments
 (0)