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

319 lines
10 KiB
Python

# Copyright Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT
"""
Ranking Regression Tests for Origami
These tests verify that config rankings remain stable across code changes.
Rankings are compared against golden baseline files to detect unintended
changes from PRs.
Usage:
# Run tests (compares against baseline)
pytest test_ranking_regression.py -v
# Generate new baseline files (run from develop branch)
pytest test_ranking_regression.py -v --generate-baseline
# Update baseline for specific architecture
pytest test_ranking_regression.py -v --generate-baseline -k gfx942
"""
import csv
from functools import lru_cache
from pathlib import Path
import pytest
import yaml
import origami
from helpers import HARDWARE, create_config_list, get_matrix_instructions
BASELINE_DIR = Path(__file__).parent / "baselines" / "rankings"
PROBLEM_DATA_FILE = Path(__file__).parent / "data" / "problem_data.csv"
SUPPORTED_DTYPES = ["f16", "bf16", "f32", "xf32", "f8"]
TRANSPOSE_VALUES = [origami.transpose_t.T, origami.transpose_t.N]
TOP_K = 5
def is_dtype_supported(arch_name: str, dtype: str) -> bool:
"""Check if a dtype is supported for the given architecture."""
hardware = HARDWARE[arch_name]
return len(get_matrix_instructions(hardware, dtype)) > 0
def load_problem_sizes() -> list[tuple[int, int, int, int]]:
"""Load problem sizes from CSV file.
Returns:
List of (m, n, k, batch) tuples.
"""
problems = []
with open(PROBLEM_DATA_FILE, "r") as f:
reader = csv.DictReader(f)
for row in reader:
m = int(row["m"])
n = int(row["n"])
k = int(row["k"])
batch = int(row["batch_count"])
problems.append((m, n, k, batch))
return problems
TEST_PROBLEM_SIZES = load_problem_sizes()
def create_problem(
m: int,
n: int,
k: int,
dtype: str,
batch: int = 1,
transA: origami.transpose_t = origami.transpose_t.T,
transB: origami.transpose_t = origami.transpose_t.N,
) -> origami.problem_t:
"""Create a problem specification."""
problem = origami.problem_t()
problem.size = origami.dim3_t(m, n, k)
problem.batch = batch
problem.a_transpose = transA
problem.b_transpose = transB
problem.a_dtype = origami.string_to_datatype(dtype)
problem.b_dtype = origami.string_to_datatype(dtype)
problem.d_dtype = origami.string_to_datatype(dtype)
problem.c_dtype = problem.d_dtype
problem.mi_dtype = problem.a_dtype
problem.a_mx_block_size = 0
problem.b_mx_block_size = 0
return problem
def config_to_tuple(config: origami.config_t) -> tuple:
"""Convert config_t to a compact tuple."""
return (
config.mt.m, config.mt.n, config.mt.k,
config.mi.m, config.mi.n, config.mi.k,
config.occupancy, config.workgroup_mapping,
)
def result_to_config_tuple(result: origami.prediction_result_t) -> list[int]:
"""Convert prediction result to a config tuple [mt_m, mt_n, mt_k, mi_m, mi_n, mi_k, occ, wgm]."""
cfg = result.config
return [
cfg.mt.m, cfg.mt.n, cfg.mt.k,
cfg.mi.m, cfg.mi.n, cfg.mi.k,
cfg.occupancy, cfg.workgroup_mapping,
]
def generate_rankings(
arch_name: str,
dtype: str,
transA: origami.transpose_t = origami.transpose_t.T,
transB: origami.transpose_t = origami.transpose_t.N,
) -> dict[str, list[list[int]]]:
"""Generate rankings for all test problem sizes.
Returns a dict mapping problem_key -> list of config tuples.
Each config tuple is [mt_m, mt_n, mt_k, mi_m, mi_n, mi_k, occ, wgm].
"""
hardware = HARDWARE[arch_name]
configs = create_config_list(
hardware, dtype,
mt_sizes=[16, 32, 48, 96, 128, 192, 224, 256, 336, 448, 512],
depth_unroll=[16, 32, 64, 128, 512, 1024],
occupancy_values=[1, 2],
wgm_values=[1, 4, 8],
)
if not configs:
return {}
rankings = {}
for m, n, k, batch in TEST_PROBLEM_SIZES:
problem = create_problem(m, n, k, dtype, batch, transA, transB)
try:
ranked = origami.select_topk_configs(problem, hardware, configs, TOP_K, origami.model_t.gemm)
if ranked:
key = f"{m}x{n}x{k}x{batch}"
rankings[key] = [result_to_config_tuple(r) for r in ranked]
except Exception:
pass
return rankings
try:
from yaml import CSafeDumper as SafeDumper, CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeDumper, SafeLoader
def get_baseline_path(arch_name: str) -> Path:
"""Get the path to the baseline file for an architecture."""
return BASELINE_DIR / f"{arch_name}.yaml"
@lru_cache(maxsize=None)
def load_arch_baseline(arch_name: str) -> dict | None:
"""Load and cache the full baseline for an architecture.
Returns the parsed YAML dict, or None if file doesn't exist.
"""
path = get_baseline_path(arch_name)
if not path.exists():
return None
with open(path, "r") as f:
return yaml.load(f, Loader=SafeLoader)
def transpose_key(transA: origami.transpose_t, transB: origami.transpose_t) -> str:
"""Convert transpose values to a string key for baseline storage."""
a = "T" if transA == origami.transpose_t.T else "N"
b = "T" if transB == origami.transpose_t.T else "N"
return f"{a}{b}"
def load_baseline(
arch_name: str, dtype: str, transA: origami.transpose_t, transB: origami.transpose_t
) -> dict[str, list[list[int]]] | None:
"""Load baseline rankings for a specific arch/dtype/transpose combination."""
baseline = load_arch_baseline(arch_name)
if baseline is None:
return None
try:
return baseline[dtype][transpose_key(transA, transB)]
except KeyError:
return None
def save_baseline(
arch_name: str,
dtype: str,
transA: origami.transpose_t,
transB: origami.transpose_t,
rankings: dict[str, list[list[int]]],
) -> None:
"""Save rankings to the architecture's YAML baseline file."""
BASELINE_DIR.mkdir(parents=True, exist_ok=True)
path = get_baseline_path(arch_name)
if path.exists():
with open(path, "r") as f:
baseline = yaml.load(f, Loader=SafeLoader) or {}
else:
baseline = {}
if dtype not in baseline:
baseline[dtype] = {}
baseline[dtype][transpose_key(transA, transB)] = rankings
# Write with compact format: flow style for problem entries, sorted keys
# Custom representer to use flow style for the innermost lists (config lists per problem)
class CompactDumper(SafeDumper):
pass
def represent_problem_configs(dumper, data):
# Use flow style for list of config lists (the value under each problem key)
return dumper.represent_sequence('tag:yaml.org,2002:seq', data, flow_style=True)
# Apply flow style to lists that contain lists (problem -> [[config], [config], ...])
CompactDumper.add_representer(list, represent_problem_configs)
with open(path, "w") as f:
yaml.dump(baseline, f, Dumper=CompactDumper, default_flow_style=False, sort_keys=True, width=1000)
# Clear cache so subsequent loads see the updated file
load_arch_baseline.cache_clear()
def compare_rankings(
current: dict[str, list[list[int]]], baseline: dict[str, list[list[int]]]
) -> list[str]:
"""
Compare current rankings against baseline.
Returns a list of differences found.
"""
differences = []
for problem_key, base_configs in baseline.items():
if problem_key not in current:
differences.append(f"Missing problem: {problem_key}")
continue
curr_configs = current[problem_key]
if len(curr_configs) != len(base_configs):
differences.append(
f"{problem_key}: Different rank count (curr={len(curr_configs)}, base={len(base_configs)})"
)
for rank_idx, (curr_cfg, base_cfg) in enumerate(zip(curr_configs, base_configs)):
if curr_cfg != base_cfg:
differences.append(
f"{problem_key} rank {rank_idx}: Config mismatch\n"
f" Current: MT={tuple(curr_cfg[0:3])}, MI={tuple(curr_cfg[3:6])}, occ={curr_cfg[6]}, wgm={curr_cfg[7]}\n"
f" Baseline: MT={tuple(base_cfg[0:3])}, MI={tuple(base_cfg[3:6])}, occ={base_cfg[6]}, wgm={base_cfg[7]}"
)
for problem_key in current:
if problem_key not in baseline:
differences.append(f"New problem not in baseline: {problem_key}")
return differences
@pytest.mark.regression
class TestRankingRegression:
"""Test suite for ranking regression tests."""
@pytest.mark.parametrize("arch_name", list(HARDWARE.keys()))
@pytest.mark.parametrize("dtype", SUPPORTED_DTYPES)
@pytest.mark.parametrize("transA", TRANSPOSE_VALUES)
@pytest.mark.parametrize("transB", TRANSPOSE_VALUES)
def test_ranking_stability(
self,
arch_name: str,
dtype: str,
transA: origami.transpose_t,
transB: origami.transpose_t,
generate_baseline: bool,
):
"""Test that rankings remain stable compared to baseline."""
trans_str = transpose_key(transA, transB)
if not is_dtype_supported(arch_name, dtype):
pytest.skip(f"No {dtype} support for {arch_name}")
current = generate_rankings(arch_name, dtype, transA, transB)
if not current:
pytest.skip(f"No valid configs generated for {arch_name}/{dtype}/{trans_str}")
if generate_baseline:
save_baseline(arch_name, dtype, transA, transB, current)
pytest.skip(f"Generated baseline for {arch_name}/{dtype}/{trans_str}")
baseline = load_baseline(arch_name, dtype, transA, transB)
if baseline is None:
pytest.fail(
f"No baseline file found for {arch_name}/{dtype}/{trans_str}. "
f"Run with --generate-baseline to create it."
)
differences = compare_rankings(current, baseline)
if differences:
diff_summary = "\n".join(differences[:10])
if len(differences) > 10:
diff_summary += f"\n... and {len(differences) - 10} more differences"
pytest.fail(
f"Ranking regression detected for {arch_name}/{dtype}/{trans_str}:\n{diff_summary}"
)