Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

267 lines
10 KiB
Python

#!/usr/bin/env python3
# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT
"""CPU-only unit tests for the microscaling (mx) GEMM TileEngine -> Dispatcher bridge.
Locks the config name format, the codegen-JSON projection, the dtype/layout/warp-tile
validity gate, the e8m0 scale codec, the fp8/fp4 quantization round-trips, and the numpy
microscaled reference. No GPU, no hipcc, no Old-TE builder import required.
"""
import sys
import unittest
from pathlib import Path
import numpy as np
_DISP = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(_DISP / "python"))
sys.path.insert(0, str(_DISP / "codegen"))
from mx_gemm_utils import ( # noqa: E402
SCALE_BLOCK,
E8M0_ONE,
MxGemmKernelConfig,
MxGemmProblem,
default_fp8_config,
default_fp4_config,
e8m0_to_float,
float_to_e8m0,
quantize_fp8,
dequantize_fp8,
quantize_fp4_packed,
dequantize_fp4_packed,
mx_gemm_reference,
)
class TestConfigName(unittest.TestCase):
def test_fallback_name_prefix(self):
cfg = default_fp8_config()
self.assertTrue(cfg._fallback_name().startswith("mx_gemm_fp8_rcr_"))
def test_fallback_name_encodes_tiles(self):
cfg = MxGemmKernelConfig(
datatype="fp4", tile_m=64, tile_n=128, tile_k=256,
warp_m=1, warp_n=2, warp_k=1,
warp_tile_m=16, warp_tile_n=16, warp_tile_k=128,
)
name = cfg._fallback_name()
self.assertIn("_fp4_rcr_", name)
self.assertIn("64x128x256", name)
self.assertIn("1x2x1", name)
self.assertIn("16x16x128", name)
self.assertNotIn(" ", name)
def test_persistent_suffix_only_when_set(self):
self.assertNotIn("True", default_fp8_config()._fallback_name())
cfg = default_fp8_config()
cfg.persistent = True
self.assertIn("True", cfg._fallback_name())
class TestCodegenJson(unittest.TestCase):
def test_projection_roundtrip(self):
cfg = MxGemmKernelConfig(
datatype="fp8", tile_m=128, tile_n=128, tile_k=128,
warp_m=2, warp_n=2, warp_k=1,
warp_tile_m=16, warp_tile_n=16, warp_tile_k=128,
k_block_per_cu=3,
# Pin the arch so to_codegen_config() does not shell out to rocminfo;
# keeps this a CPU-only test on non-ROCm runners.
gpu_target="gfx950",
)
j = cfg.to_codegen_config()
self.assertEqual(j["datatype"], "fp8")
self.assertEqual(j["layout"], "rcr")
self.assertEqual(j["tile_config"]["tile_k"], 128)
self.assertEqual(j["tile_config"]["warp_tile_k"], 128)
self.assertEqual(j["k_block_per_cu"], 3)
class TestValidity(unittest.TestCase):
def test_default_configs_valid(self):
self.assertTrue(default_fp8_config().is_valid())
self.assertTrue(default_fp4_config().is_valid())
def test_non_rcr_rejected(self):
cfg = default_fp8_config()
cfg.layout = "rrr"
self.assertFalse(cfg.is_valid())
def test_bad_dtype_rejected(self):
cfg = default_fp8_config()
cfg.datatype = "bf16"
self.assertFalse(cfg.is_valid())
def test_bf8_rejected(self):
# Old-TE argparse (choices=["fp4","fp8"]) + validate_gemm_mx never
# compile bf8/e5m2 for mx_gemm, so the bridge must reject it too. Assert
# both the config-level gate (is_valid) and, when importable, the
# codegen-level gate (_validate) refuse dtype="bf8".
cfg = default_fp8_config()
cfg.datatype = "bf8"
# Pin the arch so to_codegen_config() below stays CPU-only (no rocminfo).
cfg.gpu_target = "gfx950"
self.assertFalse(cfg.is_valid())
try:
from unified_mx_gemm_codegen import _validate # noqa: E402
except Exception as exc: # noqa: BLE001
self.skipTest(f"codegen import unavailable: {exc}")
with self.assertRaises(Exception):
_validate(cfg.to_codegen_config())
def test_wrong_warp_tile_rejected(self):
cfg = default_fp8_config()
cfg.warp_tile_k = 64
self.assertFalse(cfg.is_valid())
def test_indivisible_tile_rejected(self):
cfg = default_fp8_config()
cfg.tile_m = 100 # not a multiple of warp_m * warp_tile_m (2*16=32)
self.assertFalse(cfg.is_valid())
class TestProblem(unittest.TestCase):
def test_scale_k_and_flops(self):
p = MxGemmProblem(M=64, N=128, K=256)
self.assertEqual(p.scale_k, 256 // SCALE_BLOCK)
self.assertEqual(p.flops, 2 * 64 * 128 * 256)
def test_k_not_multiple_of_32_raises(self):
with self.assertRaises(ValueError):
MxGemmProblem(M=32, N=32, K=48)
class TestE8m0Codec(unittest.TestCase):
def test_one_is_byte_127(self):
self.assertEqual(int(float_to_e8m0(np.float32(1.0))), E8M0_ONE)
self.assertEqual(float(e8m0_to_float(E8M0_ONE)), 1.0)
def test_power_of_two_roundtrip(self):
for s in (0.25, 0.5, 1.0, 2.0, 4.0, 8.0):
b = float_to_e8m0(np.float32(s))
self.assertAlmostEqual(float(e8m0_to_float(b)), s, places=6)
def test_255_is_nan(self):
with np.errstate(over="ignore"):
self.assertTrue(np.isnan(float(e8m0_to_float(255))))
def test_non_positive_raises(self):
# Contract is a strictly-positive power-of-two; a 0.0/negative scale is a
# caller bug and must fail loudly rather than silently encode 1.0.
for bad in (0.0, -1.0, -0.0):
with self.assertRaises(ValueError):
float_to_e8m0(np.float32(bad))
with self.assertRaises(ValueError):
float_to_e8m0(np.array([1.0, 0.0, 2.0], np.float32))
def test_non_finite_raises(self):
with np.errstate(invalid="ignore"):
for bad in (np.inf, np.nan):
with self.assertRaises(ValueError):
float_to_e8m0(np.float32(bad))
class TestFp8Codec(unittest.TestCase):
def test_known_bytes(self):
self.assertEqual(int(quantize_fp8(np.array([1.0], np.float32))[0]), 0x38)
self.assertEqual(int(quantize_fp8(np.array([-2.0], np.float32))[0]), 0xC0)
def test_roundtrip_grid(self):
grid = np.array([-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0], np.float32)
vals = np.tile(grid, (4, 1))
self.assertTrue(np.array_equal(dequantize_fp8(quantize_fp8(vals)), vals))
def test_off_grid_raises(self):
# The vectorized codec must reject values not on the exact e4m3 grid
# instead of snapping them to a neighbour byte.
with self.assertRaises(KeyError):
quantize_fp8(np.array([[0.3]], np.float32))
def test_neg_zero_collapses_to_zero_byte(self):
self.assertEqual(int(quantize_fp8(np.array([-0.0], np.float32))[0]), 0x00)
def test_shape_preserved_2d(self):
vals = np.full((3, 5), 1.0, np.float32)
self.assertEqual(quantize_fp8(vals).shape, (3, 5))
class TestFp4Codec(unittest.TestCase):
def test_pack_two_per_byte(self):
# one row, K=2 -> a single packed byte; low nibble even-K, high nibble odd-K.
vals = np.array([[1.0, 2.0]], np.float32) # codes: 1.0->2, 2.0->4
packed = quantize_fp4_packed(vals)
self.assertEqual(packed.shape, (1, 1))
self.assertEqual(int(packed[0, 0]), (4 << 4) | 2)
def test_roundtrip_grid(self):
grid = np.array([-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0], np.float32)
rng = np.random.default_rng(0)
vals = rng.choice(grid, size=(4, 8)).astype(np.float32)
packed = quantize_fp4_packed(vals)
self.assertEqual(packed.shape, (4, 4))
self.assertTrue(np.array_equal(dequantize_fp4_packed(packed, 8), vals))
def test_off_grid_raises(self):
# 2.5 exists in fp8 e4m3 but NOT in the fp4 e2m1 grid -> must reject.
with self.assertRaises(KeyError):
quantize_fp4_packed(np.array([[2.5, 1.0]], np.float32))
class TestReference(unittest.TestCase):
def _inputs(self, M, N, K, seed=0):
rng = np.random.default_rng(seed)
grid = np.array([-2.0, -1.0, -0.5, 0.5, 1.0, 2.0], np.float32)
A = rng.choice(grid, size=(M, K)).astype(np.float32)
B = rng.choice(grid, size=(K, N)).astype(np.float32)
return A, B
def test_unit_scales_equals_plain_matmul(self):
M, N, K = 4, 3, 32
A, B = self._inputs(M, N, K)
prob = MxGemmProblem(M=M, N=N, K=K)
one = int(float_to_e8m0(np.float32(1.0)))
sa = np.full((M, prob.scale_k), one, np.uint8)
sb = np.full((N, prob.scale_k), one, np.uint8)
ref = mx_gemm_reference(A, B, sa, sb, prob).astype(np.float32)
plain = (A @ B).astype(np.float16).astype(np.float32)
self.assertTrue(np.allclose(ref, plain, atol=1e-2))
def test_power_of_two_scale_multiplies(self):
M, N, K = 4, 3, 32
A, B = self._inputs(M, N, K, seed=1)
prob = MxGemmProblem(M=M, N=N, K=K)
two = int(float_to_e8m0(np.float32(2.0)))
sa = np.full((M, prob.scale_k), two, np.uint8)
sb = np.full((N, prob.scale_k), two, np.uint8)
ref = mx_gemm_reference(A, B, sa, sb, prob).astype(np.float32)
# scale_a=2 and scale_b=2 -> product scaled by 4.
expected = (4.0 * (A @ B)).astype(np.float16).astype(np.float32)
self.assertTrue(np.allclose(ref, expected, atol=1e-1))
class TestCodegenNameContract(unittest.TestCase):
"""Optional: byte-exact name parity when the Old-TE builder is importable."""
def test_codegen_name_matches_config(self):
try:
from unified_mx_gemm_codegen import kernel_name
except Exception as exc: # noqa: BLE001
self.skipTest(f"codegen import unavailable: {exc}")
cfg = default_fp8_config()
# Pin the arch so to_codegen_config() stays CPU-only (no rocminfo).
cfg.gpu_target = "gfx950"
try:
name = kernel_name(cfg.to_codegen_config())
except Exception as exc: # noqa: BLE001
self.skipTest(f"Old-TE builder unavailable: {exc}")
self.assertTrue(name.startswith("mx_gemm_fp8_rcr_"))
if __name__ == "__main__":
unittest.main()