Consolidate NVFP4 tests into a single file

Review follow-up: fold the three NVFP4 test files into
test_tilelang_language_nvf4_mma_block_scale.py and drop the deep
contract-oracle suites.

- The example CLI file is gone; its tail-tile e2e run and the
  unsupported-tail rejection test move here, the source-string checks are
  dropped.
- The quantize scale-layout file is gone; the load-bearing packer contracts
  move here (device word-offset fixed cases, packer vs independent byte
  oracle, CuTeDSL canonical SF byte equivalence, packer/encoder known values
  and rejections, torch-reference quantizer contracts, the GPU quantizer
  cross-check, and swizzle tail padding).
- The OMMA selector / issue-schedule / package-lifecycle oracle suites are
  removed from both files; the production contract stays enforced by the
  lowering asserts and the e2e correctness runs.

3 files / 1836 lines -> 1 file / 923 lines; 39 tests pass locally on
RTX 5090 (full-suite run, fresh JIT cache).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
qutao
2026-07-24 16:59:24 +08:00
co-authored by Claude Opus 4.8
parent b6f70a42a2
commit c34e7502d4
3 changed files with 349 additions and 1262 deletions
@@ -1,13 +1,31 @@
import importlib.util
import sys
from pathlib import Path
import pytest
torch = pytest.importorskip("torch")
import tilelang
import tilelang.language as T
import tilelang.testing
from tilelang import tvm
from tilelang.cuda.intrinsics.layout.mma_layout import mma_load_a_32x32_to_shared_16x64_layout
from tilelang.cuda.intrinsics.macro.mma_macro_generator import SM120BlockScaledFullTilePackageContract
from tilelang.intrinsics import TensorCoreIntrinEmitter, get_swizzle_layout
from tilelang.quantize.nvfp4 import blockscaled_chunk_kmajor_word_offset
from tilelang.quantize import (
pack_blockscaled_chunk_kmajor_scale_bytes,
quantize_bf16_to_nvfp4_blockscaled,
swizzle_blockscaled_chunk_kmajor_scale_words,
unswizzle_blockscaled_chunk_kmajor_scale_words,
)
from tilelang.quantize.nvfp4 import (
blockscaled_chunk_kmajor_word_offset,
decode_packed_fp4_e2m1,
decode_ue4m3_scale_bytes,
encode_fp4_e2m1_values,
encode_ue4m3_scale_bytes,
pack_nvfp4_scale_bytes,
)
from tilelang.transform import simplify_prim_func
@@ -41,32 +59,6 @@ def _make_blockscale_emitter(**kwargs):
)
def _make_sm120_fulltile_contract():
emitter = _make_blockscale_emitter(
a_dtype=T.float4_e2m1fn,
b_dtype=T.float4_e2m1fn,
accum_dtype=T.float32,
a_transposed=False,
b_transposed=True,
block_row_warps=2,
block_col_warps=2,
warp_row_tiles=64,
warp_col_tiles=64,
chunk=256,
reduce_k=1,
num_elems_per_byte=2,
)
return SM120BlockScaledFullTilePackageContract.for_package_pingpong(
emitter,
sf_layout="blockscaled_chunk_kmajor",
)
def _oracle_blockscaled_chunk_kmajor_flat_word(row: int, kblock: int) -> int:
physical_row, physical_word = blockscaled_chunk_kmajor_word_offset(row, kblock)
return physical_row * 4 + physical_word
def _make_swizzle_layout(shared_buf):
dtype = shared_buf.dtype
shape = shared_buf.shape
@@ -433,125 +425,6 @@ def test_nvf4_mma_block_scale_rejects_incompatible_dtypes(dtype_kwargs):
)
def test_sm120_fulltile_package_contract_matches_current_pingpong_path():
contract = _make_sm120_fulltile_contract()
assert (contract.tile_m, contract.tile_n, contract.tile_k) == (128, 128, 256)
assert (contract.block_row_warps, contract.block_col_warps) == (2, 2)
assert (contract.warp_rows, contract.warp_cols) == (4, 4)
assert (contract.warp_row_tiles, contract.warp_col_tiles) == (64, 64)
assert (contract.kblocks, contract.micro_size_k) == (4, 64)
assert (contract.scale_package_words_sfa, contract.scale_package_words_sfb) == (2, 2)
assert (contract.issue_count_per_warp, contract.issue_count_per_warpgroup) == (32, 128)
def test_sm120_fulltile_package_contract_describes_compact_selector_copy_view():
contract = _make_sm120_fulltile_contract()
assert contract.compact_selector_scale_rows(lane=0, warp_m=0, warp_n=0) == ((0, 32), (0, 32))
assert contract.compact_selector_scale_rows(lane=1, warp_m=0, warp_n=0) == ((8, 40), (8, 40))
assert contract.compact_selector_scale_rows(lane=2, warp_m=1, warp_n=1) == ((80, 112), (80, 112))
assert contract.compact_selector_scale_rows(lane=31, warp_m=1, warp_n=1) == ((95, 127), (95, 127))
@pytest.mark.parametrize("lane", [0, 1, 2, 17, 31])
@pytest.mark.parametrize("warp_m", [0, 1])
@pytest.mark.parametrize("warp_n", [0, 1])
@pytest.mark.parametrize("kblock", [0, 2, 3])
def test_sm120_fulltile_package_contract_word_offsets_match_source_layout_oracle(lane, warp_m, warp_n, kblock):
contract = _make_sm120_fulltile_contract()
sfa_rows, sfb_rows = contract.compact_selector_scale_rows(lane=lane, warp_m=warp_m, warp_n=warp_n)
expected_sfa = tuple(_oracle_blockscaled_chunk_kmajor_flat_word(row, kblock) for row in sfa_rows)
expected_sfb = tuple(_oracle_blockscaled_chunk_kmajor_flat_word(row, kblock) for row in sfb_rows)
assert contract.compact_selector_scale_word_offsets(lane=lane, warp_m=warp_m, warp_n=warp_n, kblock=kblock) == (
expected_sfa,
expected_sfb,
)
def test_sm120_fulltile_package_contract_describes_pingpong_lifecycle():
contract = _make_sm120_fulltile_contract()
assert contract.package_pingpong_lifecycle() == (
("copy", 0, 0),
("copy", 1, 1),
("gemm", 0, 0),
("copy", 0, 2),
("gemm", 1, 1),
("copy", 1, 3),
("gemm", 0, 2),
("gemm", 1, 3),
)
def test_sm120_fulltile_package_contract_pingpong_lifecycle_is_data_ready():
contract = _make_sm120_fulltile_contract()
package_kblock = {}
gemmed_kblocks = []
for op, package_id, kblock in contract.package_pingpong_lifecycle():
if op == "copy":
package_kblock[package_id] = kblock
elif op == "gemm":
assert package_kblock[package_id] == kblock
gemmed_kblocks.append(kblock)
else:
raise AssertionError(f"unexpected package lifecycle op: {op}")
assert gemmed_kblocks == [0, 1, 2, 3]
def test_sm120_fulltile_package_contract_describes_omma_sf_issue_schedule():
contract = _make_sm120_fulltile_contract()
schedule = contract.omma_sf_issue_schedule_per_warp()
assert len(schedule) == contract.issue_count_per_warp
assert schedule[0] == (0, 0, 0, 0, 0, 0, 0)
assert schedule[1] == (0, 0, 1, 0, 0, 0, 1)
assert schedule[2] == (0, 1, 0, 0, 0, 0, 2)
assert schedule[3] == (0, 1, 1, 0, 0, 0, 3)
assert schedule[16] == (2, 0, 0, 1, 0, 0, 0)
assert schedule[-1] == (3, 3, 1, 1, 1, 1, 3)
for mma_i, mma_j, n8_half, sfa_word, sfb_word, scale_a_tid, scale_b_tid in schedule:
assert sfa_word == (0 if mma_i < 2 else 1)
assert sfb_word == (0 if mma_j < 2 else 1)
assert scale_a_tid == (mma_i & 1)
assert scale_b_tid == (mma_j & 1) * 2 + n8_half
assert sorted({issue[5] for issue in schedule}) == [0, 1]
assert sorted({issue[6] for issue in schedule}) == [0, 1, 2, 3]
assert sum(1 for issue in schedule if issue[3] == 0) == 16
assert sum(1 for issue in schedule if issue[3] == 1) == 16
assert sum(1 for issue in schedule if issue[4] == 0) == 16
assert sum(1 for issue in schedule if issue[4] == 1) == 16
def test_sm120_fulltile_package_contract_rejects_shape_drift():
emitter = _make_blockscale_emitter(
a_dtype=T.float4_e2m1fn,
b_dtype=T.float4_e2m1fn,
accum_dtype=T.float32,
a_transposed=False,
b_transposed=True,
block_row_warps=2,
block_col_warps=4,
warp_row_tiles=64,
warp_col_tiles=64,
chunk=256,
reduce_k=1,
num_elems_per_byte=2,
)
with pytest.raises(ValueError, match="128x128x256"):
SM120BlockScaledFullTilePackageContract.for_package_pingpong(
emitter,
sf_layout="blockscaled_chunk_kmajor",
)
@tilelang.testing.requires_cuda
@tilelang.testing.requires_cuda_compute_version_eq(12, 0)
@pytest.mark.parametrize("K", [64, 128, 256])
@@ -719,3 +592,332 @@ def test_nvf4_mma_block_scale_varying_scale_correctness():
if __name__ == "__main__":
tilelang.testing.main()
# ---------------------------------------------------------------------------
# Example tail-tile behavior (moved from test_tilelang_sm120_nvfp4_example_cli).
# ---------------------------------------------------------------------------
def _load_sm120_example(monkeypatch):
repo_root = Path(__file__).resolve().parents[3]
example = repo_root / "examples/gemm_sm120/sm120_nvfp4_blockscaled_gemm.py"
monkeypatch.setattr(sys, "argv", [str(example)])
spec = importlib.util.spec_from_file_location("sm120_nvfp4_blockscaled_gemm_example", example)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
@tilelang.testing.requires_cuda_compute_version_eq(12, 0)
def test_sm120_nvfp4_example_kernel_handles_mn_tail_tiles(monkeypatch):
import pytest
torch = pytest.importorskip("torch")
from tilelang.quantize import swizzle_blockscaled_chunk_kmajor_scale_words
module = _load_sm120_example(monkeypatch)
for M, N, K in [(257, 384, 512), (130, 128, 256), (128, 136, 256)]:
kernel = module.sm120_nvfp4_blockscaled_gemm(M, N, K)
A = module._make_packed_fp4(M, K, seed=3)
B = module._make_packed_fp4(N, K, seed=4)
SFA_semantic = module._make_binary_scale_words(M, K, seed=5)
SFB_semantic = module._make_binary_scale_words(N, K, seed=6)
SFA = swizzle_blockscaled_chunk_kmajor_scale_words(SFA_semantic).reshape(-1, 4)
SFB = swizzle_blockscaled_chunk_kmajor_scale_words(SFB_semantic).reshape(-1, 4)
assert SFA.shape[0] % 128 == 0
assert SFB.shape[0] % 128 == 0
C = torch.empty((M, N), device="cuda", dtype=torch.bfloat16)
kernel(A, B, SFA, SFB, C)
torch.cuda.synchronize()
module._verify(A, B, SFA_semantic, SFB_semantic, C, torch.bfloat16)
def test_sm120_nvfp4_example_kernel_rejects_unsupported_tails(monkeypatch):
import pytest
module = _load_sm120_example(monkeypatch)
# simultaneous M and N tails hit a known copy-lowering boundary bug
with pytest.raises(ValueError, match="simultaneous M and N tail"):
module.sm120_nvfp4_blockscaled_gemm(257, 136, 512)
# bf16 output rows must stay 16-byte aligned
with pytest.raises(AssertionError, match="multiple of 8"):
module.sm120_nvfp4_blockscaled_gemm(128, 130, 256)
# ---------------------------------------------------------------------------
# Scale layout / packer contract (moved from
# testing/python/quantize/test_tilelang_quantize_nvfp4_scale_layout).
# These pin the blockscaled_chunk_kmajor packing against independent oracles,
# the CuTeDSL canonical SF byte layout, and the maint TileLang quantizer.
# ---------------------------------------------------------------------------
def _load_maint_quantizer():
import importlib.util
path = Path(__file__).resolve().parents[3] / "maint/gemm/gemm_sm120/tilelang_nvfp4_quantizer.py"
spec = importlib.util.spec_from_file_location("tilelang_nvfp4_quantizer", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.tilelang_quantize_bf16_to_nvfp4_blockscaled
def _pack_semantic_scale_words(scale_bytes):
scale_i64 = scale_bytes.to(torch.int64).reshape(scale_bytes.shape[0], scale_bytes.shape[1] // 4, 4)
words = scale_i64[:, :, 0]
words = words | (scale_i64[:, :, 1] << 8)
words = words | (scale_i64[:, :, 2] << 16)
words = words | (scale_i64[:, :, 3] << 24)
return words.to(torch.uint32).contiguous()
def _expected_blockscaled_chunk_kmajor_byte_location(row: int, k16_idx: int, k64_cols: int = 4) -> tuple[int, int, int]:
k64_word = k16_idx // 4
byte_lane = k16_idx % 4
row_block = row // 128
row_in_block = row % 128
flat_word = row_block * 128 * k64_cols + k64_word * 128 + (row_in_block % 32) * 4 + (row_in_block // 32)
physical_row = flat_word // k64_cols
physical_word = flat_word % k64_cols
return physical_row, physical_word, byte_lane
def _packed_byte(packed, row: int, word: int, byte_lane: int) -> int:
return (int(packed[row, word].item()) >> (8 * byte_lane)) & 0xFF
def _unpack_with_oracle(packed, rows: int, k16_cols: int):
out = torch.empty((rows, k16_cols), dtype=torch.uint8)
k64_cols = k16_cols // 4
for row in range(rows):
for k16_idx in range(k16_cols):
physical_row, physical_word, byte_lane = _expected_blockscaled_chunk_kmajor_byte_location(row, k16_idx, k64_cols)
out[row, k16_idx] = _packed_byte(packed, physical_row, physical_word, byte_lane)
return out
def test_blockscaled_chunk_kmajor_word_offset_fixed_cases():
expected = {
(0, 0): (0, 0),
(0, 1): (32, 0),
(0, 2): (64, 0),
(0, 3): (96, 0),
(31, 0): (31, 0),
(32, 0): (0, 1),
(63, 1): (63, 1),
(64, 2): (64, 2),
(96, 3): (96, 3),
(127, 3): (127, 3),
}
for (row, k64_word), physical in expected.items():
assert blockscaled_chunk_kmajor_word_offset(row, k64_word) == physical
def test_pack_blockscaled_chunk_kmajor_scale_bytes_fixed_byte_offsets():
rows = 256
k16_cols = 32
scale_bytes = torch.zeros((rows, k16_cols), dtype=torch.uint8)
cases = [
(0, 0, 0x11),
(32, 0, 0x22),
(64, 8, 0x33),
(96, 12, 0x44),
(127, 15, 0x55),
(128, 16, 0x66),
(159, 31, 0x77),
(255, 27, 0x88),
]
for row, k16_idx, value in cases:
scale_bytes[row, k16_idx] = value
packed = pack_blockscaled_chunk_kmajor_scale_bytes(scale_bytes)
assert packed.shape == (rows, k16_cols // 4)
assert packed.dtype == torch.uint32
for row, k16_idx, value in cases:
physical_row, physical_word, byte_lane = _expected_blockscaled_chunk_kmajor_byte_location(row, k16_idx, packed.shape[1])
assert _packed_byte(packed, physical_row, physical_word, byte_lane) == value
def test_pack_blockscaled_chunk_kmajor_scale_bytes_random_binary_512x32_matches_oracle():
rows = 512
k16_cols = 512 // 16
generator = torch.Generator(device="cpu").manual_seed(17)
scale_bytes = torch.randint(0, 2, (rows, k16_cols), generator=generator, dtype=torch.uint8) * 0x38
packed = pack_blockscaled_chunk_kmajor_scale_bytes(scale_bytes)
assert packed.shape == (rows, k16_cols // 4)
assert packed.dtype == torch.uint32
assert torch.equal(_unpack_with_oracle(packed, rows, k16_cols), scale_bytes)
def test_pack_blockscaled_chunk_kmajor_scale_bytes_matches_cutedsl_blocked_sf_layout():
"""Byte-level cross-compatibility with the CuTeDSL/CUTLASS canonical SF layout.
CuTeDSL builds SFA/SFB with ``blockscaled_utils.tile_atom_to_shape_SF``:
atom ``((32,4),(16,4)):((16,4),(0,1))`` tiled with order ``(2,1,3)``, e.g.
for ``(MN=256, K=512, L=1)`` the layout prints as
``(((32,4),2),((16,4),8),(1,1)):(((16,4),4096),((0,1),512),(0,0))``.
The packed uint32 tensor must carry exactly those bytes so one buffer can
feed both the TileLang SM120 path and a CuTeDSL NVFP4 blockscaled GEMM
(``tl_words.view(torch.uint8)`` / ``sf_u8.view(torch.uint32)`` are
zero-copy bridges between the two views).
"""
for rows, k in ((128, 256), (256, 512), (384, 1024)):
k16_cols = k // 16
rest_k = k16_cols // 4
generator = torch.Generator(device="cpu").manual_seed(rows + k)
scale_bytes = torch.randint(0, 256, (rows, k16_cols), generator=generator, dtype=torch.uint8)
packed_bytes = pack_blockscaled_chunk_kmajor_scale_bytes(scale_bytes).view(torch.uint8).reshape(-1)
m = torch.arange(rows).unsqueeze(1)
k16 = torch.arange(k16_cols).unsqueeze(0)
cutedsl_offset = (m % 32) * 16 + ((m // 32) % 4) * 4 + (m // 128) * (512 * rest_k) + (k16 % 4) + (k16 // 4) * 512
assert torch.equal(packed_bytes[cutedsl_offset.reshape(-1)].reshape(rows, k16_cols), scale_bytes)
def test_pack_blockscaled_chunk_kmajor_scale_bytes_matches_word_swizzle():
rows = 512
k16_cols = 512 // 16
scale_bytes = torch.arange(rows * k16_cols, dtype=torch.uint8).reshape(rows, k16_cols)
semantic_words = _pack_semantic_scale_words(scale_bytes)
assert torch.equal(pack_blockscaled_chunk_kmajor_scale_bytes(scale_bytes), swizzle_blockscaled_chunk_kmajor_scale_words(semantic_words))
def test_pack_nvfp4_scale_bytes_default_matches_blockscaled_chunk_kmajor_layout():
rows = 512
k16_cols = 512 // 16
scale_bytes = torch.arange(rows * k16_cols, dtype=torch.uint8).reshape(rows, k16_cols)
expected = pack_blockscaled_chunk_kmajor_scale_bytes(scale_bytes)
actual = pack_nvfp4_scale_bytes(scale_bytes)
assert torch.equal(actual, expected)
def test_blockscaled_chunk_kmajor_scale_packer_rejects_invalid_shapes():
# rows that are not a multiple of 128 are zero-padded, not rejected
padded = pack_blockscaled_chunk_kmajor_scale_bytes(torch.zeros((127, 16), dtype=torch.uint8))
assert padded.shape == (128, 4)
with pytest.raises(ValueError, match="K/16 columns multiple of 16"):
pack_blockscaled_chunk_kmajor_scale_bytes(torch.zeros((128, 12), dtype=torch.uint8))
with pytest.raises(TypeError, match="torch.uint8"):
pack_blockscaled_chunk_kmajor_scale_bytes(torch.zeros((128, 16), dtype=torch.int32))
def test_encode_fp4_e2m1_values_and_pack_order():
values = torch.tensor([[0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, -0.5, -1.0]], dtype=torch.float32)
codes = encode_fp4_e2m1_values(values)
assert torch.equal(codes, torch.tensor([[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x9, 0xA]], dtype=torch.uint8))
packed = (codes[:, 0::2] | (codes[:, 1::2] << 4)).contiguous().view(torch.int8)
assert torch.equal(decode_packed_fp4_e2m1(packed), values)
def test_encode_ue4m3_scale_bytes_known_values():
values = torch.tensor([0.0, 2.0**-9, 2.0**-6, 1.0, 2.0, 448.0], dtype=torch.float32)
encoded = encode_ue4m3_scale_bytes(values, rounding="nearest")
assert torch.equal(encoded, torch.tensor([0x00, 0x01, 0x08, 0x38, 0x40, 0x7E], dtype=torch.uint8))
torch.testing.assert_close(decode_ue4m3_scale_bytes(encoded), values)
assert torch.isnan(decode_ue4m3_scale_bytes(torch.tensor([0x7F], dtype=torch.uint8))).all()
def test_quantize_nvfp4_blockscaled_bf16_activation_contract():
rows = 128
cols = 256
x = torch.zeros((rows, cols), dtype=torch.bfloat16)
pattern = torch.tensor([0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0, 0.0])
x[0, :16] = pattern.to(torch.bfloat16)
packed_fp4, packed_scales, scale_bytes = quantize_bf16_to_nvfp4_blockscaled(x, return_scale_bytes=True)
assert packed_fp4.shape == (rows, cols // 2)
assert packed_fp4.dtype == torch.int8
assert packed_scales.shape == (rows, cols // 64)
assert packed_scales.dtype == torch.uint32
assert scale_bytes.shape == (rows, cols // 16)
assert scale_bytes.dtype == torch.uint8
assert scale_bytes[0, 0].item() == 0x38
assert torch.equal(packed_scales, pack_blockscaled_chunk_kmajor_scale_bytes(scale_bytes))
decoded = decode_packed_fp4_e2m1(packed_fp4) * decode_ue4m3_scale_bytes(scale_bytes).repeat_interleave(16, dim=1)
torch.testing.assert_close(decoded[0, :16], pattern, rtol=0.0, atol=0.0)
def test_quantize_nvfp4_blockscaled_random_bf16_has_bounded_error():
rows = 128
cols = 256
generator = torch.Generator(device="cpu").manual_seed(19)
x = (torch.randn((rows, cols), generator=generator, dtype=torch.float32) * 2.0).to(torch.bfloat16)
packed_fp4, scale_source, scale_bytes = quantize_bf16_to_nvfp4_blockscaled(x, return_scale_bytes=True)
decoded = decode_packed_fp4_e2m1(packed_fp4) * decode_ue4m3_scale_bytes(scale_bytes).repeat_interleave(16, dim=1)
scale = decode_ue4m3_scale_bytes(scale_bytes).repeat_interleave(16, dim=1)
error = (decoded - x.to(torch.float32)).abs()
assert torch.isfinite(decoded).all()
assert torch.all(error <= scale + 1e-6)
assert torch.equal(scale_source, pack_blockscaled_chunk_kmajor_scale_bytes(scale_bytes))
def test_quantize_nvfp4_blockscaled_explicit_layout_matches_default():
rows = 128
cols = 256
generator = torch.Generator(device="cpu").manual_seed(23)
x = (torch.randn((rows, cols), generator=generator, dtype=torch.float32) * 2.0).to(torch.bfloat16)
default = quantize_bf16_to_nvfp4_blockscaled(x)
explicit = quantize_bf16_to_nvfp4_blockscaled(x, scale_layout="blockscaled_chunk_kmajor")
assert torch.equal(explicit[0], default[0])
assert torch.equal(explicit[1], default[1])
@tilelang.testing.requires_cuda
@tilelang.testing.requires_cuda_compute_version_ge(10, 0)
@pytest.mark.parametrize("rows, cols", [(128, 256), (256, 512)])
def test_tilelang_quantize_nvfp4_blockscaled_matches_reference_layout_and_error_bound(rows, cols):
generator = torch.Generator(device="cuda").manual_seed(rows + cols)
x = (torch.randn((rows, cols), generator=generator, device="cuda", dtype=torch.float32) * 2.0).to(torch.bfloat16)
tilelang_quantize = _load_maint_quantizer()
packed_tl, scale_source_tl = tilelang_quantize(x)
_, scale_source_ref, scale_bytes_ref = quantize_bf16_to_nvfp4_blockscaled(x, return_scale_bytes=True)
assert packed_tl.shape == (rows, cols // 2)
assert packed_tl.dtype == torch.int8
assert scale_source_tl.shape == (rows, cols // 64)
assert scale_source_tl.dtype == torch.uint32
assert torch.equal(scale_source_tl.cpu(), scale_source_ref.cpu())
semantic_words = unswizzle_blockscaled_chunk_kmajor_scale_words(scale_source_tl)
assert torch.equal(swizzle_blockscaled_chunk_kmajor_scale_words(semantic_words).cpu(), scale_source_tl.cpu())
scale = decode_ue4m3_scale_bytes(scale_bytes_ref).repeat_interleave(16, dim=1)
decoded = decode_packed_fp4_e2m1(packed_tl) * scale
error = (decoded - x.to(torch.float32)).abs()
assert torch.isfinite(decoded).all()
assert torch.all(error <= scale + 1e-6)
def test_swizzle_blockscaled_chunk_kmajor_pads_rows_to_full_tiles():
rows, cols = 130, 8
generator = torch.Generator(device="cpu").manual_seed(23)
words = torch.randint(0, 2**31, (rows, cols), generator=generator, dtype=torch.int64).to(torch.uint32)
swizzled = swizzle_blockscaled_chunk_kmajor_scale_words(words)
assert swizzled.shape == (256, cols)
back = unswizzle_blockscaled_chunk_kmajor_scale_words(swizzled)
assert torch.equal(back[:rows], words)
assert bool((back[rows:] == 0).all())
@@ -1,101 +0,0 @@
import importlib.util
import sys
from pathlib import Path
import tilelang.testing
def _load_sm120_example(monkeypatch):
repo_root = Path(__file__).resolve().parents[3]
example = repo_root / "examples/gemm_sm120/sm120_nvfp4_blockscaled_gemm.py"
monkeypatch.setattr(sys, "argv", [str(example)])
spec = importlib.util.spec_from_file_location("sm120_nvfp4_blockscaled_gemm_example", example)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
def test_sm120_nvfp4_example_cli_keeps_internal_strategy_fixed(monkeypatch):
module = _load_sm120_example(monkeypatch)
args = module.parse_args()
assert not hasattr(args, "micro_pipeline")
assert not hasattr(args, "sf_layout")
assert not hasattr(args, "scale_storage_layout")
assert not hasattr(args, "manual_ws2")
assert module._SM120_SCALE_LAYOUT == "blockscaled_chunk_kmajor"
def test_sm120_nvfp4_example_source_has_no_legacy_strategy_flags():
repo_root = Path(__file__).resolve().parents[3]
source = (repo_root / "examples/gemm_sm120/sm120_nvfp4_blockscaled_gemm.py").read_text()
forbidden_flags = [
"--micro-pipeline",
"--manual-ws2",
"--manual-ws2-split",
"--manual-ws2-sf-load",
"--manual-ws2-sf-layout",
"--manual-ws2-ab-shared-storage",
"--manual-ws2-ab-copy-view",
"--scale-storage-layout",
"--sf-layout",
"--path",
"--run-cutlass",
"--cutlass-build-dir",
"--cutlass-binary",
"--rebuild-cutlass",
"--cmake",
"--nvcc",
]
for flag in forbidden_flags:
assert flag not in source
forbidden_harness_terms = [
"build_cutlass",
"subprocess",
"CMAKE_CUDA_COMPILER",
]
for term in forbidden_harness_terms:
assert term not in source
@tilelang.testing.requires_cuda_compute_version_eq(12, 0)
def test_sm120_nvfp4_example_kernel_handles_mn_tail_tiles(monkeypatch):
import pytest
torch = pytest.importorskip("torch")
from tilelang.quantize import swizzle_blockscaled_chunk_kmajor_scale_words
module = _load_sm120_example(monkeypatch)
for M, N, K in [(257, 384, 512), (130, 128, 256), (128, 136, 256)]:
kernel = module.sm120_nvfp4_blockscaled_gemm(M, N, K)
A = module._make_packed_fp4(M, K, seed=3)
B = module._make_packed_fp4(N, K, seed=4)
SFA_semantic = module._make_binary_scale_words(M, K, seed=5)
SFB_semantic = module._make_binary_scale_words(N, K, seed=6)
SFA = swizzle_blockscaled_chunk_kmajor_scale_words(SFA_semantic).reshape(-1, 4)
SFB = swizzle_blockscaled_chunk_kmajor_scale_words(SFB_semantic).reshape(-1, 4)
assert SFA.shape[0] % 128 == 0
assert SFB.shape[0] % 128 == 0
C = torch.empty((M, N), device="cuda", dtype=torch.bfloat16)
kernel(A, B, SFA, SFB, C)
torch.cuda.synchronize()
module._verify(A, B, SFA_semantic, SFB_semantic, C, torch.bfloat16)
def test_sm120_nvfp4_example_kernel_rejects_unsupported_tails(monkeypatch):
import pytest
module = _load_sm120_example(monkeypatch)
# simultaneous M and N tails hit a known copy-lowering boundary bug
with pytest.raises(ValueError, match="simultaneous M and N tail"):
module.sm120_nvfp4_blockscaled_gemm(257, 136, 512)
# bf16 output rows must stay 16-byte aligned
with pytest.raises(AssertionError, match="multiple of 8"):
module.sm120_nvfp4_blockscaled_gemm(128, 130, 256)
File diff suppressed because it is too large Load Diff