Files
TheRock/rocm-libraries/projects/composablekernel/dispatcher/tests/test_multi_abd_bridge.py
T
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

263 lines
9.9 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 multi_abd GEMM bridge.
The Tile Engine -> Dispatcher multi_abd bridge relies on one hard invariant:
``GemmKernelConfig.name`` must reproduce, byte-for-byte, the kernel stem that
``unified_gemm_codegen.py`` bakes into the generated kernel (and that the .so
reports at runtime). For multi_abd that stem carries the 4-char (A,B,E,D)
layout plus a ``_multiabd_a{na}_b{nb}_d{nd}_{aop}_{bop}_{cdeop}`` suffix, so
distinct tensor counts / element-wise ops can never collapse onto one kernel.
These tests exercise only pure host-side logic (name generation, the codegen
JSON projection, and the shipped configs/*.json). No GPU, hipcc, or build is
required, so the suite runs green in CPU-only CI.
Run: python3 -m pytest tests/test_multi_abd_bridge.py -v
"""
import ctypes
import json
import sys
import unittest
from pathlib import Path
from unittest import mock
import numpy as np
SCRIPT_DIR = Path(__file__).parent.resolve()
DISPATCHER_DIR = SCRIPT_DIR.parent
REPO_ROOT = DISPATCHER_DIR.parent
sys.path.insert(0, str(DISPATCHER_DIR / "python"))
from gemm_utils import ( # noqa: E402
GemmKernelConfig,
MultiABDDispatcherLib,
_output_dtype,
_dtype_from_kernel_name,
)
# The shipped multi_abd sweep configs the bridge codegens from.
_CONFIG_DIR = (
REPO_ROOT
/ "tile_engine"
/ "ops"
/ "gemm"
/ "gemm_multi_abd"
/ "configs"
)
def _make_config(**overrides):
"""A canonical multi_abd config; overrides tweak individual fields."""
kw = dict(
dtype_a="fp16",
dtype_b="fp16",
dtype_c="fp16",
dtype_acc="fp32",
layout_a="row",
layout_b="col",
layout_c="row",
layout_d="row",
variant="multi_abd",
num_a_tensors=2,
num_b_tensors=2,
num_d_tensors=2,
)
kw.update(overrides)
return GemmKernelConfig(**kw)
class TestMultiAbdName(unittest.TestCase):
"""The multi_abd kernel-name contract (the byte-parity invariant)."""
def test_name_carries_multiabd_suffix(self):
cfg = _make_config()
name = cfg.name
# 4-char (A,B,E,D) layout, not the 3-char C layout.
self.assertIn("_rcrr_", name)
# multiabd tensor-count + op suffix, exactly as codegen emits it.
self.assertTrue(
name.endswith(
"_multiabd_a2_b2_d2_PassThrough_PassThrough_PassThrough"
),
name,
)
def test_full_stem_is_stable(self):
# Pin the entire stem so any drift in the naming scheme is caught.
cfg = _make_config(
tile_m=128, tile_n=128, tile_k=32,
wave_m=2, wave_n=2, wave_k=1,
warp_tile_m=32, warp_tile_n=32, warp_tile_k=16,
pipeline="compv4", scheduler="intrawave", epilogue="cshuffle",
pad_m=True, pad_n=True, pad_k=True, persistent=False,
)
expected = (
"gemm_fp16_rcrr_compv4_cshuffle_intrawave"
"_True_True_True_False"
"_128x128x32_2x2x1_32x32x16"
"_multiabd_a2_b2_d2_PassThrough_PassThrough_PassThrough"
)
self.assertEqual(cfg.name, expected)
def test_tensor_counts_change_the_name(self):
# Distinct tensor counts must not collapse onto one kernel name.
base = _make_config().name
more = _make_config(num_a_tensors=3, num_d_tensors=1).name
self.assertNotEqual(base, more)
self.assertIn("_a3_", more)
self.assertIn("_d1_", more)
def test_elementwise_ops_change_the_name(self):
base = _make_config().name
scaled = _make_config(cde_elementwise_op="AddScale").name
self.assertNotEqual(base, scaled)
self.assertTrue(scaled.endswith("_PassThrough_PassThrough_AddScale"))
def test_dtype_recovers_from_name(self):
# The runner reads the input dtype straight out of the compiled .so
# name, so every dtype the bridge builds must round-trip.
for dtype in ("fp16", "bf16"):
cfg = _make_config(
dtype_a=dtype, dtype_b=dtype, dtype_c=_output_dtype(dtype),
)
self.assertEqual(_dtype_from_kernel_name(cfg.name), dtype)
def test_name_carries_four_char_layout(self):
# multi_abd uses the 4-char (A,B,E,D) layout code in the stem; the D
# char must reflect layout_d independently of the C layout.
for la, lb, lc, ld in (
("row", "col", "row", "row"),
("row", "row", "row", "col"),
("col", "col", "row", "row"),
):
cfg = _make_config(
layout_a=la, layout_b=lb, layout_c=lc, layout_d=ld,
)
self.assertIn(f"_{cfg.layout4}_", cfg.name)
class TestMultiAbdCodegenJson(unittest.TestCase):
"""The codegen JSON projection must carry the multi_abd block."""
def test_codegen_json_has_multi_abd_block(self):
cfg = _make_config(
num_a_tensors=2, num_b_tensors=2, num_d_tensors=2,
cde_elementwise_op="AddScale",
)
j = cfg.to_codegen_json()
self.assertIn("multi_abd_config", j)
mabd = j["multi_abd_config"]
self.assertEqual(mabd["num_a_tensors"], 2)
self.assertEqual(mabd["num_b_tensors"], 2)
self.assertEqual(mabd["num_d_tensors"], 2)
self.assertEqual(mabd["cde_elementwise_op"], "AddScale")
def test_codegen_json_core_blocks(self):
j = _make_config().to_codegen_json()
self.assertIn("tile_config", j)
self.assertIn("trait_config", j)
# dispatcher wave_* is projected onto codegen warp_* (warps per block).
self.assertEqual(j["tile_config"]["warp_m"], [2])
class TestMultiAbdShippedConfigs(unittest.TestCase):
"""The configs/*.json the bridge codegens from must be well-formed sweeps."""
def test_config_dir_exists(self):
self.assertTrue(_CONFIG_DIR.is_dir(), _CONFIG_DIR)
def test_configs_are_valid_sweeps(self):
configs = sorted(_CONFIG_DIR.glob("*.json"))
self.assertGreater(len(configs), 0, "no multi_abd configs shipped")
for path in configs:
with self.subTest(config=path.name):
with open(path) as f:
data = json.load(f)
self.assertIn("tile_config", data)
self.assertIn("trait_config", data)
tc = data["tile_config"]
for key in (
"tile_m", "tile_n", "tile_k",
"warp_m", "warp_n", "warp_k",
"warp_tile_m", "warp_tile_n", "warp_tile_k",
):
self.assertIn(key, tc, f"{path.name} missing {key}")
tr = data["trait_config"]
for key in ("pipeline", "scheduler", "epilogue"):
self.assertIn(key, tr, f"{path.name} missing {key}")
class TestMultiAbdAbiMarshalling(unittest.TestCase):
"""GPU-free tests of the C-ABI marshalling in ``MultiABDDispatcherLib``.
The ``.so`` is mocked (``ctypes.CDLL`` patched), so no generated kernel or
GPU is needed. These cover the highest-risk-but-previously-untested surface:
the declared ABI, the pointer/stride array packing, the positional argument
order, and status forwarding.
"""
def _make_lib(self, status=0):
fake = mock.MagicMock()
fake.dispatcher_run_multi_abd.return_value = status
with mock.patch("ctypes.CDLL", return_value=fake):
lib = MultiABDDispatcherLib(Path("/nonexistent/multi_abd.so"))
return lib, fake
def test_run_abi_argtypes(self):
# The declared argtypes must match, in count and key types, the
# positional arguments run() actually passes (19).
_, fake = self._make_lib()
argt = fake.dispatcher_run_multi_abd.argtypes
self.assertEqual(len(argt), 19)
for i in range(3): # A/B/D host-pointer arrays
self.assertEqual(argt[i], ctypes.POINTER(ctypes.c_void_p))
self.assertEqual(argt[3], ctypes.c_void_p) # E host pointer
for i in (4, 5, 6): # A/B/D leading-stride arrays
self.assertEqual(argt[i], ctypes.POINTER(ctypes.c_int64))
self.assertEqual(argt[-1], ctypes.POINTER(ctypes.c_float)) # time_ms
def test_run_marshals_pointer_arrays_counts_and_order(self):
lib, fake = self._make_lib(status=0)
na, nb, nd = 2, 2, 2
as_arrays = [np.zeros(4, np.float16) for _ in range(na)]
bs_arrays = [np.zeros(4, np.float16) for _ in range(nb)]
ds_arrays = [np.zeros(4, np.float16) for _ in range(nd)]
e = np.zeros(4, np.float16)
status, _ = lib.run(
as_arrays, bs_arrays, ds_arrays, e,
M=2, N=2, K=2, elem_a=2, elem_b=2, elem_d=2, elem_e=2,
stride_as=[2, 2], stride_bs=[2, 2], stride_ds=[2, 2], stride_e=2,
)
self.assertEqual(status, 0)
fake.dispatcher_run_multi_abd.assert_called_once()
args = fake.dispatcher_run_multi_abd.call_args[0]
self.assertEqual(len(args), 19)
# A/B/D host-pointer arrays carry exactly one entry per tensor.
self.assertEqual(len(args[0]), na)
self.assertEqual(len(args[1]), nb)
self.assertEqual(len(args[2]), nd)
# Tensor counts occupy positions 12/13/14; M/N/K positions 15/16/17.
self.assertEqual((args[12], args[13], args[14]), (na, nb, nd))
self.assertEqual((args[15], args[16], args[17]), (2, 2, 2))
def test_run_forwards_nonzero_status(self):
# A thin shim must surface the C error code (e.g. -3 tensor-count
# mismatch) verbatim rather than swallowing it.
lib, fake = self._make_lib(status=-3)
a = [np.zeros(4, np.float16)]
status, _ = lib.run(
a, a, a, np.zeros(4, np.float16),
M=1, N=1, K=1, elem_a=2, elem_b=2, elem_d=2, elem_e=2,
stride_as=[1], stride_bs=[1], stride_ds=[1], stride_e=1,
)
self.assertEqual(status, -3)
if __name__ == "__main__":
unittest.main()