From 0c31b5a12171eb538aba0dc3f9f0b23b2cd066d8 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Sun, 4 Oct 2026 15:42:57 -0400 Subject: [PATCH 01/52] hkp: packaging probes stream A+B --- .../probes/tests/conftest.py | 95 +++++++ .../probes/tests/test_hkp_probe_assert.py | 208 +++++++++++++++ .../tests/test_hkp_probe_derive_root.py | 125 +++++++++ .../tools/hkp_probe_assert.py | 241 ++++++++++++++++++ .../tools/hkp_probe_derive_root.py | 69 +++++ 5 files changed, 738 insertions(+) create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py create mode 100755 dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py new file mode 100644 index 000000000000..75b264c17a94 --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py @@ -0,0 +1,95 @@ +import hashlib +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +_PROBES_TESTS_DIR = Path(__file__).resolve().parent +_PKG_DIR = _PROBES_TESTS_DIR.parent.parent +_HKP_DIR = _PKG_DIR.parent + +ARCH = "gfx950" +STAMP_NAME = ".hkp-packed.stamp" +HKP_PACK = _PKG_DIR / "tools" / "hkp_pack.py" +PROBE_ASSERT = _PKG_DIR / "tools" / "hkp_probe_assert.py" +ROCKE_FIXTURE = _PKG_DIR / "tests" / "fixtures" / "rocke" +_ROCKE_SOURCE_DIRS = ( + _HKP_DIR / "rocke" / "platform" / "python", + _HKP_DIR / "rocke" / "library", +) + + +@pytest.fixture(scope="session") +def rocm_kpack_dir(): + """The rocm_kpack python dir; a missing one is a hard failure, never a skip.""" + value = os.environ.get("HIPKERNELPROVIDER_ROCM_KPACK_DIR") + if not value or not Path(value).is_dir(): + pytest.fail( + "HIPKERNELPROVIDER_ROCM_KPACK_DIR must name the rocm_kpack python dir" + ) + return value + + +@pytest.fixture(scope="session") +def hipcc(): + value = os.environ.get("HKP_HIPCC") + if not value: + pytest.fail("HKP_HIPCC must name the hipcc driver") + return value + + +@pytest.fixture(scope="session") +def comgr_lib(): + """The comgr library the pack is steered to, or None when the environment + does not pin one (rocke then resolves its own).""" + return os.environ.get("ROCKE_COMGR_LIB") or None + + +@pytest.fixture(scope="module") +def packed_root(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): + """Pack the rocke fixture once for gfx950 and return the output root. + + The stamp file is written by CMake in production; it is created here so the + stamp assertion has something to find. + """ + work = tmp_path_factory.mktemp("probe_pack") + out = work / "out" + wheel_stamp = work / "rocke-wheel.stamp" + wheel_stamp.write_text(hashlib.sha256(b"probe-test-wheel").hexdigest() + "\n") + env = dict(os.environ) + env["PYTHONPATH"] = os.pathsep.join( + [str(p) for p in _ROCKE_SOURCE_DIRS] + [env.get("PYTHONPATH", "")] + ).rstrip(os.pathsep) + if comgr_lib: + env["ROCKE_COMGR_LIB"] = comgr_lib + result = subprocess.run( + [ + sys.executable, + str(HKP_PACK), + "--source-root", + str(ROCKE_FIXTURE), + "--out-root", + str(out), + "--arches", + ARCH, + "--hipcc", + hipcc, + "--inter-root", + str(work / "inter"), + "--kpack-python-dir", + rocm_kpack_dir, + "--source-label", + "probe_assert_test", + "--rocke-wheel-stamp", + str(wheel_stamp), + ], + env=env, + capture_output=True, + text=True, + ) + if result.returncode != 0: + pytest.fail(f"hkp_pack failed ({result.returncode}):\n{result.stderr}") + (out / STAMP_NAME).write_text("") + return out diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py new file mode 100644 index 000000000000..19e80da1f415 --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py @@ -0,0 +1,208 @@ +import json +import shutil +import subprocess +import sys + +import pytest + +from conftest import ARCH, PROBE_ASSERT, STAMP_NAME + +KDP = f"{ARCH}/attention.kdp.json" + + +class _Tree: + """A mutable copy of the packed output plus the assert-tool invocation.""" + + def __init__(self, root, rocm_kpack_dir, comgr_path): + self.root = root + self.rocm_kpack_dir = rocm_kpack_dir + self.comgr_path = comgr_path + + def kdp(self): + return json.loads((self.root / KDP).read_text(encoding="utf-8")) + + def mutate_ukd(self, edit): + kdp = self.kdp() + edit(kdp["kernelDescriptors"][0]) + (self.root / KDP).write_text(json.dumps(kdp, indent=2), encoding="utf-8") + + def mutate_kdp(self, edit): + kdp = self.kdp() + edit(kdp) + (self.root / KDP).write_text(json.dumps(kdp, indent=2), encoding="utf-8") + + def run(self, expect_ukds=1, expect_comgr=None, root=None): + cmd = [ + sys.executable, + str(PROBE_ASSERT), + "--out-root", + str(self.root if root is None else root), + "--arch", + ARCH, + "--kind", + "rocke", + "--kpack-python-dir", + self.rocm_kpack_dir, + "--stamp-name", + STAMP_NAME, + "--expect-ukds", + str(expect_ukds), + ] + if expect_comgr is not None: + cmd += ["--expect-comgr", str(expect_comgr)] + return subprocess.run(cmd, capture_output=True, text=True) + + +@pytest.fixture +def tree(packed_root, tmp_path, rocm_kpack_dir, comgr_lib): + copy = tmp_path / "out" + shutil.copytree(packed_root, copy) + comgr = ( + comgr_lib + or json.loads((copy / KDP).read_text(encoding="utf-8"))["kernelDescriptors"][0][ + "provenance" + ]["comgr_path"] + ) + return _Tree(copy, rocm_kpack_dir, comgr) + + +def _assert_fails(result, assertion_id): + assert result.returncode == 1, result.stdout + result.stderr + lines = [ + ln + for ln in result.stderr.splitlines() + if ln.startswith("hkp_probe_assert: FAIL") + ] + assert lines, result.stderr + assert any(f"FAIL {assertion_id}:" in ln for ln in lines), result.stderr + + +def test_pristine_pack_passes(tree): + result = tree.run() + assert result.returncode == 0, result.stderr + assert result.stderr == "" + + +def test_pristine_pack_passes_with_expected_comgr(tree): + result = tree.run(expect_comgr=tree.comgr_path) + assert result.returncode == 0, result.stderr + + +def test_out_root_missing(tree): + result = tree.run(root=tree.root / "nope") + _assert_fails(result, "out-root-missing") + assert "build target hkp_packaging_probes first" in result.stderr + + +def test_stamp_missing(tree): + (tree.root / STAMP_NAME).unlink() + _assert_fails(tree.run(), "stamp-missing") + + +def test_arch_dir_missing(tree): + shutil.rmtree(tree.root / ARCH) + _assert_fails(tree.run(), "arch-dir-missing") + + +def test_extra_arch_dir(tree): + (tree.root / "gfx942").mkdir() + _assert_fails(tree.run(), "extra-arch-dir") + + +def test_kpack_missing(tree): + (tree.root / ARCH / "kpack" / f"hip_kernel_provider_{ARCH}.kpack").unlink() + _assert_fails(tree.run(), "kpack-missing") + + +def test_kpack_empty(tree): + (tree.root / ARCH / "kpack" / f"hip_kernel_provider_{ARCH}.kpack").write_bytes(b"") + _assert_fails(tree.run(), "kpack-empty") + + +def test_no_kdp(tree): + (tree.root / KDP).unlink() + _assert_fails(tree.run(), "no-kdp") + + +def test_ukd_count_mismatch(tree): + _assert_fails(tree.run(expect_ukds=2), "ukd-count") + + +def test_ukd_count_zero_is_failure(tree): + tree.mutate_kdp(lambda kdp: kdp.update(kernelDescriptors=[])) + _assert_fails(tree.run(expect_ukds=0), "ukd-count") + + +def test_ukd_kind(tree): + tree.mutate_ukd(lambda u: u["kernel_source"].update(kind="rocke")) + _assert_fails(tree.run(), "ukd-kind") + + +def test_arch_field_ukd(tree): + tree.mutate_ukd(lambda u: u.update(arch=["gfx942"])) + _assert_fails(tree.run(), "arch-field") + + +def test_arch_field_kdp(tree): + tree.mutate_kdp(lambda kdp: kdp.update(arch=[ARCH, "gfx942"])) + _assert_fails(tree.run(), "arch-field") + + +def test_kpack_toc(tree): + tree.mutate_ukd(lambda u: u["kernel_source"].update(toc_key="no_such_key")) + _assert_fails(tree.run(), "kpack-toc") + + +def test_sha256(tree): + tree.mutate_ukd(lambda u: u["kernel_source"].update(sha256="0" * 64)) + _assert_fails(tree.run(), "sha256") + + +def test_signature(tree): + tree.mutate_ukd(lambda u: u["kernel_source"].update(signature=[])) + _assert_fails(tree.run(), "signature") + + +def test_symbol(tree): + tree.mutate_ukd(lambda u: u["kernel_source"].update(symbol="no_such_symbol_xyz")) + _assert_fails(tree.run(), "symbol") + + +def test_provenance_origin(tree): + tree.mutate_ukd(lambda u: u["provenance"].update(origin_kind="hip")) + _assert_fails(tree.run(), "provenance-origin") + + +def test_provenance_wheel_absent(tree): + tree.mutate_ukd(lambda u: u["provenance"].pop("rocke_wheel_sha256")) + _assert_fails(tree.run(), "provenance-wheel") + + +def test_provenance_wheel_empty(tree): + tree.mutate_ukd(lambda u: u["provenance"].update(rocke_wheel_sha256="")) + _assert_fails(tree.run(), "provenance-wheel") + + +def test_provenance_comgr_wrong_expected(tree): + result = tree.run(expect_comgr=tree.root / "not" / "libamd_comgr.so") + _assert_fails(result, "provenance-comgr") + + +def test_provenance_comgr_empty_without_expectation(tree): + tree.mutate_ukd(lambda u: u["provenance"].update(comgr_path="")) + _assert_fails(tree.run(), "provenance-comgr") + + +def test_corrupt_kpack_reports_toc_failure(tree): + kpack_path = tree.root / ARCH / "kpack" / f"hip_kernel_provider_{ARCH}.kpack" + kpack_path.write_bytes(b"not a kpack archive" * 8) + result = tree.run() + _assert_fails(result, "kpack-toc") + assert "Traceback" not in result.stderr + + +def test_malformed_kdp_json_reports_no_kdp(tree): + (tree.root / KDP).write_text("{ not json", encoding="utf-8") + result = tree.run() + _assert_fails(result, "no-kdp") + assert "Traceback" not in result.stderr diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py new file mode 100644 index 000000000000..bc9f88c13131 --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py @@ -0,0 +1,125 @@ +"""Tests for hkp_probe_derive_root.py (pure stdlib; no compile, no kpack).""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +_DP = Path(__file__).resolve().parents[2] +_TOOL = _DP / "tools" / "hkp_probe_derive_root.py" +_PRODUCTION_ROOT = ( + _DP.parent + / "src/engines/kernel_ingestor_engine/descriptors/rocKE/gfx950_attention_dense" +) +_PRODUCTION_KDP = "gfx950_attention_dense.kdp.json" +_PRODUCTION_INSTANCE = "attention_dense.bf16_d64_hq8_kv1_c1_bm256_bn64.gfx950" + + +def _run(src: Path, kdp: str, instance: str, out: Path): + return subprocess.run( + [ + sys.executable, + str(_TOOL), + "--from", + str(src), + "--kdp", + kdp, + "--instance-name", + instance, + "--out", + str(out), + ], + capture_output=True, + text=True, + check=False, + ) + + +def _write_synthetic_root(root: Path, names: list[str]) -> None: + root.mkdir() + kdp = { + "version": "1.0", + "id": "kdp-id", + "arch": "gfx950", + "kernelDescriptors": [{"name": n, "payload": i} for i, n in enumerate(names)], + } + (root / "x.kdp.json").write_text(json.dumps(kdp, indent=2) + "\n") + (root / "x.kmd.json").write_text('{"id": "kmd"}\n') + + +def test_instance_absent_exits_2(tmp_path): + src = tmp_path / "src" + _write_synthetic_root(src, ["a", "b"]) + r = _run(src, "x.kdp.json", "missing", tmp_path / "out") + assert r.returncode == 2 + assert ( + r.stderr.strip() + == "hkp_probe_derive: instance 'missing' not found in x.kdp.json (2 descriptors)" + ) + assert not (tmp_path / "out").exists() + + +def test_instance_duplicated_exits_2(tmp_path): + src = tmp_path / "src" + _write_synthetic_root(src, ["a", "dup", "dup"]) + r = _run(src, "x.kdp.json", "dup", tmp_path / "out") + assert r.returncode == 2 + assert "hkp_probe_derive: instance 'dup' matched 2 times" in r.stderr + assert not (tmp_path / "out").exists() + + +def test_kdp_absent_exits_2(tmp_path): + src = tmp_path / "src" + _write_synthetic_root(src, ["a"]) + r = _run(src, "nope.kdp.json", "a", tmp_path / "out") + assert r.returncode == 2 + assert r.stderr.strip() == f"hkp_probe_derive: nope.kdp.json not found under {src}" + + +def test_stale_output_removed(tmp_path): + src = tmp_path / "src" + _write_synthetic_root(src, ["a", "b"]) + out = tmp_path / "out" + out.mkdir() + (out / "stale.txt").write_text("stale") + r = _run(src, "x.kdp.json", "b", out) + assert r.returncode == 0, r.stderr + assert not (out / "stale.txt").exists() + kdp = json.loads((out / "x.kdp.json").read_text()) + assert [d["name"] for d in kdp["kernelDescriptors"]] == ["b"] + assert (out / "x.kmd.json").read_bytes() == (src / "x.kmd.json").read_bytes() + + +def test_real_production_root(tmp_path): + out = tmp_path / "out" + r = _run(_PRODUCTION_ROOT, _PRODUCTION_KDP, _PRODUCTION_INSTANCE, out) + assert r.returncode == 0, r.stderr + + src_kdp = json.loads((_PRODUCTION_ROOT / _PRODUCTION_KDP).read_text()) + out_kdp = json.loads((out / _PRODUCTION_KDP).read_text()) + assert [d["name"] for d in out_kdp["kernelDescriptors"]] == [_PRODUCTION_INSTANCE] + assert list(out_kdp) == list(src_kdp) + for key in ("id", "engine", "dispatch", "matchers", "arch", "provenance"): + assert out_kdp[key] == src_kdp[key] + + siblings = sorted( + p.name for p in _PRODUCTION_ROOT.iterdir() if p.name != _PRODUCTION_KDP + ) + assert len(siblings) == 5 + for name in siblings: + assert (out / name).read_bytes() == (_PRODUCTION_ROOT / name).read_bytes() + assert sorted(p.name for p in out.iterdir()) == sorted(siblings + [_PRODUCTION_KDP]) + + +def test_production_instance_exactly_once(): + kdp = json.loads((_PRODUCTION_ROOT / _PRODUCTION_KDP).read_text()) + names = [d["name"] for d in kdp["kernelDescriptors"]] + assert names.count(_PRODUCTION_INSTANCE) == 1 + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-v"])) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py new file mode 100755 index 000000000000..351a1763188c --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +"""Assert the output of a packaging probe. + +A packaging probe packs one integration's descriptors for one explicit +architecture. This tool inspects the packed output root and fails, with one +greppable line per failure, unless every shipped descriptor is a well-formed +kpack-backed kernel produced by the expected producer: + + hkp_probe_assert: FAIL : + +Exit status is 0 only when every assertion holds, 1 when any assertion fails, +and 2 for an unusable invocation (rocm_kpack not importable). Plain python, not +pytest: a script has no skip path, so it cannot pass without having checked. + +Assertion ids: + out-root-missing --out-root is not a directory + stamp-missing / does not exist + arch-dir-missing // does not exist + extra-arch-dir another gfx* directory sits beside // + kpack-missing //kpack/hip_kernel_provider_.kpack absent + kpack-empty that archive has size 0 + no-kdp no *.kdp.json under //, or one is not valid JSON + ukd-count kpack-kind UKD count differs from --expect-ukds (or is 0) + ukd-kind a UKD's kernel_source.kind is not "kpack" + arch-field a KDP or UKD `arch` is not [] + kpack-toc the archive is unreadable or has no entry for a UKD's toc_key + sha256 the archive blob's sha256 differs from kernel_source.sha256 + signature a UKD's signature is not a non-empty list + symbol a UKD's symbol does not appear in its archive blob + provenance-origin provenance.origin_kind is not the --kind producer + provenance-wheel provenance.rocke_wheel_sha256 is absent or empty + provenance-comgr provenance.comgr_path is not the expected comgr library +""" + +import argparse +import hashlib +import json +import os +import sys +from pathlib import Path + +_PREFIX = "hkp_probe_assert" + + +class _Failures: + def __init__(self): + self.lines = [] + + def add(self, assertion_id, detail): + self.lines.append(f"{_PREFIX}: FAIL {assertion_id}: {detail}") + + def __bool__(self): + return bool(self.lines) + + +def _parse_args(argv): + p = argparse.ArgumentParser( + description="Assert the output of a hip-kernel-provider packaging probe." + ) + p.add_argument("--out-root", required=True, type=Path) + p.add_argument("--arch", required=True) + p.add_argument("--kind", required=True, choices=["rocke"]) + p.add_argument("--kpack-python-dir", required=True) + p.add_argument("--stamp-name", required=True) + p.add_argument("--expect-ukds", required=True, type=int) + p.add_argument("--expect-comgr", default=None) + return p.parse_args(argv) + + +def _load_kpack(kpack_python_dir): + if kpack_python_dir not in sys.path: + sys.path.insert(0, kpack_python_dir) + from rocm_kpack import kpack + + return kpack + + +def _same_path(a, b): + return os.path.normcase(os.path.realpath(a)) == os.path.normcase( + os.path.realpath(b) + ) + + +def _check_layout(args, failures): + """Checks 2-4. Returns the arch dir, or None when later checks are moot.""" + out = args.out_root + if not (out / args.stamp_name).exists(): + failures.add("stamp-missing", f"{out / args.stamp_name} does not exist") + + arch_dir = out / args.arch + if not arch_dir.is_dir(): + failures.add("arch-dir-missing", f"{arch_dir} is not a directory") + arch_dir = None + others = sorted( + c.name + for c in out.iterdir() + if c.is_dir() and c.name.startswith("gfx") and c.name != args.arch + ) + if others: + failures.add( + "extra-arch-dir", + f"expected only {args.arch} under {out}, also found {', '.join(others)}", + ) + return arch_dir + + +def _check_kpack(args, arch_dir, failures): + """Check 4. Returns the archive path when it exists and is non-empty.""" + kpack_path = arch_dir / "kpack" / f"hip_kernel_provider_{args.arch}.kpack" + if not kpack_path.is_file(): + failures.add("kpack-missing", f"{kpack_path} does not exist") + return None + if kpack_path.stat().st_size == 0: + failures.add("kpack-empty", f"{kpack_path} has size 0") + return None + return kpack_path + + +def _check_ukd(args, kdp_name, ukd, archive, failures): + label = f"{kdp_name}:{ukd.get('name', ukd.get('id', '?'))}" + source = ukd.get("kernel_source") or {} + kind = source.get("kind") + if kind != "kpack": + failures.add("ukd-kind", f'{label} kernel_source.kind is {kind!r}, not "kpack"') + return False + + if "arch" in ukd and ukd["arch"] != [args.arch]: + failures.add("arch-field", f"{label} arch is {ukd['arch']!r}") + + signature = source.get("signature") + if not isinstance(signature, list) or not signature: + failures.add("signature", f"{label} signature is {signature!r}") + + prov = ukd.get("provenance") or {} + origin = prov.get("origin_kind") + if origin != args.kind: + failures.add( + "provenance-origin", f"{label} origin_kind is {origin!r}, not {args.kind!r}" + ) + wheel = prov.get("rocke_wheel_sha256") + if not isinstance(wheel, str) or not wheel: + failures.add("provenance-wheel", f"{label} rocke_wheel_sha256 is {wheel!r}") + + comgr = prov.get("comgr_path") + if args.expect_comgr is None: + if not isinstance(comgr, str) or not comgr: + failures.add("provenance-comgr", f"{label} comgr_path is {comgr!r}") + elif not isinstance(comgr, str) or not _same_path(comgr, args.expect_comgr): + failures.add( + "provenance-comgr", + f"{label} comgr_path is {comgr!r}, expected {args.expect_comgr}", + ) + + if archive is None: + return True + toc_key = source.get("toc_key") + blob = archive.get_kernel(toc_key, args.arch) if toc_key else None + if blob is None: + failures.add( + "kpack-toc", f"{label} toc_key {toc_key!r} has no {args.arch} archive entry" + ) + return True + digest = hashlib.sha256(blob).hexdigest() + if digest != source.get("sha256"): + failures.add( + "sha256", + f"{label} archive blob sha256 {digest} != " + f"descriptor {source.get('sha256')!r}", + ) + symbol = source.get("symbol") + if not isinstance(symbol, str) or not symbol or symbol.encode("utf-8") not in blob: + failures.add("symbol", f"{label} symbol {symbol!r} not found in archive blob") + return True + + +def _check_descriptors(args, arch_dir, archive, failures): + """Checks 5-7: every UKD of every shipped KDP.""" + kdps = sorted(arch_dir.rglob("*.kdp.json")) + if not kdps: + failures.add("no-kdp", f"no *.kdp.json under {arch_dir}") + kpack_ukds = 0 + for kdp_path in kdps: + name = kdp_path.relative_to(arch_dir).as_posix() + try: + kdp = json.loads(kdp_path.read_text(encoding="utf-8")) + except (OSError, ValueError) as exc: + failures.add("no-kdp", f"{name} is unreadable: {type(exc).__name__}: {exc}") + continue + if not isinstance(kdp, dict): + failures.add("no-kdp", f"{name} is not a JSON object") + continue + if "arch" in kdp and kdp["arch"] != [args.arch]: + failures.add("arch-field", f"{name} arch is {kdp['arch']!r}") + for ukd in kdp.get("kernelDescriptors", []): + if _check_ukd(args, name, ukd, archive, failures): + kpack_ukds += 1 + if kpack_ukds != args.expect_ukds or kpack_ukds < 1: + failures.add( + "ukd-count", + f"{kpack_ukds} kpack UKDs under {arch_dir}, expected {args.expect_ukds} " + "(and at least 1)", + ) + + +def run(args): + failures = _Failures() + if not args.out_root.is_dir(): + failures.add("out-root-missing", "build target hkp_packaging_probes first") + return failures + + arch_dir = _check_layout(args, failures) + if arch_dir is None: + return failures + kpack_path = _check_kpack(args, arch_dir, failures) + archive = None + if kpack_path is not None: + kpack = _load_kpack(args.kpack_python_dir) + try: + archive = kpack.PackedKernelArchive.read(kpack_path) + except Exception as exc: + failures.add( + "kpack-toc", f"{kpack_path} is unreadable: {type(exc).__name__}: {exc}" + ) + _check_descriptors(args, arch_dir, archive, failures) + return failures + + +def main(argv=None): + args = _parse_args(sys.argv[1:] if argv is None else argv) + try: + failures = run(args) + except ImportError as exc: + print(f"{_PREFIX}: error: cannot import rocm_kpack: {exc}", file=sys.stderr) + return 2 + for line in failures.lines: + print(line, file=sys.stderr) + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py new file mode 100644 index 000000000000..ed52d96c9e4f --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Derive a one-kernel descriptor root from a production descriptor directory. + +Copies every file under `--from` unchanged except the named KDP, whose +`kernelDescriptors` list is reduced to the single UKD whose `name` equals +`--instance-name`. The output directory is removed first so a stale derived root +cannot survive. + +Exit code 0 on success. Exit code 2, with a one-line `hkp_probe_derive: ...` +message on stderr, when the KDP is missing or the instance name does not match +exactly one descriptor. +""" + +from __future__ import annotations + +import argparse +import json +import shutil +import sys +from pathlib import Path + +_PREFIX = "hkp_probe_derive:" + + +def derive_root(src: Path, kdp_name: str, instance_name: str, out: Path) -> str | None: + """Write the derived root to `out`; return an error message, or None on success.""" + kdp_path = src / kdp_name + if not kdp_path.is_file(): + return f"{_PREFIX} {kdp_name} not found under {src}" + + kdp = json.loads(kdp_path.read_text(encoding="utf-8")) + descriptors = kdp["kernelDescriptors"] + matches = [d for d in descriptors if d.get("name") == instance_name] + if not matches: + return ( + f"{_PREFIX} instance '{instance_name}' not found in {kdp_name} " + f"({len(descriptors)} descriptors)" + ) + if len(matches) > 1: + return ( + f"{_PREFIX} instance '{instance_name}' matched {len(matches)} times " + f"in {kdp_name} ({len(descriptors)} descriptors)" + ) + + if out.exists(): + shutil.rmtree(out) + shutil.copytree(src, out) + kdp["kernelDescriptors"] = matches + (out / kdp_name).write_text(json.dumps(kdp, indent=2) + "\n", encoding="utf-8") + return None + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--from", dest="src", type=Path, required=True) + parser.add_argument("--kdp", required=True, help="KDP file name under --from") + parser.add_argument("--instance-name", required=True, help="UKD name to keep") + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args(argv) + + error = derive_root(args.src, args.kdp, args.instance_name, args.out) + if error is not None: + print(error, file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From e59593493064af20c3142c46fb4e8b0f8bacb45b Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Sun, 4 Oct 2026 15:43:00 -0400 Subject: [PATCH 02/52] hkp: packaging probes stream C --- .../descriptor-packaging/CMakeLists.txt | 3 + .../cmake/HkpPackaging.cmake | 5 + .../cmake/HkpPackagingProbes.cmake | 228 ++++++++++++++++++ .../descriptor-packaging/probes/probes.cmake | 21 ++ 4 files changed, 257 insertions(+) create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/CMakeLists.txt b/dnn-providers/hip-kernel-provider/descriptor-packaging/CMakeLists.txt index 42c445d0d227..ffcc570dd840 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/CMakeLists.txt +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/CMakeLists.txt @@ -6,4 +6,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") include(HkpPackaging) +option(HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES + "CI-only, non-shipping: register packaging probes (build-tree ctest entries hkp-probe-*)" OFF) + hkp_add_packaging() diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake index a215e89ca256..e79387954419 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake @@ -27,6 +27,7 @@ set(HKP_PACK_STAMP_NAME ".hkp-packed.stamp" CACHE INTERNAL "Name of the completion stamp each pack writes inside its output root") include(KpackPython) +include(HkpPackagingProbes) # --------------------------------------------------------------------------- # hkp_resolve_kpack( ) @@ -1442,6 +1443,10 @@ function(hkp_add_packaging) PACK_JOBS 1) hkp_register_tests("${_rocm_kpack_dir}" "${HKP_HIPCC}" "${_rocke_comgr_lib}") + if(HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES) + hkp_load_packaging_probes( + "${_rocm_kpack_dir}" "${HKP_HIPCC}" "${_rocke_comgr_lib}" ${_rocke_args}) + endif() endfunction() # --------------------------------------------------------------------------- diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake new file mode 100644 index 000000000000..8c58a8a946b2 --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -0,0 +1,228 @@ +# Copyright © Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +# +# Packaging probes: CI-only, build-tree-only checks that an integration's descriptors +# pack for an explicit architecture, independent of the build's GPU_TARGETS. A probe +# packs through hkp_wire_pack_target with the toolchain hkp_add_packaging resolved, +# then a ctest entry asserts the packed output. Nothing here installs or ships. +# +# This module holds function definitions only; it has no configure effect until +# hkp_load_packaging_probes() runs, which hkp_add_packaging calls under +# HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES. + +include_guard(GLOBAL) + +# --------------------------------------------------------------------------- +# _hkp_probe_check_out_root( ) +# FATAL when a probe's output root lies inside a descriptor tree the install rules +# ship; probe output must never be installed. +# --------------------------------------------------------------------------- +function(_hkp_probe_check_out_root name out_root) + foreach(_shipped IN ITEMS + "${HIPKERNELPROVIDER_DESCRIPTOR_BUILD_DIR}" + "${HIPKERNELPROVIDER_TEST_DESCRIPTOR_BUILD_DIR}") + if(_shipped) + string(FIND "${out_root}/" "${_shipped}/" _pos) + if(_pos EQUAL 0) + message(FATAL_ERROR + "hkp probe '${name}': output root ${out_root} is inside the " + "shipped descriptor tree ${_shipped}; probe output must not be " + "installed.") + endif() + endif() + endforeach() +endfunction() + +# --------------------------------------------------------------------------- +# _hkp_probe_derive_root( ) +# Derive the probe root at configure time (FATAL with the tool's stderr when the +# instance is missing or ambiguous) and re-run configure when a source file changes. +# --------------------------------------------------------------------------- +function(_hkp_probe_derive_root name from_dir kdp instance out_dir) + file(GLOB_RECURSE _derive_inputs CONFIGURE_DEPENDS "${from_dir}/*") + set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS ${_derive_inputs}) + execute_process( + COMMAND "${Python3_EXECUTABLE}" + "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" + --from "${from_dir}" + --kdp "${kdp}" + --instance-name "${instance}" + --out "${out_dir}" + RESULT_VARIABLE _derive_rc + OUTPUT_VARIABLE _derive_out + ERROR_VARIABLE _derive_err) + if(NOT _derive_rc EQUAL 0) + message(FATAL_ERROR + "hkp probe '${name}': deriving the probe root failed (exit " + "${_derive_rc}).\n${_derive_err}") + endif() +endfunction() + +# --------------------------------------------------------------------------- +# hkp_add_packaging_probe(NAME ARCH KIND rocke +# DERIVE_FROM KDP INSTANCE +# [PACK_JOBS ]) +# Declare one probe: pack a descriptor root for ARCH and assert the output. +# +# DERIVE_FROM copies at configure time into the build tree with the named KDP +# trimmed to the one UKD whose `name` is INSTANCE, so the probe packs the real +# descriptors but compiles a single kernel and the assertion expects exactly one +# UKD. A missing or ambiguous INSTANCE is a configure error, never a skip. +# +# Creates pack target hkp_packaging_probe_ (stamp and output under +# ${CMAKE_BINARY_DIR}/hkp-probes//out) and ctest entry hkp-probe-. Only +# callable from a probes file loaded by hkp_load_packaging_probes(). +# --------------------------------------------------------------------------- +function(hkp_add_packaging_probe) + cmake_parse_arguments(PARSE_ARGV 0 ARG "" + "NAME;ARCH;KIND;DERIVE_FROM;KDP;INSTANCE;PACK_JOBS" "") + + if(ARG_UNPARSED_ARGUMENTS) + message(FATAL_ERROR + "hkp probe: unknown arguments: ${ARG_UNPARSED_ARGUMENTS}") + endif() + foreach(_required NAME ARCH KIND DERIVE_FROM KDP INSTANCE) + if(NOT ARG_${_required}) + message(FATAL_ERROR + "hkp probe: hkp_add_packaging_probe requires ${_required}.") + endif() + endforeach() + if(NOT ARG_KIND STREQUAL "rocke") + message(FATAL_ERROR + "hkp probe '${ARG_NAME}': unsupported KIND '${ARG_KIND}'. Supported " + "kinds: rocke.") + endif() + + get_property(_toolchain_set GLOBAL PROPERTY HKP_PROBE_TOOLCHAIN_SET) + if(NOT _toolchain_set) + message(FATAL_ERROR + "hkp probe '${ARG_NAME}': hkp_add_packaging_probe is only valid from a " + "probes file loaded by hkp_load_packaging_probes().") + endif() + get_property(_names GLOBAL PROPERTY HKP_PROBE_NAMES) + if(ARG_NAME IN_LIST _names) + message(FATAL_ERROR "hkp probe '${ARG_NAME}' is declared twice.") + endif() + + get_property(_kpack_dir GLOBAL PROPERTY HKP_PROBE_ROCM_KPACK_DIR) + get_property(_hipcc GLOBAL PROPERTY HKP_PROBE_HIPCC) + get_property(_comgr_lib GLOBAL PROPERTY HKP_PROBE_COMGR_LIB) + get_property(_rocke_args GLOBAL PROPERTY HKP_PROBE_ROCKE_ARGS) + + set(_probe_dir "${CMAKE_BINARY_DIR}/hkp-probes/${ARG_NAME}") + set(_out_root "${_probe_dir}/out") + + _hkp_probe_check_out_root("${ARG_NAME}" "${_out_root}") + + set(_root "${_probe_dir}/root") + _hkp_probe_derive_root("${ARG_NAME}" "${ARG_DERIVE_FROM}" "${ARG_KDP}" + "${ARG_INSTANCE}" "${_root}") + + if(NOT IS_DIRECTORY "${_root}") + message(FATAL_ERROR + "hkp probe '${ARG_NAME}': probe root is not a directory: ${_root}") + endif() + _hkp_root_covers_any_arch(_covers "${_root}" "${ARG_ARCH}") + if(NOT _covers) + message(FATAL_ERROR + "hkp probe '${ARG_NAME}': no *.kdp.json under ${_root} ships for " + "${ARG_ARCH}, so the pack would prune everything.") + endif() + + set(_pack_jobs 1) + if(ARG_PACK_JOBS) + set(_pack_jobs "${ARG_PACK_JOBS}") + endif() + hkp_wire_pack_target( + NAME "probe_${ARG_NAME}" + SOURCE_ROOT "${_root}" + ARCHES "${ARG_ARCH}" + HIPCC "${_hipcc}" + ROCM_KPACK_DIR "${_kpack_dir}" + OUT_ROOT "${_out_root}" + ${_rocke_args} + PACK_JOBS "${_pack_jobs}") + + set(_assert_args + --out-root "${_out_root}" + --arch "${ARG_ARCH}" + --kind "${ARG_KIND}" + --kpack-python-dir "${_kpack_dir}" + --stamp-name "${HKP_PACK_STAMP_NAME}" + --expect-ukds 1) + if(_comgr_lib) + list(APPEND _assert_args --expect-comgr "${_comgr_lib}") + endif() + add_test(NAME "hkp-probe-${ARG_NAME}" + COMMAND "${Python3_EXECUTABLE}" + "${HKP_PKG_DIR}/tools/hkp_probe_assert.py" ${_assert_args}) + set_tests_properties("hkp-probe-${ARG_NAME}" PROPERTIES + ENVIRONMENT "PYTHONPATH=${HKP_PYTHON_ROOT}") + + set_property(GLOBAL APPEND PROPERTY HKP_PROBE_NAMES "${ARG_NAME}") +endfunction() + +# --------------------------------------------------------------------------- +# hkp_load_packaging_probes( [...]) +# Store the toolchain hkp_add_packaging resolved, load the declared probes from +# probes/probes.cmake, register the probe tooling tests, write the manifest of ctest +# names (${CMAKE_BINARY_DIR}/hkp-probes/manifest.txt) and define the aggregate target +# hkp_packaging_probes. Configuration fails when no probe is declared or pytest is not +# importable by Python3_EXECUTABLE. +# --------------------------------------------------------------------------- +function(hkp_load_packaging_probes rocm_kpack_dir hipcc rocke_comgr_lib) + set_property(GLOBAL PROPERTY HKP_PROBE_ROCM_KPACK_DIR "${rocm_kpack_dir}") + set_property(GLOBAL PROPERTY HKP_PROBE_HIPCC "${hipcc}") + set_property(GLOBAL PROPERTY HKP_PROBE_COMGR_LIB "${rocke_comgr_lib}") + set_property(GLOBAL PROPERTY HKP_PROBE_ROCKE_ARGS "${ARGN}") + set_property(GLOBAL PROPERTY HKP_PROBE_NAMES "") + set_property(GLOBAL PROPERTY HKP_PROBE_TOOLCHAIN_SET TRUE) + + include("${HKP_PKG_DIR}/probes/probes.cmake") + + get_property(_names GLOBAL PROPERTY HKP_PROBE_NAMES) + if(NOT _names) + message(FATAL_ERROR + "hkp probe: HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES is ON but " + "${HKP_PKG_DIR}/probes/probes.cmake declared no probe.") + endif() + + execute_process( + COMMAND "${Python3_EXECUTABLE}" -c "import pytest" + RESULT_VARIABLE _pytest_rc + OUTPUT_QUIET ERROR_QUIET) + if(NOT _pytest_rc EQUAL 0) + message(FATAL_ERROR + "hkp: pytest is not importable by ${Python3_EXECUTABLE}, so the " + "descriptor-packaging tests cannot run. Install pytest for that " + "interpreter, or configure with " + "-DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=OFF.") + endif() + + # Same environment contract as the pytest entries of hkp_register_tests: the probe + # tooling tests read the kpack dir, hipcc and comgr from it and fail when absent. + set(_pyenv "PYTHONPATH=${HKP_PYTHON_ROOT}" "HKP_HIPCC=${hipcc}") + if(rocm_kpack_dir) + list(APPEND _pyenv "HIPKERNELPROVIDER_ROCM_KPACK_DIR=${rocm_kpack_dir}") + endif() + if(rocke_comgr_lib) + list(APPEND _pyenv "ROCKE_COMGR_LIB=${rocke_comgr_lib}") + endif() + add_test(NAME hkp-probe-tools + COMMAND "${Python3_EXECUTABLE}" -m pytest "${HKP_PKG_DIR}/probes/tests" -v) + set_tests_properties(hkp-probe-tools PROPERTIES ENVIRONMENT "${_pyenv}") + + set(_test_names hkp-probe-tools) + set(_targets "") + foreach(_name IN LISTS _names) + list(APPEND _test_names "hkp-probe-${_name}") + list(APPEND _targets "hkp_packaging_probe_${_name}") + endforeach() + list(SORT _test_names) + string(REPLACE ";" "\n" _manifest "${_test_names}") + file(WRITE "${CMAKE_BINARY_DIR}/hkp-probes/manifest.txt" "${_manifest}\n") + + add_custom_target(hkp_packaging_probes COMMENT "hkp: packaging probes") + add_dependencies(hkp_packaging_probes ${_targets}) +endfunction() diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake new file mode 100644 index 000000000000..e7808b9b300b --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake @@ -0,0 +1,21 @@ +# Copyright © Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +# +# Packaging probes, one declaration per integration and architecture. Loaded by +# hkp_load_packaging_probes() under HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES; see +# HkpPackagingProbes.cmake for the arguments. + +# gfx950 dense attention, rocKE producer. The probe packs the production descriptors +# trimmed to one instance. INSTANCE names the UKD whose compile path every dense +# attention variant shares, so one compile stands for the family. The key is the UKD +# `name`, not its `id`: ids are regenerated on every ingestor run, names encode the +# specialization. When that instance is renamed or removed, configure fails naming it; +# pick another instance of the same family from the KDP and update INSTANCE. +hkp_add_packaging_probe( + NAME gfx950_attention_dense + ARCH gfx950 + KIND rocke + DERIVE_FROM + "${HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT}/rocKE/gfx950_attention_dense" + KDP gfx950_attention_dense.kdp.json + INSTANCE "attention_dense.bf16_d64_hq8_kv1_c1_bm256_bn64.gfx950") From 3bfe7517b035b35a3b2b7365203d2d2fc450d819 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Sun, 4 Oct 2026 16:09:46 -0400 Subject: [PATCH 03/52] hkp: probe derivation status line --- .../descriptor-packaging/cmake/HkpPackagingProbes.cmake | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake index 8c58a8a946b2..cf1f368fc336 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -57,6 +57,8 @@ function(_hkp_probe_derive_root name from_dir kdp instance out_dir) "hkp probe '${name}': deriving the probe root failed (exit " "${_derive_rc}).\n${_derive_err}") endif() + message(STATUS + "hkp: probe ${name}: derived root ${out_dir} (instance '${instance}' of ${kdp})") endfunction() # --------------------------------------------------------------------------- From 4a2821717552e8af9a2f6460a1f99390eaab8ca0 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Sun, 4 Oct 2026 16:14:38 -0400 Subject: [PATCH 04/52] ci: hipdnn packaging probe Linux job, junit check, probe docs and runbook step --- .github/scripts/check_probe_junit.py | 111 ++++++++++ .../scripts/tests/check_probe_junit_test.py | 104 +++++++++ .github/workflows/hipdnn-superbuild-ci.yml | 161 ++++++++++++++ .../descriptor-packaging/README.md | 203 ++++++++++++++++++ .../skills/hipdnn-ingestor-engine/RUNBOOK.md | 9 + 5 files changed, 588 insertions(+) create mode 100755 .github/scripts/check_probe_junit.py create mode 100644 .github/scripts/tests/check_probe_junit_test.py diff --git a/.github/scripts/check_probe_junit.py b/.github/scripts/check_probe_junit.py new file mode 100755 index 000000000000..04b83b2ded59 --- /dev/null +++ b/.github/scripts/check_probe_junit.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT + +"""Prove that every declared hipDNN packaging probe ran and passed. + +The packaging-probe CI jobs run ctest with --output-junit. ctest exits 0 for a +junit file that lists fewer tests than were declared, and a skipped test is not +a failure to it. This script compares the junit file against the manifest the +CMake configure wrote (one ctest test name per line) so that "declared N, ran +fewer", "skipped" and "ran nothing" are all red. + +Usage: + check_probe_junit.py --junit build/probe-junit.xml \ + --manifest build/hkp-probes/manifest.txt + +Exit codes: + 0 every manifest name ran exactly once; no failure, error or skip + 1 one or more checks failed; each prints + "check_probe_junit: FAIL : " +""" + +import argparse +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +# One probe plus the "hkp-probe-tools" test. +MIN_MANIFEST_ENTRIES = 2 + + +def read_manifest(path: Path) -> list[str]: + return [line.strip() for line in path.read_text().splitlines() if line.strip()] + + +def check(junit_path: Path, manifest_path: Path) -> list[tuple[str, str]]: + """Return (id, detail) for every failed check; empty means green.""" + try: + names = read_manifest(manifest_path) + except OSError as exc: + return [("manifest-unreadable", f"{manifest_path}: {exc}")] + + failures: list[tuple[str, str]] = [] + if len(names) < MIN_MANIFEST_ENTRIES: + failures.append( + ( + "manifest-too-small", + f"{manifest_path} lists {len(names)} test(s), expected at least " + f"{MIN_MANIFEST_ENTRIES} (one probe plus hkp-probe-tools)", + ) + ) + + try: + root = ET.parse(junit_path).getroot() + except (OSError, ET.ParseError) as exc: + failures.append(("junit-unreadable", f"{junit_path}: {exc}")) + return failures + + cases = list(root.iter("testcase")) + ran = [case.get("name", "") for case in cases] + + if len(cases) != len(names): + failures.append( + ( + "count-mismatch", + f"junit has {len(cases)} testcase(s), manifest declares {len(names)}", + ) + ) + for name in names: + if name not in ran: + failures.append( + ("name-missing", f"declared test '{name}' is not in the junit") + ) + + for case in cases: + name = case.get("name", "") + if case.find("skipped") is not None or case.get("status") == "notrun": + failures.append(("skipped", f"test '{name}' did not run")) + if case.find("failure") is not None: + failures.append(("failure", f"test '{name}' failed")) + if case.find("error") is not None: + failures.append(("error", f"test '{name}' errored")) + + return failures + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument( + "--junit", required=True, type=Path, help="ctest --output-junit file" + ) + parser.add_argument( + "--manifest", + required=True, + type=Path, + help="file listing one declared ctest test name per line", + ) + args = parser.parse_args(argv) + + failures = check(args.junit, args.manifest) + for check_id, detail in failures: + print(f"check_probe_junit: FAIL {check_id}: {detail}") + if failures: + return 1 + count = len(read_manifest(args.manifest)) + print(f"check_probe_junit: OK {count} declared test(s) ran and passed") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/tests/check_probe_junit_test.py b/.github/scripts/tests/check_probe_junit_test.py new file mode 100644 index 000000000000..e97d3e88ca10 --- /dev/null +++ b/.github/scripts/tests/check_probe_junit_test.py @@ -0,0 +1,104 @@ +# Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT + +import os +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, os.fspath(Path(__file__).parent.parent)) +import check_probe_junit as cpj + +MANIFEST = ["hkp-probe-gfx950_attention_dense", "hkp-probe-tools"] + + +def case_xml(name: str, child: str = "") -> str: + body = f"<{child}/>" if child else "" + status = "notrun" if child == "skipped" else "run" + return f'{body}' + + +def junit_xml(cases: list[str]) -> str: + return ( + '' + f'{"".join(cases)}' + ) + + +class CheckProbeJunitTest(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.dir = Path(tmp.name) + + def run_check(self, xml: str, manifest: list[str]) -> list[str]: + junit_path = self.dir / "probe-junit.xml" + manifest_path = self.dir / "manifest.txt" + junit_path.write_text(xml) + manifest_path.write_text("".join(f"{n}\n" for n in manifest)) + return [check_id for check_id, _ in cpj.check(junit_path, manifest_path)] + + def test_all_declared_tests_passed(self): + xml = junit_xml([case_xml(n) for n in MANIFEST]) + self.assertEqual(self.run_check(xml, MANIFEST), []) + + def test_skipped_test_fails(self): + xml = junit_xml([case_xml(MANIFEST[0]), case_xml(MANIFEST[1], "skipped")]) + self.assertEqual(self.run_check(xml, MANIFEST), ["skipped"]) + + def test_fewer_testcases_than_manifest_fails(self): + xml = junit_xml([case_xml(MANIFEST[0])]) + self.assertEqual( + self.run_check(xml, MANIFEST), ["count-mismatch", "name-missing"] + ) + + def test_empty_manifest_fails(self): + xml = junit_xml([case_xml(n) for n in MANIFEST]) + self.assertIn("manifest-too-small", self.run_check(xml, [])) + + def test_manifest_without_probe_fails(self): + xml = junit_xml([case_xml("hkp-probe-tools")]) + self.assertEqual( + self.run_check(xml, ["hkp-probe-tools"]), ["manifest-too-small"] + ) + + def test_failure_fails(self): + xml = junit_xml([case_xml(MANIFEST[0], "failure"), case_xml(MANIFEST[1])]) + self.assertEqual(self.run_check(xml, MANIFEST), ["failure"]) + + def test_error_fails(self): + xml = junit_xml([case_xml(MANIFEST[0]), case_xml(MANIFEST[1], "error")]) + self.assertEqual(self.run_check(xml, MANIFEST), ["error"]) + + def test_declared_name_missing_with_equal_count_fails(self): + xml = junit_xml([case_xml(MANIFEST[0]), case_xml("hkp-probe-other")]) + self.assertEqual(self.run_check(xml, MANIFEST), ["name-missing"]) + + def test_unparseable_junit_fails(self): + self.assertEqual(self.run_check("]), both of which need a concrete gfx target rather + # than a TheRock family name. Must differ from every probe's arch so that a probe + # that stops passing its own arch to the packer goes red. + GPU_TARGET: gfx942 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: | + .dvc + .github + cmake + dnn-providers + projects/hipdnn + projects/miopen + test + + # Shared CI constants, kpack-ref among them, defined in one place. + - name: Load CI environment + id: ci-env + uses: ./.github/actions/ci-env + + - name: Set environment + run: | + V="${{ runner.temp }}/rocm_wheels" + echo "VENV_PATH=$V" >> "$GITHUB_ENV" + echo "VENV_PYTHON=$V/bin/python" >> "$GITHUB_ENV" + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.12' + + - name: Check build tools + run: | + command -v ninja >/dev/null || { echo "ninja is not on this runner" >&2; exit 1; } + + - name: Install ROCm wheels + run: | + echo "Creating Python virtual environment at $VENV_PATH" + python -m venv "$VENV_PATH" + source "$VENV_PATH/bin/activate" + + echo "Installing ROCm wheels from multi-arch nightlies (device: $GPU_TARGET)" + pip install --index-url "https://nightly.repo.amd.com/rocm/whl-next/" "rocm[libraries,devel,device-$GPU_TARGET]" + + echo "Initializing ROCm SDK" + rocm-sdk init + + SITE_PACKAGES=$("$VENV_PYTHON" -c "import site; print(site.getsitepackages()[0])") + ROCM_SDK_PATH="$SITE_PACKAGES/_rocm_sdk_devel" + echo "ROCM_SDK_PATH=$ROCM_SDK_PATH" + echo "ROCM_SDK_PATH=$ROCM_SDK_PATH" >> "$GITHUB_ENV" + + # rocKE loads libamd_comgr through ctypes, not through CMake, so + # -DROCM_PATH below is invisible to it. It resolves via $ROCM_PATH in + # the environment or a globbed /opt/rocm*, and this runner is + # wheel-only: there is no /opt/rocm, and the devel wheel's lib/ holds + # headers and CMake config, not the runtime .so. comgr ships in the + # sibling _rocm_sdk_core wheel, so name it explicitly. + # + # Exported under HKP_ROCKE_COMGR_LIB and fed to the configure step as + # -DHIPKERNELPROVIDER_ROCKE_COMGR_LIB, not as rocke's own + # ROCKE_COMGR_LIB: the cache variable is the channel hkp already + # forwards to the configure probe, the pack step and the ctest + # entries, so all four ask about the same library. Exporting rocke's + # runtime name directly would instead override every python process + # in every later step of this job, including ones that should be + # exercising normal resolution. + COMGR_LIB=$(ls "$SITE_PACKAGES/_rocm_sdk_core/lib/libamd_comgr.so".* 2>/dev/null | head -n1) + test -n "$COMGR_LIB" || { echo "no libamd_comgr.so under $SITE_PACKAGES/_rocm_sdk_core/lib" >&2; exit 1; } + echo "HKP_ROCKE_COMGR_LIB=$COMGR_LIB" + echo "HKP_ROCKE_COMGR_LIB=$COMGR_LIB" >> "$GITHUB_ENV" + + - name: Check ROCm wheel freshness + run: | + "$VENV_PYTHON" .github/scripts/check_wheel_freshness.py --max-age-days 14 + + - name: Install rocm_kpack + run: | + set -euo pipefail + # Sparse, blobless, depth-1 checkout of just shared/kpack: the pinned + # commit lives in the rocm-systems monorepo, so cloning it whole (or + # letting pip do it via a git+ URL) would pull far more than the packer. + KPACK_SRC="${RUNNER_TEMP}/rocm-systems-kpack" + rm -rf "$KPACK_SRC" + mkdir -p "$KPACK_SRC" + git -C "$KPACK_SRC" init -q . + git -C "$KPACK_SRC" remote add origin https://github.com/ROCm/rocm-systems.git + git -C "$KPACK_SRC" sparse-checkout init --cone + git -C "$KPACK_SRC" sparse-checkout set shared/kpack + git -C "$KPACK_SRC" fetch --depth 1 --filter=blob:none origin "${{ steps.ci-env.outputs.kpack-ref }}" + git -C "$KPACK_SRC" checkout -q FETCH_HEAD + # Confirms the fetch landed on the pinned commit. + fetched=$(git -C "$KPACK_SRC" rev-parse HEAD) + test "$fetched" = "${{ steps.ci-env.outputs.kpack-ref }}" || { echo "rocm-systems is $fetched, expected ${{ steps.ci-env.outputs.kpack-ref }}" >&2; exit 1; } + # Installing the directory rebuilds and reinstalls every time, so a + # warm venv cannot keep an older build of this same 0.1.0 version. + "$VENV_PYTHON" -m pip install "$KPACK_SRC/shared/kpack" + # Derived from the installed module, which also proves it imports. + KPACK_PYTHON_DIR=$("$VENV_PYTHON" -c "import pathlib, rocm_kpack; print(pathlib.Path(rocm_kpack.__file__).parent.parent.as_posix())") + test -n "$KPACK_PYTHON_DIR" || { echo "empty rocm_kpack python dir" >&2; exit 1; } + echo "KPACK_PYTHON_DIR=$KPACK_PYTHON_DIR" >> "$GITHUB_ENV" + + # Must precede configure: configuration fails if pytest is not importable + # by the interpreter descriptor-packaging registers its tests under. The + # interpreter is named explicitly because it must be the one passed as + # -DPython3_EXECUTABLE below -- a bare `pip` here would install into the + # setup-python tool-cache interpreter instead. + - name: Install Python dependencies + run: | + "$VENV_PYTHON" -m pip install --upgrade -r .github/requirements-hipdnn-superbuild-ci.txt + + # HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES registers the probe ctest + # entries and the hkp_packaging_probes target. The component list is + # reduced to what the probes need, and GPU_TARGETS is the lane's gfx942, + # not a probe arch: each probe passes its own arch to the packer. + - name: Configure superbuild + run: | + cmake --preset hipdnn-dev-all -GNinja \ + -DROCM_LIBS_ENABLE_COMPONENTS="hipdnn;hip-kernel-provider" \ + -DROCM_PATH="$ROCM_SDK_PATH" \ + -DCMAKE_PROGRAM_PATH="$ROCM_SDK_PATH/lib/llvm/bin" \ + -DGPU_TARGETS="$GPU_TARGET" \ + -DROCM_LIBS_ENABLE_ROOT_CTEST=ON \ + -DENABLE_CLANG_FORMAT=OFF \ + -DENABLE_CLANG_TIDY=OFF \ + -DHIPKERNELPROVIDER_ENABLE_ROCKE=ON \ + -DHIPDNN_ENABLE_KERNEL_INGESTOR=ON \ + -DHIPDNN_ENABLE_SDPA=ON \ + -DHIPKERNELPROVIDER_KPACK_PYTHON_DIR="$KPACK_PYTHON_DIR" \ + -DHIPKERNELPROVIDER_ROCKE_COMGR_LIB="$HKP_ROCKE_COMGR_LIB" \ + -DPython_EXECUTABLE="$VENV_PYTHON" \ + -DPython3_EXECUTABLE="$VENV_PYTHON" \ + -DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON + + # An unknown target (option not applied) fails this step. + - name: Build packaging probes + run: cmake --build build --target hkp_packaging_probes + + # --no-tests=error makes an empty selection fail. + - name: Run packaging probes + run: ctest --test-dir build -R '^hkp-probe-' --no-tests=error --output-on-failure --output-junit probe-junit.xml + + # ctest reports skipped tests and fewer-than-declared tests as success; + # this compares the junit against the manifest configure wrote. + - name: Check packaging probes ran + run: | + "$VENV_PYTHON" .github/scripts/check_probe_junit.py \ + --junit build/probe-junit.xml \ + --manifest build/hkp-probes/manifest.txt + hipdnn_build_summary: name: hipDNN Superbuild Summary if: always() needs: - hipdnn-windows-superbuild - hipdnn-linux-superbuild + - hipdnn-probe-linux runs-on: ubuntu-24.04 steps: - name: Report results diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index b0529fe49b35..26094891a49d 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -248,6 +248,209 @@ pass `1`, or `2` for `integration`, and the production root passes none, so the its default. The reason is in the `hkp_wire_pack_target()` header in `cmake/HkpPackaging.cmake`. +## Packaging probes + +A packaging probe proves that one integration's descriptors **pack for one explicit +architecture**, independent of the architectures the build itself targets. The packer's +architecture list is `GPU_TARGETS` and nothing else, so a lane that builds gfx942 (Linux) +or gfx1151 (Windows) never exercises the gfx950 production bundle. A probe closes that +gap: it runs the real packer through `hkp_wire_pack_target()` with its own architecture, +then a ctest entry asserts the packed output. + +**What a probe proves** + +- The real production descriptors (UUID ids, the specialization contract, the real + KMD/UED/UDD/UHD) load, pass the pack-time agreement check and compile through comgr. +- The rocKE sources come from the wheel the packer is given, not from the in-tree source + tree the pytest suite imports. +- The architecture and output wiring of the CMake pack target: `--arches`, the stamp, the + interpreter and wheel directory, comgr forwarding. +- The output: a kpack archive for exactly that architecture, with the expected UKD count + and shipped provenance. + +**What it does not prove** + +- Instances other than the named representative (see below). +- Device behavior: no kernel is launched, no numerics are checked. +- Anything about integrations without a probe. +- That the CI job runs: that is the workflow's job (`--no-tests=error`, the manifest check + below). + +Probes are build-tree ctest entries only. They exist only under +`HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` (default `OFF`, CI-only), are never installed +and write under `/hkp-probes//`, outside both shipped descriptor trees. +With the option `OFF` the only configure difference is that cache entry. + +### Running the probes locally + +From the repository root. The paths below are the workstation's; substitute your ROCm and +rocm_kpack locations. The `GPU_TARGETS` value must differ from every probe architecture +(see "Adding a probe"). + +```bash +cmake --preset hipdnn-dev-all -GNinja -B build-probe \ + -DROCM_LIBS_ENABLE_COMPONENTS="hipdnn;hip-kernel-provider" \ + -DGPU_TARGETS=gfx942 \ + -DHIPKERNELPROVIDER_ENABLE_ROCKE=ON -DHIPDNN_ENABLE_KERNEL_INGESTOR=ON \ + -DHIPDNN_ENABLE_SDPA=ON \ + -DHIPKERNELPROVIDER_KPACK_PYTHON_DIR=/opt/rocm-kpack/python \ + -DCMAKE_PREFIX_PATH=/opt/rocm -DROCM_PATH=/opt/rocm \ + -DPython3_EXECUTABLE=/usr/bin/python3 -DPython_EXECUTABLE=/usr/bin/python3 \ + -DENABLE_CLANG_FORMAT=OFF -DENABLE_CLANG_TIDY=OFF -DROCM_LIBS_ENABLE_ROOT_CTEST=ON \ + -DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON +cmake --build build-probe --target hkp_packaging_probes +ctest --test-dir build-probe -R '^hkp-probe-' --no-tests=error --output-on-failure \ + --output-junit probe-junit.xml +python3 .github/scripts/check_probe_junit.py \ + --junit build-probe/probe-junit.xml --manifest build-probe/hkp-probes/manifest.txt +``` + +The selector is `^hkp-probe-`, anchored: `-R` is an unanchored regex. It matches every +probe (`hkp-probe-`) and `hkp-probe-tools`, the pytest suite of the probe tooling +(`probes/tests/`). `--no-tests=error` makes an empty selection a failure instead of a +green run that proved nothing. `manifest.txt` is written at configure and lists the +ctest names that must run; the junit check fails unless every one ran with no failure, +error or skip. Configuring without the option and running the same build and ctest lines +fails (unknown target, then "No tests were found"). + +Locally on Linux the build takes about 35 s (the wheel environment plus a one-instance +pack) and the two ctest entries about 3 s. There is no local Windows reproduction. + +### `hkp_add_packaging_probe` arguments + +Declared in `probes/probes.cmake`, defined in `cmake/HkpPackagingProbes.cmake`. + +| Argument | Meaning | +|---|---| +| `NAME` | Probe name. Pack target `hkp_packaging_probe_`, ctest entry `hkp-probe-`. Declaring a name twice is a configure error. | +| `ARCH` | The one architecture to pack, e.g. `gfx950`. Passed to the packer as `--arches`. | +| `KIND` | Producer kind. Only `rocke` is supported; any other value is a configure error naming the supported kinds. | +| `DERIVE_FROM` | Directory of the real production descriptors to derive the probe root from. | +| `KDP` | File name of the KDP inside `DERIVE_FROM` whose `kernelDescriptors` is trimmed. | +| `INSTANCE` | The UKD `name` kept as the representative instance. | +| `PACK_JOBS` | Optional prewarm worker count; defaults to `1`. | + +There is one mode, `DERIVE_FROM`. A probe has no `ROOT` argument for a hand-written or +frozen root: a frozen copy drifts silently from the production schema, which is exactly +what a probe exists to catch. The expected UKD count is fixed at one. + +Configuration fails, never skips, when: `INSTANCE` is not found exactly once in `KDP`; +the derived root has no KDP covering `ARCH`; the probe output root would sit under a +shipped descriptor tree; no probe is declared; or pytest is not importable by +`Python3_EXECUTABLE`. + +### The derived-root representative-instance rule + +At configure, `tools/hkp_probe_derive_root.py` copies the `DERIVE_FROM` directory into +`/hkp-probes//root` unchanged, except that `KDP`'s `kernelDescriptors` is +reduced to the single UKD whose `name` equals `INSTANCE`. The probe therefore packs the +real descriptors but compiles one kernel (about 1 s) instead of the whole family. + +- **The key is the UKD `name`, not its `id`.** Ids are `uuid4` values regenerated on every + ingestor run; names encode the specialization and are unique. Keying on the id would + turn every legitimate regeneration into a red probe. +- **Choose the instance whose compile path every variant of that family shares.** One + compile then stands for the family. Breakage specific to the other instances is not + caught; that is the accepted gap of the design. +- **When the named instance is renamed or removed**, configure fails with + `hkp_probe_derive: instance '' not found in ( descriptors)` (or + `... matched times`). That is the intended signal. Pick another instance of the + same family from the KDP and update `INSTANCE` in `probes/probes.cmake`. +- The derive step re-runs when any file under `DERIVE_FROM` changes. + +### Assertion ids + +`tools/hkp_probe_assert.py` prints one line per failure, `hkp_probe_assert: FAIL : +`, and exits 1; the ids are stable and greppable. + +| Id | Fails when | +|---|---| +| `out-root-missing` | The probe output root does not exist: build `hkp_packaging_probes` first. | +| `stamp-missing` | The pack stamp is absent. | +| `arch-dir-missing` / `extra-arch-dir` | `//` is absent, or another `gfx*` directory is present. | +| `kpack-missing` / `kpack-empty` | `//kpack/hip_kernel_provider_.kpack` is absent or empty. | +| `no-kdp` | No `*.kdp.json` under `/`. | +| `ukd-kind` | A shipped UKD's `kernel_source.kind` is not `kpack`. | +| `arch-field` | A KDP or UKD `arch` is not exactly the probe architecture. | +| `kpack-toc` | The archive has no entry for a UKD's `toc_key` and architecture. | +| `sha256` | The blob's sha256 differs from `kernel_source.sha256`. | +| `signature` | A UKD has an empty `signature`. | +| `symbol` | A UKD's `symbol` does not appear in its blob. | +| `provenance-origin` | `origin_kind` is not `rocke` (for `KIND rocke`). | +| `provenance-wheel` | `rocke_wheel_sha256` is missing or empty: the wheel did not supply rocKE. | +| `provenance-comgr` | The comgr recorded at pack time is not the expected library (or is empty). Catches a stale system comgr shadowing the intended one. | +| `ukd-count` | The number of kpack-kind UKDs is not the expected one, or is zero. | + +### Adding a probe + +Required for every new architecture or integration. **No CMake guard enforces this:** a +new integration without a probe configures, builds and passes CI, and its packaging for +an architecture the lanes do not build is then unchecked. + +1. Add one `hkp_add_packaging_probe` declaration to `probes/probes.cmake`, with a comment + stating why the chosen `INSTANCE` represents its family. No workflow edit: the jobs + build `hkp_packaging_probes` and select `^hkp-probe-`, and the manifest counts every + declared probe. +2. **The probe architecture must differ from the lane's `GPU_TARGETS`** (gfx942 on the + Linux job, gfx1151 on the Windows job). If they are equal, the architecture-wiring + mutation below becomes unobservable on that job. If a probe must use a lane's + architecture, change the job's `GPU_TARGETS` first. +3. Reconfigure and run the local block above; the new `hkp-probe-` entry and the + manifest line must appear. +4. Run the mutation checks below. Each must turn the probe red, and each edit must be + reverted afterwards. + +### Mutation checks when adding a probe + +After each mutation: reconfigure, then build `hkp_packaging_probes`, then run the probe +ctest entry, then revert the edit. + +```bash +cmake build-probe +cmake --build build-probe --target hkp_packaging_probes +ctest --test-dir build-probe -R '^hkp-probe-$' --output-on-failure +``` + +1. **Derived descriptors reach the compiler.** + - Rename the representative UKD in the production KDP: configure must fail with + `hkp_probe_derive: instance '' not found`. + - Set a compile-affecting spec field of that UKD to a value the builder rejects (for + dense attention: `block_n` in `kernel_source.spec` from `64` to `7`): the build must + fail in the packer (`invalid spec ...`). + - Make a UKD `metadata` field disagree with the compiled specialization (`block_n` in + `metadata` from `64` to `128`): the build must fail with the pack-time agreement + error (`compiled specialization disagrees with metadata`). + - Also run `hip-kernel-provider-hkp-pack` on the mutated tree and record whether it + stays green. For the gfx950 dense-attention probe it does, which is what makes the + probe more than the existing test. +2. **The wheel supplies rocKE.** In `rocke/platform/pyproject.toml` change + `rocke = ["**/*.json"]` to `rocke = ["analysis/**/*.json"]`, so the built wheel lacks + `core/arch/data/arch_specs.json`. The probe build must fail with + `FileNotFoundError ... arch_specs.json` from the private wheel directory, while + `hip-kernel-provider-hkp-pack` (in-tree source) stays green. + + **Procedure hazard: remove the stale build products before and after this mutation.** + Both are gitignored, and either carries `arch_specs.json` into the rebuilt wheel, so + the edit is otherwise a silent no-op and the probe stays green: + + ```bash + rm -rf dnn-providers/hip-kernel-provider/rocke/platform/build \ + dnn-providers/hip-kernel-provider/rocke/platform/python/rocke.egg-info + ``` + + A fresh checkout (CI) has neither directory. +3. **The architecture and outputs are wired.** In `cmake/HkpPackagingProbes.cmake`: + - Replace the `ARCHES` argument of the pack target with the lane's architecture + (`gfx942`): the build must fail with `produced nothing: no KDP survived arch pruning`. + - Replace it with an empty string: the build succeeds, and the ctest entry must fail + `hkp_probe_assert: FAIL arch-dir-missing`. + - After a good build, delete `build-probe/hkp-probes//out/.hkp-packed.stamp`: the + ctest entry must fail `hkp_probe_assert: FAIL stamp-missing`. + +Every mutation above turned the gfx950 dense-attention probe red when run locally on +Linux. The Windows half of the wheel check, and comgr forwarding (covered only by +`provenance-comgr`), are verified by CI only. + ## Running the tests ```bash diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md index cec1045d7b50..db7eb609dd69 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md @@ -335,6 +335,15 @@ native pack whose symbols a bundle's UKDs must name before it serves. The packag dependencies are documented from the repository root in `dnn-providers/hip-kernel-provider/descriptor-packaging/README.md`. +**Required: add a packaging probe for every new architecture or integration.** CI builds +only a few architectures, so packaging for the others breaks silently unless a probe packs +them. Declare one `hkp_add_packaging_probe(...)` in +`dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake` (arch, `KIND +rocke`, the production directory, KDP file and a representative UKD name), then run the +mutation checks listed under "Packaging probes" in the descriptor-packaging README. No +CMake guard detects a missing probe, so this step is the only enforcement; no workflow +edit is needed. + Build the provider, validator and required test targets through the configured superbuild. For packaged engines, run `hkp_packaging_product` after the full build and after any reconfigure; require the final staged descriptors, not a packaging stamp or an From 8044b32cecbf2615edd88249d281eb77deaa94cd Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Sun, 4 Oct 2026 16:15:35 -0400 Subject: [PATCH 05/52] ci: hipdnn packaging probe Windows job --- .github/workflows/hipdnn-superbuild-ci.yml | 166 +++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/.github/workflows/hipdnn-superbuild-ci.yml b/.github/workflows/hipdnn-superbuild-ci.yml index b72de3c912e6..147a9f3e7bbf 100644 --- a/.github/workflows/hipdnn-superbuild-ci.yml +++ b/.github/workflows/hipdnn-superbuild-ci.yml @@ -678,6 +678,171 @@ jobs: --junit build/probe-junit.xml \ --manifest build/hkp-probes/manifest.txt + # Packs the production gfx950 rocKE descriptors for an architecture this + # workflow does not build, on Windows. Only the packaging-probe ctest entries + # run; the build is limited to the probe closure (no hipDNN C++). No PATH + # step: the probe ctest entries run python only and read files. + hipdnn-probe-windows: + name: HipDNN Packaging Probe Windows + runs-on: azure-windows-scale-rocm + defaults: + run: + shell: bash + env: + # Feeds both -DGPU_TARGETS and the multi-arch nightly device extra + # (rocm[...,device-]), both of which need a concrete gfx target rather + # than a TheRock family name. Must differ from every probe's arch so that a probe + # that stops passing its own arch to the packer goes red. + GPU_TARGET: gfx1151 + steps: + - name: Configure git longpaths + run: git config --global core.longpaths true + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: | + .dvc + .github + cmake + dnn-providers + projects/hipdnn + projects/miopen + test + + # Shared CI constants, kpack-ref among them, defined in one place. + - name: Load CI environment + id: ci-env + uses: ./.github/actions/ci-env + + - name: Set environment + run: | + V='B:\rocm_wheels' + echo "VENV_PATH=$V" >> "$GITHUB_ENV" + echo "VENV_PYTHON=$V/Scripts/python.exe" >> "$GITHUB_ENV" + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.12' + + - name: Configure MSVC + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 + + - name: Install build tools + run: | + choco source disable -n=chocolatey + choco source add -n=internal -s http://nexus-service.nexus-ns.svc.cluster.local:8081/repository/choco-group/ --priority=1 + choco install --no-progress -y ninja --version 1.12.1 + + - name: Install ROCm wheels + shell: pwsh + run: | + Write-Host "Creating Python virtual environment at $env:VENV_PATH" + python -m venv $env:VENV_PATH + & "$env:VENV_PATH\Scripts\Activate.ps1" + + Write-Host "Installing ROCm wheels from multi-arch nightlies (device: $env:GPU_TARGET)" + pip install --index-url "https://nightly.repo.amd.com/rocm/whl-next/" "rocm[libraries,devel,device-$env:GPU_TARGET]" + if ($LASTEXITCODE -ne 0) { throw "Failed to install ROCm wheels" } + + Write-Host "Initializing ROCm SDK" + rocm-sdk init + if ($LASTEXITCODE -ne 0) { throw "Failed to initialize ROCm SDK" } + + $RocmSdkPath = "$env:VENV_PATH\Lib\site-packages\_rocm_sdk_devel" -replace '\\', '/' + Write-Host "ROCM_SDK_PATH=$RocmSdkPath" + echo "ROCM_SDK_PATH=$RocmSdkPath" >> $env:GITHUB_ENV + + - name: Check ROCm wheel freshness + shell: pwsh + run: | + & $env:VENV_PYTHON .github/scripts/check_wheel_freshness.py --max-age-days 14 + if ($LASTEXITCODE -ne 0) { throw "ROCm nightly wheels are stale" } + + - name: Install rocm_kpack + shell: pwsh + run: | + $python = $env:VENV_PYTHON + # Sparse, blobless, depth-1 checkout of just shared/kpack: the pinned + # commit lives in the rocm-systems monorepo, so cloning it whole (or + # letting pip do it via a git+ URL) would pull far more than the packer. + $kpackSrc = Join-Path $env:RUNNER_TEMP "rocm-systems-kpack" + if (Test-Path $kpackSrc) { Remove-Item -Recurse -Force $kpackSrc } + New-Item -ItemType Directory -Path $kpackSrc | Out-Null + # pwsh does not stop on a native command's exit code, so check the + # setup calls as a group before the guarded fetch. + git -C $kpackSrc init -q . + git -C $kpackSrc remote add origin https://github.com/ROCm/rocm-systems.git + git -C $kpackSrc sparse-checkout init --cone + git -C $kpackSrc sparse-checkout set shared/kpack + if ($LASTEXITCODE -ne 0) { throw "Failed to prepare the rocm-systems sparse checkout" } + git -C $kpackSrc fetch --depth 1 --filter=blob:none origin "${{ steps.ci-env.outputs.kpack-ref }}" + if ($LASTEXITCODE -ne 0) { throw "Failed to fetch rocm-systems ${{ steps.ci-env.outputs.kpack-ref }}" } + git -C $kpackSrc checkout -q FETCH_HEAD + if ($LASTEXITCODE -ne 0) { throw "Failed to check out ${{ steps.ci-env.outputs.kpack-ref }}" } + # Confirms the fetch landed on the pinned commit. + $fetched = (& git -C $kpackSrc rev-parse HEAD).Trim() + if ($fetched -ne "${{ steps.ci-env.outputs.kpack-ref }}") { throw "rocm-systems is $fetched, expected ${{ steps.ci-env.outputs.kpack-ref }}" } + # Installing the directory rebuilds and reinstalls every time, so a + # warm venv cannot keep an older build of this same 0.1.0 version. + & $python -m pip install (Join-Path $kpackSrc "shared\kpack") + if ($LASTEXITCODE -ne 0) { throw "Failed to install rocm_kpack" } + # From the installed module, which also proves it imports. + $sitePackages = (& $python -c "import pathlib, rocm_kpack; print(pathlib.Path(rocm_kpack.__file__).parent.parent.as_posix())") + if ($LASTEXITCODE -ne 0) { throw "rocm_kpack is not importable after install" } + if ([string]::IsNullOrWhiteSpace($sitePackages)) { throw "empty rocm_kpack python dir" } + echo "KPACK_PYTHON_DIR=$sitePackages" >> $env:GITHUB_ENV + + # Must precede configure: configuration fails if pytest is not importable + # by the interpreter descriptor-packaging registers its tests under. The + # interpreter is named explicitly because it must be the one passed as + # -DPython3_EXECUTABLE below -- a bare `pip` here would install into the + # setup-python tool-cache interpreter instead. + - name: Install Python dependencies + run: | + "$VENV_PYTHON" -m pip install --upgrade -r .github/requirements-hipdnn-superbuild-ci.txt + + # HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES registers the probe ctest + # entries and the hkp_packaging_probes target. The component list is + # reduced to what the probes need, and GPU_TARGETS is the lane's gfx1151, + # not a probe arch: each probe passes its own arch to the packer. The + # amd_comgr library rocKE loads is derived from the amd_comgr package at + # configure time, so no HIPKERNELPROVIDER_ROCKE_COMGR_LIB is passed. + - name: Configure superbuild + run: | + cmake --preset hipdnn-dev-all -GNinja \ + -DROCM_LIBS_ENABLE_COMPONENTS="hipdnn;hip-kernel-provider" \ + -DROCM_PATH="$ROCM_SDK_PATH" \ + -DCMAKE_PROGRAM_PATH="$ROCM_SDK_PATH/lib/llvm/bin" \ + -DGPU_TARGETS="$GPU_TARGET" \ + -DROCM_LIBS_ENABLE_ROOT_CTEST=ON \ + -DENABLE_CLANG_FORMAT=OFF \ + -DENABLE_CLANG_TIDY=OFF \ + -DHIPKERNELPROVIDER_ENABLE_ROCKE=ON \ + -DHIPDNN_ENABLE_KERNEL_INGESTOR=ON \ + -DHIPDNN_ENABLE_SDPA=ON \ + -DHIPKERNELPROVIDER_KPACK_PYTHON_DIR="$KPACK_PYTHON_DIR" \ + -DPython_EXECUTABLE="$VENV_PYTHON" \ + -DPython3_EXECUTABLE="$VENV_PYTHON" \ + -DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON + + # An unknown target (option not applied) fails this step. + - name: Build packaging probes + run: cmake --build build --target hkp_packaging_probes + + # --no-tests=error makes an empty selection fail. + - name: Run packaging probes + run: ctest --test-dir build -R '^hkp-probe-' --no-tests=error --output-on-failure --output-junit probe-junit.xml + + # ctest reports skipped tests and fewer-than-declared tests as success; + # this compares the junit against the manifest configure wrote. + - name: Check packaging probes ran + run: | + "$VENV_PYTHON" .github/scripts/check_probe_junit.py \ + --junit build/probe-junit.xml \ + --manifest build/hkp-probes/manifest.txt + hipdnn_build_summary: name: hipDNN Superbuild Summary if: always() @@ -685,6 +850,7 @@ jobs: - hipdnn-windows-superbuild - hipdnn-linux-superbuild - hipdnn-probe-linux + - hipdnn-probe-windows runs-on: ubuntu-24.04 steps: - name: Report results From 1e764580ca140ddc56469975c2acb7f1a7540be9 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Sun, 4 Oct 2026 16:18:36 -0400 Subject: [PATCH 06/52] hkp probes: README claims match measured evidence --- .../hip-kernel-provider/descriptor-packaging/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 26094891a49d..aeb6ac5befce 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -264,7 +264,7 @@ then a ctest entry asserts the packed output. - The rocKE sources come from the wheel the packer is given, not from the in-tree source tree the pytest suite imports. - The architecture and output wiring of the CMake pack target: `--arches`, the stamp, the - interpreter and wheel directory, comgr forwarding. + interpreter and wheel directory, comgr forwarding (the last is asserted via `provenance-comgr` only; no local mutation measures it). - The output: a kpack archive for exactly that architecture, with the expected UKD count and shipped provenance. @@ -279,7 +279,7 @@ then a ctest entry asserts the packed output. Probes are build-tree ctest entries only. They exist only under `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` (default `OFF`, CI-only), are never installed and write under `/hkp-probes//`, outside both shipped descriptor trees. -With the option `OFF` the only configure difference is that cache entry. +With the option `OFF` the only cache or behavior difference is that cache entry (the new module also becomes a CMake regeneration input, which changes no target). ### Running the probes locally From a291859ce50217b839038605a7130da5c3c63fb4 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Sun, 4 Oct 2026 16:55:08 -0400 Subject: [PATCH 07/52] hkp probes: include the probe module only inside the option-guarded hook --- .../descriptor-packaging/cmake/HkpPackaging.cmake | 2 +- .../descriptor-packaging/cmake/HkpPackagingProbes.cmake | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake index e79387954419..2990be331d41 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake @@ -27,7 +27,6 @@ set(HKP_PACK_STAMP_NAME ".hkp-packed.stamp" CACHE INTERNAL "Name of the completion stamp each pack writes inside its output root") include(KpackPython) -include(HkpPackagingProbes) # --------------------------------------------------------------------------- # hkp_resolve_kpack( ) @@ -1444,6 +1443,7 @@ function(hkp_add_packaging) hkp_register_tests("${_rocm_kpack_dir}" "${HKP_HIPCC}" "${_rocke_comgr_lib}") if(HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES) + include("${HKP_PKG_DIR}/cmake/HkpPackagingProbes.cmake") hkp_load_packaging_probes( "${_rocm_kpack_dir}" "${HKP_HIPCC}" "${_rocke_comgr_lib}" ${_rocke_args}) endif() diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake index cf1f368fc336..f6f83e9f23c6 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -6,9 +6,9 @@ # packs through hkp_wire_pack_target with the toolchain hkp_add_packaging resolved, # then a ctest entry asserts the packed output. Nothing here installs or ships. # -# This module holds function definitions only; it has no configure effect until -# hkp_load_packaging_probes() runs, which hkp_add_packaging calls under -# HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES. +# This module is included only by the guarded hook in hkp_add_packaging, so with the +# option OFF it is never read. It holds function definitions only; configure effects +# start when hkp_load_packaging_probes() runs. include_guard(GLOBAL) From de8e7fa9741092a9fdd78f47849537acbda29ac6 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Sun, 4 Oct 2026 18:22:38 -0400 Subject: [PATCH 08/52] hkp probes: README states the OFF-mode delta accurately --- .../hip-kernel-provider/descriptor-packaging/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index aeb6ac5befce..9cafda7b380a 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -279,7 +279,7 @@ then a ctest entry asserts the packed output. Probes are build-tree ctest entries only. They exist only under `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` (default `OFF`, CI-only), are never installed and write under `/hkp-probes//`, outside both shipped descriptor trees. -With the option `OFF` the only cache or behavior difference is that cache entry (the new module also becomes a CMake regeneration input, which changes no target). +With the option `OFF` the only difference is that cache entry; the probe module is never read. ### Running the probes locally From 572dfc83e356eb58a6bc077b3e8e226f3e13fa5a Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Sun, 4 Oct 2026 18:35:17 -0400 Subject: [PATCH 09/52] ci: run the packaging probes inside the existing superbuild lanes --- .github/scripts/check_probe_junit.py | 2 +- .github/workflows/hipdnn-superbuild-ci.yml | 367 ++---------------- .../descriptor-packaging/README.md | 2 +- 3 files changed, 40 insertions(+), 331 deletions(-) diff --git a/.github/scripts/check_probe_junit.py b/.github/scripts/check_probe_junit.py index 04b83b2ded59..87d506d1bc95 100755 --- a/.github/scripts/check_probe_junit.py +++ b/.github/scripts/check_probe_junit.py @@ -4,7 +4,7 @@ """Prove that every declared hipDNN packaging probe ran and passed. -The packaging-probe CI jobs run ctest with --output-junit. ctest exits 0 for a +The superbuild lanes run the packaging probes through ctest --output-junit. ctest exits 0 for a junit file that lists fewer tests than were declared, and a skipped test is not a failure to it. This script compares the junit file against the manifest the CMake configure wrote (one ctest test name per line) so that "declared N, ran diff --git a/.github/workflows/hipdnn-superbuild-ci.yml b/.github/workflows/hipdnn-superbuild-ci.yml index 147a9f3e7bbf..7b8f93ca66bc 100644 --- a/.github/workflows/hipdnn-superbuild-ci.yml +++ b/.github/workflows/hipdnn-superbuild-ci.yml @@ -226,11 +226,19 @@ jobs: -DFETCHCONTENT_SOURCE_DIR_NANOBIND="$THIRD_PARTY_DEPS_DIR/nanobind-2.12.0" \ -DFETCHCONTENT_SOURCE_DIR_TSL_ROBIN_MAP="$THIRD_PARTY_DEPS_DIR/robin-map-1.4.1" \ -DPython_EXECUTABLE="$VENV_PYTHON" \ - -DPython3_EXECUTABLE="$VENV_PYTHON" + -DPython3_EXECUTABLE="$VENV_PYTHON" \ + -DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON - name: Build run: cmake --build build + # The probes pack the production gfx950 rocKE descriptors for an + # architecture this lane does not build (GPU_TARGETS is the lane's own). + # Their pack targets are not part of the default build, and the ctest + # entries fail without their output, so build them before "Run tests". + - name: Build packaging probes + run: cmake --build build --target hkp_packaging_probes + # Packing is skipped with a STATUS message, so a resolution regression # would otherwise pass CI while producing no archives. - name: Verify kpack archives were produced @@ -248,6 +256,16 @@ jobs: - name: Run tests run: ctest --test-dir build --output-on-failure + # ctest reports skipped and missing tests as success. Re-run the probe + # entries alone and compare the junit against the manifest configure + # wrote, so a probe that did not run fails the job. + - name: Check packaging probes ran + run: | + ctest --test-dir build -R '^hkp-probe-' --no-tests=error --output-on-failure --output-junit probe-junit.xml + "$VENV_PYTHON" .github/scripts/check_probe_junit.py \ + --junit build/probe-junit.xml \ + --manifest build/hkp-probes/manifest.txt + - name: Patch ROCm SDK with superbuild outputs shell: pwsh run: | @@ -470,11 +488,19 @@ jobs: -DFETCHCONTENT_SOURCE_DIR_NANOBIND="$THIRD_PARTY_DEPS_DIR/nanobind-2.12.0" \ -DFETCHCONTENT_SOURCE_DIR_TSL_ROBIN_MAP="$THIRD_PARTY_DEPS_DIR/robin-map-1.4.1" \ -DPython_EXECUTABLE="$VENV_PYTHON" \ - -DPython3_EXECUTABLE="$VENV_PYTHON" + -DPython3_EXECUTABLE="$VENV_PYTHON" \ + -DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON - name: Build run: cmake --build build + # The probes pack the production gfx950 rocKE descriptors for an + # architecture this lane does not build (GPU_TARGETS is the lane's own). + # Their pack targets are not part of the default build, and the ctest + # entries fail without their output, so build them before "Run tests". + - name: Build packaging probes + run: cmake --build build --target hkp_packaging_probes + # Packing is skipped with a STATUS message, so a resolution regression # would otherwise pass CI while producing no archives. - name: Verify kpack archives were produced @@ -488,6 +514,16 @@ jobs: - name: Run tests run: ctest --test-dir build --output-on-failure + # ctest reports skipped and missing tests as success. Re-run the probe + # entries alone and compare the junit against the manifest configure + # wrote, so a probe that did not run fails the job. + - name: Check packaging probes ran + run: | + ctest --test-dir build -R '^hkp-probe-' --no-tests=error --output-on-failure --output-junit probe-junit.xml + "$VENV_PYTHON" .github/scripts/check_probe_junit.py \ + --junit build/probe-junit.xml \ + --manifest build/hkp-probes/manifest.txt + # Golden-data validation used to live here as a schedule-gated re-run of the # external integration targets with HIPDNN_TEST_VERIFICATION_MODE=golden-check. # That spelling is retired: it is now the hipdnn_golden_data_tests binary, @@ -518,339 +554,12 @@ jobs: LD_LIBRARY_PATH="$ROCM_SDK_PATH/lib:${LD_LIBRARY_PATH:-}" \ "$VENV_PYTHON" -m pytest -q projects/hipdnn/python/frontend_wheel_package/tests - # Packs the production gfx950 rocKE descriptors for an architecture this - # workflow does not build. Only the packaging-probe ctest entries run; the - # build is limited to the probe closure (no hipDNN C++). - hipdnn-probe-linux: - name: HipDNN Packaging Probe Linux - runs-on: azure-linux-scale-rocm - env: - # Feeds both -DGPU_TARGETS and the multi-arch nightly device extra - # (rocm[...,device-]), both of which need a concrete gfx target rather - # than a TheRock family name. Must differ from every probe's arch so that a probe - # that stops passing its own arch to the packer goes red. - GPU_TARGET: gfx942 - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - sparse-checkout: | - .dvc - .github - cmake - dnn-providers - projects/hipdnn - projects/miopen - test - - # Shared CI constants, kpack-ref among them, defined in one place. - - name: Load CI environment - id: ci-env - uses: ./.github/actions/ci-env - - - name: Set environment - run: | - V="${{ runner.temp }}/rocm_wheels" - echo "VENV_PATH=$V" >> "$GITHUB_ENV" - echo "VENV_PYTHON=$V/bin/python" >> "$GITHUB_ENV" - - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.12' - - - name: Check build tools - run: | - command -v ninja >/dev/null || { echo "ninja is not on this runner" >&2; exit 1; } - - - name: Install ROCm wheels - run: | - echo "Creating Python virtual environment at $VENV_PATH" - python -m venv "$VENV_PATH" - source "$VENV_PATH/bin/activate" - - echo "Installing ROCm wheels from multi-arch nightlies (device: $GPU_TARGET)" - pip install --index-url "https://nightly.repo.amd.com/rocm/whl-next/" "rocm[libraries,devel,device-$GPU_TARGET]" - - echo "Initializing ROCm SDK" - rocm-sdk init - - SITE_PACKAGES=$("$VENV_PYTHON" -c "import site; print(site.getsitepackages()[0])") - ROCM_SDK_PATH="$SITE_PACKAGES/_rocm_sdk_devel" - echo "ROCM_SDK_PATH=$ROCM_SDK_PATH" - echo "ROCM_SDK_PATH=$ROCM_SDK_PATH" >> "$GITHUB_ENV" - - # rocKE loads libamd_comgr through ctypes, not through CMake, so - # -DROCM_PATH below is invisible to it. It resolves via $ROCM_PATH in - # the environment or a globbed /opt/rocm*, and this runner is - # wheel-only: there is no /opt/rocm, and the devel wheel's lib/ holds - # headers and CMake config, not the runtime .so. comgr ships in the - # sibling _rocm_sdk_core wheel, so name it explicitly. - # - # Exported under HKP_ROCKE_COMGR_LIB and fed to the configure step as - # -DHIPKERNELPROVIDER_ROCKE_COMGR_LIB, not as rocke's own - # ROCKE_COMGR_LIB: the cache variable is the channel hkp already - # forwards to the configure probe, the pack step and the ctest - # entries, so all four ask about the same library. Exporting rocke's - # runtime name directly would instead override every python process - # in every later step of this job, including ones that should be - # exercising normal resolution. - COMGR_LIB=$(ls "$SITE_PACKAGES/_rocm_sdk_core/lib/libamd_comgr.so".* 2>/dev/null | head -n1) - test -n "$COMGR_LIB" || { echo "no libamd_comgr.so under $SITE_PACKAGES/_rocm_sdk_core/lib" >&2; exit 1; } - echo "HKP_ROCKE_COMGR_LIB=$COMGR_LIB" - echo "HKP_ROCKE_COMGR_LIB=$COMGR_LIB" >> "$GITHUB_ENV" - - - name: Check ROCm wheel freshness - run: | - "$VENV_PYTHON" .github/scripts/check_wheel_freshness.py --max-age-days 14 - - - name: Install rocm_kpack - run: | - set -euo pipefail - # Sparse, blobless, depth-1 checkout of just shared/kpack: the pinned - # commit lives in the rocm-systems monorepo, so cloning it whole (or - # letting pip do it via a git+ URL) would pull far more than the packer. - KPACK_SRC="${RUNNER_TEMP}/rocm-systems-kpack" - rm -rf "$KPACK_SRC" - mkdir -p "$KPACK_SRC" - git -C "$KPACK_SRC" init -q . - git -C "$KPACK_SRC" remote add origin https://github.com/ROCm/rocm-systems.git - git -C "$KPACK_SRC" sparse-checkout init --cone - git -C "$KPACK_SRC" sparse-checkout set shared/kpack - git -C "$KPACK_SRC" fetch --depth 1 --filter=blob:none origin "${{ steps.ci-env.outputs.kpack-ref }}" - git -C "$KPACK_SRC" checkout -q FETCH_HEAD - # Confirms the fetch landed on the pinned commit. - fetched=$(git -C "$KPACK_SRC" rev-parse HEAD) - test "$fetched" = "${{ steps.ci-env.outputs.kpack-ref }}" || { echo "rocm-systems is $fetched, expected ${{ steps.ci-env.outputs.kpack-ref }}" >&2; exit 1; } - # Installing the directory rebuilds and reinstalls every time, so a - # warm venv cannot keep an older build of this same 0.1.0 version. - "$VENV_PYTHON" -m pip install "$KPACK_SRC/shared/kpack" - # Derived from the installed module, which also proves it imports. - KPACK_PYTHON_DIR=$("$VENV_PYTHON" -c "import pathlib, rocm_kpack; print(pathlib.Path(rocm_kpack.__file__).parent.parent.as_posix())") - test -n "$KPACK_PYTHON_DIR" || { echo "empty rocm_kpack python dir" >&2; exit 1; } - echo "KPACK_PYTHON_DIR=$KPACK_PYTHON_DIR" >> "$GITHUB_ENV" - - # Must precede configure: configuration fails if pytest is not importable - # by the interpreter descriptor-packaging registers its tests under. The - # interpreter is named explicitly because it must be the one passed as - # -DPython3_EXECUTABLE below -- a bare `pip` here would install into the - # setup-python tool-cache interpreter instead. - - name: Install Python dependencies - run: | - "$VENV_PYTHON" -m pip install --upgrade -r .github/requirements-hipdnn-superbuild-ci.txt - - # HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES registers the probe ctest - # entries and the hkp_packaging_probes target. The component list is - # reduced to what the probes need, and GPU_TARGETS is the lane's gfx942, - # not a probe arch: each probe passes its own arch to the packer. - - name: Configure superbuild - run: | - cmake --preset hipdnn-dev-all -GNinja \ - -DROCM_LIBS_ENABLE_COMPONENTS="hipdnn;hip-kernel-provider" \ - -DROCM_PATH="$ROCM_SDK_PATH" \ - -DCMAKE_PROGRAM_PATH="$ROCM_SDK_PATH/lib/llvm/bin" \ - -DGPU_TARGETS="$GPU_TARGET" \ - -DROCM_LIBS_ENABLE_ROOT_CTEST=ON \ - -DENABLE_CLANG_FORMAT=OFF \ - -DENABLE_CLANG_TIDY=OFF \ - -DHIPKERNELPROVIDER_ENABLE_ROCKE=ON \ - -DHIPDNN_ENABLE_KERNEL_INGESTOR=ON \ - -DHIPDNN_ENABLE_SDPA=ON \ - -DHIPKERNELPROVIDER_KPACK_PYTHON_DIR="$KPACK_PYTHON_DIR" \ - -DHIPKERNELPROVIDER_ROCKE_COMGR_LIB="$HKP_ROCKE_COMGR_LIB" \ - -DPython_EXECUTABLE="$VENV_PYTHON" \ - -DPython3_EXECUTABLE="$VENV_PYTHON" \ - -DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON - - # An unknown target (option not applied) fails this step. - - name: Build packaging probes - run: cmake --build build --target hkp_packaging_probes - - # --no-tests=error makes an empty selection fail. - - name: Run packaging probes - run: ctest --test-dir build -R '^hkp-probe-' --no-tests=error --output-on-failure --output-junit probe-junit.xml - - # ctest reports skipped tests and fewer-than-declared tests as success; - # this compares the junit against the manifest configure wrote. - - name: Check packaging probes ran - run: | - "$VENV_PYTHON" .github/scripts/check_probe_junit.py \ - --junit build/probe-junit.xml \ - --manifest build/hkp-probes/manifest.txt - - # Packs the production gfx950 rocKE descriptors for an architecture this - # workflow does not build, on Windows. Only the packaging-probe ctest entries - # run; the build is limited to the probe closure (no hipDNN C++). No PATH - # step: the probe ctest entries run python only and read files. - hipdnn-probe-windows: - name: HipDNN Packaging Probe Windows - runs-on: azure-windows-scale-rocm - defaults: - run: - shell: bash - env: - # Feeds both -DGPU_TARGETS and the multi-arch nightly device extra - # (rocm[...,device-]), both of which need a concrete gfx target rather - # than a TheRock family name. Must differ from every probe's arch so that a probe - # that stops passing its own arch to the packer goes red. - GPU_TARGET: gfx1151 - steps: - - name: Configure git longpaths - run: git config --global core.longpaths true - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - sparse-checkout: | - .dvc - .github - cmake - dnn-providers - projects/hipdnn - projects/miopen - test - - # Shared CI constants, kpack-ref among them, defined in one place. - - name: Load CI environment - id: ci-env - uses: ./.github/actions/ci-env - - - name: Set environment - run: | - V='B:\rocm_wheels' - echo "VENV_PATH=$V" >> "$GITHUB_ENV" - echo "VENV_PYTHON=$V/Scripts/python.exe" >> "$GITHUB_ENV" - - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.12' - - - name: Configure MSVC - uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 - - - name: Install build tools - run: | - choco source disable -n=chocolatey - choco source add -n=internal -s http://nexus-service.nexus-ns.svc.cluster.local:8081/repository/choco-group/ --priority=1 - choco install --no-progress -y ninja --version 1.12.1 - - - name: Install ROCm wheels - shell: pwsh - run: | - Write-Host "Creating Python virtual environment at $env:VENV_PATH" - python -m venv $env:VENV_PATH - & "$env:VENV_PATH\Scripts\Activate.ps1" - - Write-Host "Installing ROCm wheels from multi-arch nightlies (device: $env:GPU_TARGET)" - pip install --index-url "https://nightly.repo.amd.com/rocm/whl-next/" "rocm[libraries,devel,device-$env:GPU_TARGET]" - if ($LASTEXITCODE -ne 0) { throw "Failed to install ROCm wheels" } - - Write-Host "Initializing ROCm SDK" - rocm-sdk init - if ($LASTEXITCODE -ne 0) { throw "Failed to initialize ROCm SDK" } - - $RocmSdkPath = "$env:VENV_PATH\Lib\site-packages\_rocm_sdk_devel" -replace '\\', '/' - Write-Host "ROCM_SDK_PATH=$RocmSdkPath" - echo "ROCM_SDK_PATH=$RocmSdkPath" >> $env:GITHUB_ENV - - - name: Check ROCm wheel freshness - shell: pwsh - run: | - & $env:VENV_PYTHON .github/scripts/check_wheel_freshness.py --max-age-days 14 - if ($LASTEXITCODE -ne 0) { throw "ROCm nightly wheels are stale" } - - - name: Install rocm_kpack - shell: pwsh - run: | - $python = $env:VENV_PYTHON - # Sparse, blobless, depth-1 checkout of just shared/kpack: the pinned - # commit lives in the rocm-systems monorepo, so cloning it whole (or - # letting pip do it via a git+ URL) would pull far more than the packer. - $kpackSrc = Join-Path $env:RUNNER_TEMP "rocm-systems-kpack" - if (Test-Path $kpackSrc) { Remove-Item -Recurse -Force $kpackSrc } - New-Item -ItemType Directory -Path $kpackSrc | Out-Null - # pwsh does not stop on a native command's exit code, so check the - # setup calls as a group before the guarded fetch. - git -C $kpackSrc init -q . - git -C $kpackSrc remote add origin https://github.com/ROCm/rocm-systems.git - git -C $kpackSrc sparse-checkout init --cone - git -C $kpackSrc sparse-checkout set shared/kpack - if ($LASTEXITCODE -ne 0) { throw "Failed to prepare the rocm-systems sparse checkout" } - git -C $kpackSrc fetch --depth 1 --filter=blob:none origin "${{ steps.ci-env.outputs.kpack-ref }}" - if ($LASTEXITCODE -ne 0) { throw "Failed to fetch rocm-systems ${{ steps.ci-env.outputs.kpack-ref }}" } - git -C $kpackSrc checkout -q FETCH_HEAD - if ($LASTEXITCODE -ne 0) { throw "Failed to check out ${{ steps.ci-env.outputs.kpack-ref }}" } - # Confirms the fetch landed on the pinned commit. - $fetched = (& git -C $kpackSrc rev-parse HEAD).Trim() - if ($fetched -ne "${{ steps.ci-env.outputs.kpack-ref }}") { throw "rocm-systems is $fetched, expected ${{ steps.ci-env.outputs.kpack-ref }}" } - # Installing the directory rebuilds and reinstalls every time, so a - # warm venv cannot keep an older build of this same 0.1.0 version. - & $python -m pip install (Join-Path $kpackSrc "shared\kpack") - if ($LASTEXITCODE -ne 0) { throw "Failed to install rocm_kpack" } - # From the installed module, which also proves it imports. - $sitePackages = (& $python -c "import pathlib, rocm_kpack; print(pathlib.Path(rocm_kpack.__file__).parent.parent.as_posix())") - if ($LASTEXITCODE -ne 0) { throw "rocm_kpack is not importable after install" } - if ([string]::IsNullOrWhiteSpace($sitePackages)) { throw "empty rocm_kpack python dir" } - echo "KPACK_PYTHON_DIR=$sitePackages" >> $env:GITHUB_ENV - - # Must precede configure: configuration fails if pytest is not importable - # by the interpreter descriptor-packaging registers its tests under. The - # interpreter is named explicitly because it must be the one passed as - # -DPython3_EXECUTABLE below -- a bare `pip` here would install into the - # setup-python tool-cache interpreter instead. - - name: Install Python dependencies - run: | - "$VENV_PYTHON" -m pip install --upgrade -r .github/requirements-hipdnn-superbuild-ci.txt - - # HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES registers the probe ctest - # entries and the hkp_packaging_probes target. The component list is - # reduced to what the probes need, and GPU_TARGETS is the lane's gfx1151, - # not a probe arch: each probe passes its own arch to the packer. The - # amd_comgr library rocKE loads is derived from the amd_comgr package at - # configure time, so no HIPKERNELPROVIDER_ROCKE_COMGR_LIB is passed. - - name: Configure superbuild - run: | - cmake --preset hipdnn-dev-all -GNinja \ - -DROCM_LIBS_ENABLE_COMPONENTS="hipdnn;hip-kernel-provider" \ - -DROCM_PATH="$ROCM_SDK_PATH" \ - -DCMAKE_PROGRAM_PATH="$ROCM_SDK_PATH/lib/llvm/bin" \ - -DGPU_TARGETS="$GPU_TARGET" \ - -DROCM_LIBS_ENABLE_ROOT_CTEST=ON \ - -DENABLE_CLANG_FORMAT=OFF \ - -DENABLE_CLANG_TIDY=OFF \ - -DHIPKERNELPROVIDER_ENABLE_ROCKE=ON \ - -DHIPDNN_ENABLE_KERNEL_INGESTOR=ON \ - -DHIPDNN_ENABLE_SDPA=ON \ - -DHIPKERNELPROVIDER_KPACK_PYTHON_DIR="$KPACK_PYTHON_DIR" \ - -DPython_EXECUTABLE="$VENV_PYTHON" \ - -DPython3_EXECUTABLE="$VENV_PYTHON" \ - -DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON - - # An unknown target (option not applied) fails this step. - - name: Build packaging probes - run: cmake --build build --target hkp_packaging_probes - - # --no-tests=error makes an empty selection fail. - - name: Run packaging probes - run: ctest --test-dir build -R '^hkp-probe-' --no-tests=error --output-on-failure --output-junit probe-junit.xml - - # ctest reports skipped tests and fewer-than-declared tests as success; - # this compares the junit against the manifest configure wrote. - - name: Check packaging probes ran - run: | - "$VENV_PYTHON" .github/scripts/check_probe_junit.py \ - --junit build/probe-junit.xml \ - --manifest build/hkp-probes/manifest.txt - hipdnn_build_summary: name: hipDNN Superbuild Summary if: always() needs: - hipdnn-windows-superbuild - hipdnn-linux-superbuild - - hipdnn-probe-linux - - hipdnn-probe-windows runs-on: ubuntu-24.04 steps: - name: Report results diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 9cafda7b380a..c308c9da1773 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -388,7 +388,7 @@ new integration without a probe configures, builds and passes CI, and its packag an architecture the lanes do not build is then unchecked. 1. Add one `hkp_add_packaging_probe` declaration to `probes/probes.cmake`, with a comment - stating why the chosen `INSTANCE` represents its family. No workflow edit: the jobs + stating why the chosen `INSTANCE` represents its family. No workflow edit: the superbuild lanes build `hkp_packaging_probes` and select `^hkp-probe-`, and the manifest counts every declared probe. 2. **The probe architecture must differ from the lane's `GPU_TARGETS`** (gfx942 on the From 8a6f2584bbfc2b8c070d49081daa8329634e5b87 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 5 Oct 2026 16:20:50 +0000 Subject: [PATCH 10/52] ci: correct the packaging-probe step comment (pack targets are part of all) --- .github/workflows/hipdnn-superbuild-ci.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/hipdnn-superbuild-ci.yml b/.github/workflows/hipdnn-superbuild-ci.yml index 7b8f93ca66bc..8496d481ee54 100644 --- a/.github/workflows/hipdnn-superbuild-ci.yml +++ b/.github/workflows/hipdnn-superbuild-ci.yml @@ -232,10 +232,12 @@ jobs: - name: Build run: cmake --build build - # The probes pack the production gfx950 rocKE descriptors for an + # The probes pack the production gfx950 descriptors for an # architecture this lane does not build (GPU_TARGETS is the lane's own). - # Their pack targets are not part of the default build, and the ctest - # entries fail without their output, so build them before "Run tests". + # Their pack targets are part of `all` (ALL custom targets), so Build above + # already ran them; this step stays so a probe failure is labelled and the + # aggregate target is exercised. The ctest entries fail without their + # output. - name: Build packaging probes run: cmake --build build --target hkp_packaging_probes @@ -494,10 +496,12 @@ jobs: - name: Build run: cmake --build build - # The probes pack the production gfx950 rocKE descriptors for an + # The probes pack the production gfx950 descriptors for an # architecture this lane does not build (GPU_TARGETS is the lane's own). - # Their pack targets are not part of the default build, and the ctest - # entries fail without their output, so build them before "Run tests". + # Their pack targets are part of `all` (ALL custom targets), so Build above + # already ran them; this step stays so a probe failure is labelled and the + # aggregate target is exercised. The ctest entries fail without their + # output. - name: Build packaging probes run: cmake --build build --target hkp_packaging_probes From 0482449eef08826e9ad715f0a82142f7c5332c91 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 5 Oct 2026 16:20:51 +0000 Subject: [PATCH 11/52] hkp probes: producer-generic probe site, one hkp_add_packaging_probe(ARCH ...) line per arch Derive keeps one UKD per compile group (rocke: builder; hip: source+build) among UKDs matching ARCH using the packer's arch rules, writes expect.json, and is write-if-different so a reconfigure does not repack. Assert takes expect.json and a provenance rules table keyed by origin kind. KIND/KDP/INSTANCE are removed. Docs and skills updated. --- .../scripts/tests/check_probe_junit_test.py | 2 +- dnn-providers/hip-kernel-provider/README.md | 2 + .../descriptor-packaging/README.md | 114 +++--- .../cmake/HkpPackagingProbes.cmake | 99 ++--- .../descriptor-packaging/probes/probes.cmake | 26 +- .../probes/tests/conftest.py | 44 ++- .../probes/tests/test_hkp_probe_assert.py | 96 +++-- .../tests/test_hkp_probe_derive_root.py | 346 ++++++++++++++---- .../probes/tests/test_hkp_probe_end_to_end.py | 138 +++++++ .../tools/hkp_probe_assert.py | 127 +++++-- .../tools/hkp_probe_derive_root.py | 228 ++++++++++-- .../descriptors/README.md | 9 + .../skills/hipdnn-ingestor-engine/RUNBOOK.md | 20 +- .../ai/skills/hipdnn-ingestor-engine/SKILL.md | 6 + .../skills/hipdnn-ingestor-engine/extend.md | 7 + .../ai/skills/hipdnn-superbuild-test/SKILL.md | 9 + .../ai/skills/hipdnn-superbuild/SKILL.md | 1 + 17 files changed, 989 insertions(+), 285 deletions(-) create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py mode change 100644 => 100755 dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py diff --git a/.github/scripts/tests/check_probe_junit_test.py b/.github/scripts/tests/check_probe_junit_test.py index e97d3e88ca10..899f3c00801f 100644 --- a/.github/scripts/tests/check_probe_junit_test.py +++ b/.github/scripts/tests/check_probe_junit_test.py @@ -10,7 +10,7 @@ sys.path.insert(0, os.fspath(Path(__file__).parent.parent)) import check_probe_junit as cpj -MANIFEST = ["hkp-probe-gfx950_attention_dense", "hkp-probe-tools"] +MANIFEST = ["hkp-probe-gfx950", "hkp-probe-tools"] def case_xml(name: str, child: str = "") -> str: diff --git a/dnn-providers/hip-kernel-provider/README.md b/dnn-providers/hip-kernel-provider/README.md index 0ed706985d23..b0b71fc820e0 100644 --- a/dnn-providers/hip-kernel-provider/README.md +++ b/dnn-providers/hip-kernel-provider/README.md @@ -78,6 +78,8 @@ Beyond the shared third-party set, the prefixes must provide HIP/HIPRTC, `hipdnn Descriptor packaging (`HIPDNN_ENABLE_KERNEL_INGESTOR=ON`) additionally requires a supplied `Python3_EXECUTABLE` with pip, `msgpack`, and `zstandard`, a kpack source tree, and local rocKE wheels. The `hkp_rocke_wheel_python_interp` target installs those wheels only into build-owned storage with no index access or dependency resolution, then uses the supplied interpreter with a subprocess-scoped private import path. It does not install rocKE into the parent Python environment. See [Kernel packing](../../projects/hipdnn/docs/Building.md#kernel-packing-rocm_kpack) for wheel supply modes and prerequisites. +Packaging probes check that the production descriptors pack for an architecture the build does not target. They are opt-in per architecture (one `hkp_add_packaging_probe(ARCH )` line in `descriptor-packaging/probes/probes.cmake`; a new pack under an already probed architecture in the production root is covered automatically) and superbuild-only: the `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` option is `OFF` by default. A probe packs one instance per compile group; the lane's own architecture is covered in full by the normal build. They do not cover ASM SDPA, C++/runtime gfx950 paths, install staging, or standalone-UKD references (probe derive fails configure on those). See the [descriptor-packaging README](descriptor-packaging/README.md#packaging-probes). + ### Testing After building, run the test suites: diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index c308c9da1773..52d68a0d9243 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -270,16 +270,24 @@ then a ctest entry asserts the packed output. **What it does not prove** -- Instances other than the named representative (see below). +- Variants within a compile group: one instance per group is packed (see below). The + lane's own architecture is covered in full by the normal build. +- ASM SDPA (prebuilt `.co` files that kpack only stores: nothing compiles and nothing + reads the archive), the C++/runtime gfx950 paths, and install staging. - Device behavior: no kernel is launched, no numerics are checked. - Anything about integrations without a probe. - That the CI job runs: that is the workflow's job (`--no-tests=error`, the manifest check below). -Probes are build-tree ctest entries only. They exist only under -`HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` (default `OFF`, CI-only), are never installed -and write under `/hkp-probes//`, outside both shipped descriptor trees. -With the option `OFF` the only difference is that cache entry; the probe module is never read. +Probes are **opt-in per architecture and superbuild-only**: they exist only under +`HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` (default `OFF`, CI-only), declared one +`hkp_add_packaging_probe(ARCH ...)` line per architecture. A new pack under an already +probed architecture in the production root is covered automatically. With the option +`ON`, the pack targets and `hkp_packaging_probes` are part of `all`; the CI +"Build packaging probes" step builds them explicitly for labelling. Probe output is never +installed and is written under `/hkp-probes//`, outside both shipped +descriptor trees. With the option `OFF` the only difference is that cache entry; the probe +module is never read. ### Running the probes locally @@ -313,50 +321,62 @@ ctest names that must run; the junit check fails unless every one ran with no fa error or skip. Configuring without the option and running the same build and ctest lines fails (unknown target, then "No tests were found"). -Locally on Linux the build takes about 35 s (the wheel environment plus a one-instance +Locally on Linux the build takes about 35 s (the wheel environment plus a one-instance-per-group pack) and the two ctest entries about 3 s. There is no local Windows reproduction. ### `hkp_add_packaging_probe` arguments -Declared in `probes/probes.cmake`, defined in `cmake/HkpPackagingProbes.cmake`. +Declared in `probes/probes.cmake`, defined in `cmake/HkpPackagingProbes.cmake`. The +normal declaration is one line per architecture: `hkp_add_packaging_probe(ARCH gfx950)`. | Argument | Meaning | |---|---| -| `NAME` | Probe name. Pack target `hkp_packaging_probe_`, ctest entry `hkp-probe-`. Declaring a name twice is a configure error. | -| `ARCH` | The one architecture to pack, e.g. `gfx950`. Passed to the packer as `--arches`. | -| `KIND` | Producer kind. Only `rocke` is supported; any other value is a configure error naming the supported kinds. | -| `DERIVE_FROM` | Directory of the real production descriptors to derive the probe root from. | -| `KDP` | File name of the KDP inside `DERIVE_FROM` whose `kernelDescriptors` is trimmed. | -| `INSTANCE` | The UKD `name` kept as the representative instance. | +| `ARCH` | Required. The one architecture to pack, e.g. `gfx950`. Passed to the packer as `--arches`. | +| `ROOT` | Optional descriptor root to probe. Defaults to the production root (`HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT`). | +| `NAME` | Optional probe name. Defaults to `` for the production root and `_` for another `ROOT`. Pack target `hkp_packaging_probe_`, ctest entry `hkp-probe-` (e.g. `hkp-probe-gfx950`). Declaring a name twice is a configure error. | | `PACK_JOBS` | Optional prewarm worker count; defaults to `1`. | -There is one mode, `DERIVE_FROM`. A probe has no `ROOT` argument for a hand-written or -frozen root: a frozen copy drifts silently from the production schema, which is exactly -what a probe exists to catch. The expected UKD count is fixed at one. - -Configuration fails, never skips, when: `INSTANCE` is not found exactly once in `KDP`; -the derived root has no KDP covering `ARCH`; the probe output root would sit under a -shipped descriptor tree; no probe is declared; or pytest is not importable by +A probe has no producer-kind or per-KDP argument: the producer kinds present in the root +are discovered from its descriptors, and the assertion expects each UKD's provenance by +its authored `kernel_source.kind`. Probes cover `hip` and `rocke`, the kinds the packer +compiles to kpack output today. Any other kind in a UKD kept for the probed arch is +rejected at configure until its row is added in both tables: `_GROUP_FIELDS` in +`tools/hkp_probe_derive_root.py` and `_PROVENANCE_RULES` in `tools/hkp_probe_assert.py`. + +Configuration fails, never skips, when: `ARCH` is missing; `ROOT` is not a directory; +no KDP under the root ships for `ARCH`; a +KDP shipping for `ARCH` references a standalone UKD (a `kernelDescriptors` entry that is +not an object; derive supports inline UKDs only); the probe output root would sit under a +shipped descriptor tree; a UKD kept for `ARCH` has a kind other than `hip` or `rocke`; no probe is declared; or pytest is not importable by `Python3_EXECUTABLE`. -### The derived-root representative-instance rule +### The derived-root compile-group rule + +At configure, `tools/hkp_probe_derive_root.py` copies the root into +`/hkp-probes//root` unchanged, except that every KDP shipping for `ARCH` has +its `kernelDescriptors` reduced to one UKD per **compile group**, among the UKDs that +themselves ship for `ARCH`. The group is the part of a UKD that selects its compile path: + +| Producer kind | Compile group | +|---|---| +| `rocke` | `(kind, builder)` | +| `hip` | `(kind, source, build)` | +| `hsaco`, `kpack`, `embedded_source` | one per kind | -At configure, `tools/hkp_probe_derive_root.py` copies the `DERIVE_FROM` directory into -`/hkp-probes//root` unchanged, except that `KDP`'s `kernelDescriptors` is -reduced to the single UKD whose `name` equals `INSTANCE`. The probe therefore packs the -real descriptors but compiles one kernel (about 1 s) instead of the whole family. +The kept UKD is the first by sorted `name`. The probe therefore packs the real +descriptors and compiles every distinct compile path once instead of every variant. +Today the production gfx950 KDP (840 `rocke` UKDs sharing one builder) keeps one. +- **Variants inside a group are not compiled.** Breakage specific to one spec of a shared + builder is not caught; that is the accepted gap of the design. - **The key is the UKD `name`, not its `id`.** Ids are `uuid4` values regenerated on every - ingestor run; names encode the specialization and are unique. Keying on the id would - turn every legitimate regeneration into a red probe. -- **Choose the instance whose compile path every variant of that family shares.** One - compile then stands for the family. Breakage specific to the other instances is not - caught; that is the accepted gap of the design. -- **When the named instance is renamed or removed**, configure fails with - `hkp_probe_derive: instance '' not found in ( descriptors)` (or - `... matched times`). That is the intended signal. Pick another instance of the - same family from the KDP and update `INSTANCE` in `probes/probes.cmake`. -- The derive step re-runs when any file under `DERIVE_FROM` changes. + ingestor run; names encode the specialization and are unique. +- **Arch matching follows the packer** (`arch_matches`, `_arch_subset_ok`): an empty or + absent `arch` list is a wildcard. +- Derive also writes `expect.json` (kept KDP, UKD name and kind) beside the root; the + assertion expects exactly that set. +- The derive step re-runs when any file under the root changes, and rewrites only files + whose content differs, so a reconfigure leaves the pack stamp fresh. ### Assertion ids @@ -376,10 +396,11 @@ real descriptors but compiles one kernel (about 1 s) instead of the whole family | `sha256` | The blob's sha256 differs from `kernel_source.sha256`. | | `signature` | A UKD has an empty `signature`. | | `symbol` | A UKD's `symbol` does not appear in its blob. | -| `provenance-origin` | `origin_kind` is not `rocke` (for `KIND rocke`). | +| `provenance-origin` | `origin_kind` differs from the UKD's expected kind (the authored `kernel_source.kind`). | +| `no-rules-for-kind` | A UKD's expected kind has no row in the provenance rules table of `tools/hkp_probe_assert.py`: a new producer kind must add its row (and its compile-group row in `tools/hkp_probe_derive_root.py`). | | `provenance-wheel` | `rocke_wheel_sha256` is missing or empty: the wheel did not supply rocKE. | | `provenance-comgr` | The comgr recorded at pack time is not the expected library (or is empty). Catches a stale system comgr shadowing the intended one. | -| `ukd-count` | The number of kpack-kind UKDs is not the expected one, or is zero. | +| `ukd-count` | The number of kpack-kind UKDs differs from the expected list (`expect.json`, written by derive), is zero, or a shipped UKD is not in the list. | ### Adding a probe @@ -387,10 +408,10 @@ Required for every new architecture or integration. **No CMake guard enforces th new integration without a probe configures, builds and passes CI, and its packaging for an architecture the lanes do not build is then unchecked. -1. Add one `hkp_add_packaging_probe` declaration to `probes/probes.cmake`, with a comment - stating why the chosen `INSTANCE` represents its family. No workflow edit: the superbuild lanes - build `hkp_packaging_probes` and select `^hkp-probe-`, and the manifest counts every - declared probe. +1. Add one `hkp_add_packaging_probe(ARCH )` line to `probes/probes.cmake`. No + workflow edit: the superbuild lanes build `hkp_packaging_probes` and select + `^hkp-probe-`, and the manifest counts every declared probe. A new pack under an + architecture that is already probed needs no new line. 2. **The probe architecture must differ from the lane's `GPU_TARGETS`** (gfx942 on the Linux job, gfx1151 on the Windows job). If they are equal, the architecture-wiring mutation below becomes unobservable on that job. If a probe must use a lane's @@ -412,8 +433,11 @@ ctest --test-dir build-probe -R '^hkp-probe-$' --output-on-failure ``` 1. **Derived descriptors reach the compiler.** - - Rename the representative UKD in the production KDP: configure must fail with - `hkp_probe_derive: instance '' not found`. + - Replace the `ARCH` of the probe with an architecture no KDP in the root ships for: + configure must fail (no KDP under the root ships for it). + - Add a standalone-UKD reference (a non-object `kernelDescriptors` entry) to a KDP that + ships for the probe architecture: configure must fail with + `standalone UKD references unsupported by probe derive`. - Set a compile-affecting spec field of that UKD to a value the builder rejects (for dense attention: `block_n` in `kernel_source.spec` from `64` to `7`): the build must fail in the packer (`invalid spec ...`). @@ -447,8 +471,8 @@ ctest --test-dir build-probe -R '^hkp-probe-$' --output-on-failure - After a good build, delete `build-probe/hkp-probes//out/.hkp-packed.stamp`: the ctest entry must fail `hkp_probe_assert: FAIL stamp-missing`. -Every mutation above turned the gfx950 dense-attention probe red when run locally on -Linux. The Windows half of the wheel check, and comgr forwarding (covered only by +The wheel, architecture-wiring, spec and metadata mutations above turned the gfx950 probe +red when run locally on Linux. The Windows half of the wheel check, and comgr forwarding (covered only by `provenance-comgr`), are verified by CI only. ## Running the tests diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake index f6f83e9f23c6..825e9ccf3172 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -34,11 +34,16 @@ function(_hkp_probe_check_out_root name out_root) endfunction() # --------------------------------------------------------------------------- -# _hkp_probe_derive_root( ) -# Derive the probe root at configure time (FATAL with the tool's stderr when the -# instance is missing or ambiguous) and re-run configure when a source file changes. +# _hkp_probe_derive_root( ) +# Derive the probe root at configure time (FATAL with the tool's stderr when the root +# cannot be derived, e.g. a KDP for references a standalone UKD) and re-run +# configure when a source file changes. The tool writes /../expect.json, the +# list of kept UKDs the assertion expects; receives its path. FATAL when +# that list is empty: no KDP under ships for , so the pack would +# prune everything. Deriving is idempotent, so a reconfigure leaves the derived root's +# mtimes alone and does not re-pack. # --------------------------------------------------------------------------- -function(_hkp_probe_derive_root name from_dir kdp instance out_dir) +function(_hkp_probe_derive_root name from_dir arch out_dir expect_var) file(GLOB_RECURSE _derive_inputs CONFIGURE_DEPENDS "${from_dir}/*") set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_derive_inputs}) @@ -46,8 +51,7 @@ function(_hkp_probe_derive_root name from_dir kdp instance out_dir) COMMAND "${Python3_EXECUTABLE}" "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" --from "${from_dir}" - --kdp "${kdp}" - --instance-name "${instance}" + --arch "${arch}" --out "${out_dir}" RESULT_VARIABLE _derive_rc OUTPUT_VARIABLE _derive_out @@ -57,43 +61,62 @@ function(_hkp_probe_derive_root name from_dir kdp instance out_dir) "hkp probe '${name}': deriving the probe root failed (exit " "${_derive_rc}).\n${_derive_err}") endif() + + get_filename_component(_expect "${out_dir}/../expect.json" ABSOLUTE) + file(READ "${_expect}" _expect_json) + string(JSON _expect_count LENGTH "${_expect_json}") + if(_expect_count EQUAL 0) + message(FATAL_ERROR + "hkp probe '${name}': no *.kdp.json under ${from_dir} ships for ${arch}, " + "so the pack would prune everything.") + endif() message(STATUS - "hkp: probe ${name}: derived root ${out_dir} (instance '${instance}' of ${kdp})") + "hkp: probe ${name}: derived root ${out_dir} (${_expect_count} UKD(s) for ${arch})") + set(${expect_var} "${_expect}" PARENT_SCOPE) endfunction() # --------------------------------------------------------------------------- -# hkp_add_packaging_probe(NAME ARCH KIND rocke -# DERIVE_FROM KDP INSTANCE -# [PACK_JOBS ]) +# hkp_add_packaging_probe(ARCH [ROOT ] [NAME ] [PACK_JOBS ]) # Declare one probe: pack a descriptor root for ARCH and assert the output. # -# DERIVE_FROM copies at configure time into the build tree with the named KDP -# trimmed to the one UKD whose `name` is INSTANCE, so the probe packs the real -# descriptors but compiles a single kernel and the assertion expects exactly one -# UKD. A missing or ambiguous INSTANCE is a configure error, never a skip. +# ROOT defaults to the production descriptors, HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_ +# SOURCE_ROOT. It is copied at configure time into the build tree with every KDP that +# ships for ARCH trimmed to one UKD per compile group (see hkp_probe_derive_root.py), +# so the probe packs the real descriptors but compiles each distinct compile path +# once. The assertion expects exactly the kept UKDs, each with the provenance of its +# producer kind (see hkp_probe_assert.py). Adding a pack under an already probed ARCH +# needs no new declaration. # -# Creates pack target hkp_packaging_probe_ (stamp and output under -# ${CMAKE_BINARY_DIR}/hkp-probes//out) and ctest entry hkp-probe-. Only -# callable from a probes file loaded by hkp_load_packaging_probes(). +# NAME defaults to for the production root and _ for +# another ROOT. Creates pack target hkp_packaging_probe_ (stamp and output under +# ${CMAKE_BINARY_DIR}/hkp-probes//out) and ctest entry hkp-probe-. The +# pack target is part of `all` (hkp_wire_pack_target declares it so), as is the +# aggregate hkp_packaging_probes. Only callable from a probes file loaded by +# hkp_load_packaging_probes(). # --------------------------------------------------------------------------- function(hkp_add_packaging_probe) - cmake_parse_arguments(PARSE_ARGV 0 ARG "" - "NAME;ARCH;KIND;DERIVE_FROM;KDP;INSTANCE;PACK_JOBS" "") + cmake_parse_arguments(PARSE_ARGV 0 ARG "" "ARCH;ROOT;NAME;PACK_JOBS" "") if(ARG_UNPARSED_ARGUMENTS) message(FATAL_ERROR "hkp probe: unknown arguments: ${ARG_UNPARSED_ARGUMENTS}") endif() - foreach(_required NAME ARCH KIND DERIVE_FROM KDP INSTANCE) - if(NOT ARG_${_required}) - message(FATAL_ERROR - "hkp probe: hkp_add_packaging_probe requires ${_required}.") - endif() - endforeach() - if(NOT ARG_KIND STREQUAL "rocke") + if(NOT ARG_ARCH) + message(FATAL_ERROR "hkp probe: hkp_add_packaging_probe requires ARCH.") + endif() + if(NOT ARG_ROOT) + set(ARG_ROOT "${HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT}") + set(_default_name "${ARG_ARCH}") + else() + get_filename_component(_root_base "${ARG_ROOT}" NAME) + set(_default_name "${_root_base}_${ARG_ARCH}") + endif() + if(NOT ARG_NAME) + set(ARG_NAME "${_default_name}") + endif() + if(NOT IS_DIRECTORY "${ARG_ROOT}") message(FATAL_ERROR - "hkp probe '${ARG_NAME}': unsupported KIND '${ARG_KIND}'. Supported " - "kinds: rocke.") + "hkp probe '${ARG_NAME}': ROOT is not a directory: '${ARG_ROOT}'") endif() get_property(_toolchain_set GLOBAL PROPERTY HKP_PROBE_TOOLCHAIN_SET) @@ -118,19 +141,8 @@ function(hkp_add_packaging_probe) _hkp_probe_check_out_root("${ARG_NAME}" "${_out_root}") set(_root "${_probe_dir}/root") - _hkp_probe_derive_root("${ARG_NAME}" "${ARG_DERIVE_FROM}" "${ARG_KDP}" - "${ARG_INSTANCE}" "${_root}") - - if(NOT IS_DIRECTORY "${_root}") - message(FATAL_ERROR - "hkp probe '${ARG_NAME}': probe root is not a directory: ${_root}") - endif() - _hkp_root_covers_any_arch(_covers "${_root}" "${ARG_ARCH}") - if(NOT _covers) - message(FATAL_ERROR - "hkp probe '${ARG_NAME}': no *.kdp.json under ${_root} ships for " - "${ARG_ARCH}, so the pack would prune everything.") - endif() + _hkp_probe_derive_root("${ARG_NAME}" "${ARG_ROOT}" "${ARG_ARCH}" "${_root}" + _expect_file) set(_pack_jobs 1) if(ARG_PACK_JOBS) @@ -149,10 +161,9 @@ function(hkp_add_packaging_probe) set(_assert_args --out-root "${_out_root}" --arch "${ARG_ARCH}" - --kind "${ARG_KIND}" + --expect "${_expect_file}" --kpack-python-dir "${_kpack_dir}" - --stamp-name "${HKP_PACK_STAMP_NAME}" - --expect-ukds 1) + --stamp-name "${HKP_PACK_STAMP_NAME}") if(_comgr_lib) list(APPEND _assert_args --expect-comgr "${_comgr_lib}") endif() diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake index e7808b9b300b..7e91b2791d0d 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake @@ -1,21 +1,13 @@ # Copyright © Advanced Micro Devices, Inc., or its affiliates. # SPDX-License-Identifier: MIT # -# Packaging probes, one declaration per integration and architecture. Loaded by -# hkp_load_packaging_probes() under HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES; see -# HkpPackagingProbes.cmake for the arguments. +# Packaging probes, one line per architecture. Loaded by hkp_load_packaging_probes() +# under HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES; see HkpPackagingProbes.cmake for the +# arguments. +# +# A probe packs the production descriptors for ARCH, one instance per compile group, +# independent of the build's GPU_TARGETS. Probe an architecture the lane does not build +# itself: the lane's own architectures are covered in full by the normal build. A new +# architecture is covered by adding one line here. -# gfx950 dense attention, rocKE producer. The probe packs the production descriptors -# trimmed to one instance. INSTANCE names the UKD whose compile path every dense -# attention variant shares, so one compile stands for the family. The key is the UKD -# `name`, not its `id`: ids are regenerated on every ingestor run, names encode the -# specialization. When that instance is renamed or removed, configure fails naming it; -# pick another instance of the same family from the KDP and update INSTANCE. -hkp_add_packaging_probe( - NAME gfx950_attention_dense - ARCH gfx950 - KIND rocke - DERIVE_FROM - "${HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT}/rocKE/gfx950_attention_dense" - KDP gfx950_attention_dense.kdp.json - INSTANCE "attention_dense.bf16_d64_hq8_kv1_c1_bm256_bn64.gfx950") +hkp_add_packaging_probe(ARCH gfx950) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py index 75b264c17a94..0ac3d074d9d3 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py @@ -10,11 +10,18 @@ _PKG_DIR = _PROBES_TESTS_DIR.parent.parent _HKP_DIR = _PKG_DIR.parent +# hkp_pack is imported to pin the probe tools' arch rules to the packer's. +if str(_PKG_DIR / "python") not in sys.path: + sys.path.insert(0, str(_PKG_DIR / "python")) + ARCH = "gfx950" STAMP_NAME = ".hkp-packed.stamp" HKP_PACK = _PKG_DIR / "tools" / "hkp_pack.py" PROBE_ASSERT = _PKG_DIR / "tools" / "hkp_probe_assert.py" -ROCKE_FIXTURE = _PKG_DIR / "tests" / "fixtures" / "rocke" +PROBE_DERIVE = _PKG_DIR / "tools" / "hkp_probe_derive_root.py" +FIXTURES = _PKG_DIR / "tests" / "fixtures" +ROCKE_FIXTURE = FIXTURES / "rocke" +MAIN_FIXTURE = FIXTURES / "main" _ROCKE_SOURCE_DIRS = ( _HKP_DIR / "rocke" / "platform" / "python", _HKP_DIR / "rocke" / "library", @@ -47,14 +54,12 @@ def comgr_lib(): return os.environ.get("ROCKE_COMGR_LIB") or None -@pytest.fixture(scope="module") -def packed_root(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): - """Pack the rocke fixture once for gfx950 and return the output root. +def pack_root(src, work, rocm_kpack_dir, hipcc, comgr_lib): + """Pack `src` for ARCH with the real packer; return the output root. The stamp file is written by CMake in production; it is created here so the stamp assertion has something to find. """ - work = tmp_path_factory.mktemp("probe_pack") out = work / "out" wheel_stamp = work / "rocke-wheel.stamp" wheel_stamp.write_text(hashlib.sha256(b"probe-test-wheel").hexdigest() + "\n") @@ -69,7 +74,7 @@ def packed_root(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): sys.executable, str(HKP_PACK), "--source-root", - str(ROCKE_FIXTURE), + str(src), "--out-root", str(out), "--arches", @@ -93,3 +98,30 @@ def packed_root(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): pytest.fail(f"hkp_pack failed ({result.returncode}):\n{result.stderr}") (out / STAMP_NAME).write_text("") return out + + +def run_assert(out_root, expect_path, rocm_kpack_dir, expect_comgr=None): + cmd = [ + sys.executable, + str(PROBE_ASSERT), + "--out-root", + str(out_root), + "--arch", + ARCH, + "--expect", + str(expect_path), + "--kpack-python-dir", + rocm_kpack_dir, + "--stamp-name", + STAMP_NAME, + ] + if expect_comgr is not None: + cmd += ["--expect-comgr", str(expect_comgr)] + return subprocess.run(cmd, capture_output=True, text=True) + + +@pytest.fixture(scope="module") +def packed_root(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): + """The rocke fixture packed once for ARCH.""" + work = tmp_path_factory.mktemp("probe_pack") + return pack_root(ROCKE_FIXTURE, work, rocm_kpack_dir, hipcc, comgr_lib) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py index 19e80da1f415..4f3f15c80af7 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py @@ -1,22 +1,34 @@ import json import shutil -import subprocess -import sys import pytest -from conftest import ARCH, PROBE_ASSERT, STAMP_NAME +from conftest import ARCH, STAMP_NAME, run_assert KDP = f"{ARCH}/attention.kdp.json" +KDP_IN_ARCH_DIR = "attention.kdp.json" class _Tree: - """A mutable copy of the packed output plus the assert-tool invocation.""" + """A mutable copy of the packed rocke output plus the assert-tool invocation.""" - def __init__(self, root, rocm_kpack_dir, comgr_path): + def __init__(self, root, expect_path, rocm_kpack_dir, comgr_path): self.root = root + self.expect_path = expect_path self.rocm_kpack_dir = rocm_kpack_dir self.comgr_path = comgr_path + self.ukd_name = self.kdp()["kernelDescriptors"][0]["name"] + self.write_expect([self.entry()]) + + def entry(self, name=None, kind="rocke"): + return { + "kdp": KDP_IN_ARCH_DIR, + "name": self.ukd_name if name is None else name, + "kind": kind, + } + + def write_expect(self, entries): + self.expect_path.write_text(json.dumps(entries), encoding="utf-8") def kdp(self): return json.loads((self.root / KDP).read_text(encoding="utf-8")) @@ -31,26 +43,13 @@ def mutate_kdp(self, edit): edit(kdp) (self.root / KDP).write_text(json.dumps(kdp, indent=2), encoding="utf-8") - def run(self, expect_ukds=1, expect_comgr=None, root=None): - cmd = [ - sys.executable, - str(PROBE_ASSERT), - "--out-root", - str(self.root if root is None else root), - "--arch", - ARCH, - "--kind", - "rocke", - "--kpack-python-dir", + def run(self, expect_comgr=None, root=None): + return run_assert( + self.root if root is None else root, + self.expect_path, self.rocm_kpack_dir, - "--stamp-name", - STAMP_NAME, - "--expect-ukds", - str(expect_ukds), - ] - if expect_comgr is not None: - cmd += ["--expect-comgr", str(expect_comgr)] - return subprocess.run(cmd, capture_output=True, text=True) + expect_comgr, + ) @pytest.fixture @@ -63,7 +62,7 @@ def tree(packed_root, tmp_path, rocm_kpack_dir, comgr_lib): "provenance" ]["comgr_path"] ) - return _Tree(copy, rocm_kpack_dir, comgr) + return _Tree(copy, tmp_path / "expect.json", rocm_kpack_dir, comgr) def _assert_fails(result, assertion_id): @@ -125,12 +124,22 @@ def test_no_kdp(tree): def test_ukd_count_mismatch(tree): - _assert_fails(tree.run(expect_ukds=2), "ukd-count") + tree.write_expect([tree.entry(), tree.entry(name="a_second_ukd")]) + _assert_fails(tree.run(), "ukd-count") def test_ukd_count_zero_is_failure(tree): tree.mutate_kdp(lambda kdp: kdp.update(kernelDescriptors=[])) - _assert_fails(tree.run(expect_ukds=0), "ukd-count") + tree.write_expect([]) + _assert_fails(tree.run(), "ukd-count") + + +def test_shipped_ukd_missing_from_expect(tree): + # Same count, different identity: the shipped UKD is not the one expected. + tree.write_expect([tree.entry(name="not_the_shipped_ukd")]) + result = tree.run() + _assert_fails(result, "ukd-count") + assert tree.ukd_name in result.stderr def test_ukd_kind(tree): @@ -173,6 +182,39 @@ def test_provenance_origin(tree): _assert_fails(tree.run(), "provenance-origin") +def test_expected_kind_differs_from_shipped_origin(tree): + # The pack records rocke; the expectation says hip. + tree.write_expect([tree.entry(kind="hip")]) + _assert_fails(tree.run(), "provenance-origin") + + +def test_expected_kind_without_rules(tree): + tree.write_expect([tree.entry(kind="mystery")]) + result = tree.run() + _assert_fails(result, "no-rules-for-kind") + assert "mystery" in result.stderr + + +def test_hip_kind_needs_no_rocke_provenance(tree): + # A hip UKD records neither the wheel digest nor comgr; stripping them from a + # UKD expected as hip must not fail. + def as_hip(u): + u["provenance"] = {"origin_kind": "hip", "hipcc_version": "test"} + + tree.mutate_ukd(as_hip) + tree.write_expect([tree.entry(kind="hip")]) + result = tree.run() + assert result.returncode == 0, result.stderr + + +@pytest.mark.parametrize("content", ["{ not json", "{}", '[{"kdp": "x"}]']) +def test_unusable_expect_exits_2(tree, content): + tree.expect_path.write_text(content, encoding="utf-8") + result = tree.run() + assert result.returncode == 2, result.stderr + assert "Traceback" not in result.stderr + + def test_provenance_wheel_absent(tree): tree.mutate_ukd(lambda u: u["provenance"].pop("rocke_wheel_sha256")) _assert_fails(tree.run(), "provenance-wheel") diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py index bc9f88c13131..524feb4b33ae 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py @@ -2,34 +2,27 @@ from __future__ import annotations +import itertools import json +import os import subprocess import sys from pathlib import Path import pytest -_DP = Path(__file__).resolve().parents[2] -_TOOL = _DP / "tools" / "hkp_probe_derive_root.py" -_PRODUCTION_ROOT = ( - _DP.parent - / "src/engines/kernel_ingestor_engine/descriptors/rocKE/gfx950_attention_dense" -) -_PRODUCTION_KDP = "gfx950_attention_dense.kdp.json" -_PRODUCTION_INSTANCE = "attention_dense.bf16_d64_hq8_kv1_c1_bm256_bn64.gfx950" +from conftest import MAIN_FIXTURE, PROBE_DERIVE -def _run(src: Path, kdp: str, instance: str, out: Path): +def _run(src: Path, arch: str, out: Path): return subprocess.run( [ sys.executable, - str(_TOOL), + str(PROBE_DERIVE), "--from", str(src), - "--kdp", - kdp, - "--instance-name", - instance, + "--arch", + arch, "--out", str(out), ], @@ -39,87 +32,294 @@ def _run(src: Path, kdp: str, instance: str, out: Path): ) -def _write_synthetic_root(root: Path, names: list[str]) -> None: - root.mkdir() - kdp = { - "version": "1.0", - "id": "kdp-id", - "arch": "gfx950", - "kernelDescriptors": [{"name": n, "payload": i} for i, n in enumerate(names)], +def _derive(src: Path, arch: str, out: Path): + r = _run(src, arch, out) + assert r.returncode == 0, r.stderr + return json.loads((out.parent / "expect.json").read_text()) + + +def _rocke(name, builder="b1", arch=None, **extra): + ukd = { + "id": f"id-{name}", + "name": name, + "kernel_source": { + "kind": "rocke", + "source": "s.py", + "builder": builder, + "spec": {"n": len(name)}, + }, + } + if arch is not None: + ukd["arch"] = arch + return ukd + + +def _hip(name, source="a.cpp", build=None, arch=None): + ukd = { + "id": f"id-{name}", + "name": name, + "kernel_source": { + "kind": "hip", + "source": source, + "entry": name, + "build": build if build is not None else {}, + }, } - (root / "x.kdp.json").write_text(json.dumps(kdp, indent=2) + "\n") + if arch is not None: + ukd["arch"] = arch + return ukd + + +def _other(kind, name): + return {"id": f"id-{name}", "name": name, "kernel_source": {"kind": kind}} + + +def _write_root(root: Path, ukds, kdp_arch=("gfx950",), kdp_file="x.kdp.json"): + root.mkdir(parents=True, exist_ok=True) + kdp = {"version": "1.0", "id": "kdp-id", "kernelDescriptors": list(ukds)} + if kdp_arch is not None: + kdp["arch"] = list(kdp_arch) + (root / kdp_file).parent.mkdir(parents=True, exist_ok=True) + (root / kdp_file).write_text(json.dumps(kdp, indent=2) + "\n") (root / "x.kmd.json").write_text('{"id": "kmd"}\n') -def test_instance_absent_exits_2(tmp_path): +def _kept(out: Path, kdp_file="x.kdp.json"): + kdp = json.loads((out / kdp_file).read_text()) + return [d["name"] for d in kdp["kernelDescriptors"]] + + +# --- compile-group selection ------------------------------------------------ +def test_one_ukd_per_compile_group(tmp_path): + ukds = [ + _rocke("r_b", "b1"), + _rocke("r_a", "b1"), + _rocke("r_c", "b2"), + _hip("h_b", "a.cpp", {"defines": {"X": 1}}), + _hip("h_a", "a.cpp", {"defines": {"X": 1}}), + _hip("h_other_build", "a.cpp", {"defines": {"X": 2}}), + _hip("h_other_source", "b.cpp", {"defines": {"X": 1}}), + ] + _write_root(tmp_path / "src", ukds) + expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") + # rocke: 2 builders; hip: 3 (source, build) pairs. + assert {(e["kind"], e["name"]) for e in expect} == { + ("rocke", "r_a"), + ("rocke", "r_c"), + ("hip", "h_a"), + ("hip", "h_other_build"), + ("hip", "h_other_source"), + } + assert sorted(_kept(tmp_path / "w" / "root")) == sorted(e["name"] for e in expect) + + +def test_pick_is_independent_of_authored_order(tmp_path): + ukds = [_rocke(n) for n in ("m", "c", "x", "a2", "a1")] + for i, order in enumerate(itertools.permutations(ukds, 3)): + src, out = tmp_path / f"src{i}", tmp_path / f"w{i}" / "root" + _write_root(src, order) + expect = _derive(src, "gfx950", out) + want = min(u["name"] for u in order) + assert [e["name"] for e in expect] == [want] + assert _kept(out) == [want] + + +def test_hip_build_key_ignores_dict_key_order(tmp_path): + ukds = [ + _hip("h1", build={"defines": {"A": 1, "B": 2}}), + _hip("h2", build={"defines": {"B": 2, "A": 1}}), + ] + _write_root(tmp_path / "src", ukds) + assert [ + e["name"] for e in _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") + ] == ["h1"] + + +def test_pick_only_among_ukds_shipping_for_arch(tmp_path): + # "a_*" sorts first but ships for gfx942 only; a wildcard UKD and an explicit + # match remain candidates of the same group. + ukds = [ + _rocke("a_942_only", arch=["gfx942"]), + _rocke("b_wild"), + _rocke("c_explicit", arch=["gfx950"]), + _rocke("d_empty", arch=[]), + ] + _write_root(tmp_path / "src", ukds, kdp_arch=("gfx942", "gfx950")) + expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") + assert [e["name"] for e in expect] == ["b_wild"] + expect = _derive(tmp_path / "src", "gfx942", tmp_path / "w2" / "root") + assert [e["name"] for e in expect] == ["a_942_only"] + + +def test_kdp_not_shipping_for_arch_is_copied_unchanged(tmp_path): src = tmp_path / "src" - _write_synthetic_root(src, ["a", "b"]) - r = _run(src, "x.kdp.json", "missing", tmp_path / "out") - assert r.returncode == 2 - assert ( - r.stderr.strip() - == "hkp_probe_derive: instance 'missing' not found in x.kdp.json (2 descriptors)" + _write_root(src, [_rocke("a"), _rocke("b", "b2")], kdp_arch=("gfx942",)) + assert _derive(src, "gfx950", tmp_path / "w" / "root") == [] + assert (tmp_path / "w/root/x.kdp.json").read_bytes() == ( + src / "x.kdp.json" + ).read_bytes() + + +def test_wildcard_kdp_ships_everywhere(tmp_path): + _write_root(tmp_path / "src", [_rocke("a")], kdp_arch=None) + assert [ + e["name"] for e in _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") + ] == ["a"] + + +def test_expect_records_kdp_relative_path(tmp_path): + _write_root(tmp_path / "src", [_rocke("a")], kdp_file="sub/dir/x.kdp.json") + expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") + assert expect == [{"kdp": "sub/dir/x.kdp.json", "name": "a", "kind": "rocke"}] + + +# --- configure-time refusals (exit 2, nothing written) ---------------------- +def _refused(tmp_path, ukds, message, kdp_arch=("gfx950",)): + _write_root(tmp_path / "src", ukds, kdp_arch=kdp_arch) + r = _run(tmp_path / "src", "gfx950", tmp_path / "w" / "root") + assert r.returncode == 2, r.stderr + assert r.stderr.startswith("hkp_probe_derive:") + assert message in r.stderr + assert not (tmp_path / "w").exists() + + +def test_standalone_ukd_reference_exits_2(tmp_path): + _refused( + tmp_path, + [_rocke("a"), "ukd-standalone-id"], + "standalone UKD references unsupported by probe derive", + ) + + +def test_standalone_reference_in_real_fixture_exits_2(tmp_path): + # tests/fixtures/main: pointwise.kdp.json ships for gfx950 and references + # ukd-pointwise-add-f32-b128 by id. + r = _run(MAIN_FIXTURE, "gfx950", tmp_path / "w" / "root") + assert r.returncode == 2, r.stderr + assert "pointwise.kdp.json" in r.stderr + assert "ukd-pointwise-add-f32-b128" in r.stderr + assert not (tmp_path / "w").exists() + + +def test_standalone_reference_in_other_arch_kdp_is_ignored(tmp_path): + _write_root( + tmp_path / "src", [_rocke("a"), "ukd-standalone-id"], kdp_arch=("gfx942",) + ) + assert _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") == [] + + +@pytest.mark.parametrize("kind", ["hsaco", "embedded_source", "kpack", "mystery"]) +def test_kind_without_probe_support_exits_2(tmp_path, kind): + _refused( + tmp_path, + [_rocke("a"), _other(kind, "m")], + f"kind {kind!r} has no probe support", ) - assert not (tmp_path / "out").exists() -def test_instance_duplicated_exits_2(tmp_path): +def test_unsupported_kind_for_other_arch_is_ignored(tmp_path): + ukds = [_rocke("a"), dict(_other("hsaco", "m"), arch=["gfx942"])] + _write_root(tmp_path / "src", ukds, kdp_arch=("gfx942", "gfx950")) + assert [ + e["name"] for e in _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") + ] == ["a"] + + +def test_ukd_arch_outside_kdp_arch_exits_2(tmp_path): + # The packer rejects this root; derive must not hide it by dropping the UKD. + _refused(tmp_path, [_rocke("a", arch=["gfx942"])], "not a subset of the KDP arch") + + +def test_kept_ukds_with_equal_names_exit_2(tmp_path): + _refused(tmp_path, [_rocke("same", "b1"), _rocke("same", "b2")], "share a name") + + +def test_malformed_kdp_exits_2(tmp_path): src = tmp_path / "src" - _write_synthetic_root(src, ["a", "dup", "dup"]) - r = _run(src, "x.kdp.json", "dup", tmp_path / "out") + src.mkdir() + (src / "bad.kdp.json").write_text("{ not json") + r = _run(src, "gfx950", tmp_path / "w" / "root") assert r.returncode == 2 - assert "hkp_probe_derive: instance 'dup' matched 2 times" in r.stderr - assert not (tmp_path / "out").exists() + assert "bad.kdp.json is unreadable" in r.stderr + assert "Traceback" not in r.stderr + +# --- idempotence ------------------------------------------------------------ +def _stamp_old(out: Path): + for p in [*out.rglob("*"), out.parent / "expect.json"]: + if p.is_file(): + os.utime(p, ns=(10**18, 10**18)) -def test_kdp_absent_exits_2(tmp_path): + +def _mtimes(out: Path): + return { + p.relative_to(out.parent).as_posix(): p.stat().st_mtime_ns + for p in [*out.rglob("*"), out.parent / "expect.json"] + if p.is_file() + } + + +def test_second_derive_touches_nothing(tmp_path): src = tmp_path / "src" - _write_synthetic_root(src, ["a"]) - r = _run(src, "nope.kdp.json", "a", tmp_path / "out") - assert r.returncode == 2 - assert r.stderr.strip() == f"hkp_probe_derive: nope.kdp.json not found under {src}" + _write_root(src, [_rocke("b"), _rocke("a")]) + out = tmp_path / "w" / "root" + first = _derive(src, "gfx950", out) + _stamp_old(out) + before = _mtimes(out) + assert before and set(before) >= { + "root/x.kdp.json", + "root/x.kmd.json", + "expect.json", + } + assert _derive(src, "gfx950", out) == first + assert _mtimes(out) == before -def test_stale_output_removed(tmp_path): +def test_rederive_follows_source_changes(tmp_path): src = tmp_path / "src" - _write_synthetic_root(src, ["a", "b"]) - out = tmp_path / "out" - out.mkdir() + _write_root(src, [_rocke("b"), _rocke("a")]) + out = tmp_path / "w" / "root" + _derive(src, "gfx950", out) (out / "stale.txt").write_text("stale") - r = _run(src, "x.kdp.json", "b", out) - assert r.returncode == 0, r.stderr - assert not (out / "stale.txt").exists() - kdp = json.loads((out / "x.kdp.json").read_text()) - assert [d["name"] for d in kdp["kernelDescriptors"]] == ["b"] - assert (out / "x.kmd.json").read_bytes() == (src / "x.kmd.json").read_bytes() + (out / "stale_dir").mkdir() + (out / "stale_dir" / "f.json").write_text("{}") + (src / "x.kmd.json").write_text('{"id": "kmd2"}\n') + _write_root(src, [_rocke("b"), _rocke("0first")]) + expect = _derive(src, "gfx950", out) + assert [e["name"] for e in expect] == ["0first"] + assert _kept(out) == ["0first"] + assert (out / "x.kmd.json").read_text() == '{"id": "kmd"}\n' + assert sorted(p.name for p in out.rglob("*")) == ["x.kdp.json", "x.kmd.json"] -def test_real_production_root(tmp_path): - out = tmp_path / "out" - r = _run(_PRODUCTION_ROOT, _PRODUCTION_KDP, _PRODUCTION_INSTANCE, out) - assert r.returncode == 0, r.stderr +# --- arch rules pinned to the packer's -------------------------------------- +_ARCH_LISTS = [None, [], ["gfx942"], ["gfx950"], ["gfx942", "gfx950"], ["gfx90a"]] - src_kdp = json.loads((_PRODUCTION_ROOT / _PRODUCTION_KDP).read_text()) - out_kdp = json.loads((out / _PRODUCTION_KDP).read_text()) - assert [d["name"] for d in out_kdp["kernelDescriptors"]] == [_PRODUCTION_INSTANCE] - assert list(out_kdp) == list(src_kdp) - for key in ("id", "engine", "dispatch", "matchers", "arch", "provenance"): - assert out_kdp[key] == src_kdp[key] - siblings = sorted( - p.name for p in _PRODUCTION_ROOT.iterdir() if p.name != _PRODUCTION_KDP - ) - assert len(siblings) == 5 - for name in siblings: - assert (out / name).read_bytes() == (_PRODUCTION_ROOT / name).read_bytes() - assert sorted(p.name for p in out.iterdir()) == sorted(siblings + [_PRODUCTION_KDP]) +def _derive_module(): + sys.path.insert(0, str(PROBE_DERIVE.parent)) + try: + import hkp_probe_derive_root + finally: + sys.path.pop(0) + return hkp_probe_derive_root -def test_production_instance_exactly_once(): - kdp = json.loads((_PRODUCTION_ROOT / _PRODUCTION_KDP).read_text()) - names = [d["name"] for d in kdp["kernelDescriptors"]] - assert names.count(_PRODUCTION_INSTANCE) == 1 +@pytest.mark.parametrize("doc_arch", _ARCH_LISTS) +@pytest.mark.parametrize("arch", ["gfx942", "gfx950"]) +def test_arch_matches_agrees_with_packer(doc_arch, arch): + from hkp_pack import descriptors + + doc = {} if doc_arch is None else {"arch": doc_arch} + assert _derive_module().arch_matches(doc, arch) == descriptors.arch_matches( + doc, arch + ) + +@pytest.mark.parametrize("kdp_arch", _ARCH_LISTS) +@pytest.mark.parametrize("ukd_arch", _ARCH_LISTS) +def test_arch_subset_agrees_with_packer(ukd_arch, kdp_arch): + from hkp_pack import descriptors -if __name__ == "__main__": - sys.exit(pytest.main([__file__, "-v"])) + got = _derive_module().arch_subset_ok(ukd_arch or [], kdp_arch or []) + assert got == descriptors._arch_subset_ok(ukd_arch or [], kdp_arch or []) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py new file mode 100644 index 000000000000..a5d0aec8dcd9 --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py @@ -0,0 +1,138 @@ +"""derive -> real pack -> assert, for hip-only and mixed rocke+hip roots. + +The roots are built from the packer's own fixtures. Real hipcc and rocke compiles: +this is the proof the probe tools work for producers other than rocke and that the +KDP paths derive records are the ones the packed output carries. +""" + +import json +import shutil +import subprocess +import sys + +import pytest + +from conftest import ( + MAIN_FIXTURE, + PROBE_DERIVE, + ROCKE_FIXTURE, + pack_root, + run_assert, +) + +# What derive keeps from the main fixture for gfx950: pointwise.kdp.json (gfx942 and +# gfx950) holds two hip UKDs in one compile group and the wildcard KDP one more in +# another; the gfx942-only KDPs ship nothing. +_HIP_EXPECT = [ + {"kdp": "pointwise.kdp.json", "name": "PointwiseAdd f32 block64", "kind": "hip"}, + { + "kdp": "pointwise_wild.kdp.json", + "name": "PointwiseAdd f32 block256", + "kind": "hip", + }, +] + + +def _hip_source(dst): + """The main fixture without its standalone UKD, which derive refuses.""" + shutil.copytree(MAIN_FIXTURE, dst) + (dst / "pointwise_add_b128.ukd.json").unlink() + kdp = json.loads((dst / "pointwise.kdp.json").read_text()) + kdp["kernelDescriptors"] = [ + u for u in kdp["kernelDescriptors"] if isinstance(u, dict) + ] + (dst / "pointwise.kdp.json").write_text(json.dumps(kdp, indent=2)) + return dst + + +def _derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib): + derived = work / "probe" / "root" + r = subprocess.run( + [ + sys.executable, + str(PROBE_DERIVE), + "--from", + str(src), + "--arch", + "gfx950", + "--out", + str(derived), + ], + capture_output=True, + text=True, + ) + assert r.returncode == 0, r.stderr + out = pack_root(derived, work, rocm_kpack_dir, hipcc, comgr_lib) + return out, work / "probe" / "expect.json" + + +@pytest.fixture(scope="module") +def hip_probe(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): + work = tmp_path_factory.mktemp("hip_probe") + return _derive_and_pack( + _hip_source(work / "src"), work, rocm_kpack_dir, hipcc, comgr_lib + ) + + +@pytest.fixture(scope="module") +def mixed_probe(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): + work = tmp_path_factory.mktemp("mixed_probe") + src = work / "src" + _hip_source(src / "hip") + shutil.copytree(ROCKE_FIXTURE, src / "rocke") + return _derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib) + + +def test_hip_root_passes(hip_probe, rocm_kpack_dir): + out, expect = hip_probe + assert json.loads(expect.read_text()) == _HIP_EXPECT + result = run_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 0, result.stderr + + +def test_hip_root_with_broken_hip_source_does_not_pack( + tmp_path, rocm_kpack_dir, hipcc, comgr_lib +): + src = _hip_source(tmp_path / "src") + (src / "PointwiseAdd.cpp").write_text("this is not C++\n") + derived = tmp_path / "probe" / "root" + subprocess.run( + [ + sys.executable, + str(PROBE_DERIVE), + "--from", + str(src), + "--arch", + "gfx950", + "--out", + str(derived), + ], + check=True, + ) + with pytest.raises(pytest.fail.Exception, match="hkp_pack failed"): + pack_root(derived, tmp_path, rocm_kpack_dir, hipcc, comgr_lib) + + +def test_mixed_root_passes_with_per_ukd_kinds(mixed_probe, rocm_kpack_dir, comgr_lib): + out, expect = mixed_probe + entries = json.loads(expect.read_text()) + assert {(e["kdp"], e["kind"]) for e in entries} == { + ("hip/pointwise.kdp.json", "hip"), + ("hip/pointwise_wild.kdp.json", "hip"), + ("rocke/attention.kdp.json", "rocke"), + } + result = run_assert(out, expect, rocm_kpack_dir, expect_comgr=comgr_lib) + assert result.returncode == 0, result.stderr + + +def test_mixed_root_origin_is_checked_per_ukd(mixed_probe, rocm_kpack_dir, tmp_path): + # Expecting every UKD to be hip (a single global kind) must fail on the rocke one. + out, expect = mixed_probe + entries = [dict(e, kind="hip") for e in json.loads(expect.read_text())] + wrong = tmp_path / "expect.json" + wrong.write_text(json.dumps(entries)) + result = run_assert(out, wrong, rocm_kpack_dir) + assert result.returncode == 1 + assert "FAIL provenance-origin:" in result.stderr + assert "rocke/attention.kdp.json" in result.stderr + assert "hip/pointwise" not in result.stderr diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py index 351a1763188c..f3124e9659b9 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py @@ -1,16 +1,19 @@ #!/usr/bin/env python3 """Assert the output of a packaging probe. -A packaging probe packs one integration's descriptors for one explicit -architecture. This tool inspects the packed output root and fails, with one -greppable line per failure, unless every shipped descriptor is a well-formed -kpack-backed kernel produced by the expected producer: +A packaging probe packs one descriptor root for one explicit architecture. +This tool inspects the packed output root and fails, with one greppable line per +failure, unless every shipped descriptor is a well-formed kpack-backed kernel and +the shipped set is the one `--expect` lists (hkp_probe_derive_root.py writes it): +a JSON list of {"kdp": , "name": , "kind": }. Each UKD's provenance is checked +against the rules of its expected kind (_PROVENANCE_RULES). hkp_probe_assert: FAIL : Exit status is 0 only when every assertion holds, 1 when any assertion fails, -and 2 for an unusable invocation (rocm_kpack not importable). Plain python, not -pytest: a script has no skip path, so it cannot pass without having checked. +and 2 for an unusable invocation (rocm_kpack not importable, --expect +unreadable or malformed). Plain python, not pytest: a script has no skip path, so it cannot pass without having checked. Assertion ids: out-root-missing --out-root is not a directory @@ -20,16 +23,19 @@ kpack-missing //kpack/hip_kernel_provider_.kpack absent kpack-empty that archive has size 0 no-kdp no *.kdp.json under //, or one is not valid JSON - ukd-count kpack-kind UKD count differs from --expect-ukds (or is 0) + ukd-count kpack-kind UKD count differs from the --expect list length (or + is 0), or a shipped UKD is not in the list ukd-kind a UKD's kernel_source.kind is not "kpack" arch-field a KDP or UKD `arch` is not [] kpack-toc the archive is unreadable or has no entry for a UKD's toc_key sha256 the archive blob's sha256 differs from kernel_source.sha256 signature a UKD's signature is not a non-empty list symbol a UKD's symbol does not appear in its archive blob - provenance-origin provenance.origin_kind is not the --kind producer - provenance-wheel provenance.rocke_wheel_sha256 is absent or empty - provenance-comgr provenance.comgr_path is not the expected comgr library + no-rules-for-kind a UKD's expected kind has no entry in _PROVENANCE_RULES + provenance-origin provenance.origin_kind is not the UKD's expected kind + provenance-wheel rocke only: provenance.rocke_wheel_sha256 absent or empty + provenance-comgr rocke only: provenance.comgr_path is not the expected comgr + library """ import argparse @@ -41,6 +47,16 @@ _PREFIX = "hkp_probe_assert" +# Provenance checks required per producer kind (the authored kernel_source.kind, +# which the packer records as provenance.origin_kind). The kinds are those the packer +# compiles to kpack output. A new producer needs one row here AND one row in +# hkp_probe_derive_root.py's _GROUP_FIELDS; a kind with no row fails +# `no-rules-for-kind`. hip's `hipcc_version` is optional and unchecked. +_PROVENANCE_RULES = { + "rocke": ("wheel", "comgr"), + "hip": (), +} + class _Failures: def __init__(self): @@ -59,14 +75,40 @@ def _parse_args(argv): ) p.add_argument("--out-root", required=True, type=Path) p.add_argument("--arch", required=True) - p.add_argument("--kind", required=True, choices=["rocke"]) p.add_argument("--kpack-python-dir", required=True) p.add_argument("--stamp-name", required=True) - p.add_argument("--expect-ukds", required=True, type=int) + p.add_argument("--expect", required=True, type=Path) p.add_argument("--expect-comgr", default=None) return p.parse_args(argv) +class _UsageError(Exception): + pass + + +def _load_expect(path): + """Read --expect into {(kdp, name): kind}.""" + try: + entries = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError) as exc: + raise _UsageError(f"cannot read --expect {path}: {type(exc).__name__}: {exc}") + if not isinstance(entries, list): + raise _UsageError(f"--expect {path} is not a JSON list") + expected = {} + for entry in entries: + if not isinstance(entry, dict) or not all( + isinstance(entry.get(k), str) for k in ("kdp", "name", "kind") + ): + raise _UsageError( + f"--expect {path} entry {entry!r} lacks string kdp, name and kind" + ) + key = (entry["kdp"], entry["name"]) + if key in expected: + raise _UsageError(f"--expect {path} lists {key} twice") + expected[key] = entry["kind"] + return expected + + def _load_kpack(kpack_python_dir): if kpack_python_dir not in sys.path: sys.path.insert(0, kpack_python_dir) @@ -116,6 +158,35 @@ def _check_kpack(args, arch_dir, failures): return kpack_path +def _check_provenance(args, label, ukd, kind, failures): + rules = _PROVENANCE_RULES.get(kind) + if rules is None: + failures.add( + "no-rules-for-kind", f"{label} expected kind {kind!r} has no rules" + ) + return + prov = ukd.get("provenance") or {} + origin = prov.get("origin_kind") + if origin != kind: + failures.add( + "provenance-origin", f"{label} origin_kind is {origin!r}, not {kind!r}" + ) + if "wheel" in rules: + wheel = prov.get("rocke_wheel_sha256") + if not isinstance(wheel, str) or not wheel: + failures.add("provenance-wheel", f"{label} rocke_wheel_sha256 is {wheel!r}") + if "comgr" in rules: + comgr = prov.get("comgr_path") + if args.expect_comgr is None: + if not isinstance(comgr, str) or not comgr: + failures.add("provenance-comgr", f"{label} comgr_path is {comgr!r}") + elif not isinstance(comgr, str) or not _same_path(comgr, args.expect_comgr): + failures.add( + "provenance-comgr", + f"{label} comgr_path is {comgr!r}, expected {args.expect_comgr}", + ) + + def _check_ukd(args, kdp_name, ukd, archive, failures): label = f"{kdp_name}:{ukd.get('name', ukd.get('id', '?'))}" source = ukd.get("kernel_source") or {} @@ -131,25 +202,13 @@ def _check_ukd(args, kdp_name, ukd, archive, failures): if not isinstance(signature, list) or not signature: failures.add("signature", f"{label} signature is {signature!r}") - prov = ukd.get("provenance") or {} - origin = prov.get("origin_kind") - if origin != args.kind: - failures.add( - "provenance-origin", f"{label} origin_kind is {origin!r}, not {args.kind!r}" - ) - wheel = prov.get("rocke_wheel_sha256") - if not isinstance(wheel, str) or not wheel: - failures.add("provenance-wheel", f"{label} rocke_wheel_sha256 is {wheel!r}") - - comgr = prov.get("comgr_path") - if args.expect_comgr is None: - if not isinstance(comgr, str) or not comgr: - failures.add("provenance-comgr", f"{label} comgr_path is {comgr!r}") - elif not isinstance(comgr, str) or not _same_path(comgr, args.expect_comgr): + expected_kind = args.expected.get((kdp_name, ukd.get("name"))) + if expected_kind is None: failures.add( - "provenance-comgr", - f"{label} comgr_path is {comgr!r}, expected {args.expect_comgr}", + "ukd-count", f"{label} is not in the --expect list ({args.expect})" ) + else: + _check_provenance(args, label, ukd, expected_kind, failures) if archive is None: return True @@ -194,10 +253,11 @@ def _check_descriptors(args, arch_dir, archive, failures): for ukd in kdp.get("kernelDescriptors", []): if _check_ukd(args, name, ukd, archive, failures): kpack_ukds += 1 - if kpack_ukds != args.expect_ukds or kpack_ukds < 1: + expected = len(args.expected) + if kpack_ukds != expected or kpack_ukds < 1: failures.add( "ukd-count", - f"{kpack_ukds} kpack UKDs under {arch_dir}, expected {args.expect_ukds} " + f"{kpack_ukds} kpack UKDs under {arch_dir}, expected {expected} " "(and at least 1)", ) @@ -227,6 +287,11 @@ def run(args): def main(argv=None): args = _parse_args(sys.argv[1:] if argv is None else argv) + try: + args.expected = _load_expect(args.expect) + except _UsageError as exc: + print(f"{_PREFIX}: error: {exc}", file=sys.stderr) + return 2 try: failures = run(args) except ImportError as exc: diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py old mode 100644 new mode 100755 index ed52d96c9e4f..e0742c60977c --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py @@ -1,19 +1,38 @@ #!/usr/bin/env python3 -"""Derive a one-kernel descriptor root from a production descriptor directory. +"""Derive a minimal descriptor root for a packaging probe. -Copies every file under `--from` unchanged except the named KDP, whose -`kernelDescriptors` list is reduced to the single UKD whose `name` equals -`--instance-name`. The output directory is removed first so a stale derived root -cannot survive. +Copies every file under `--from` to `--out`, except that each KDP shipping for +`--arch` has its inline `kernelDescriptors` reduced to one UKD per compile group +among the UKDs that themselves ship for `--arch`: -Exit code 0 on success. Exit code 2, with a one-line `hkp_probe_derive: ...` -message on stderr, when the KDP is missing or the instance name does not match -exactly one descriptor. + rocke (kind, builder) + hip (kind, source, build) + +The pick within a group is the first by sorted UKD `name`. Packing the derived +root therefore exercises every compile path once instead of every variant. +KDPs that do not ship for `--arch` are copied unchanged; the packer prunes them. + +`expect.json` is written beside `--out` (`/expect.json`): a list of +{"kdp": , "name": , "kind": }, one entry per kept UKD, for hkp_probe_assert.py. + +Files are written only when their content differs and files absent from the +source are removed, so deriving twice leaves every mtime of the derived root +alone and the pack stamp stays fresh across a reconfigure. + +Arch rules are the packer's (`hkp_pack.descriptors.arch_matches` and +`_arch_subset_ok`): an empty or absent `arch` list is a wildcard. + +Exit code 0 on success. Exit code 2, with a `hkp_probe_derive: ...` message on +stderr, when the root cannot be derived: a KDP shipping for `--arch` references +a standalone UKD (`kernelDescriptors` entry that is not an object), a kept UKD +has a kind other than rocke or hip, a UKD is malformed, or the source is unreadable. """ from __future__ import annotations import argparse +import filecmp import json import shutil import sys @@ -21,44 +40,187 @@ _PREFIX = "hkp_probe_derive:" +# The kinds the packer compiles to kpack output, and the kernel_source fields that +# define a compile group within a KDP. A new producer needs one row here AND one row +# in hkp_probe_assert.py's _PROVENANCE_RULES; any other kind is rejected. +_GROUP_FIELDS = { + "rocke": ("builder",), + "hip": ("source", "build"), +} -def derive_root(src: Path, kdp_name: str, instance_name: str, out: Path) -> str | None: - """Write the derived root to `out`; return an error message, or None on success.""" - kdp_path = src / kdp_name - if not kdp_path.is_file(): - return f"{_PREFIX} {kdp_name} not found under {src}" - - kdp = json.loads(kdp_path.read_text(encoding="utf-8")) - descriptors = kdp["kernelDescriptors"] - matches = [d for d in descriptors if d.get("name") == instance_name] - if not matches: - return ( - f"{_PREFIX} instance '{instance_name}' not found in {kdp_name} " - f"({len(descriptors)} descriptors)" + +class DeriveError(Exception): + pass + + +def arch_matches(doc, arch): + """Port of hkp_pack.descriptors.arch_matches: empty or absent list = wildcard.""" + archs = doc.get("arch") + if not archs: + return True + return arch in archs + + +def arch_subset_ok(ukd_arch, kdp_arch): + """Port of hkp_pack.descriptors._arch_subset_ok: empty on either side = wildcard.""" + if not ukd_arch or not kdp_arch: + return True + return set(ukd_arch) <= set(kdp_arch) + + +def _group_key(ukd, where): + source = ukd.get("kernel_source") + kind = source.get("kind") if isinstance(source, dict) else None + if kind not in _GROUP_FIELDS: + raise DeriveError( + f"{where} kind {kind!r} has no probe support: add its compile-group " + "fields here and its provenance rules in hkp_probe_assert.py, once " + "the packer produces kpack output for it" ) - if len(matches) > 1: - return ( - f"{_PREFIX} instance '{instance_name}' matched {len(matches)} times " - f"in {kdp_name} ({len(descriptors)} descriptors)" + return (kind,) + tuple( + json.dumps(source.get(f), sort_keys=True) for f in _GROUP_FIELDS[kind] + ) + + +def _derive_kdp(kdp, rel, arch): + """Return the kept UKDs of a KDP shipping for `arch`, in authored order.""" + entries = kdp.get("kernelDescriptors") + if not isinstance(entries, list): + raise DeriveError(f"{rel} 'kernelDescriptors' is not a list") + candidates = [] + for entry in entries: + if not isinstance(entry, dict): + raise DeriveError( + f"{rel} references a standalone UKD ({entry!r}): standalone UKD " + "references unsupported by probe derive" + ) + where = f"{rel}: UKD '{entry.get('id', '?')}'" + if not isinstance(entry.get("name"), str): + raise DeriveError(f"{where} has no string 'name'") + if not arch_subset_ok(entry.get("arch") or [], kdp.get("arch") or []): + raise DeriveError( + f"{where} arch {entry.get('arch')} is not a subset of the KDP " + f"arch {kdp.get('arch')}" + ) + if arch_matches(entry, arch): + candidates.append((_group_key(entry, where), entry)) + + picks = {} + for key, entry in sorted(candidates, key=lambda c: c[1]["name"]): + picks.setdefault(key, entry) + kept = [e for e in entries if any(e is p for p in picks.values())] + names = [e["name"] for e in kept] + if len(set(names)) != len(names): + raise DeriveError( + f"{rel} kept UKDs from different compile groups share a name, so " + f"expect.json could not tell them apart: {sorted(names)}" ) + return kept + + +def _plan(src, arch): + """Return (files, expect): files maps relative posix path -> source Path or bytes.""" + files = {} + for p in sorted(src.rglob("*")): + if p.is_file(): + files[p.relative_to(src).as_posix()] = p + + expect = [] + for rel in sorted(files): + if not rel.endswith(".kdp.json"): + continue + # The packer skips dot-prefixed paths; so does derive. + if any(part.startswith(".") for part in rel.split("/")): + continue + try: + kdp = json.loads(files[rel].read_text(encoding="utf-8")) + except (OSError, ValueError) as exc: + raise DeriveError(f"{rel} is unreadable: {type(exc).__name__}: {exc}") + if not isinstance(kdp, dict): + raise DeriveError(f"{rel} is not a JSON object") + if not arch_matches(kdp, arch): + continue + kept = _derive_kdp(kdp, rel, arch) + if not kept: + # Every UKD filters out for this arch: the packer drops the whole KDP. + continue + kdp["kernelDescriptors"] = kept + files[rel] = (json.dumps(kdp, indent=2) + "\n").encode("utf-8") + expect.extend( + {"kdp": rel, "name": u["name"], "kind": u["kernel_source"]["kind"]} + for u in kept + ) + return files, expect + + +def _same(dest, content): + if not dest.is_file() or dest.is_symlink(): + return False + if isinstance(content, bytes): + return dest.read_bytes() == content + return filecmp.cmp(content, dest, shallow=False) + + +def _write(dest, content): + dest.parent.mkdir(parents=True, exist_ok=True) + if dest.is_dir() and not dest.is_symlink(): + shutil.rmtree(dest) + elif dest.is_symlink(): + dest.unlink() + if isinstance(content, bytes): + dest.write_bytes(content) + else: + shutil.copyfile(content, dest) + + +def _sync(out, files): + """Make `out` hold exactly `files`, touching only what differs.""" + out.mkdir(parents=True, exist_ok=True) + for rel, content in files.items(): + dest = out / rel + if not _same(dest, content): + _write(dest, content) + wanted = set(files) + # Deepest first so emptied directories can go after their files. + for p in sorted(out.rglob("*"), key=lambda q: len(q.parts), reverse=True): + rel = p.relative_to(out).as_posix() + if p.is_symlink() or p.is_file(): + if rel not in wanted: + p.unlink() + elif p.is_dir() and not any(w.startswith(rel + "/") for w in wanted): + shutil.rmtree(p) + + +def derive_root(src: Path, arch: str, out: Path) -> str | None: + """Write the derived root to `out`; return an error message, or None on success.""" + if not src.is_dir(): + return f"{_PREFIX} {src} is not a directory" + try: + files, expect = _plan(src, arch) + except DeriveError as exc: + return f"{_PREFIX} {exc}" - if out.exists(): - shutil.rmtree(out) - shutil.copytree(src, out) - kdp["kernelDescriptors"] = matches - (out / kdp_name).write_text(json.dumps(kdp, indent=2) + "\n", encoding="utf-8") + _sync(out, files) + expect_text = json.dumps(expect, indent=2) + "\n" + expect_path = out.parent / "expect.json" + if ( + not expect_path.is_file() + or expect_path.read_text(encoding="utf-8") != expect_text + ): + expect_path.write_text(expect_text, encoding="utf-8") return None def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--from", dest="src", type=Path, required=True) - parser.add_argument("--kdp", required=True, help="KDP file name under --from") - parser.add_argument("--instance-name", required=True, help="UKD name to keep") + parser.add_argument( + "--arch", required=True, help="architecture to probe, e.g. gfx950" + ) parser.add_argument("--out", type=Path, required=True) args = parser.parse_args(argv) - error = derive_root(args.src, args.kdp, args.instance_name, args.out) + error = derive_root(args.src, args.arch, args.out) if error is not None: print(error, file=sys.stderr) return 2 diff --git a/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/descriptors/README.md b/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/descriptors/README.md index 1346181dbd2a..696433f9b1a7 100644 --- a/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/descriptors/README.md +++ b/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/descriptors/README.md @@ -37,6 +37,15 @@ stale product tree is removed — not an error. A KDP that *is* present but prun architecture is a hard failure for a root the build NAMED, and dormancy for this root reached as the built-in default. +Packaging probes read this root too. They are opt-in per architecture (one +`hkp_add_packaging_probe(ARCH )` line) and superbuild-only +(`HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES`, `OFF` by default); a new bundle under an +already probed architecture is covered automatically. A probe packs one instance per +compile group, so the lane's own architecture is still covered in full by the normal +build. A KDP shipping for a probed architecture must keep its `kernelDescriptors` +inline: a standalone-UKD reference fails probe configure. See "Packaging probes" in +`descriptor-packaging/README.md`. + Do not delete this README: git tracks no empty directory, and the cache variable's set-but-not-a-directory check is fatal. diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md index db7eb609dd69..5228438ac9c4 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md @@ -335,14 +335,18 @@ native pack whose symbols a bundle's UKDs must name before it serves. The packag dependencies are documented from the repository root in `dnn-providers/hip-kernel-provider/descriptor-packaging/README.md`. -**Required: add a packaging probe for every new architecture or integration.** CI builds -only a few architectures, so packaging for the others breaks silently unless a probe packs -them. Declare one `hkp_add_packaging_probe(...)` in -`dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake` (arch, `KIND -rocke`, the production directory, KDP file and a representative UKD name), then run the -mutation checks listed under "Packaging probes" in the descriptor-packaging README. No -CMake guard detects a missing probe, so this step is the only enforcement; no workflow -edit is needed. +**Required: add a packaging probe for every new architecture.** CI builds only a few +architectures, so packaging for the others breaks silently unless a probe packs them. +Probes are opt-in per architecture: declare one `hkp_add_packaging_probe(ARCH )` line +in `dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake`, then run +the mutation checks listed under "Packaging probes" in the descriptor-packaging README. A +new pack under an architecture that is already probed, in the production root, is covered +automatically and needs no new line. Probes are superbuild-only +(`HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES`, OFF by default) and pack one instance per +compile group; the lane's own architecture is covered in full by the normal build. They +do not cover ASM SDPA, C++/runtime gfx950 paths, install staging, or standalone-UKD +references (derive fails configure on those). No CMake guard detects a missing probe, so +this step is the only enforcement; no workflow edit is needed. Build the provider, validator and required test targets through the configured superbuild. For packaged engines, run `hkp_packaging_product` after the full build and diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md index 19d6680f12d9..b6fffedfc992 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md @@ -60,6 +60,12 @@ Completion requires: - Exact-engine quick/standard numerics and required negative cases, with selected, served, skipped and failed counts. Zero selected, all-skipped or another engine's work is not correctness evidence. An extension must dispatch its new candidate. +- For a new architecture, one `hkp_add_packaging_probe(ARCH )` line (opt-in per + architecture, superbuild-only, option `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` OFF by + default; one instance per compile group; excludes ASM SDPA, C++/runtime gfx950 paths, + install staging and standalone-UKD references), with the probe ctest entry + `hkp-probe-` run. A new pack under an already probed architecture is covered + automatically. - Rebuilt/reinstalled artifacts after tuning or regeneration, then fresh final gates. Every final corpus input has an attributable runtime outcome and the complete join required by [workloads.md](workloads.md); missing, ambiguous or error outcomes block diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md index 30163d2bdc5e..d3e5b6754561 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md @@ -99,6 +99,13 @@ through that suite's ordinary host run. [native-pack.md](native-pack.md) owns eligibility. Device proof must explicitly select and numerically verify the new candidate; passing the unchanged default is not extension acceptance. +An addition under an architecture that already has a packaging probe is covered by it +automatically; a new architecture needs one `hkp_add_packaging_probe(ARCH )` line +(opt-in per architecture, superbuild-only, option `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` +OFF by default). The probe packs one instance per compile group and does not cover ASM +SDPA, C++/runtime gfx950 paths, install staging or standalone-UKD references (derive fails +configure on those); the lane's own architecture is covered in full by the normal build. + The handoff identifies retained IDs/references, changed/new files, baseline/final installations, whole-engine results and the addition's actual dispatch. RUNBOOK stage 5 contains the bounded HALF/256 pointwise example; its one-element source is not diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild-test/SKILL.md b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild-test/SKILL.md index f943e959ed23..c640ac40d943 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild-test/SKILL.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild-test/SKILL.md @@ -117,6 +117,15 @@ The exact article/engine/config for the external suite is resolved at build time discovery for available commands, then inspect the specific CTest registration when proving a named engine; a generic `command:` line is not that proof. +## Packaging probes + +When the build was configured with `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON` +(superbuild-only, OFF by default), the probe ctest entries are `hkp-probe-` plus +`hkp-probe-tools`; select them with the anchored `-R '^hkp-probe-'` and +`--no-tests=error`. Probes are opt-in per architecture and pack one instance per compile +group; the lane's own architecture is covered in full by the normal build. A pass does not +cover ASM SDPA, C++/runtime gfx950 paths, install staging or standalone-UKD references. + ## Ingestor proof boundaries [The ingestor RUNBOOK](../hipdnn-ingestor-engine/RUNBOOK.md) is the sole ordered diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md index 135da2b8a5ba..bd7b2248c3b8 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md @@ -79,6 +79,7 @@ on Windows the wheel venv's `_rocm_sdk_devel/bin`. | `HIPKERNELPROVIDER_PRODUCTION_SOURCE_ROOT` | the in-tree `.../kernel_ingestor_engine/descriptors` | `CACHE PATH` naming the authored tree production packaging compiles from. Packaging requires at least one non-hidden `*.kdp.json`; with none it is dormant. The default root is also dormant when no KDP declares any selected packaging architecture. Dormancy removes any stale product tree and is not an error. Set but not a directory is fatal. | | `HIPKERNELPROVIDER_KPACK_PYTHON_DIR` | unset | Directory **containing** `rocm_kpack/`; this locates a package, not a compiler interpreter. | | `Python3_EXECUTABLE` | system | Explicit environment for packaging dependencies such as `msgpack` and `zstandard`; production compilation retains its selected hermetic wheel interpreter. | +| `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` | OFF | CI-only packaging probes, opt-in per architecture (one `hkp_add_packaging_probe(ARCH )` line in `descriptor-packaging/probes/probes.cmake`); superbuild-only. Packs one instance per compile group for an architecture the lane does not build, so use an architecture other than `GPU_TARGETS`. When ON, the pack targets are part of `all`; `cmake --build --target hkp_packaging_probes` builds them alone and `ctest -R '^hkp-probe-' --no-tests=error` runs them (ctest entry `hkp-probe-`). Excludes ASM SDPA, C++/runtime gfx950 paths, install staging and standalone-UKD references (derive fails configure on those). | These flags only do anything on a preset that actually builds hip-kernel-provider. The default `hipdnn-providers` preset does **not** include it; the presets that do are From ef392f9ba78f9706eb1885ff42878f2c0e0ccee1 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 5 Oct 2026 19:29:23 +0000 Subject: [PATCH 12/52] hkp probes: one kind registry shared by derive and assert; add hsaco tools/hkp_probe_kinds.py holds one entry per supported kernel_source.kind (compile-group fields, provenance checks). hsaco, packed by develop since #12729, is the third entry. Remove the production-tree count pin. --- .../descriptor-packaging/README.md | 13 ++-- .../probes/tests/conftest.py | 2 + .../tests/test_hkp_probe_derive_root.py | 47 +++++++++++++- .../probes/tests/test_hkp_probe_end_to_end.py | 58 +++++++++++++++++ .../tools/hkp_probe_assert.py | 62 ++++++++++--------- .../tools/hkp_probe_derive_root.py | 24 +++---- .../tools/hkp_probe_kinds.py | 30 +++++++++ 7 files changed, 183 insertions(+), 53 deletions(-) create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index f0a2b880685e..e9fb42dbf866 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -353,16 +353,17 @@ normal declaration is one line per architecture: `hkp_add_packaging_probe(ARCH g A probe has no producer-kind or per-KDP argument: the producer kinds present in the root are discovered from its descriptors, and the assertion expects each UKD's provenance by -its authored `kernel_source.kind`. Probes cover `hip` and `rocke`, the kinds the packer -compiles to kpack output today. Any other kind in a UKD kept for the probed arch is -rejected at configure until its row is added in both tables: `_GROUP_FIELDS` in -`tools/hkp_probe_derive_root.py` and `_PROVENANCE_RULES` in `tools/hkp_probe_assert.py`. +its authored `kernel_source.kind`. Probes cover `hip`, `rocke` and `hsaco`, the kinds registered in `tools/hkp_probe_kinds.py` +(per kind: the `kernel_source` fields that define a compile group, and the provenance +checks a shipped UKD must satisfy). Any other kind in a UKD kept for the probed arch is +rejected at configure, and fails `no-rules-for-kind` in the assertion, until it gets +one entry there. Configuration fails, never skips, when: `ARCH` is missing; `ROOT` is not a directory; no KDP under the root ships for `ARCH`; a KDP shipping for `ARCH` references a standalone UKD (a `kernelDescriptors` entry that is not an object; derive supports inline UKDs only); the probe output root would sit under a -shipped descriptor tree; a UKD kept for `ARCH` has a kind other than `hip` or `rocke`; no probe is declared; or pytest is not importable by +shipped descriptor tree; a UKD kept for `ARCH` has a kind not registered in `tools/hkp_probe_kinds.py`; no probe is declared; or pytest is not importable by `Python3_EXECUTABLE`. ### The derived-root compile-group rule @@ -412,7 +413,7 @@ Today the production gfx950 KDP (840 `rocke` UKDs sharing one builder) keeps one | `signature` | A UKD has an empty `signature`. | | `symbol` | A UKD's `symbol` does not appear in its blob. | | `provenance-origin` | `origin_kind` differs from the UKD's expected kind (the authored `kernel_source.kind`). | -| `no-rules-for-kind` | A UKD's expected kind has no row in the provenance rules table of `tools/hkp_probe_assert.py`: a new producer kind must add its row (and its compile-group row in `tools/hkp_probe_derive_root.py`). | +| `no-rules-for-kind` | A UKD's expected kind has no entry in `tools/hkp_probe_kinds.py`: a new producer kind must add its entry. | | `provenance-wheel` | `rocke_wheel_sha256` is missing or empty: the wheel did not supply rocKE. | | `provenance-comgr` | The comgr recorded at pack time is not the expected library (or is empty). Catches a stale system comgr shadowing the intended one. | | `ukd-count` | The number of kpack-kind UKDs differs from the expected list (`expect.json`, written by derive), is zero, or a shipped UKD is not in the list. | diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py index 0ac3d074d9d3..cd0d6666fc4f 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py @@ -22,6 +22,8 @@ FIXTURES = _PKG_DIR / "tests" / "fixtures" ROCKE_FIXTURE = FIXTURES / "rocke" MAIN_FIXTURE = FIXTURES / "main" +EMPTY_ARCH_FIXTURE = FIXTURES / "empty_arch" +HSACO_FIXTURES = FIXTURES / "hsaco" _ROCKE_SOURCE_DIRS = ( _HKP_DIR / "rocke" / "platform" / "python", _HKP_DIR / "rocke" / "library", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py index 524feb4b33ae..a6c590f81143 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py @@ -113,6 +113,26 @@ def test_one_ukd_per_compile_group(tmp_path): assert sorted(_kept(tmp_path / "w" / "root")) == sorted(e["name"] for e in expect) +def test_hsaco_compiles_nothing_so_one_per_kdp(tmp_path): + ukds = [_other("hsaco", "z_hsaco"), _other("hsaco", "y_hsaco"), _rocke("r")] + _write_root(tmp_path / "src", ukds) + expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") + assert {(e["kind"], e["name"]) for e in expect} == { + ("hsaco", "y_hsaco"), + ("rocke", "r"), + } + + +def test_hsaco_ukd_for_other_arch_is_ignored(tmp_path): + ukds = [ + dict(_other("hsaco", "a_942"), arch=["gfx942"]), + dict(_other("hsaco", "b_950"), arch=["gfx950"]), + ] + _write_root(tmp_path / "src", ukds, kdp_arch=("gfx942", "gfx950")) + expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") + assert [e["name"] for e in expect] == ["b_950"] + + def test_pick_is_independent_of_authored_order(tmp_path): ukds = [_rocke(n) for n in ("m", "c", "x", "a2", "a1")] for i, order in enumerate(itertools.permutations(ukds, 3)): @@ -208,7 +228,7 @@ def test_standalone_reference_in_other_arch_kdp_is_ignored(tmp_path): assert _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") == [] -@pytest.mark.parametrize("kind", ["hsaco", "embedded_source", "kpack", "mystery"]) +@pytest.mark.parametrize("kind", ["embedded_source", "kpack", "mystery"]) def test_kind_without_probe_support_exits_2(tmp_path, kind): _refused( tmp_path, @@ -218,7 +238,7 @@ def test_kind_without_probe_support_exits_2(tmp_path, kind): def test_unsupported_kind_for_other_arch_is_ignored(tmp_path): - ukds = [_rocke("a"), dict(_other("hsaco", "m"), arch=["gfx942"])] + ukds = [_rocke("a"), dict(_other("embedded_source", "m"), arch=["gfx942"])] _write_root(tmp_path / "src", ukds, kdp_arch=("gfx942", "gfx950")) assert [ e["name"] for e in _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") @@ -323,3 +343,26 @@ def test_arch_subset_agrees_with_packer(ukd_arch, kdp_arch): got = _derive_module().arch_subset_ok(ukd_arch or [], kdp_arch or []) assert got == descriptors._arch_subset_ok(ukd_arch or [], kdp_arch or []) + + +# --- the kinds registry ----------------------------------------------------- +def _tool_module(name): + sys.path.insert(0, str(PROBE_DERIVE.parent)) + try: + return __import__(name) + finally: + sys.path.pop(0) + + +def test_kinds_registry_is_complete(): + kinds = _tool_module("hkp_probe_kinds").KINDS + checks = _tool_module("hkp_probe_assert")._PROVENANCE_CHECKS + assert kinds + used = set() + for kind, entry in kinds.items(): + assert all(isinstance(f, str) and f for f in entry.group_by), kind + # Every named provenance check has an implementation in the assert tool. + assert set(entry.provenance) <= set(checks), kind + used |= set(entry.provenance) + # ... and no implementation is dead. + assert used == set(checks) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py index a5d0aec8dcd9..1dad2341b253 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py @@ -13,6 +13,9 @@ import pytest from conftest import ( + ARCH, + EMPTY_ARCH_FIXTURE, + HSACO_FIXTURES, MAIN_FIXTURE, PROBE_DERIVE, ROCKE_FIXTURE, @@ -45,6 +48,23 @@ def _hip_source(dst): return dst +def _hsaco_source(dst, arch=ARCH): + """empty_arch's single UKD rewritten to an authored hsaco for `arch`.""" + shutil.copytree(EMPTY_ARCH_FIXTURE, dst) + shutil.copyfile(HSACO_FIXTURES / arch / "HsacoFixture.co", dst / "HsacoFixture.co") + kdp = json.loads((dst / "solo.kdp.json").read_text()) + kdp["arch"] = [arch] + ukd = kdp["kernelDescriptors"][0] + ukd["arch"] = [arch] + ukd["kernel_source"] = { + "kind": "hsaco", + "file": "HsacoFixture.co", + "symbol": "HsacoFixtureAdd", + } + (dst / "solo.kdp.json").write_text(json.dumps(kdp, indent=2)) + return dst + + def _derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib): derived = work / "probe" / "root" r = subprocess.run( @@ -136,3 +156,41 @@ def test_mixed_root_origin_is_checked_per_ukd(mixed_probe, rocm_kpack_dir, tmp_p assert "FAIL provenance-origin:" in result.stderr assert "rocke/attention.kdp.json" in result.stderr assert "hip/pointwise" not in result.stderr + + +def test_hsaco_root_passes(tmp_path, rocm_kpack_dir, hipcc, comgr_lib): + src = _hsaco_source(tmp_path / "src") + out, expect = _derive_and_pack(src, tmp_path, rocm_kpack_dir, hipcc, comgr_lib) + entries = json.loads(expect.read_text()) + assert [(e["kdp"], e["kind"]) for e in entries] == [("solo.kdp.json", "hsaco")] + result = run_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 0, result.stderr + + +def test_hip_and_hsaco_mixed_root_passes(tmp_path, rocm_kpack_dir, hipcc, comgr_lib): + src = tmp_path / "src" + _hip_source(src / "hip") + _hsaco_source(src / "hsaco") + out, expect = _derive_and_pack(src, tmp_path, rocm_kpack_dir, hipcc, comgr_lib) + kinds = {e["kdp"]: e["kind"] for e in json.loads(expect.read_text())} + assert kinds == { + "hip/pointwise.kdp.json": "hip", + "hip/pointwise_wild.kdp.json": "hip", + "hsaco/solo.kdp.json": "hsaco", + } + result = run_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 0, result.stderr + + +def test_hsaco_ukd_for_other_arch_is_not_packed_or_expected( + tmp_path, rocm_kpack_dir, hipcc, comgr_lib +): + # gfx942-only hsaco beside a gfx950 hip root: derive ignores it, the pack ships + # nothing for it, and the assertion still passes on the hip UKDs alone. + src = tmp_path / "src" + _hip_source(src / "hip") + _hsaco_source(src / "hsaco", arch="gfx942") + out, expect = _derive_and_pack(src, tmp_path, rocm_kpack_dir, hipcc, comgr_lib) + assert {e["kind"] for e in json.loads(expect.read_text())} == {"hip"} + result = run_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 0, result.stderr diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py index f3124e9659b9..6b145ba6b268 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py @@ -7,7 +7,7 @@ the shipped set is the one `--expect` lists (hkp_probe_derive_root.py writes it): a JSON list of {"kdp": , "name": , "kind": }. Each UKD's provenance is checked -against the rules of its expected kind (_PROVENANCE_RULES). +against the rules of its expected kind (hkp_probe_kinds.KINDS). hkp_probe_assert: FAIL : @@ -31,10 +31,10 @@ sha256 the archive blob's sha256 differs from kernel_source.sha256 signature a UKD's signature is not a non-empty list symbol a UKD's symbol does not appear in its archive blob - no-rules-for-kind a UKD's expected kind has no entry in _PROVENANCE_RULES + no-rules-for-kind a UKD's expected kind has no entry in hkp_probe_kinds.KINDS provenance-origin provenance.origin_kind is not the UKD's expected kind - provenance-wheel rocke only: provenance.rocke_wheel_sha256 absent or empty - provenance-comgr rocke only: provenance.comgr_path is not the expected comgr + provenance-wheel kinds with the wheel check (rocke): provenance.rocke_wheel_sha256 absent or empty + provenance-comgr kinds with the comgr check (rocke): provenance.comgr_path is not the expected comgr library """ @@ -45,17 +45,9 @@ import sys from pathlib import Path -_PREFIX = "hkp_probe_assert" +from hkp_probe_kinds import KINDS -# Provenance checks required per producer kind (the authored kernel_source.kind, -# which the packer records as provenance.origin_kind). The kinds are those the packer -# compiles to kpack output. A new producer needs one row here AND one row in -# hkp_probe_derive_root.py's _GROUP_FIELDS; a kind with no row fails -# `no-rules-for-kind`. hip's `hipcc_version` is optional and unchecked. -_PROVENANCE_RULES = { - "rocke": ("wheel", "comgr"), - "hip": (), -} +_PREFIX = "hkp_probe_assert" class _Failures: @@ -158,9 +150,31 @@ def _check_kpack(args, arch_dir, failures): return kpack_path +def _check_wheel(args, label, prov, failures): + wheel = prov.get("rocke_wheel_sha256") + if not isinstance(wheel, str) or not wheel: + failures.add("provenance-wheel", f"{label} rocke_wheel_sha256 is {wheel!r}") + + +def _check_comgr(args, label, prov, failures): + comgr = prov.get("comgr_path") + if args.expect_comgr is None: + if not isinstance(comgr, str) or not comgr: + failures.add("provenance-comgr", f"{label} comgr_path is {comgr!r}") + elif not isinstance(comgr, str) or not _same_path(comgr, args.expect_comgr): + failures.add( + "provenance-comgr", + f"{label} comgr_path is {comgr!r}, expected {args.expect_comgr}", + ) + + +# Implementations of the check names listed in hkp_probe_kinds.Kind.provenance. +_PROVENANCE_CHECKS = {"wheel": _check_wheel, "comgr": _check_comgr} + + def _check_provenance(args, label, ukd, kind, failures): - rules = _PROVENANCE_RULES.get(kind) - if rules is None: + entry = KINDS.get(kind) + if entry is None: failures.add( "no-rules-for-kind", f"{label} expected kind {kind!r} has no rules" ) @@ -171,20 +185,8 @@ def _check_provenance(args, label, ukd, kind, failures): failures.add( "provenance-origin", f"{label} origin_kind is {origin!r}, not {kind!r}" ) - if "wheel" in rules: - wheel = prov.get("rocke_wheel_sha256") - if not isinstance(wheel, str) or not wheel: - failures.add("provenance-wheel", f"{label} rocke_wheel_sha256 is {wheel!r}") - if "comgr" in rules: - comgr = prov.get("comgr_path") - if args.expect_comgr is None: - if not isinstance(comgr, str) or not comgr: - failures.add("provenance-comgr", f"{label} comgr_path is {comgr!r}") - elif not isinstance(comgr, str) or not _same_path(comgr, args.expect_comgr): - failures.add( - "provenance-comgr", - f"{label} comgr_path is {comgr!r}, expected {args.expect_comgr}", - ) + for name in entry.provenance: + _PROVENANCE_CHECKS[name](args, label, prov, failures) def _check_ukd(args, kdp_name, ukd, archive, failures): diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py index e0742c60977c..6ff0a0ef8134 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py @@ -5,8 +5,8 @@ `--arch` has its inline `kernelDescriptors` reduced to one UKD per compile group among the UKDs that themselves ship for `--arch`: - rocke (kind, builder) - hip (kind, source, build) + one entry per kind in hkp_probe_kinds.py: rocke (kind, builder), hip (kind, source, + build), hsaco (kind), i.e. one per KDP The pick within a group is the first by sorted UKD `name`. Packing the derived root therefore exercises every compile path once instead of every variant. @@ -26,7 +26,7 @@ Exit code 0 on success. Exit code 2, with a `hkp_probe_derive: ...` message on stderr, when the root cannot be derived: a KDP shipping for `--arch` references a standalone UKD (`kernelDescriptors` entry that is not an object), a kept UKD -has a kind other than rocke or hip, a UKD is malformed, or the source is unreadable. +has a kind not in hkp_probe_kinds.KINDS, a UKD is malformed, or the source is unreadable. """ from __future__ import annotations @@ -40,13 +40,7 @@ _PREFIX = "hkp_probe_derive:" -# The kinds the packer compiles to kpack output, and the kernel_source fields that -# define a compile group within a KDP. A new producer needs one row here AND one row -# in hkp_probe_assert.py's _PROVENANCE_RULES; any other kind is rejected. -_GROUP_FIELDS = { - "rocke": ("builder",), - "hip": ("source", "build"), -} +from hkp_probe_kinds import KINDS class DeriveError(Exception): @@ -71,14 +65,14 @@ def arch_subset_ok(ukd_arch, kdp_arch): def _group_key(ukd, where): source = ukd.get("kernel_source") kind = source.get("kind") if isinstance(source, dict) else None - if kind not in _GROUP_FIELDS: + if kind not in KINDS: raise DeriveError( - f"{where} kind {kind!r} has no probe support: add its compile-group " - "fields here and its provenance rules in hkp_probe_assert.py, once " - "the packer produces kpack output for it" + f"{where} kind {kind!r} has no probe support: add an entry to " + "KINDS in hkp_probe_kinds.py, once the packer produces kpack output " + "for it" ) return (kind,) + tuple( - json.dumps(source.get(f), sort_keys=True) for f in _GROUP_FIELDS[kind] + json.dumps(source.get(f), sort_keys=True) for f in KINDS[kind].group_by ) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py new file mode 100644 index 000000000000..b88f81dce036 --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py @@ -0,0 +1,30 @@ +"""The producer kinds a packaging probe supports: one entry per kernel_source.kind. + +Shared by hkp_probe_derive_root.py (compile-group selection) and +hkp_probe_assert.py (provenance rules). Only kinds the packer packs to kpack output +belong here. A kind with no entry is rejected by derive at configure time and fails +`no-rules-for-kind` in the assertion, so a new producer cannot pass a probe without +being described here. Adding a producer is one entry in KINDS. +""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Kind: + # kernel_source fields that define a compile group within a KDP: UKDs of one kind + # agreeing on all of them share a compile path, so one stands for the group. Empty + # means the kind compiles nothing and one UKD per KDP stands for it. + group_by: tuple[str, ...] + # Names of provenance checks (hkp_probe_assert._PROVENANCE_CHECKS) a shipped UKD of + # this kind must satisfy, beyond provenance.origin_kind == the kind itself. + provenance: tuple[str, ...] + + +KINDS = { + "rocke": Kind(group_by=("builder",), provenance=("wheel", "comgr")), + "hip": Kind(group_by=("source", "build"), provenance=()), + "hsaco": Kind(group_by=(), provenance=()), +} From 023c8af942bda3ac94bffbdaf3ee1abae2217bfe Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 5 Oct 2026 19:39:49 +0000 Subject: [PATCH 13/52] hkp probes: docs state which kinds cannot be probed; drop stale counts --- .../descriptor-packaging/README.md | 16 +++++++++------- .../tools/hkp_probe_derive_root.py | 11 +++++++---- .../tools/hkp_probe_kinds.py | 8 +++++++- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index e9fb42dbf866..2d7955a94787 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -336,8 +336,8 @@ ctest names that must run; the junit check fails unless every one ran with no fa error or skip. Configuring without the option and running the same build and ctest lines fails (unknown target, then "No tests were found"). -Locally on Linux the build takes about 35 s (the wheel environment plus a one-instance-per-group -pack) and the two ctest entries about 3 s. There is no local Windows reproduction. +Locally on Linux the build is quick (the wheel environment plus a one-instance-per-group +pack) and the ctest entries a few seconds. There is no local Windows reproduction. ### `hkp_add_packaging_probe` arguments @@ -355,9 +355,11 @@ A probe has no producer-kind or per-KDP argument: the producer kinds present in are discovered from its descriptors, and the assertion expects each UKD's provenance by its authored `kernel_source.kind`. Probes cover `hip`, `rocke` and `hsaco`, the kinds registered in `tools/hkp_probe_kinds.py` (per kind: the `kernel_source` fields that define a compile group, and the provenance -checks a shipped UKD must satisfy). Any other kind in a UKD kept for the probed arch is -rejected at configure, and fails `no-rules-for-kind` in the assertion, until it gets -one entry there. +checks a shipped UKD must satisfy). Any other kind in a UKD kept for the probed arch makes +configure fail. A kind the packer packs to kpack becomes probeable with one entry there. +Pass-through kinds (`embedded_source`, which the packer ships as authored, not as kpack) and +kinds the packer does not pack to kpack cannot be probed by adding an entry: such a UKD must +live in a KDP that does not ship for a probed arch, or the probe needs a design decision. Configuration fails, never skips, when: `ARCH` is missing; `ROOT` is not a directory; no KDP under the root ships for `ARCH`; a @@ -377,11 +379,11 @@ themselves ship for `ARCH`. The group is the part of a UKD that selects its comp |---|---| | `rocke` | `(kind, builder)` | | `hip` | `(kind, source, build)` | -| `hsaco`, `kpack`, `embedded_source` | one per kind | +| `hsaco` | one per KDP (compiles nothing) | The kept UKD is the first by sorted `name`. The probe therefore packs the real descriptors and compiles every distinct compile path once instead of every variant. -Today the production gfx950 KDP (840 `rocke` UKDs sharing one builder) keeps one. +A production KDP whose UKDs share one builder keeps one UKD. - **Variants inside a group are not compiled.** Breakage specific to one spec of a shared builder is not caught; that is the accepted gap of the design. diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py index 6ff0a0ef8134..6c7ad82c77f0 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py @@ -6,7 +6,7 @@ among the UKDs that themselves ship for `--arch`: one entry per kind in hkp_probe_kinds.py: rocke (kind, builder), hip (kind, source, - build), hsaco (kind), i.e. one per KDP + build), hsaco (kind: compiles nothing, so one UKD per KDP) The pick within a group is the first by sorted UKD `name`. Packing the derived root therefore exercises every compile path once instead of every variant. @@ -67,9 +67,12 @@ def _group_key(ukd, where): kind = source.get("kind") if isinstance(source, dict) else None if kind not in KINDS: raise DeriveError( - f"{where} kind {kind!r} has no probe support: add an entry to " - "KINDS in hkp_probe_kinds.py, once the packer produces kpack output " - "for it" + f"{where} kind {kind!r} has no probe support. A kind the packer packs to " + "kpack needs one entry in KINDS (hkp_probe_kinds.py). A pass-through kind " + "(embedded_source: shipped as authored, not as kpack) or any kind the " + "packer does not pack to kpack cannot be probed by adding an entry: keep " + "such UKDs in a KDP that does not ship for the probed arch, or decide how " + "the probe should cover them" ) return (kind,) + tuple( json.dumps(source.get(f), sort_keys=True) for f in KINDS[kind].group_by diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py index b88f81dce036..2f702935eb3a 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py @@ -4,7 +4,13 @@ hkp_probe_assert.py (provenance rules). Only kinds the packer packs to kpack output belong here. A kind with no entry is rejected by derive at configure time and fails `no-rules-for-kind` in the assertion, so a new producer cannot pass a probe without -being described here. Adding a producer is one entry in KINDS. +being described here. A new kind that packs to kpack is one entry in KINDS. + +Pass-through kinds (embedded_source: the packer ships it as authored, not as kpack) and +kinds the packer does not pack to kpack cannot be probed by adding an entry: the +assertion expects a kpack archive entry per UKD. A UKD of such a kind in a KDP that +ships for a probed arch makes configure fail; it must live in a KDP that does not ship +for a probed arch, or the probe needs a design decision. """ from __future__ import annotations From 66cd576fb9bd948d22d4a52c0a0bbbfbba203efa Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 5 Oct 2026 20:35:06 +0000 Subject: [PATCH 14/52] hkp probes: registry output type (kpack | passthrough); probe embedded_source Assert dispatches on the expected kind's output type: kpack kinds keep the archive checks, pass-through kinds are checked for an unchanged kernel_source, arch and provenance. Archive checks run only when a kpack UKD is expected. expect.json carries the authored kernel_source for pass-through kinds. --- .../descriptor-packaging/README.md | 42 ++- .../tests/test_hkp_probe_derive_root.py | 33 ++- .../tests/test_hkp_probe_passthrough.py | 251 ++++++++++++++++++ .../tools/hkp_probe_assert.py | 175 +++++++++--- .../tools/hkp_probe_derive_root.py | 22 +- .../tools/hkp_probe_kinds.py | 31 ++- 6 files changed, 477 insertions(+), 77 deletions(-) create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_passthrough.py diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 2d7955a94787..3bf011d1f0df 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -353,13 +353,28 @@ normal declaration is one line per architecture: `hkp_add_packaging_probe(ARCH g A probe has no producer-kind or per-KDP argument: the producer kinds present in the root are discovered from its descriptors, and the assertion expects each UKD's provenance by -its authored `kernel_source.kind`. Probes cover `hip`, `rocke` and `hsaco`, the kinds registered in `tools/hkp_probe_kinds.py` -(per kind: the `kernel_source` fields that define a compile group, and the provenance -checks a shipped UKD must satisfy). Any other kind in a UKD kept for the probed arch makes -configure fail. A kind the packer packs to kpack becomes probeable with one entry there. -Pass-through kinds (`embedded_source`, which the packer ships as authored, not as kpack) and -kinds the packer does not pack to kpack cannot be probed by adding an entry: such a UKD must -live in a KDP that does not ship for a probed arch, or the probe needs a design decision. +its authored `kernel_source.kind`. Probes cover `hip`, `rocke`, `hsaco` and +`embedded_source`, the kinds registered in `tools/hkp_probe_kinds.py`. Each entry gives the +`kernel_source` fields that define a compile group, the provenance checks a shipped UKD must +satisfy, and the packed **output type**: `kpack` (the packer packs the UKD into the archive +and ships a `kind: kpack` UKD: `hip`, `rocke`, `hsaco`) or `passthrough` (the packer ships the +UKD as authored and nothing enters the archive: `embedded_source`). A kind whose output is +one of these types, using provenance checks that already exist, is probeable with one entry +there plus one end-to-end test case; the kinds list in this paragraph is edited with it. A +kind the packer does not support yet needs packer work first. Any kind not registered, in a +UKD kept for the probed arch, makes configure fail and fails `no-rules-for-kind` in the +assertion; such a UKD must live in a KDP that does not ship for a probed arch, or the probe +needs a design decision. + +The archive rule: the archive checks (`kpack-missing`, `kpack-empty`, `kpack-toc`, `sha256`, +`symbol`, `signature`) run only when the expected list holds at least one kpack-output UKD. +A root of only pass-through UKDs ships descriptors and no `kpack/` directory; the archive is +then not required and an archive that happens to exist is ignored. + +Non-goals: a pass-through kind probes one UKD per KDP (`group_by` is empty), not every +`source_file` and `entry_point`; standalone-UKD references are rejected by derive; a further +output type (for example a runtime-compiled kind) is a new decision when it lands, not an +entry. Configuration fails, never skips, when: `ARCH` is missing; `ROOT` is not a directory; no KDP under the root ships for `ARCH`; a @@ -380,6 +395,7 @@ themselves ship for `ARCH`. The group is the part of a UKD that selects its comp | `rocke` | `(kind, builder)` | | `hip` | `(kind, source, build)` | | `hsaco` | one per KDP (compiles nothing) | +| `embedded_source` | one per KDP (compiles nothing; ships as authored, no archive entry) | The kept UKD is the first by sorted `name`. The probe therefore packs the real descriptors and compiles every distinct compile path once instead of every variant. @@ -391,7 +407,7 @@ A production KDP whose UKDs share one builder keeps one UKD. ingestor run; names encode the specialization and are unique. - **Arch matching follows the packer** (`arch_matches`, `_arch_subset_ok`): an empty or absent `arch` list is a wildcard. -- Derive also writes `expect.json` (kept KDP, UKD name and kind) beside the root; the +- Derive also writes `expect.json` (kept KDP, UKD name and kind; pass-through kinds also carry the authored `kernel_source`) beside the root; the assertion expects exactly that set. - The derive step re-runs when any file under the root changes, and rewrites only files whose content differs, so a reconfigure leaves the pack stamp fresh. @@ -406,10 +422,12 @@ A production KDP whose UKDs share one builder keeps one UKD. | `out-root-missing` | The probe output root does not exist: build `hkp_packaging_probes` first. | | `stamp-missing` | The pack stamp is absent. | | `arch-dir-missing` / `extra-arch-dir` | `//` is absent, or another `gfx*` directory is present. | -| `kpack-missing` / `kpack-empty` | `//kpack/hip_kernel_provider_.kpack` is absent or empty. | +| `kpack-missing` / `kpack-empty` | Kpack output is expected and `//kpack/hip_kernel_provider_.kpack` is absent or empty. | | `no-kdp` | No `*.kdp.json` under `/`. | -| `ukd-kind` | A shipped UKD's `kernel_source.kind` is not `kpack`. | -| `arch-field` | A KDP or UKD `arch` is not exactly the probe architecture. | +| `ukd-kind` | A shipped UKD's `kernel_source.kind` differs from what its expected kind's output type requires: `kpack` for kpack output, the authored kind for pass-through output. | +| `arch-field` | A KDP or UKD `arch` is not exactly the probe architecture (always required for a pass-through UKD). | +| `passthrough-source` | A pass-through UKD's `kernel_source` differs from the authored one recorded in `expect.json`. | +| `passthrough-provenance` | A pass-through UKD's `provenance.source_label` is empty, or `provenance.source_file` differs from `kernel_source.source_file`. | | `kpack-toc` | The archive has no entry for a UKD's `toc_key` and architecture. | | `sha256` | The blob's sha256 differs from `kernel_source.sha256`. | | `signature` | A UKD has an empty `signature`. | @@ -418,7 +436,7 @@ A production KDP whose UKDs share one builder keeps one UKD. | `no-rules-for-kind` | A UKD's expected kind has no entry in `tools/hkp_probe_kinds.py`: a new producer kind must add its entry. | | `provenance-wheel` | `rocke_wheel_sha256` is missing or empty: the wheel did not supply rocKE. | | `provenance-comgr` | The comgr recorded at pack time is not the expected library (or is empty). Catches a stale system comgr shadowing the intended one. | -| `ukd-count` | The number of kpack-kind UKDs differs from the expected list (`expect.json`, written by derive), is zero, or a shipped UKD is not in the list. | +| `ukd-count` | The number of shipped UKDs found in the expected list (`expect.json`, written by derive) differs from its length, is zero, or a shipped UKD is not in the list. | ### Adding a probe diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py index a6c590f81143..974704dc812a 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py @@ -133,6 +133,27 @@ def test_hsaco_ukd_for_other_arch_is_ignored(tmp_path): assert [e["name"] for e in expect] == ["b_950"] +def test_passthrough_kind_expect_carries_authored_kernel_source(tmp_path): + emb = { + "id": "id-e", + "name": "e", + "kernel_source": { + "kind": "embedded_source", + "source_file": "k/a.cpp", + "entry_point": "Entry", + }, + } + other = dict(emb, id="id-e2", name="e2") + _write_root(tmp_path / "src", [other, emb, _rocke("r")]) + expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") + by_name = {e["name"]: e for e in expect} + # One passthrough UKD per KDP (nothing compiles); it records its authored block. + assert set(by_name) == {"e", "r"} + assert by_name["e"]["kernel_source"] == emb["kernel_source"] + # kpack-output entries keep the three-field schema. + assert set(by_name["r"]) == {"kdp", "name", "kind"} + + def test_pick_is_independent_of_authored_order(tmp_path): ukds = [_rocke(n) for n in ("m", "c", "x", "a2", "a1")] for i, order in enumerate(itertools.permutations(ukds, 3)): @@ -228,7 +249,7 @@ def test_standalone_reference_in_other_arch_kdp_is_ignored(tmp_path): assert _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") == [] -@pytest.mark.parametrize("kind", ["embedded_source", "kpack", "mystery"]) +@pytest.mark.parametrize("kind", ["kpack", "mystery"]) def test_kind_without_probe_support_exits_2(tmp_path, kind): _refused( tmp_path, @@ -238,7 +259,7 @@ def test_kind_without_probe_support_exits_2(tmp_path, kind): def test_unsupported_kind_for_other_arch_is_ignored(tmp_path): - ukds = [_rocke("a"), dict(_other("embedded_source", "m"), arch=["gfx942"])] + ukds = [_rocke("a"), dict(_other("kpack", "m"), arch=["gfx942"])] _write_root(tmp_path / "src", ukds, kdp_arch=("gfx942", "gfx950")) assert [ e["name"] for e in _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") @@ -366,3 +387,11 @@ def test_kinds_registry_is_complete(): used |= set(entry.provenance) # ... and no implementation is dead. assert used == set(checks) + + +def test_every_registered_output_type_has_checks(): + kinds = _tool_module("hkp_probe_kinds").KINDS + outputs = _tool_module("hkp_probe_assert")._OUTPUT_CHECKS + assert {k.output for k in kinds.values()} <= set(outputs) + # ... and no check set is dead. + assert set(outputs) <= {k.output for k in kinds.values()} diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_passthrough.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_passthrough.py new file mode 100644 index 000000000000..05b0d86e5cae --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_passthrough.py @@ -0,0 +1,251 @@ +"""Pass-through output (embedded_source): derive -> real pack -> assert. + +The roots are packed by the real packer. A root holding only pass-through UKDs +ships descriptors and no kpack archive, which is what these tests rely on. +""" + +import json +import shutil +import subprocess +import sys + +import pytest + +from conftest import ( + ARCH, + EMPTY_ARCH_FIXTURE, + MAIN_FIXTURE, + PROBE_DERIVE, + pack_root, + run_assert, +) + +_EMBEDDED = { + "kind": "embedded_source", + "source_file": "kernels/PointwiseAdd.cpp", + "entry_point": "PointwiseAdd", +} +KDP = "emb/solo.kdp.json" + + +def _embedded_source(dst): + shutil.copytree(EMPTY_ARCH_FIXTURE, dst) + kdp = json.loads((dst / "solo.kdp.json").read_text()) + kdp["arch"] = [ARCH] + kdp["kernelDescriptors"][0]["kernel_source"] = dict(_EMBEDDED) + (dst / "solo.kdp.json").write_text(json.dumps(kdp, indent=2)) + return dst + + +def _hip_source(dst): + shutil.copytree(MAIN_FIXTURE, dst) + (dst / "pointwise_add_b128.ukd.json").unlink() + kdp = json.loads((dst / "pointwise.kdp.json").read_text()) + kdp["kernelDescriptors"] = [ + u for u in kdp["kernelDescriptors"] if isinstance(u, dict) + ] + (dst / "pointwise.kdp.json").write_text(json.dumps(kdp, indent=2)) + return dst + + +def _derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib): + derived = work / "probe" / "root" + r = subprocess.run( + [ + sys.executable, + str(PROBE_DERIVE), + "--from", + str(src), + "--arch", + ARCH, + "--out", + str(derived), + ], + capture_output=True, + text=True, + ) + assert r.returncode == 0, r.stderr + out = pack_root(derived, work, rocm_kpack_dir, hipcc, comgr_lib) + return out, work / "probe" / "expect.json" + + +class _Tree: + def __init__(self, root, expect, rocm_kpack_dir, kdp): + self.root, self.expect_path, self.rocm_kpack_dir, self.kdp_rel = ( + root, + expect, + rocm_kpack_dir, + kdp, + ) + + def _path(self): + return self.root / ARCH / self.kdp_rel + + def mutate(self, name, edit): + kdp = json.loads(self._path().read_text()) + (ukd,) = [u for u in kdp["kernelDescriptors"] if u["name"] == name] + edit(ukd) + self._path().write_text(json.dumps(kdp, indent=2)) + + def mutate_expect(self, edit): + entries = json.loads(self.expect_path.read_text()) + edit(entries) + self.expect_path.write_text(json.dumps(entries)) + + def run(self): + return run_assert(self.root, self.expect_path, self.rocm_kpack_dir) + + +def _fails(result, assertion_id): + assert result.returncode == 1, result.stdout + result.stderr + assert f"FAIL {assertion_id}:" in result.stderr, result.stderr + + +@pytest.fixture(scope="module") +def emb_packed(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): + work = tmp_path_factory.mktemp("emb_probe") + _embedded_source(work / "src" / "emb") + return _derive_and_pack(work / "src", work, rocm_kpack_dir, hipcc, comgr_lib) + + +@pytest.fixture(scope="module") +def mixed_packed(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): + work = tmp_path_factory.mktemp("emb_hip_probe") + src = work / "src" + _embedded_source(src / "emb") + _hip_source(src / "hip") + return _derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib) + + +@pytest.fixture +def emb(emb_packed, tmp_path, rocm_kpack_dir): + out, expect = emb_packed + shutil.copytree(out, tmp_path / "out") + shutil.copyfile(expect, tmp_path / "expect.json") + tree = _Tree( + tmp_path / "out", tmp_path / "expect.json", rocm_kpack_dir, "emb/solo.kdp.json" + ) + tree.name = json.loads(expect.read_text())[0]["name"] + return tree + + +@pytest.fixture +def mixed(mixed_packed, tmp_path, rocm_kpack_dir): + out, expect = mixed_packed + shutil.copytree(out, tmp_path / "out") + shutil.copyfile(expect, tmp_path / "expect.json") + return tmp_path / "out", tmp_path / "expect.json" + + +def test_passthrough_only_root_ships_no_archive_and_passes(emb_packed, rocm_kpack_dir): + out, expect = emb_packed + entries = json.loads(expect.read_text()) + assert [(e["kdp"], e["kind"]) for e in entries] == [(KDP, "embedded_source")] + assert entries[0]["kernel_source"] == _EMBEDDED + # The packer's real output: descriptors, no archive. + assert not (out / ARCH / "kpack").exists() + assert (out / ARCH / KDP).is_file() + result = run_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 0, result.stderr + + +def test_stray_archive_is_ignored_without_expected_kpack_ukds(emb, rocm_kpack_dir): + (emb.root / ARCH / "kpack").mkdir() + (emb.root / ARCH / "kpack" / f"hip_kernel_provider_{ARCH}.kpack").write_bytes(b"") + result = emb.run() + assert result.returncode == 0, result.stderr + + +def test_mixed_hip_and_embedded_source_root_passes(mixed, rocm_kpack_dir): + out, expect = mixed + kinds = {e["kdp"]: e["kind"] for e in json.loads(expect.read_text())} + assert kinds == { + "emb/solo.kdp.json": "embedded_source", + "hip/pointwise.kdp.json": "hip", + "hip/pointwise_wild.kdp.json": "hip", + } + assert (out / ARCH / "kpack").is_dir() + result = run_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 0, result.stderr + + +def test_mixed_root_still_runs_archive_checks_for_the_kpack_ukds(mixed, rocm_kpack_dir): + out, expect = mixed + kdp = out / ARCH / "hip" / "pointwise.kdp.json" + doc = json.loads(kdp.read_text()) + doc["kernelDescriptors"][0]["kernel_source"]["sha256"] = "0" * 64 + kdp.write_text(json.dumps(doc)) + _fails(run_assert(out, expect, rocm_kpack_dir), "sha256") + + +def test_mixed_root_missing_archive_fails(mixed, rocm_kpack_dir): + out, expect = mixed + shutil.rmtree(out / ARCH / "kpack") + _fails(run_assert(out, expect, rocm_kpack_dir), "kpack-missing") + + +# --- one mutation per pass-through assertion and sub-condition --------------- +def test_passthrough_source_edited(emb): + emb.mutate(emb.name, lambda u: u["kernel_source"].update(source_file="other.cpp")) + _fails(emb.run(), "passthrough-source") + + +def test_passthrough_source_entry_point_edited(emb): + emb.mutate(emb.name, lambda u: u["kernel_source"].update(entry_point="Other")) + _fails(emb.run(), "passthrough-source") + + +def test_passthrough_arch_absent(emb): + emb.mutate(emb.name, lambda u: u.pop("arch")) + _fails(emb.run(), "arch-field") + + +def test_passthrough_arch_wrong(emb): + emb.mutate(emb.name, lambda u: u.update(arch=["gfx942"])) + _fails(emb.run(), "arch-field") + + +def test_passthrough_origin(emb): + emb.mutate(emb.name, lambda u: u["provenance"].update(origin_kind="hip")) + _fails(emb.run(), "provenance-origin") + + +def test_passthrough_source_label_empty(emb): + emb.mutate(emb.name, lambda u: u["provenance"].update(source_label="")) + _fails(emb.run(), "passthrough-provenance") + + +def test_passthrough_source_label_absent(emb): + emb.mutate(emb.name, lambda u: u["provenance"].pop("source_label")) + _fails(emb.run(), "passthrough-provenance") + + +def test_passthrough_provenance_source_file_differs(emb): + emb.mutate(emb.name, lambda u: u["provenance"].update(source_file="x.cpp")) + _fails(emb.run(), "passthrough-provenance") + + +def test_passthrough_shipped_as_kpack_fails_ukd_kind(emb): + emb.mutate(emb.name, lambda u: u["kernel_source"].update(kind="kpack")) + _fails(emb.run(), "ukd-kind") + + +def test_kpack_kind_shipped_as_passthrough_fails_ukd_kind(mixed, rocm_kpack_dir): + out, expect = mixed + kdp = out / ARCH / "hip" / "pointwise.kdp.json" + doc = json.loads(kdp.read_text()) + doc["kernelDescriptors"][0]["kernel_source"] = dict(_EMBEDDED) + kdp.write_text(json.dumps(doc)) + _fails(run_assert(out, expect, rocm_kpack_dir), "ukd-kind") + + +def test_expect_entry_of_passthrough_kind_without_kernel_source_exits_2(emb): + emb.mutate_expect(lambda entries: entries[0].pop("kernel_source")) + result = emb.run() + assert result.returncode == 2, result.stderr + assert "Traceback" not in result.stderr + + +def test_expected_passthrough_but_shipped_ukd_missing_fails_count(emb): + emb.mutate_expect(lambda entries: entries.append(dict(entries[0], name="another"))) + _fails(emb.run(), "ukd-count") diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py index 6b145ba6b268..c529757fb942 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py @@ -3,39 +3,53 @@ A packaging probe packs one descriptor root for one explicit architecture. This tool inspects the packed output root and fails, with one greppable line per -failure, unless every shipped descriptor is a well-formed kpack-backed kernel and -the shipped set is the one `--expect` lists (hkp_probe_derive_root.py writes it): -a JSON list of {"kdp": , "name": , "kind": }. Each UKD's provenance is checked -against the rules of its expected kind (hkp_probe_kinds.KINDS). +failure, unless the shipped set is the one `--expect` lists +(hkp_probe_derive_root.py writes it) and every UKD is well formed for its kind: a JSON +list of {"kdp": , "name": , "kind": +}, plus the authored "kernel_source" for kinds whose +output is a pass-through. The checks applied to a UKD follow its EXPECTED kind's output +type (hkp_probe_kinds.KINDS): `kpack` UKDs are checked against the archive; +`passthrough` UKDs must ship as authored. The archive checks (kpack-missing, +kpack-empty, kpack-toc, sha256, symbol, signature) run only when the expect list holds +at least one kpack-output UKD; a pass-through-only root ships no archive. hkp_probe_assert: FAIL : Exit status is 0 only when every assertion holds, 1 when any assertion fails, and 2 for an unusable invocation (rocm_kpack not importable, --expect -unreadable or malformed). Plain python, not pytest: a script has no skip path, so it cannot pass without having checked. +unreadable or malformed). Plain python, not pytest: a script has no skip path, so it +cannot pass without having checked. Assertion ids: out-root-missing --out-root is not a directory stamp-missing / does not exist arch-dir-missing // does not exist extra-arch-dir another gfx* directory sits beside // - kpack-missing //kpack/hip_kernel_provider_.kpack absent + kpack-missing kpack output expected but //kpack/hip_kernel_provider_ + .kpack is absent kpack-empty that archive has size 0 no-kdp no *.kdp.json under //, or one is not valid JSON - ukd-count kpack-kind UKD count differs from the --expect list length (or - is 0), or a shipped UKD is not in the list - ukd-kind a UKD's kernel_source.kind is not "kpack" - arch-field a KDP or UKD `arch` is not [] + ukd-count the number of shipped UKDs found in the --expect list differs + from its length (or is 0), or a shipped UKD is not in the list + ukd-kind a UKD's shipped kernel_source.kind is not what its expected + kind's output type requires ("kpack", or the authored kind for + a pass-through) + arch-field a KDP `arch`, or a UKD `arch` (always required for a + pass-through UKD), is not [] + passthrough-source a pass-through UKD's kernel_source differs from the authored one + passthrough-provenance + a pass-through UKD's provenance.source_label is empty or its + provenance.source_file differs from kernel_source.source_file kpack-toc the archive is unreadable or has no entry for a UKD's toc_key sha256 the archive blob's sha256 differs from kernel_source.sha256 signature a UKD's signature is not a non-empty list symbol a UKD's symbol does not appear in its archive blob no-rules-for-kind a UKD's expected kind has no entry in hkp_probe_kinds.KINDS provenance-origin provenance.origin_kind is not the UKD's expected kind - provenance-wheel kinds with the wheel check (rocke): provenance.rocke_wheel_sha256 absent or empty - provenance-comgr kinds with the comgr check (rocke): provenance.comgr_path is not the expected comgr - library + provenance-wheel kinds with the wheel check (rocke): provenance.rocke_wheel_sha256 + absent or empty + provenance-comgr kinds with the comgr check (rocke): provenance.comgr_path is not + the expected comgr library """ import argparse @@ -79,7 +93,7 @@ class _UsageError(Exception): def _load_expect(path): - """Read --expect into {(kdp, name): kind}.""" + """Read --expect into {(kdp, name): entry}; entry keeps `kind` and `kernel_source`.""" try: entries = json.loads(path.read_text(encoding="utf-8")) except (OSError, ValueError) as exc: @@ -94,13 +108,26 @@ def _load_expect(path): raise _UsageError( f"--expect {path} entry {entry!r} lacks string kdp, name and kind" ) + if _output_of(entry["kind"]) == "passthrough" and not isinstance( + entry.get("kernel_source"), dict + ): + raise _UsageError( + f"--expect {path} entry {entry!r}: a {entry['kind']} entry needs the " + "authored kernel_source object" + ) key = (entry["kdp"], entry["name"]) if key in expected: raise _UsageError(f"--expect {path} lists {key} twice") - expected[key] = entry["kind"] + expected[key] = entry return expected +def _output_of(kind): + """The registered output type of `kind`, or None when it has no entry.""" + entry = KINDS.get(kind) + return entry.output if entry is not None else None + + def _load_kpack(kpack_python_dir): if kpack_python_dir not in sys.path: sys.path.insert(0, kpack_python_dir) @@ -189,13 +216,16 @@ def _check_provenance(args, label, ukd, kind, failures): _PROVENANCE_CHECKS[name](args, label, prov, failures) -def _check_ukd(args, kdp_name, ukd, archive, failures): - label = f"{kdp_name}:{ukd.get('name', ukd.get('id', '?'))}" +def _check_kpack_ukd(args, label, ukd, expected, archive, failures): source = ukd.get("kernel_source") or {} kind = source.get("kind") if kind != "kpack": - failures.add("ukd-kind", f'{label} kernel_source.kind is {kind!r}, not "kpack"') - return False + failures.add( + "ukd-kind", + f"{label} kernel_source.kind is {kind!r}; expected kind " + f'{expected["kind"]!r} has kpack output, which ships "kpack"', + ) + return if "arch" in ukd and ukd["arch"] != [args.arch]: failures.add("arch-field", f"{label} arch is {ukd['arch']!r}") @@ -204,23 +234,17 @@ def _check_ukd(args, kdp_name, ukd, archive, failures): if not isinstance(signature, list) or not signature: failures.add("signature", f"{label} signature is {signature!r}") - expected_kind = args.expected.get((kdp_name, ukd.get("name"))) - if expected_kind is None: - failures.add( - "ukd-count", f"{label} is not in the --expect list ({args.expect})" - ) - else: - _check_provenance(args, label, ukd, expected_kind, failures) + _check_provenance(args, label, ukd, expected["kind"], failures) if archive is None: - return True + return toc_key = source.get("toc_key") blob = archive.get_kernel(toc_key, args.arch) if toc_key else None if blob is None: failures.add( "kpack-toc", f"{label} toc_key {toc_key!r} has no {args.arch} archive entry" ) - return True + return digest = hashlib.sha256(blob).hexdigest() if digest != source.get("sha256"): failures.add( @@ -231,6 +255,66 @@ def _check_ukd(args, kdp_name, ukd, archive, failures): symbol = source.get("symbol") if not isinstance(symbol, str) or not symbol or symbol.encode("utf-8") not in blob: failures.add("symbol", f"{label} symbol {symbol!r} not found in archive blob") + + +def _check_passthrough_ukd(args, label, ukd, expected, archive, failures): + source = ukd.get("kernel_source") or {} + kind = source.get("kind") + if kind != expected["kind"]: + failures.add( + "ukd-kind", + f"{label} kernel_source.kind is {kind!r}; expected kind " + f'{expected["kind"]!r} has pass-through output, which ships as authored', + ) + return + + if ukd.get("arch") != [args.arch]: + failures.add("arch-field", f"{label} arch is {ukd.get('arch')!r}") + + if source != expected["kernel_source"]: + failures.add( + "passthrough-source", + f"{label} kernel_source is {source!r}, authored " + f"{expected['kernel_source']!r}", + ) + + _check_provenance(args, label, ukd, expected["kind"], failures) + + prov = ukd.get("provenance") or {} + label_value = prov.get("source_label") + if not isinstance(label_value, str) or not label_value: + failures.add( + "passthrough-provenance", f"{label} source_label is {label_value!r}" + ) + if prov.get("source_file") != source.get("source_file"): + failures.add( + "passthrough-provenance", + f"{label} provenance.source_file {prov.get('source_file')!r} != " + f"kernel_source.source_file {source.get('source_file')!r}", + ) + + +# Per-UKD check set for each output type named by hkp_probe_kinds.Kind.output. +_OUTPUT_CHECKS = {"kpack": _check_kpack_ukd, "passthrough": _check_passthrough_ukd} + + +def _check_ukd(args, kdp_name, ukd, archive, failures): + """Check one shipped UKD against its expectation. True when it was expected.""" + label = f"{kdp_name}:{ukd.get('name', ukd.get('id', '?'))}" + expected = args.expected.get((kdp_name, ukd.get("name"))) + if expected is None: + failures.add( + "ukd-count", f"{label} is not in the --expect list ({args.expect})" + ) + return False + output = _output_of(expected["kind"]) + if output is None: + failures.add( + "no-rules-for-kind", + f"{label} expected kind {expected['kind']!r} has no rules", + ) + return True + _OUTPUT_CHECKS[output](args, label, ukd, expected, archive, failures) return True @@ -239,7 +323,7 @@ def _check_descriptors(args, arch_dir, archive, failures): kdps = sorted(arch_dir.rglob("*.kdp.json")) if not kdps: failures.add("no-kdp", f"no *.kdp.json under {arch_dir}") - kpack_ukds = 0 + matched = 0 for kdp_path in kdps: name = kdp_path.relative_to(arch_dir).as_posix() try: @@ -254,16 +338,21 @@ def _check_descriptors(args, arch_dir, archive, failures): failures.add("arch-field", f"{name} arch is {kdp['arch']!r}") for ukd in kdp.get("kernelDescriptors", []): if _check_ukd(args, name, ukd, archive, failures): - kpack_ukds += 1 + matched += 1 expected = len(args.expected) - if kpack_ukds != expected or kpack_ukds < 1: + if matched != expected or matched < 1: failures.add( "ukd-count", - f"{kpack_ukds} kpack UKDs under {arch_dir}, expected {expected} " + f"{matched} expected UKDs shipped under {arch_dir}, expected {expected} " "(and at least 1)", ) +def _needs_archive(args): + """Whether any expected UKD has kpack output, so the archive must exist.""" + return any(_output_of(e["kind"]) == "kpack" for e in args.expected.values()) + + def run(args): failures = _Failures() if not args.out_root.is_dir(): @@ -273,16 +362,18 @@ def run(args): arch_dir = _check_layout(args, failures) if arch_dir is None: return failures - kpack_path = _check_kpack(args, arch_dir, failures) archive = None - if kpack_path is not None: - kpack = _load_kpack(args.kpack_python_dir) - try: - archive = kpack.PackedKernelArchive.read(kpack_path) - except Exception as exc: - failures.add( - "kpack-toc", f"{kpack_path} is unreadable: {type(exc).__name__}: {exc}" - ) + if _needs_archive(args): + kpack_path = _check_kpack(args, arch_dir, failures) + if kpack_path is not None: + kpack = _load_kpack(args.kpack_python_dir) + try: + archive = kpack.PackedKernelArchive.read(kpack_path) + except Exception as exc: + failures.add( + "kpack-toc", + f"{kpack_path} is unreadable: {type(exc).__name__}: {exc}", + ) _check_descriptors(args, arch_dir, archive, failures) return failures diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py index 6c7ad82c77f0..fba46451685e 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py @@ -14,7 +14,8 @@ `expect.json` is written beside `--out` (`/expect.json`): a list of {"kdp": , "name": , "kind": }, one entry per kept UKD, for hkp_probe_assert.py. +kernel_source.kind>}, one entry per kept UKD, for hkp_probe_assert.py. Entries of kinds +whose packed output is a pass-through also carry the authored "kernel_source". Files are written only when their content differs and files absent from the source are removed, so deriving twice leaves every mtime of the derived root @@ -68,11 +69,10 @@ def _group_key(ukd, where): if kind not in KINDS: raise DeriveError( f"{where} kind {kind!r} has no probe support. A kind the packer packs to " - "kpack needs one entry in KINDS (hkp_probe_kinds.py). A pass-through kind " - "(embedded_source: shipped as authored, not as kpack) or any kind the " - "packer does not pack to kpack cannot be probed by adding an entry: keep " - "such UKDs in a KDP that does not ship for the probed arch, or decide how " - "the probe should cover them" + "kpack or ships as authored (pass-through) needs one entry in KINDS " + "(hkp_probe_kinds.py). A kind the packer does not support yet needs " + "packer work first; until then keep such UKDs in a KDP that does not " + "ship for the probed arch" ) return (kind,) + tuple( json.dumps(source.get(f), sort_keys=True) for f in KINDS[kind].group_by @@ -143,10 +143,12 @@ def _plan(src, arch): continue kdp["kernelDescriptors"] = kept files[rel] = (json.dumps(kdp, indent=2) + "\n").encode("utf-8") - expect.extend( - {"kdp": rel, "name": u["name"], "kind": u["kernel_source"]["kind"]} - for u in kept - ) + for u in kept: + entry = {"kdp": rel, "name": u["name"], "kind": u["kernel_source"]["kind"]} + if KINDS[entry["kind"]].output == "passthrough": + # The packer ships the block as authored; the assertion compares it. + entry["kernel_source"] = u["kernel_source"] + expect.append(entry) return files, expect diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py index 2f702935eb3a..08c5106af8a2 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py @@ -1,16 +1,22 @@ """The producer kinds a packaging probe supports: one entry per kernel_source.kind. -Shared by hkp_probe_derive_root.py (compile-group selection) and -hkp_probe_assert.py (provenance rules). Only kinds the packer packs to kpack output -belong here. A kind with no entry is rejected by derive at configure time and fails -`no-rules-for-kind` in the assertion, so a new producer cannot pass a probe without -being described here. A new kind that packs to kpack is one entry in KINDS. - -Pass-through kinds (embedded_source: the packer ships it as authored, not as kpack) and -kinds the packer does not pack to kpack cannot be probed by adding an entry: the -assertion expects a kpack archive entry per UKD. A UKD of such a kind in a KDP that -ships for a probed arch makes configure fail; it must live in a KDP that does not ship -for a probed arch, or the probe needs a design decision. +Shared by hkp_probe_derive_root.py (compile-group selection, expect.json) and +hkp_probe_assert.py (per-UKD checks). A kind with no entry is rejected by derive at +configure time and fails `no-rules-for-kind` in the assertion, so a new producer cannot +pass a probe without being described here. + +Each entry also names the packed OUTPUT TYPE of the kind: + kpack the packer compiles/packs the UKD into the kpack archive and ships a + `kind: kpack` UKD (hip, rocke, hsaco) + passthrough the packer ships the UKD as authored; nothing enters the archive + (embedded_source) +A new kind whose output is one of these, with provenance checks that already exist by +name, is one entry in KINDS plus one end-to-end test case. A kind needing a new +provenance check adds it to hkp_probe_assert._PROVENANCE_CHECKS; a new output type adds +its check set to hkp_probe_assert._OUTPUT_CHECKS; a kind the packer does not support +needs packer work first. A UKD of an unregistered kind in a KDP that ships for a probed +arch makes configure fail; it must live in a KDP that does not ship for a probed arch, +or the probe needs a decision. """ from __future__ import annotations @@ -27,10 +33,13 @@ class Kind: # Names of provenance checks (hkp_probe_assert._PROVENANCE_CHECKS) a shipped UKD of # this kind must satisfy, beyond provenance.origin_kind == the kind itself. provenance: tuple[str, ...] + # Packed output type: a key of hkp_probe_assert._OUTPUT_CHECKS. + output: str = "kpack" KINDS = { "rocke": Kind(group_by=("builder",), provenance=("wheel", "comgr")), "hip": Kind(group_by=("source", "build"), provenance=()), "hsaco": Kind(group_by=(), provenance=()), + "embedded_source": Kind(group_by=(), provenance=(), output="passthrough"), } From 1e4093988a1173ff5984dcce48b8be2504d02685 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 5 Oct 2026 21:18:53 +0000 Subject: [PATCH 15/52] hkp probes: docs list embedded_source in derive; state hkp-probe-tools cost --- .../hip-kernel-provider/descriptor-packaging/README.md | 2 +- .../descriptor-packaging/tools/hkp_probe_derive_root.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 3bf011d1f0df..275641cb213b 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -337,7 +337,7 @@ error or skip. Configuring without the option and running the same build and cte fails (unknown target, then "No tests were found"). Locally on Linux the build is quick (the wheel environment plus a one-instance-per-group -pack) and the ctest entries a few seconds. There is no local Windows reproduction. +pack) and the per-arch ctest entries a few seconds; `hkp-probe-tools` (the probe tooling's own pytest suite, which really compiles hip and rocke fixtures) takes longer. There is no local Windows reproduction. ### `hkp_add_packaging_probe` arguments diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py index fba46451685e..c28029b509a4 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py @@ -6,7 +6,7 @@ among the UKDs that themselves ship for `--arch`: one entry per kind in hkp_probe_kinds.py: rocke (kind, builder), hip (kind, source, - build), hsaco (kind: compiles nothing, so one UKD per KDP) + build), hsaco and embedded_source (kind: compile nothing, so one UKD per KDP) The pick within a group is the first by sorted UKD `name`. Packing the derived root therefore exercises every compile path once instead of every variant. From d951d4f33d6b5e74b9d08ccd994f0b6029e5fe4b Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 5 Oct 2026 21:51:32 +0000 Subject: [PATCH 16/52] feat(hipdnn): generic GPU target table and tier algebra Add the shared gpu_generic_targets.json (LLVM provenance block), its configure-time validator and generated constexpr table, the SDK tier helpers (archTier, archesCompete, expanded archOverlaps/archCovers) and the Python mirror in hkp_pack.generic_targets, with golden vectors. --- .../python/hkp_pack/generic_targets.py | 200 ++++++++++++++++ .../tests/test_hkp_generic_targets.py | 148 ++++++++++++ projects/hipdnn/plugin_sdk/CMakeLists.txt | 63 +++++ .../cmake/GpuGenericTargetsTable.hpp.in | 28 +++ .../cmake/ValidateGpuGenericTargets.cmake | 143 ++++++++++++ .../cmake/hipdnn_plugin_sdkConfig.cmake.in | 4 + .../plugin_sdk/data/gpu_generic_targets.json | 21 ++ .../hipdnn_plugin_sdk/GpuGenericTargets.hpp | 94 ++++++++ .../ingestor/DeviceProperties.hpp | 148 ++++++++++-- .../hipdnn/plugin_sdk/tests/CMakeLists.txt | 64 ++++++ .../tests/cmake/CheckGpuGenericTargets.cmake | 17 ++ .../tests/data/arch_tier_vectors.json | 75 ++++++ .../bad_generic_name.json | 33 +++ .../generic_targets_invalid/bad_member.json | 33 +++ .../bad_retrieved_on.json | 33 +++ .../duplicate_member.json | 33 +++ .../empty_generics.json | 18 ++ .../empty_members.json | 30 +++ .../missing_comment.json | 32 +++ .../missing_license.json | 32 +++ .../missing_permalink.json | 32 +++ .../missing_source.json | 20 ++ .../missing_statement.json | 32 +++ .../permalink_without_revision.json | 33 +++ .../short_revision.json | 33 +++ .../tests/ingestor/TestArchGate.cpp | 22 ++ .../tests/ingestor/TestGpuGenericTargets.cpp | 217 ++++++++++++++++++ 27 files changed, 1618 insertions(+), 20 deletions(-) create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py create mode 100644 projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in create mode 100644 projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake create mode 100644 projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json create mode 100644 projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp create mode 100644 projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake create mode 100644 projects/hipdnn/plugin_sdk/tests/data/arch_tier_vectors.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_generic_name.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_member.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_retrieved_on.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/duplicate_member.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_generics.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_members.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_comment.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_license.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_permalink.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_source.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_statement.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/permalink_without_revision.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/short_revision.json create mode 100644 projects/hipdnn/plugin_sdk/tests/ingestor/TestGpuGenericTargets.cpp diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py new file mode 100644 index 000000000000..c349638bb7e0 --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py @@ -0,0 +1,200 @@ +"""LLVM generic GPU targets as data: the shared table and the tier algebra over it. + +One JSON document (projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json) maps each +generic target (`gfx11-generic`) to the concrete processors it supports. The C++ loader +reads the same document through a generated header; this module is the Python side, and +the two are held equal by the golden vectors in plugin_sdk/tests/data/arch_tier_vectors.json. + +Membership is data, never name shape: a generic-shaped name that the table does not list +(an "unknown generic") expands to the empty set and matches no device. Provenance of the +table is validated once, by the CMake validator, not here. + +An `arch` list ranks per device: EXPLICIT (the device's own base id) beats GENERIC (a table +generic containing the device) beats UNRESTRICTED (an empty list, any device). Lists are +sequences of strings. +""" + +import json +from pathlib import Path + +from .errors import HkpPackError + +TIER_EXPLICIT = 0 +TIER_GENERIC = 1 +TIER_UNRESTRICTED = 2 + +# .../dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/ +DEFAULT_TABLE_PATH = ( + Path(__file__).resolve().parents[5] + / "projects" + / "hipdnn" + / "plugin_sdk" + / "data" + / "gpu_generic_targets.json" +) + +_GENERIC_SUFFIX = "-generic" + + +class GenericTargets: + """The generic target table: generic name -> member processor names, document order.""" + + def __init__(self, path, generics): + self._path = Path(path) + self._generics = generics + + @classmethod + def load(cls, path): + path = Path(path) + try: + text = path.read_text(encoding="utf-8") + except OSError as exc: + raise HkpPackError(f"{path}: cannot read generic target table: {exc}") + try: + doc = json.loads(text) + except ValueError as exc: + raise HkpPackError(f"{path}: generic target table is not valid JSON: {exc}") + if not isinstance(doc, dict): + raise HkpPackError(f"{path}: generic target table must be a JSON object") + version = doc.get("schemaVersion") + if type(version) is not int or version != 1: + raise HkpPackError(f"{path}: 'schemaVersion' must be 1, got {version!r}") + raw = doc.get("generics") + if not isinstance(raw, dict): + raise HkpPackError(f"{path}: 'generics' must be an object") + generics = {} + for name, members in raw.items(): + if not isinstance(members, list) or not members: + raise HkpPackError( + f"{path}: generics.{name} must be a non-empty array of names" + ) + for member in members: + if not isinstance(member, str) or not member: + raise HkpPackError( + f"{path}: generics.{name} holds a member that is not a " + f"non-empty string: {member!r}" + ) + if len(set(members)) != len(members): + raise HkpPackError( + f"{path}: generics.{name} lists a member more than once" + ) + generics[name] = tuple(members) + return cls(path, generics) + + @property + def path(self): + return self._path + + def has(self, name): + return name in self._generics + + def members(self, name): + """Members of generic @name in document order; KeyError when it is not listed.""" + return self._generics[name] + + def names(self): + return tuple(self._generics) + + +def is_generic_shaped(name): + """Shape only (`...-generic`); use GenericTargets.has for membership.""" + return name.endswith(_GENERIC_SUFFIX) + + +def _strip_features(arch): + return arch.split(":", 1)[0] + + +def _prefix_match(device, entry): + """The packer-side twin of archMatches PREFIX: @entry is @device's base id.""" + return device == entry or device.startswith(entry + ":") + + +def entry_tier(entry, device, table): + """Tier of one list entry for @device (features allowed), or None when it admits it. + + A generic-shaped entry is never EXPLICIT, so an unknown generic matches no device. + """ + if is_generic_shaped(entry): + if table.has(entry) and _strip_features(device) in table.members(entry): + return TIER_GENERIC + return None + if _prefix_match(device, entry): + return TIER_EXPLICIT + return None + + +def list_tier(entries, device, table): + """Best tier of @entries for @device; an empty list is UNRESTRICTED; None = no match.""" + if not entries: + return TIER_UNRESTRICTED + tiers = [ + t for t in (entry_tier(e, device, table) for e in entries) if t is not None + ] + return min(tiers) if tiers else None + + +def expand(entries, table): + """The set of device ids @entries admits; None (unrestricted) for an empty list. + + A generic stands for its members; an unknown generic contributes nothing. + """ + if not entries: + return None + devices = set() + for entry in entries: + if is_generic_shaped(entry): + if table.has(entry): + devices.update(table.members(entry)) + else: + devices.add(entry) + return frozenset(devices) + + +def overlaps(a, b, table): + """Can one device satisfy both lists? Empty overlaps everything.""" + expanded_a = expand(a, table) + expanded_b = expand(b, table) + if expanded_a is None or expanded_b is None: + return True + return not expanded_a.isdisjoint(expanded_b) + + +def covers(outer, inner, table): + """Is every device @inner admits also admitted by @outer? + + Empty @outer covers anything and empty @inner is covered by anything; an unknown + generic in @inner expands to nothing and is covered vacuously. + """ + expanded_outer = expand(outer, table) + expanded_inner = expand(inner, table) + if expanded_outer is None or expanded_inner is None: + return True + return expanded_inner <= expanded_outer + + +def compete(a, b, table): + """Do the lists tie on some candidate device (both match it at the same tier)? + + Candidates are every explicit id in either list and every member of every table + generic in either list. Two empty lists compete. + """ + if not a and not b: + return True + candidates = set() + for entry in (*a, *b): + if is_generic_shaped(entry): + if table.has(entry): + candidates.update(table.members(entry)) + else: + candidates.add(entry) + for device in candidates: + tier = list_tier(a, device, table) + if tier is not None and tier == list_tier(b, device, table): + return True + return False + + +def admits_target(entries, target, table): + """Does @entries admit @target: empty, listing it literally, or a generic containing it.""" + return list_tier(entries, target, table) is not None diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py new file mode 100644 index 000000000000..3e507b112160 --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py @@ -0,0 +1,148 @@ +"""The Python tier algebra over the shared generic target table. + +The golden vectors in plugin_sdk/tests/data/arch_tier_vectors.json also drive the C++ +oracle, so a disagreement between the two implementations fails one side. This test +locates the table and the vectors itself rather than through conftest.py. +""" + +import json +from pathlib import Path + +import pytest + +from hkp_pack import generic_targets +from hkp_pack.errors import HkpPackError +from hkp_pack.generic_targets import GenericTargets + +pytestmark = pytest.mark.quick + +_SDK_DIR = Path(__file__).resolve().parents[4] / "projects" / "hipdnn" / "plugin_sdk" +_TABLE = _SDK_DIR / "data" / "gpu_generic_targets.json" +_VECTORS = _SDK_DIR / "tests" / "data" / "arch_tier_vectors.json" + +_TIER_NAMES = { + generic_targets.TIER_EXPLICIT: "explicit", + generic_targets.TIER_GENERIC: "generic", + generic_targets.TIER_UNRESTRICTED: "unrestricted", + None: "none", +} + + +@pytest.fixture(scope="module") +def table(): + return GenericTargets.load(_TABLE) + + +def test_default_table_path_is_the_in_repo_table(): + assert generic_targets.DEFAULT_TABLE_PATH == _TABLE + assert _TABLE.is_file() + + +def test_loads_the_table_members_in_document_order(table): + assert table.path == _TABLE + assert sorted(table.names()) == ["gfx11-generic", "gfx12-generic"] + assert table.members("gfx11-generic") == ( + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153", + ) + assert table.members("gfx12-generic") == ("gfx1200", "gfx1201") + assert table.has("gfx11-generic") + + +@pytest.mark.parametrize( + "doc, message", + [ + ({"schemaVersion": 2, "generics": {}}, "schemaVersion"), + ({"schemaVersion": True, "generics": {}}, "schemaVersion"), + ({"generics": {}}, "schemaVersion"), + ({"schemaVersion": 1}, "'generics' must be an object"), + ({"schemaVersion": 1, "generics": []}, "'generics' must be an object"), + ({"schemaVersion": 1, "generics": {"gfx11-generic": []}}, "non-empty array"), + ( + {"schemaVersion": 1, "generics": {"gfx11-generic": "gfx1100"}}, + "non-empty array", + ), + ( + {"schemaVersion": 1, "generics": {"gfx11-generic": [1100]}}, + "non-empty string", + ), + ( + {"schemaVersion": 1, "generics": {"gfx11-generic": ["gfx1100", "gfx1100"]}}, + "more than once", + ), + ([], "must be a JSON object"), + ], +) +def test_rejects_a_malformed_table(tmp_path, doc, message): + path = tmp_path / "table.json" + path.write_text(json.dumps(doc)) + with pytest.raises(HkpPackError, match=message): + GenericTargets.load(path) + + +def test_rejects_an_unreadable_or_non_json_table(tmp_path): + with pytest.raises(HkpPackError, match="cannot read"): + GenericTargets.load(tmp_path / "absent.json") + path = tmp_path / "table.json" + path.write_text("{not json") + with pytest.raises(HkpPackError, match="not valid JSON"): + GenericTargets.load(path) + + +def _vectors(): + return json.loads(_VECTORS.read_text()) + + +def test_matches_the_golden_tier_vectors(table): + vectors = _vectors() + failures = [] + for case in vectors["tier"]: + got = _TIER_NAMES[ + generic_targets.list_tier(case["arch"], case["device"], table) + ] + if got != case["expect"]: + failures.append(("tier", case, got)) + for case in vectors["overlap"]: + got = generic_targets.overlaps(case["a"], case["b"], table) + if got != case["expect"]: + failures.append(("overlap", case, got)) + for case in vectors["covers"]: + got = generic_targets.covers(case["outer"], case["inner"], table) + if got != case["expect"]: + failures.append(("covers", case, got)) + for case in vectors["compete"]: + got = generic_targets.compete(case["a"], case["b"], table) + if got != case["expect"]: + failures.append(("compete", case, got)) + assert not failures + assert all(vectors[k] for k in ("tier", "overlap", "covers", "compete")) + + +def test_unknown_generic_is_not_a_member(table): + assert not table.has("gfx9-4-generic") + assert generic_targets.is_generic_shaped("gfx9-4-generic") + assert generic_targets.entry_tier("gfx9-4-generic", "gfx942", table) is None + assert generic_targets.entry_tier("gfx9-4-generic", "gfx9-4-generic", table) is None + assert generic_targets.expand(["gfx9-4-generic"], table) == frozenset() + assert not generic_targets.admits_target(["gfx9-4-generic"], "gfx942", table) + + +def test_expand_of_an_empty_list_is_unrestricted(table): + assert generic_targets.expand([], table) is None + assert generic_targets.expand(["gfx11-generic", "gfx942"], table) == frozenset( + [*table.members("gfx11-generic"), "gfx942"] + ) + + +def test_admits_target_is_empty_literal_or_containing_generic(table): + assert generic_targets.admits_target([], "gfx1151", table) + assert generic_targets.admits_target(["gfx1151"], "gfx1151", table) + assert generic_targets.admits_target(["gfx11-generic"], "gfx1151", table) + assert not generic_targets.admits_target(["gfx11-generic"], "gfx1154", table) + assert not generic_targets.admits_target(["gfx942"], "gfx1151", table) diff --git a/projects/hipdnn/plugin_sdk/CMakeLists.txt b/projects/hipdnn/plugin_sdk/CMakeLists.txt index 40136efea51a..28c4e251de15 100644 --- a/projects/hipdnn/plugin_sdk/CMakeLists.txt +++ b/projects/hipdnn/plugin_sdk/CMakeLists.txt @@ -1,5 +1,6 @@ # Copyright © Advanced Micro Devices, Inc., or its affiliates. # SPDX-License-Identifier: MIT +# cmake-lint: disable=E1120 include(CMakePackageConfigHelpers) @@ -21,6 +22,55 @@ hipdnn_generate_version_header(hipdnn_plugin_sdk) target_link_libraries(hipdnn_plugin_sdk INTERFACE hipdnn_flatbuffers_sdk hipdnn_data_sdk) +# Generic GPU target table: one JSON document is the source of truth. It is validated here, +# turned into a constexpr C++ table at configure time, installed beside the Config for +# the packer, and read directly by Python tools. Never keep a second copy. +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/ValidateGpuGenericTargets.cmake") +set(HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/data/gpu_generic_targets.json") +hipdnn_validate_gpu_generic_targets("${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}") +set_property( + DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}" +) + +file(READ "${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}" _generic_targets_json) +string(JSON _generic_count LENGTH "${_generic_targets_json}" generics) +set(HIPDNN_GENERIC_TARGET_MEMBER_ARRAYS "") +set(HIPDNN_GENERIC_TARGET_ROWS "") +set(HIPDNN_GENERIC_TARGET_ROW_COUNT ${_generic_count}) +math(EXPR _last_generic "${_generic_count} - 1") +foreach(_g RANGE 0 ${_last_generic}) + string(JSON _generic MEMBER "${_generic_targets_json}" generics ${_g}) + string(TOUPPER "${_generic}" _array_name) + string(REPLACE "-" "_" _array_name "${_array_name}") + string(JSON _member_count LENGTH "${_generic_targets_json}" generics "${_generic}") + math(EXPR _last_member "${_member_count} - 1") + set(_member_literals "") + foreach(_m RANGE 0 ${_last_member}) + string(JSON _member GET "${_generic_targets_json}" generics "${_generic}" ${_m}) + string(APPEND _member_literals " \"${_member}\",\n") + endforeach() + string(APPEND HIPDNN_GENERIC_TARGET_MEMBER_ARRAYS + "inline constexpr std::string_view ${_array_name}_MEMBERS[] = {\n${_member_literals}};\n\n") + string(APPEND HIPDNN_GENERIC_TARGET_ROWS + " {\"${_generic}\", ${_array_name}_MEMBERS, ${_member_count}},\n") +endforeach() +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/GpuGenericTargetsTable.hpp.in" + "${CMAKE_CURRENT_BINARY_DIR}/include/hipdnn_plugin_sdk/GpuGenericTargetsTable.hpp" @ONLY +) + +# Build-tree copy next to the build-tree Config; the installed copy sits beside the +# installed Config, so one CMAKE_CURRENT_LIST_DIR expression serves both. +set(_generic_targets_build_copy + "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/cmake/hipdnn_plugin_sdk/gpu_generic_targets.json" +) +configure_file("${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}" "${_generic_targets_build_copy}" COPYONLY) +# CACHE INTERNAL so sibling subdirectories (the hip-kernel-provider) see it; directory +# scope would not reach them. +set(HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON "${_generic_targets_build_copy}" + CACHE INTERNAL "Path to the generic GPU target table JSON shipped with the plugin SDK" +) + # The ingestor headers are wholly inside `#ifdef HIPDNN_ENABLE_KERNEL_INGESTOR`, and they # are installed whenever it is set. Carrying the definition on the exported target is what # makes them usable: without it a consumer that find_package()s this SDK gets the headers @@ -91,6 +141,19 @@ install( COMPONENT Development ) +# Install the generic target table and its generated C++ form +install( + FILES "${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hipdnn_plugin_sdk + COMPONENT Development +) + +install( + FILES "${CMAKE_CURRENT_BINARY_DIR}/include/hipdnn_plugin_sdk/GpuGenericTargetsTable.hpp" + DESTINATION ${HIP_DNN_PLUGIN_SDK_INSTALL_INCLUDE_DIR}/hipdnn_plugin_sdk + COMPONENT Development +) + install(EXPORT hipdnn_plugin_sdk_targets DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hipdnn_plugin_sdk FILE hipdnn_plugin_sdkTargets.cmake COMPONENT Development ) diff --git a/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in b/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in new file mode 100644 index 000000000000..f97ccf76d758 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in @@ -0,0 +1,28 @@ +// Copyright © Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT + +// Generated by CMake from data/gpu_generic_targets.json. Do not edit. + +#pragma once + +#include +#include + +namespace hipdnn_plugin_sdk::generated +{ + +/// One LLVM generic processor and the processors it supports, in document order. +struct GenericTargetRow +{ + std::string_view name; + const std::string_view* members; + std::size_t memberCount; +}; + +@HIPDNN_GENERIC_TARGET_MEMBER_ARRAYS@ +inline constexpr GenericTargetRow GENERIC_TARGET_ROWS[] = { +@HIPDNN_GENERIC_TARGET_ROWS@}; + +inline constexpr std::size_t GENERIC_TARGET_ROW_COUNT = @HIPDNN_GENERIC_TARGET_ROW_COUNT@; + +} // namespace hipdnn_plugin_sdk::generated diff --git a/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake b/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake new file mode 100644 index 000000000000..3360c976f99f --- /dev/null +++ b/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake @@ -0,0 +1,143 @@ +# Copyright © Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT + +# Validates data/gpu_generic_targets.json: the LLVM provenance block and the shape of the +# generic-target -> member-processor table. Every violation is a FATAL_ERROR that names +# the offending key or member, so a failing configure points at the line to fix. +# +# This is the only implementation of the provenance rules; the Python readers validate +# shape only. +# +# cmake-lint does not model string(JSON ...) and misreads its index arguments. +# cmake-lint: disable=E1120 + +# Reads the string at of into ; FATAL_ERROR if it is absent, +# not a string, or empty. +function(_hipdnn_gpu_generic_targets_get_string out json file key_path) + string(JSON value ERROR_VARIABLE err GET "${json}" ${ARGN}) + string(JSON type ERROR_VARIABLE type_err TYPE "${json}" ${ARGN}) + if(err OR type_err) + message(FATAL_ERROR "gpu_generic_targets: ${file}: missing required key '${key_path}'") + endif() + if(NOT type STREQUAL "STRING") + message(FATAL_ERROR "gpu_generic_targets: ${file}: key '${key_path}' must be a string") + endif() + if(value STREQUAL "") + message(FATAL_ERROR "gpu_generic_targets: ${file}: key '${key_path}' must be non-empty") + endif() + set(${out} "${value}" PARENT_SCOPE) +endfunction() + +# Checks the provenance block ('source') of the table. +function(_hipdnn_gpu_generic_targets_validate_source json json_path) + string(JSON source_type ERROR_VARIABLE err TYPE "${json}" source) + if(err OR NOT source_type STREQUAL "OBJECT") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: missing required object 'source'") + endif() + + foreach(key description document section repository revision permalink retrievedOn license + licenseNote statement pinnedBy) + _hipdnn_gpu_generic_targets_get_string(source_${key} "${json}" "${json_path}" "source.${key}" + source ${key}) + endforeach() + + if(NOT source_document MATCHES "AMDGPUUsage\\.rst$") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.document' must name AMDGPUUsage.rst") + endif() + if(NOT source_section MATCHES "^AMDGPU Generic Processors") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.section' must name the 'AMDGPU Generic Processors' table") + endif() + if(NOT source_repository STREQUAL "https://github.com/ROCm/llvm-project") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.repository' must be https://github.com/ROCm/llvm-project") + endif() + string(LENGTH "${source_revision}" revision_length) + if(NOT source_revision MATCHES "^[0-9a-f]+$" OR NOT revision_length EQUAL 40) + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.revision' must be a 40-digit lowercase hex commit id, got '${source_revision}'") + endif() + if(NOT source_permalink MATCHES "^https://") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.permalink' must be an https:// URL") + endif() + string(FIND "${source_permalink}" "${source_revision}" revision_at) + if(revision_at EQUAL -1) + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.permalink' must contain 'source.revision' (${source_revision}) so the link is pinned") + endif() + if(NOT source_retrievedOn MATCHES "^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.retrievedOn' must be YYYY-MM-DD, got '${source_retrievedOn}'") + endif() + if(NOT source_license STREQUAL "Apache-2.0 WITH LLVM-exception") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.license' must be 'Apache-2.0 WITH LLVM-exception', got '${source_license}'") + endif() + if(NOT source_statement MATCHES "[Oo]nly factual processor-name membership") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.statement' must state that only factual processor-name membership is recorded") + endif() + +endfunction() + +# Checks the 'generics' map: names, member shape, duplicates. +function(_hipdnn_gpu_generic_targets_validate_generics json json_path) + string(JSON generics_type ERROR_VARIABLE err TYPE "${json}" generics) + if(err OR NOT generics_type STREQUAL "OBJECT") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: missing required object 'generics'") + endif() + string(JSON generic_count LENGTH "${json}" generics) + if(generic_count EQUAL 0) + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'generics' must be non-empty") + endif() + + math(EXPR last_generic "${generic_count} - 1") + foreach(g RANGE 0 ${last_generic}) + string(JSON generic MEMBER "${json}" generics ${g}) + if(NOT generic MATCHES "^gfx[0-9]+(-[0-9]+)?-generic$") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: generic name '${generic}' does not match ^gfx[0-9]+(-[0-9]+)?-generic$") + endif() + string(JSON members_type TYPE "${json}" generics "${generic}") + if(NOT members_type STREQUAL "ARRAY") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: generics.${generic} must be an array") + endif() + string(JSON member_count LENGTH "${json}" generics "${generic}") + if(member_count EQUAL 0) + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: generics.${generic} must list at least one member") + endif() + + set(seen "") + math(EXPR last_member "${member_count} - 1") + foreach(m RANGE 0 ${last_member}) + string(JSON member_type TYPE "${json}" generics "${generic}" ${m}) + if(NOT member_type STREQUAL "STRING") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: generics.${generic}[${m}] must be a string") + endif() + string(JSON member GET "${json}" generics "${generic}" ${m}) + if(NOT member MATCHES "^gfx[0-9a-f]+$") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: member '${member}' of ${generic} does not match ^gfx[0-9a-f]+$") + endif() + list(FIND seen "${member}" seen_at) + if(NOT seen_at EQUAL -1) + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: member '${member}' is listed twice in ${generic}") + endif() + list(APPEND seen "${member}") + endforeach() + endforeach() +endfunction() + +# Validates the generic target table at : the comment and schema version, the +# provenance block, and every generic's member list. FATAL_ERROR naming the offending +# key or member on the first violation. +function(hipdnn_validate_gpu_generic_targets json_path) + if(NOT EXISTS "${json_path}") + message(FATAL_ERROR "gpu_generic_targets: cannot read '${json_path}'") + endif() + file(READ "${json_path}" json) + + _hipdnn_gpu_generic_targets_get_string(_ "${json}" "${json_path}" "\$comment" "\$comment") + + string(JSON version ERROR_VARIABLE err GET "${json}" schemaVersion) + if(err) + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: missing required key 'schemaVersion'") + endif() + if(NOT version STREQUAL "1") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'schemaVersion' must be 1, got '${version}'") + endif() + + _hipdnn_gpu_generic_targets_validate_source("${json}" "${json_path}") + _hipdnn_gpu_generic_targets_validate_generics("${json}" "${json_path}") +endfunction() diff --git a/projects/hipdnn/plugin_sdk/cmake/hipdnn_plugin_sdkConfig.cmake.in b/projects/hipdnn/plugin_sdk/cmake/hipdnn_plugin_sdkConfig.cmake.in index 30bcd5e07d56..f9608e203f35 100644 --- a/projects/hipdnn/plugin_sdk/cmake/hipdnn_plugin_sdkConfig.cmake.in +++ b/projects/hipdnn/plugin_sdk/cmake/hipdnn_plugin_sdkConfig.cmake.in @@ -15,6 +15,10 @@ if(HIPDNN_PLUGIN_SDK_ENABLE_KERNEL_INGESTOR) find_dependency(nlohmann_json REQUIRED) endif() +# The generic GPU target table sits beside this Config in both the build tree and the +# install tree. +set(HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON "${CMAKE_CURRENT_LIST_DIR}/gpu_generic_targets.json") + include("${CMAKE_CURRENT_LIST_DIR}/hipdnn_plugin_sdkTargets.cmake") check_required_components(hipdnn_plugin_sdk) diff --git a/projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json b/projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json new file mode 100644 index 000000000000..6964d10341ff --- /dev/null +++ b/projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json @@ -0,0 +1,21 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "2026-10-05", + "license": "Apache-2.0 WITH LLVM-exception", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" + }, + "generics": { + "gfx11-generic": ["gfx1100", "gfx1101", "gfx1102", "gfx1103", "gfx1150", "gfx1151", "gfx1152", "gfx1153"], + "gfx12-generic": ["gfx1200", "gfx1201"] + } +} diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp new file mode 100644 index 000000000000..4c127140b2e7 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp @@ -0,0 +1,94 @@ +// Copyright © Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT + +#pragma once + +/** + * @file GpuGenericTargets.hpp + * @brief LLVM generic GPU targets (`gfx11-generic`, ...) as data: which concrete + * processors each one supports, and how an `arch` entry ranks for a device. + * + * Membership comes from the table generated out of data/gpu_generic_targets.json; a + * name's shape never implies membership, so a generic-shaped name absent from the table + * matches no device. Nothing here allocates. + */ + +#include +#include +#include + +#include + +namespace hipdnn_plugin_sdk +{ + +/// Does @p name have the shape of a generic target (`...-generic`)? Shape only; use +/// findGenericTarget for membership in the table. +inline bool isGenericShapedArchName(std::string_view name) +{ + constexpr std::string_view SUFFIX = "-generic"; + return name.size() >= SUFFIX.size() + && name.compare(name.size() - SUFFIX.size(), std::string_view::npos, SUFFIX) == 0; +} + +/// The table row for the generic named exactly @p name, or nullptr when absent. +inline const generated::GenericTargetRow* findGenericTarget(std::string_view name) +{ + for(std::size_t i = 0; i < generated::GENERIC_TARGET_ROW_COUNT; ++i) + { + if(generated::GENERIC_TARGET_ROWS[i].name == name) + { + return &generated::GENERIC_TARGET_ROWS[i]; + } + } + return nullptr; +} + +/// Is @p baseDeviceId (features already stripped) a member of the table generic +/// @p generic? False when @p generic is not in the table. +inline bool genericTargetContains(std::string_view generic, std::string_view baseDeviceId) +{ + const auto* row = findGenericTarget(generic); + if(row == nullptr) + { + return false; + } + for(std::size_t i = 0; i < row->memberCount; ++i) + { + if(row->members[i] == baseDeviceId) + { + return true; + } + } + return false; +} + +/// How specifically an `arch` entry names a device; a lower value wins. +enum class ArchTier : int +{ + EXPLICIT = 0, ///< The entry is the device's own base id. + GENERIC = 1, ///< A table generic whose members contain the device. + UNRESTRICTED = 2, ///< An empty list: any device. +}; + +/// Rank of one list entry for @p baseDeviceId (features already stripped): EXPLICIT when +/// the entry equals it, GENERIC when a table generic contains it, else nullopt. A +/// generic-shaped entry is never EXPLICIT, so an unknown generic matches no device. +inline std::optional archEntryTier(std::string_view entry, std::string_view baseDeviceId) +{ + if(isGenericShapedArchName(entry)) + { + if(genericTargetContains(entry, baseDeviceId)) + { + return ArchTier::GENERIC; + } + return std::nullopt; + } + if(entry == baseDeviceId) + { + return ArchTier::EXPLICIT; + } + return std::nullopt; +} + +} // namespace hipdnn_plugin_sdk diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp index 26bb349bcd3a..aea6b3a4dd34 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp @@ -6,11 +6,14 @@ #ifdef HIPDNN_ENABLE_KERNEL_INGESTOR #include +#include +#include #include #include #include #include +#include namespace hipdnn_plugin_sdk::ingestor { @@ -27,39 +30,121 @@ struct DeviceProperties int multiProcessorCount = 0; ///< Compute units; 0 if unresolved. }; +/// How @p arch (a KDP's supported-target list) ranks for @p deviceArch: the best tier of +/// any entry, or nullopt when no entry admits the device. An empty list is UNRESTRICTED. +/// The device carries its features and entries are base ids, so an explicit entry is the +/// PREFIX match, not SUBSTRING or equality: `gfx942` is EXPLICIT for a device reporting +/// `gfx942:sramecc+:xnack-` and never matches `gfx950`. A generic entry is GENERIC when the +/// table lists the device's base id as a member. +inline std::optional archTier(const std::vector& arch, + std::string_view deviceArch) +{ + if(arch.empty()) + { + return ArchTier::UNRESTRICTED; + } + const auto baseDeviceId = stripArchFeatures(deviceArch); + std::optional best; + for(const auto& entry : arch) + { + std::optional tier; + if(!isGenericShapedArchName(entry) && archMatches(deviceArch, entry, ArchMatchMode::PREFIX)) + { + tier = ArchTier::EXPLICIT; + } + else + { + tier = archEntryTier(entry, baseDeviceId); + } + if(tier && (!best || static_cast(*tier) < static_cast(*best))) + { + best = tier; + } + } + return best; +} + /// Does @p arch (a KDP's supported-target list; empty admits everything) admit -/// @p deviceArch? Entries are base ids and the device carries its features, so this is -/// the PREFIX match, not SUBSTRING or equality: `gfx942` admits a device reporting -/// `gfx942:sramecc+:xnack-` and never admits `gfx950`. +/// @p deviceArch, by any tier? inline bool archSupports(const std::vector& arch, std::string_view deviceArch) { - return arch.empty() - || std::any_of(arch.begin(), arch.end(), [deviceArch](const std::string& candidate) { - return archMatches(deviceArch, candidate, ArchMatchMode::PREFIX); - }); + return archTier(arch, deviceArch).has_value(); } -/// Can one device satisfy both @p a and @p b? Empty means "every arch", so it overlaps -/// everything. Both sides are authored base ids, so an entry matches only its twin. +namespace detail +{ + +/// Does the single list entry @p entry admit the device id @p device? An explicit entry is +/// the PREFIX match; a generic admits its members; an unknown generic admits nothing. +inline bool entryAdmits(std::string_view entry, std::string_view device) +{ + if(isGenericShapedArchName(entry)) + { + return genericTargetContains(entry, stripArchFeatures(device)); + } + return archMatches(device, entry, ArchMatchMode::PREFIX); +} + +inline bool listAdmits(const std::vector& list, std::string_view device) +{ + return std::any_of(list.begin(), list.end(), [device](const std::string& entry) { + return entryAdmits(entry, device); + }); +} + +/// Calls @p visit with every device id in @p entry's expansion (a generic's members, an +/// explicit entry itself, nothing for an unknown generic) until it returns true. +template +inline bool anyExpandedMember(std::string_view entry, Visit&& visit) +{ + if(!isGenericShapedArchName(entry)) + { + return visit(entry); + } + const auto* row = findGenericTarget(entry); + if(row == nullptr) + { + return false; + } + for(std::size_t i = 0; i < row->memberCount; ++i) + { + if(visit(row->members[i])) + { + return true; + } + } + return false; +} + +/// Does some device in the expansion of @p from get admitted by @p into? +inline bool anyExpandedMemberAdmittedBy(const std::vector& from, + const std::vector& into) +{ + return std::any_of(from.begin(), from.end(), [&into](const std::string& entry) { + return anyExpandedMember( + entry, [&into](std::string_view device) { return listAdmits(into, device); }); + }); +} + +} // namespace detail + +/// Can one device satisfy both @p a and @p b? Compared over expanded member sets: a +/// generic stands for its table members. Empty means "every arch", so it overlaps +/// everything; an unknown generic expands to nothing and overlaps nothing. inline bool archOverlaps(const std::vector& a, const std::vector& b) { if(a.empty() || b.empty()) { return true; } - return std::any_of(a.begin(), a.end(), [&b](const std::string& lhs) { - return std::any_of(b.begin(), b.end(), [&lhs](const std::string& rhs) { - return archMatches(lhs, rhs, ArchMatchMode::PREFIX) - || archMatches(rhs, lhs, ArchMatchMode::PREFIX); - }); - }); + return detail::anyExpandedMemberAdmittedBy(a, b) || detail::anyExpandedMemberAdmittedBy(b, a); } /// Is every device @p inner admits also admitted by @p outer? The asymmetric counterpart /// to archOverlaps, for asking whether a kernel stays within the pack that binds it. -/// Empty @p outer admits every device, so it covers anything; empty @p inner declares no -/// restriction of its own and is covered by anything. Otherwise every @p inner entry -/// must appear in @p outer: `[gfx942]` is covered by `[gfx942, gfx950]`, not the reverse. +/// Compared over expanded member sets. Empty @p outer admits every device, so it covers +/// anything; empty @p inner declares no restriction of its own and is covered by anything; +/// an unknown generic in @p inner expands to nothing and is covered vacuously. inline bool archCovers(const std::vector& outer, const std::vector& inner) { if(outer.empty()) @@ -67,12 +152,35 @@ inline bool archCovers(const std::vector& outer, const std::vector< return true; } return std::all_of(inner.begin(), inner.end(), [&outer](const std::string& entry) { - return std::any_of(outer.begin(), outer.end(), [&entry](const std::string& candidate) { - return archMatches(entry, candidate, ArchMatchMode::PREFIX); + return !detail::anyExpandedMember(entry, [&outer](std::string_view device) { + return !detail::listAdmits(outer, device); }); }); } +/// Do @p a and @p b tie? True when some candidate device is matched by both at the same +/// tier, where the candidates are every explicit id in either list and every member of +/// every table generic in either list. Two empty lists compete (both unrestricted on +/// every device). Lists whose best tiers always differ per device do not: the better +/// tier shadows the other, so they may coexist. +inline bool archesCompete(const std::vector& a, const std::vector& b) +{ + if(a.empty() && b.empty()) + { + return true; + } + const auto tiesOn = [&a, &b](std::string_view device) { + const auto tierA = archTier(a, device); + return tierA && tierA == archTier(b, device); + }; + const auto anyTie = [&tiesOn](const std::vector& list) { + return std::any_of(list.begin(), list.end(), [&tiesOn](const std::string& entry) { + return detail::anyExpandedMember(entry, tiesOn); + }); + }; + return anyTie(a) || anyTie(b); +} + } // namespace hipdnn_plugin_sdk::ingestor #endif // HIPDNN_ENABLE_KERNEL_INGESTOR diff --git a/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt b/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt index 3d69804dd925..d076769b64b3 100644 --- a/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt +++ b/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt @@ -38,6 +38,7 @@ add_executable(hipdnn_plugin_sdk_tests $<$:ingestor/TestMatchContext.cpp> $<$:ingestor/TestDescriptors.cpp> $<$:ingestor/TestArchGate.cpp> + $<$:ingestor/TestGpuGenericTargets.cpp> $<$:ingestor/TestKernelDefinition.cpp> $<$:ingestor/TestKernelIngestorStateManager.cpp> $<$:ingestor/TestGenericPlan.cpp> @@ -50,6 +51,15 @@ add_executable(hipdnn_plugin_sdk_tests $<$:TestKernelIngestorConcurrency.cpp> ) +# The generic target table and the golden tier vectors are read from the source tree, so +# the C++ test compares the generated header against the very document it came from. +target_compile_definitions( + hipdnn_plugin_sdk_tests + PRIVATE + "HIPDNN_GPU_GENERIC_TARGETS_JSON_PATH=\"${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}\"" + "HIPDNN_ARCH_TIER_VECTORS_JSON_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}/data/arch_tier_vectors.json\"" +) + target_compile_options(hipdnn_plugin_sdk_tests PRIVATE ${HIPDNN_WARNING_COMPILE_OPTIONS} ${HIPDNN_NO_RTTI_OPTIONS}) target_link_libraries( @@ -79,6 +89,60 @@ endif() clang_tidy_check(hipdnn_plugin_sdk_tests) add_hipdnn_test(hipdnn_plugin_sdk_tests ${CMAKE_CURRENT_BINARY_DIR}) +# Table provenance (F-1): the validator must accept the real table, and reject each invalid +# fixture with the text naming that fixture's defect. Negatives match their FATAL_ERROR +# text with PASS_REGULAR_EXPRESSION, never WILL_FAIL: a validator that crashes, or cannot +# find its input, fails for an unrelated reason and would otherwise pass. CMake wraps long +# messages at spaces, so every space in the expected text matches any whitespace run. +set(_generic_targets_check "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CheckGpuGenericTargets.cmake") + +add_test( + NAME hipdnn_gpu_generic_targets_provenance + COMMAND ${CMAKE_COMMAND} -DGENERIC_TARGETS_JSON=${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE} -P + ${_generic_targets_check} +) +set_tests_properties( + hipdnn_gpu_generic_targets_provenance PROPERTIES PASS_REGULAR_EXPRESSION + "gpu_generic_targets: valid" +) +_apply_hipdnn_test_category_labels(hipdnn_gpu_generic_targets_provenance) + +# Registers ctest hipdnn_gpu_generic_targets_rejects_, which must fail with +# (spaces match any whitespace run) for data/generic_targets_invalid/.json. +function(_hipdnn_add_generic_targets_rejection_test case expected_text) + string(REPLACE " " "[ \n]+" expected_regex "${expected_text}") + set(test_name hipdnn_gpu_generic_targets_rejects_${case}) + add_test( + NAME ${test_name} + COMMAND + ${CMAKE_COMMAND} + -DGENERIC_TARGETS_JSON=${CMAKE_CURRENT_SOURCE_DIR}/data/generic_targets_invalid/${case}.json + -P ${_generic_targets_check} + ) + set_tests_properties(${test_name} PROPERTIES PASS_REGULAR_EXPRESSION "${expected_regex}") + _apply_hipdnn_test_category_labels(${test_name}) +endfunction() + +_hipdnn_add_generic_targets_rejection_test(missing_source "missing required object 'source'") +_hipdnn_add_generic_targets_rejection_test(missing_comment "missing required key '.comment'") +_hipdnn_add_generic_targets_rejection_test(missing_permalink "missing required key 'source.permalink'") +_hipdnn_add_generic_targets_rejection_test(short_revision "'source.revision' must be a 40-digit") +_hipdnn_add_generic_targets_rejection_test( + permalink_without_revision "'source.permalink' must contain 'source.revision'" +) +_hipdnn_add_generic_targets_rejection_test(bad_retrieved_on "'source.retrievedOn' must be YYYY-MM-DD") +_hipdnn_add_generic_targets_rejection_test(missing_license "missing required key 'source.license'") +_hipdnn_add_generic_targets_rejection_test(missing_statement "missing required key 'source.statement'") +_hipdnn_add_generic_targets_rejection_test(empty_generics "'generics' must be non-empty") +_hipdnn_add_generic_targets_rejection_test( + empty_members "generics.gfx12-generic must list at least one member" +) +_hipdnn_add_generic_targets_rejection_test( + duplicate_member "member 'gfx1200' is listed twice in gfx12-generic" +) +_hipdnn_add_generic_targets_rejection_test(bad_generic_name "generic name 'gfx12generic' does not match") +_hipdnn_add_generic_targets_rejection_test(bad_member "member 'gfx12zz' of gfx12-generic does not match") + # Cross-process coverage: one process writes a winner record, a SECOND process reads it # back. Two ctest invocations of the same binary, sequenced with # FIXTURES_SETUP/FIXTURES_REQUIRED, following the shape hipdnn/cmake/Tests.cmake's diff --git a/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake b/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake new file mode 100644 index 000000000000..dd12f8eba0a6 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake @@ -0,0 +1,17 @@ +# Copyright © Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT + +# Script-mode driver for hipdnn_validate_gpu_generic_targets (run with `cmake -P`): +# cmake -DGENERIC_TARGETS_JSON= -P CheckGpuGenericTargets.cmake +# Prints "gpu_generic_targets: valid" only after the table passed every rule, so a ctest +# can require that line for the real table and the validator's FATAL_ERROR text for each +# invalid fixture. A crash or a missing input matches neither. + +if(NOT GENERIC_TARGETS_JSON) + message(FATAL_ERROR "CheckGpuGenericTargets: pass -DGENERIC_TARGETS_JSON=") +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/../../cmake/ValidateGpuGenericTargets.cmake") + +hipdnn_validate_gpu_generic_targets("${GENERIC_TARGETS_JSON}") +message(STATUS "gpu_generic_targets: valid") diff --git a/projects/hipdnn/plugin_sdk/tests/data/arch_tier_vectors.json b/projects/hipdnn/plugin_sdk/tests/data/arch_tier_vectors.json new file mode 100644 index 000000000000..4a1ba2322ce0 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/arch_tier_vectors.json @@ -0,0 +1,75 @@ +{ + "tier": [ + {"arch": [], "device": "gfx1151", "expect": "unrestricted"}, + {"arch": [], "device": "gfx942:sramecc+:xnack-", "expect": "unrestricted"}, + {"arch": ["gfx1151", "gfx11-generic"], "device": "gfx1151", "expect": "explicit"}, + {"arch": ["gfx1151", "gfx11-generic"], "device": "gfx1100", "expect": "generic"}, + {"arch": ["gfx1151", "gfx11-generic"], "device": "gfx1151:sramecc+:xnack-", "expect": "explicit"}, + {"arch": ["gfx11-generic"], "device": "gfx1151:sramecc+", "expect": "generic"}, + {"arch": ["gfx11-generic"], "device": "gfx1154", "expect": "none"}, + {"arch": ["gfx11-generic"], "device": "gfx1170", "expect": "none"}, + {"arch": ["gfx12-generic"], "device": "gfx1100", "expect": "none"}, + {"arch": ["gfx12-generic"], "device": "gfx1201", "expect": "generic"}, + {"arch": ["gfx12-generic"], "device": "gfx1250", "expect": "none"}, + {"arch": ["gfx12-generic"], "device": "gfx1250:sramecc+", "expect": "none"}, + {"arch": ["gfx12-generic"], "device": "gfx1250-strict", "expect": "none"}, + {"arch": ["gfx1250"], "device": "gfx1250-strict", "expect": "none"}, + {"arch": ["gfx1250"], "device": "gfx1250", "expect": "explicit"}, + {"arch": ["gfx9-4-generic"], "device": "gfx942", "expect": "none"}, + {"arch": ["gfx9-4-generic"], "device": "gfx9-4-generic", "expect": "none"}, + {"arch": ["gfx9-4-generic", "gfx942"], "device": "gfx942", "expect": "explicit"}, + {"arch": ["gfx942"], "device": "gfx942:sramecc+:xnack-", "expect": "explicit"}, + {"arch": ["gfx942"], "device": "gfx950", "expect": "none"}, + {"arch": ["gfx94"], "device": "gfx942", "expect": "none"}, + {"arch": ["gfx942", "gfx11-generic"], "device": "gfx1100", "expect": "generic"}, + {"arch": ["gfx11-generic", "gfx12-generic"], "device": "gfx1200", "expect": "generic"} + ], + "overlap": [ + {"a": [], "b": ["gfx942"], "expect": true}, + {"a": ["gfx942"], "b": [], "expect": true}, + {"a": [], "b": [], "expect": true}, + {"a": ["gfx942"], "b": ["gfx950"], "expect": false}, + {"a": ["gfx942"], "b": ["gfx942"], "expect": true}, + {"a": ["gfx11-generic"], "b": ["gfx1151"], "expect": true}, + {"a": ["gfx1151"], "b": ["gfx11-generic"], "expect": true}, + {"a": ["gfx11-generic"], "b": ["gfx1154"], "expect": false}, + {"a": ["gfx11-generic"], "b": ["gfx12-generic"], "expect": false}, + {"a": ["gfx11-generic"], "b": ["gfx11-generic"], "expect": true}, + {"a": ["gfx9-4-generic"], "b": ["gfx9-4-generic"], "expect": false}, + {"a": ["gfx9-4-generic"], "b": [], "expect": true}, + {"a": ["gfx1151", "gfx942"], "b": ["gfx11-generic"], "expect": true}, + {"a": ["gfx1250"], "b": ["gfx12-generic"], "expect": false} + ], + "covers": [ + {"outer": [], "inner": ["gfx942"], "expect": true}, + {"outer": ["gfx942"], "inner": [], "expect": true}, + {"outer": ["gfx942", "gfx950"], "inner": ["gfx942"], "expect": true}, + {"outer": ["gfx942"], "inner": ["gfx942", "gfx950"], "expect": false}, + {"outer": ["gfx11-generic"], "inner": ["gfx1151"], "expect": true}, + {"outer": ["gfx11-generic"], "inner": ["gfx1154"], "expect": false}, + {"outer": ["gfx11-generic"], "inner": ["gfx1100", "gfx1153"], "expect": true}, + {"outer": ["gfx1151"], "inner": ["gfx11-generic"], "expect": false}, + {"outer": ["gfx11-generic"], "inner": ["gfx11-generic"], "expect": true}, + {"outer": ["gfx11-generic"], "inner": ["gfx12-generic"], "expect": false}, + {"outer": ["gfx1100", "gfx1101", "gfx1102", "gfx1103", "gfx1150", "gfx1151", "gfx1152", "gfx1153"], "inner": ["gfx11-generic"], "expect": true}, + {"outer": ["gfx12-generic"], "inner": ["gfx9-4-generic"], "expect": true}, + {"outer": ["gfx9-4-generic"], "inner": ["gfx9-4-generic"], "expect": true}, + {"outer": ["gfx9-4-generic"], "inner": ["gfx1151"], "expect": false} + ], + "compete": [ + {"a": [], "b": [], "expect": true}, + {"a": [], "b": ["gfx942"], "expect": false}, + {"a": ["gfx942"], "b": ["gfx942"], "expect": true}, + {"a": ["gfx942"], "b": ["gfx950"], "expect": false}, + {"a": ["gfx11-generic"], "b": ["gfx1151"], "expect": false}, + {"a": ["gfx11-generic"], "b": ["gfx11-generic"], "expect": true}, + {"a": ["gfx11-generic"], "b": [], "expect": false}, + {"a": ["gfx11-generic"], "b": ["gfx12-generic"], "expect": false}, + {"a": ["gfx1151", "gfx11-generic"], "b": ["gfx11-generic"], "expect": true}, + {"a": ["gfx11-generic"], "b": ["gfx11-generic", "gfx942"], "expect": true}, + {"a": ["gfx9-4-generic"], "b": ["gfx9-4-generic"], "expect": false}, + {"a": ["gfx9-4-generic"], "b": [], "expect": false}, + {"a": ["gfx1151"], "b": ["gfx11-generic", "gfx1151"], "expect": true}, + {"a": ["gfx11-generic"], "b": ["gfx1151", "gfx1100"], "expect": false} + ] +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_generic_name.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_generic_name.json new file mode 100644 index 000000000000..d41be7e1764e --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_generic_name.json @@ -0,0 +1,33 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "2026-10-05", + "license": "Apache-2.0 WITH LLVM-exception", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" + }, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12generic": [ + "gfx1200", + "gfx1201" + ] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_member.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_member.json new file mode 100644 index 000000000000..0cba6fe1d3a9 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_member.json @@ -0,0 +1,33 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "2026-10-05", + "license": "Apache-2.0 WITH LLVM-exception", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" + }, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12-generic": [ + "gfx1200", + "gfx12zz" + ] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_retrieved_on.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_retrieved_on.json new file mode 100644 index 000000000000..82664b576acd --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_retrieved_on.json @@ -0,0 +1,33 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "5 October 2026", + "license": "Apache-2.0 WITH LLVM-exception", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" + }, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12-generic": [ + "gfx1200", + "gfx1201" + ] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/duplicate_member.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/duplicate_member.json new file mode 100644 index 000000000000..66c6811c618c --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/duplicate_member.json @@ -0,0 +1,33 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "2026-10-05", + "license": "Apache-2.0 WITH LLVM-exception", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" + }, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12-generic": [ + "gfx1200", + "gfx1200" + ] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_generics.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_generics.json new file mode 100644 index 000000000000..8aa22e3e1605 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_generics.json @@ -0,0 +1,18 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "2026-10-05", + "license": "Apache-2.0 WITH LLVM-exception", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" + }, + "generics": {} +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_members.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_members.json new file mode 100644 index 000000000000..59b14606c9b5 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_members.json @@ -0,0 +1,30 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "2026-10-05", + "license": "Apache-2.0 WITH LLVM-exception", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" + }, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12-generic": [] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_comment.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_comment.json new file mode 100644 index 000000000000..d652fa95641a --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_comment.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "2026-10-05", + "license": "Apache-2.0 WITH LLVM-exception", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" + }, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12-generic": [ + "gfx1200", + "gfx1201" + ] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_license.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_license.json new file mode 100644 index 000000000000..f891afe0d4a2 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_license.json @@ -0,0 +1,32 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "2026-10-05", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" + }, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12-generic": [ + "gfx1200", + "gfx1201" + ] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_permalink.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_permalink.json new file mode 100644 index 000000000000..305d31d91986 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_permalink.json @@ -0,0 +1,32 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "retrievedOn": "2026-10-05", + "license": "Apache-2.0 WITH LLVM-exception", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" + }, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12-generic": [ + "gfx1200", + "gfx1201" + ] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_source.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_source.json new file mode 100644 index 000000000000..121282ea5343 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_source.json @@ -0,0 +1,20 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12-generic": [ + "gfx1200", + "gfx1201" + ] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_statement.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_statement.json new file mode 100644 index 000000000000..6fa4f5044151 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_statement.json @@ -0,0 +1,32 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "2026-10-05", + "license": "Apache-2.0 WITH LLVM-exception", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" + }, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12-generic": [ + "gfx1200", + "gfx1201" + ] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/permalink_without_revision.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/permalink_without_revision.json new file mode 100644 index 000000000000..55fa9ed5d679 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/permalink_without_revision.json @@ -0,0 +1,33 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "permalink": "https://github.com/ROCm/llvm-project/blob/main/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "2026-10-05", + "license": "Apache-2.0 WITH LLVM-exception", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" + }, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12-generic": [ + "gfx1200", + "gfx1201" + ] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/short_revision.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/short_revision.json new file mode 100644 index 000000000000..f8391b35f21a --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/short_revision.json @@ -0,0 +1,33 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696", + "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "2026-10-05", + "license": "Apache-2.0 WITH LLVM-exception", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" + }, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12-generic": [ + "gfx1200", + "gfx1201" + ] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp index 403ea50005c8..b003d7a3d184 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp @@ -67,6 +67,28 @@ TEST(TestIngestorArchGate, AdmitsWhenAnyListedArchMatches) EXPECT_FALSE(archSupports(family, "gfx90a")); } +TEST(TestIngestorArchGate, AdmitsADeviceThroughAGenericThatContainsIt) +{ + EXPECT_TRUE(archSupports({"gfx11-generic"}, "gfx1151")); + EXPECT_TRUE(archSupports({"gfx942", "gfx12-generic"}, "gfx1201")); +} + +TEST(TestIngestorArchGate, RefusesADeviceOutsideTheGenericsMembers) +{ + // gfx1154 and gfx1250 are real-looking siblings that no table row lists; membership is + // data, so a name that merely shares the family stem is refused. + EXPECT_FALSE(archSupports({"gfx11-generic"}, "gfx1154")); + EXPECT_FALSE(archSupports({"gfx12-generic"}, "gfx1100")); + EXPECT_FALSE(archSupports({"gfx12-generic"}, "gfx1250")); + EXPECT_FALSE(archSupports({"gfx9-4-generic"}, "gfx942")); +} + +TEST(TestIngestorArchGate, AdmitsAFeatureDecoratedDeviceThroughAGeneric) +{ + EXPECT_TRUE(archSupports({"gfx11-generic"}, "gfx1151:sramecc+:xnack-")); + EXPECT_FALSE(archSupports({"gfx12-generic"}, "gfx1250:sramecc+")); +} + TEST(TestIngestorArchGate, PrunesAPackWhoseArchExcludesTheDevice) { const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestGpuGenericTargets.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestGpuGenericTargets.cpp new file mode 100644 index 000000000000..a6859104eb4d --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestGpuGenericTargets.cpp @@ -0,0 +1,217 @@ +// Copyright © Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT + +#ifdef HIPDNN_ENABLE_KERNEL_INGESTOR + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +/** + * @file TestGpuGenericTargets.cpp + * @brief The generic GPU target table and the tier algebra over it, as C++ evaluates + * them. The table is the generated constexpr form of data/gpu_generic_targets.json + * and the golden vectors are shared with the Python implementation. + */ +namespace +{ + +using namespace hipdnn_plugin_sdk; +using namespace hipdnn_plugin_sdk::ingestor; + +using Arch = std::vector; + +nlohmann::json readJson(const char* path) +{ + std::ifstream stream(path); + EXPECT_TRUE(stream.is_open()) << "cannot open " << path; + return nlohmann::json::parse(stream); +} + +Arch toArch(const nlohmann::json& list) +{ + return list.get(); +} + +const char* tierName(const std::optional& tier) +{ + if(!tier) + { + return "none"; + } + if(*tier == ArchTier::EXPLICIT) + { + return "explicit"; + } + if(*tier == ArchTier::GENERIC) + { + return "generic"; + } + return "unrestricted"; +} + +TEST(TestGpuGenericTargets, GeneratedTableMatchesTheJsonMemberForMember) +{ + const auto json = readJson(HIPDNN_GPU_GENERIC_TARGETS_JSON_PATH); + const auto& generics = json.at("generics"); + + ASSERT_EQ(generics.size(), generated::GENERIC_TARGET_ROW_COUNT); + // Rows are matched by name: object key order is not part of the contract, member + // array order is. + for(const auto& [name, members] : generics.items()) + { + const auto* row = findGenericTarget(name); + ASSERT_NE(row, nullptr) << name; + ASSERT_EQ(row->memberCount, members.size()) << name; + for(std::size_t i = 0; i < members.size(); ++i) + { + EXPECT_EQ(row->members[i], members[i].get()) << name << "[" << i << "]"; + } + } +} + +TEST(TestGpuGenericTargets, FindsAGenericOnlyByItsExactName) +{ + const auto* row = findGenericTarget("gfx11-generic"); + ASSERT_NE(row, nullptr); + EXPECT_EQ(row->name, "gfx11-generic"); + + EXPECT_EQ(findGenericTarget("gfx11"), nullptr); + EXPECT_EQ(findGenericTarget("gfx11-generic "), nullptr); + EXPECT_EQ(findGenericTarget("GFX11-GENERIC"), nullptr); + EXPECT_EQ(findGenericTarget("gfx9-4-generic"), nullptr); + EXPECT_EQ(findGenericTarget(""), nullptr); + + EXPECT_TRUE(isGenericShapedArchName("gfx9-4-generic")); + EXPECT_FALSE(isGenericShapedArchName("gfx942")); + EXPECT_FALSE(isGenericShapedArchName("generic")); +} + +TEST(TestGpuGenericTargets, ContainsReportsMembership) +{ + EXPECT_TRUE(genericTargetContains("gfx11-generic", "gfx1100")); + EXPECT_TRUE(genericTargetContains("gfx11-generic", "gfx1153")); + EXPECT_FALSE(genericTargetContains("gfx11-generic", "gfx1154")); + EXPECT_FALSE(genericTargetContains("gfx11-generic", "gfx1200")); + EXPECT_TRUE(genericTargetContains("gfx12-generic", "gfx1201")); + EXPECT_FALSE(genericTargetContains("gfx12-generic", "gfx1250")); + EXPECT_FALSE(genericTargetContains("gfx9-4-generic", "gfx942")); +} + +TEST(TestGpuGenericTargets, EntryTierRanksExplicitAboveGeneric) +{ + EXPECT_EQ(archEntryTier("gfx1151", "gfx1151"), ArchTier::EXPLICIT); + EXPECT_EQ(archEntryTier("gfx11-generic", "gfx1151"), ArchTier::GENERIC); + EXPECT_EQ(archEntryTier("gfx1150", "gfx1151"), std::nullopt); + EXPECT_EQ(archEntryTier("gfx11-generic", "gfx1154"), std::nullopt); + EXPECT_LT(static_cast(ArchTier::EXPLICIT), static_cast(ArchTier::GENERIC)); + EXPECT_LT(static_cast(ArchTier::GENERIC), static_cast(ArchTier::UNRESTRICTED)); +} + +TEST(TestGpuGenericTargets, ListTierIsTheBestOfItsEntries) +{ + const Arch both{"gfx11-generic", "gfx1151"}; + EXPECT_EQ(archTier(both, "gfx1151"), ArchTier::EXPLICIT); + EXPECT_EQ(archTier(both, "gfx1151:sramecc+:xnack-"), ArchTier::EXPLICIT); + EXPECT_EQ(archTier(both, "gfx1100"), ArchTier::GENERIC); + EXPECT_EQ(archTier(both, "gfx1154"), std::nullopt); + EXPECT_EQ(archTier({"gfx11-generic"}, "gfx1151:sramecc+"), ArchTier::GENERIC); +} + +TEST(TestGpuGenericTargets, EmptyListIsUnrestricted) +{ + EXPECT_EQ(archTier({}, "gfx1151"), ArchTier::UNRESTRICTED); + EXPECT_EQ(archTier({}, ""), ArchTier::UNRESTRICTED); + EXPECT_TRUE(archSupports({}, "gfx1151")); +} + +TEST(TestGpuGenericTargets, OverlapIsOverExpandedMemberSets) +{ + EXPECT_TRUE(archOverlaps({"gfx11-generic"}, {"gfx1151"})); + EXPECT_TRUE(archOverlaps({"gfx1151"}, {"gfx11-generic"})); + EXPECT_TRUE(archOverlaps({"gfx11-generic"}, {"gfx11-generic"})); + EXPECT_TRUE(archOverlaps({}, {"gfx11-generic"})); + EXPECT_FALSE(archOverlaps({"gfx11-generic"}, {"gfx12-generic"})); + EXPECT_FALSE(archOverlaps({"gfx11-generic"}, {"gfx1154"})); + EXPECT_FALSE(archOverlaps({"gfx1250"}, {"gfx12-generic"})); +} + +TEST(TestGpuGenericTargets, CoversIsOverExpandedMemberSets) +{ + EXPECT_TRUE(archCovers({"gfx11-generic"}, {"gfx1151"})); + EXPECT_TRUE(archCovers({"gfx11-generic"}, {"gfx1100", "gfx1153"})); + EXPECT_FALSE(archCovers({"gfx11-generic"}, {"gfx1154"})); + EXPECT_FALSE(archCovers({"gfx1151"}, {"gfx11-generic"})); + EXPECT_TRUE(archCovers({"gfx11-generic"}, {"gfx11-generic"})); + EXPECT_FALSE(archCovers({"gfx11-generic"}, {"gfx12-generic"})); + EXPECT_TRUE(archCovers({}, {"gfx11-generic"})); + EXPECT_TRUE(archCovers({"gfx11-generic"}, {})); +} + +TEST(TestGpuGenericTargets, CompeteIsSameTierOverlap) +{ + EXPECT_TRUE(archesCompete({}, {})); + EXPECT_TRUE(archesCompete({"gfx942"}, {"gfx942"})); + EXPECT_TRUE(archesCompete({"gfx11-generic"}, {"gfx11-generic"})); + EXPECT_TRUE(archesCompete({"gfx1151", "gfx11-generic"}, {"gfx11-generic"})); + EXPECT_FALSE(archesCompete({}, {"gfx942"})); + EXPECT_FALSE(archesCompete({"gfx11-generic"}, {"gfx1151"})); + EXPECT_FALSE(archesCompete({"gfx11-generic"}, {})); + EXPECT_FALSE(archesCompete({"gfx11-generic"}, {"gfx12-generic"})); +} + +TEST(TestGpuGenericTargets, UnknownGenericExpandsToNothing) +{ + const Arch unknown{"gfx9-4-generic"}; + EXPECT_FALSE(archSupports(unknown, "gfx942")); + EXPECT_FALSE(archSupports(unknown, "gfx9-4-generic")); + EXPECT_FALSE(archOverlaps(unknown, unknown)); + EXPECT_TRUE(archOverlaps(unknown, {})); + EXPECT_TRUE(archCovers({"gfx11-generic"}, unknown)); + EXPECT_FALSE(archCovers(unknown, {"gfx1151"})); + EXPECT_FALSE(archesCompete(unknown, unknown)); +} + +TEST(TestGpuGenericTargets, MatchesGoldenTierVectors) +{ + const auto vectors = readJson(HIPDNN_ARCH_TIER_VECTORS_JSON_PATH); + + ASSERT_FALSE(vectors.at("tier").empty()); + for(const auto& v : vectors.at("tier")) + { + EXPECT_STREQ(tierName(archTier(toArch(v.at("arch")), v.at("device").get())), + v.at("expect").get().c_str()) + << v.dump(); + } + ASSERT_FALSE(vectors.at("overlap").empty()); + for(const auto& v : vectors.at("overlap")) + { + EXPECT_EQ(archOverlaps(toArch(v.at("a")), toArch(v.at("b"))), v.at("expect").get()) + << v.dump(); + } + ASSERT_FALSE(vectors.at("covers").empty()); + for(const auto& v : vectors.at("covers")) + { + EXPECT_EQ(archCovers(toArch(v.at("outer")), toArch(v.at("inner"))), + v.at("expect").get()) + << v.dump(); + } + ASSERT_FALSE(vectors.at("compete").empty()); + for(const auto& v : vectors.at("compete")) + { + EXPECT_EQ(archesCompete(toArch(v.at("a")), toArch(v.at("b"))), v.at("expect").get()) + << v.dump(); + } +} + +} // namespace + +#endif // HIPDNN_ENABLE_KERNEL_INGESTOR From 2b8afd4c08d6e5720c674d3b805a74aaab91a295 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 5 Oct 2026 22:13:55 +0000 Subject: [PATCH 17/52] fix(hipdnn): keep generic target headers clang-tidy clean --- .../hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in | 3 +++ .../include/hipdnn_plugin_sdk/GpuGenericTargets.hpp | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in b/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in index f97ccf76d758..5f0b8f533465 100644 --- a/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in +++ b/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in @@ -19,9 +19,12 @@ struct GenericTargetRow std::size_t memberCount; }; +// Constexpr tables of string_view; arrays keep the data in static storage with no allocation. +// NOLINTBEGIN(modernize-avoid-c-arrays) @HIPDNN_GENERIC_TARGET_MEMBER_ARRAYS@ inline constexpr GenericTargetRow GENERIC_TARGET_ROWS[] = { @HIPDNN_GENERIC_TARGET_ROWS@}; +// NOLINTEND(modernize-avoid-c-arrays) inline constexpr std::size_t GENERIC_TARGET_ROW_COUNT = @HIPDNN_GENERIC_TARGET_ROW_COUNT@; diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp index 4c127140b2e7..986bb2f0d509 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp @@ -34,11 +34,11 @@ inline bool isGenericShapedArchName(std::string_view name) /// The table row for the generic named exactly @p name, or nullptr when absent. inline const generated::GenericTargetRow* findGenericTarget(std::string_view name) { - for(std::size_t i = 0; i < generated::GENERIC_TARGET_ROW_COUNT; ++i) + for(const auto& row : generated::GENERIC_TARGET_ROWS) { - if(generated::GENERIC_TARGET_ROWS[i].name == name) + if(row.name == name) { - return &generated::GENERIC_TARGET_ROWS[i]; + return &row; } } return nullptr; From f0395991d49508d2e0baf8730cf7e1f5cb9a66b5 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 5 Oct 2026 22:35:28 +0000 Subject: [PATCH 18/52] hkp probes: author-selected UKDS, single CI run, review fixes - hkp_add_packaging_probe gains UKDS: the probe packs exactly the listed UKDs (by name); without it, one UKD per compile group. rocke groups by (source, builder); a missing group_by field fails derive. - check_probe_junit reads the lane's full ctest junit and checks manifest names only; disabled or not-run probes fail. Probes run once in 'Run tests'; the check runs whenever tests ran. 'Build packaging probes' step removed. - kpack UKDs must ship arch == [ARCH]; NAME/ARCH/UKDS validated; probe call guard enforced; derive tooling is a configure dependency. - Tests: helpers moved out of conftest, new UKDS/derive/assert cases, packer-only test removed. Docs: usage and extension guide, runbook final step. --- .github/scripts/check_probe_junit.py | 78 ++-- ...unit_test.py => test_check_probe_junit.py} | 69 +++- .github/workflows/hipdnn-superbuild-ci.yml | 48 +-- dnn-providers/hip-kernel-provider/README.md | 2 +- .../descriptor-packaging/README.md | 336 ++++++++++-------- .../cmake/HkpPackagingProbes.cmake | 132 +++++-- .../descriptor-packaging/probes/probes.cmake | 15 +- .../tests/{conftest.py => probe_support.py} | 56 ++- .../probes/tests/test_hkp_probe_assert.py | 48 ++- .../tests/test_hkp_probe_derive_root.py | 173 +++++++-- .../probes/tests/test_hkp_probe_end_to_end.py | 106 ++---- .../tests/test_hkp_probe_passthrough.py | 76 ++-- .../tools/hkp_probe_assert.py | 15 +- .../tools/hkp_probe_derive_root.py | 157 ++++++-- .../tools/hkp_probe_kinds.py | 8 +- .../descriptors/README.md | 15 +- .../skills/hipdnn-ingestor-engine/RUNBOOK.md | 25 +- .../ai/skills/hipdnn-ingestor-engine/SKILL.md | 9 +- .../skills/hipdnn-ingestor-engine/extend.md | 9 +- .../ai/skills/hipdnn-superbuild-test/SKILL.md | 17 +- .../ai/skills/hipdnn-superbuild/SKILL.md | 2 +- 21 files changed, 904 insertions(+), 492 deletions(-) rename .github/scripts/tests/{check_probe_junit_test.py => test_check_probe_junit.py} (56%) rename dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/{conftest.py => probe_support.py} (71%) diff --git a/.github/scripts/check_probe_junit.py b/.github/scripts/check_probe_junit.py index 87d506d1bc95..161c6e76192d 100755 --- a/.github/scripts/check_probe_junit.py +++ b/.github/scripts/check_probe_junit.py @@ -4,20 +4,25 @@ """Prove that every declared hipDNN packaging probe ran and passed. -The superbuild lanes run the packaging probes through ctest --output-junit. ctest exits 0 for a -junit file that lists fewer tests than were declared, and a skipped test is not -a failure to it. This script compares the junit file against the manifest the -CMake configure wrote (one ctest test name per line) so that "declared N, ran -fewer", "skipped" and "ran nothing" are all red. +The superbuild lanes run the whole ctest suite once with --output-junit. ctest +exits 0 when a test is disabled or not run, so this script reads that junit file +and checks it against the manifest the CMake configure wrote (one ctest test +name per line). Testcases not named in the manifest are ignored. + +For every manifest name, the junit must hold exactly one testcase with that +name, and that testcase must have run (status "run" or "fail"; without a status +attribute, no child) and must have no or child. Usage: - check_probe_junit.py --junit build/probe-junit.xml \ + check_probe_junit.py --junit build/ctest-junit.xml \ --manifest build/hkp-probes/manifest.txt Exit codes: - 0 every manifest name ran exactly once; no failure, error or skip + 0 every manifest name ran exactly once and passed 1 one or more checks failed; each prints - "check_probe_junit: FAIL : " + "check_probe_junit: FAIL : ", where is one of + manifest-unreadable, manifest-too-small, junit-unreadable, + name-missing, name-duplicate, not-run, failure, error """ import argparse @@ -33,6 +38,18 @@ def read_manifest(path: Path) -> list[str]: return [line.strip() for line in path.read_text().splitlines() if line.strip()] +def ran(case: ET.Element) -> bool: + """Return whether ctest ran the testcase. + + ctest writes status "run", "fail", "disabled" or "notrun"; only the first + two mean the test executed. + """ + status = case.get("status") + if status is None: + return case.find("skipped") is None + return status in ("run", "fail") + + def check(junit_path: Path, manifest_path: Path) -> list[tuple[str, str]]: """Return (id, detail) for every failed check; empty means green.""" try: @@ -56,30 +73,35 @@ def check(junit_path: Path, manifest_path: Path) -> list[tuple[str, str]]: failures.append(("junit-unreadable", f"{junit_path}: {exc}")) return failures - cases = list(root.iter("testcase")) - ran = [case.get("name", "") for case in cases] + cases: dict[str, list[ET.Element]] = {name: [] for name in names} + for case in root.iter("testcase"): + found = cases.get(case.get("name", "")) + if found is not None: + found.append(case) - if len(cases) != len(names): - failures.append( - ( - "count-mismatch", - f"junit has {len(cases)} testcase(s), manifest declares {len(names)}", - ) - ) - for name in names: - if name not in ran: + for name, found in cases.items(): + if not found: failures.append( ("name-missing", f"declared test '{name}' is not in the junit") ) - - for case in cases: - name = case.get("name", "") - if case.find("skipped") is not None or case.get("status") == "notrun": - failures.append(("skipped", f"test '{name}' did not run")) - if case.find("failure") is not None: - failures.append(("failure", f"test '{name}' failed")) - if case.find("error") is not None: - failures.append(("error", f"test '{name}' errored")) + continue + if len(found) > 1: + failures.append( + ( + "name-duplicate", + f"declared test '{name}' appears {len(found)} times in the junit", + ) + ) + for case in found: + if not ran(case): + status = case.get("status") + failures.append( + ("not-run", f"test '{name}' did not run (status={status})") + ) + if case.get("status") == "fail" or case.find("failure") is not None: + failures.append(("failure", f"test '{name}' failed")) + if case.find("error") is not None: + failures.append(("error", f"test '{name}' errored")) return failures diff --git a/.github/scripts/tests/check_probe_junit_test.py b/.github/scripts/tests/test_check_probe_junit.py similarity index 56% rename from .github/scripts/tests/check_probe_junit_test.py rename to .github/scripts/tests/test_check_probe_junit.py index 899f3c00801f..5d5f61476abe 100644 --- a/.github/scripts/tests/check_probe_junit_test.py +++ b/.github/scripts/tests/test_check_probe_junit.py @@ -13,10 +13,15 @@ MANIFEST = ["hkp-probe-gfx950", "hkp-probe-tools"] -def case_xml(name: str, child: str = "") -> str: +def case_xml(name: str, child: str = "", status: str | None = None) -> str: body = f"<{child}/>" if child else "" - status = "notrun" if child == "skipped" else "run" - return f'{body}' + if status is None: + status = {"skipped": "notrun", "failure": "fail"}.get(child, "run") + status_attr = f' status="{status}"' if status else "" + return ( + f'' + f"{body}" + ) def junit_xml(cases: list[str]) -> str: @@ -33,7 +38,7 @@ def setUp(self): self.dir = Path(tmp.name) def run_check(self, xml: str, manifest: list[str]) -> list[str]: - junit_path = self.dir / "probe-junit.xml" + junit_path = self.dir / "ctest-junit.xml" manifest_path = self.dir / "manifest.txt" junit_path.write_text(xml) manifest_path.write_text("".join(f"{n}\n" for n in manifest)) @@ -45,13 +50,47 @@ def test_all_declared_tests_passed(self): def test_skipped_test_fails(self): xml = junit_xml([case_xml(MANIFEST[0]), case_xml(MANIFEST[1], "skipped")]) - self.assertEqual(self.run_check(xml, MANIFEST), ["skipped"]) + self.assertEqual(self.run_check(xml, MANIFEST), ["not-run"]) - def test_fewer_testcases_than_manifest_fails(self): - xml = junit_xml([case_xml(MANIFEST[0])]) - self.assertEqual( - self.run_check(xml, MANIFEST), ["count-mismatch", "name-missing"] + def test_disabled_test_without_skipped_child_fails(self): + xml = junit_xml( + [case_xml(MANIFEST[0], status="disabled"), case_xml(MANIFEST[1])] + ) + self.assertEqual(self.run_check(xml, MANIFEST), ["not-run"]) + + def test_unknown_status_fails(self): + xml = junit_xml([case_xml(MANIFEST[0], status="bogus"), case_xml(MANIFEST[1])]) + self.assertEqual(self.run_check(xml, MANIFEST), ["not-run"]) + + def test_skipped_child_without_status_fails(self): + xml = junit_xml( + [case_xml(MANIFEST[0], "skipped", status=""), case_xml(MANIFEST[1])] + ) + self.assertEqual(self.run_check(xml, MANIFEST), ["not-run"]) + + def test_missing_status_without_skipped_child_passes(self): + xml = junit_xml([case_xml(n, status="") for n in MANIFEST]) + self.assertEqual(self.run_check(xml, MANIFEST), []) + + def test_unrelated_testcases_are_ignored(self): + xml = junit_xml( + [ + case_xml("hipdnn-unit-tests"), + case_xml(MANIFEST[0]), + case_xml("other-disabled", status="disabled"), + case_xml("other-failed", "failure"), + case_xml(MANIFEST[1]), + ] ) + self.assertEqual(self.run_check(xml, MANIFEST), []) + + def test_duplicate_declared_name_fails(self): + xml = junit_xml([case_xml(n) for n in MANIFEST] + [case_xml(MANIFEST[0])]) + self.assertEqual(self.run_check(xml, MANIFEST), ["name-duplicate"]) + + def test_declared_name_missing_fails(self): + xml = junit_xml([case_xml(MANIFEST[0]), case_xml("hipdnn-unit-tests")]) + self.assertEqual(self.run_check(xml, MANIFEST), ["name-missing"]) def test_empty_manifest_fails(self): xml = junit_xml([case_xml(n) for n in MANIFEST]) @@ -67,25 +106,25 @@ def test_failure_fails(self): xml = junit_xml([case_xml(MANIFEST[0], "failure"), case_xml(MANIFEST[1])]) self.assertEqual(self.run_check(xml, MANIFEST), ["failure"]) + def test_fail_status_without_failure_child_fails(self): + xml = junit_xml([case_xml(MANIFEST[0], status="fail"), case_xml(MANIFEST[1])]) + self.assertEqual(self.run_check(xml, MANIFEST), ["failure"]) + def test_error_fails(self): xml = junit_xml([case_xml(MANIFEST[0]), case_xml(MANIFEST[1], "error")]) self.assertEqual(self.run_check(xml, MANIFEST), ["error"]) - def test_declared_name_missing_with_equal_count_fails(self): - xml = junit_xml([case_xml(MANIFEST[0]), case_xml("hkp-probe-other")]) - self.assertEqual(self.run_check(xml, MANIFEST), ["name-missing"]) - def test_unparseable_junit_fails(self): self.assertEqual(self.run_check("> "$GITHUB_PATH" + # --output-junit is relative to --test-dir: this writes build/ctest-junit.xml. - name: Run tests - run: ctest --test-dir build --output-on-failure + id: run-tests + run: ctest --test-dir build --output-on-failure --output-junit ctest-junit.xml - # ctest reports skipped and missing tests as success. Re-run the probe - # entries alone and compare the junit against the manifest configure - # wrote, so a probe that did not run fails the job. + # ctest counts a disabled or not-run test as success. Read the junit + # "Run tests" wrote and fail if a declared packaging probe (listed in the + # manifest configure wrote) did not run or failed. Runs whenever "Run + # tests" ran, pass or fail, so a probe failure is reported by name. - name: Check packaging probes ran + if: ${{ !cancelled() && steps.run-tests.outcome != 'skipped' }} run: | - ctest --test-dir build -R '^hkp-probe-' --no-tests=error --output-on-failure --output-junit probe-junit.xml "$VENV_PYTHON" .github/scripts/check_probe_junit.py \ - --junit build/probe-junit.xml \ + --junit build/ctest-junit.xml \ --manifest build/hkp-probes/manifest.txt - name: Patch ROCm SDK with superbuild outputs @@ -496,15 +490,6 @@ jobs: - name: Build run: cmake --build build - # The probes pack the production gfx950 descriptors for an - # architecture this lane does not build (GPU_TARGETS is the lane's own). - # Their pack targets are part of `all` (ALL custom targets), so Build above - # already ran them; this step stays so a probe failure is labelled and the - # aggregate target is exercised. The ctest entries fail without their - # output. - - name: Build packaging probes - run: cmake --build build --target hkp_packaging_probes - # Packing is skipped with a STATUS message, so a resolution regression # would otherwise pass CI while producing no archives. - name: Verify kpack archives were produced @@ -515,17 +500,20 @@ jobs: test "${#kpacks[@]}" -gt 0 || { echo "no .kpack archives produced" >&2; exit 1; } printf '%s\n' "${kpacks[@]}" + # --output-junit is relative to --test-dir: this writes build/ctest-junit.xml. - name: Run tests - run: ctest --test-dir build --output-on-failure + id: run-tests + run: ctest --test-dir build --output-on-failure --output-junit ctest-junit.xml - # ctest reports skipped and missing tests as success. Re-run the probe - # entries alone and compare the junit against the manifest configure - # wrote, so a probe that did not run fails the job. + # ctest counts a disabled or not-run test as success. Read the junit + # "Run tests" wrote and fail if a declared packaging probe (listed in the + # manifest configure wrote) did not run or failed. Runs whenever "Run + # tests" ran, pass or fail, so a probe failure is reported by name. - name: Check packaging probes ran + if: ${{ !cancelled() && steps.run-tests.outcome != 'skipped' }} run: | - ctest --test-dir build -R '^hkp-probe-' --no-tests=error --output-on-failure --output-junit probe-junit.xml "$VENV_PYTHON" .github/scripts/check_probe_junit.py \ - --junit build/probe-junit.xml \ + --junit build/ctest-junit.xml \ --manifest build/hkp-probes/manifest.txt # Golden-data validation used to live here as a schedule-gated re-run of the diff --git a/dnn-providers/hip-kernel-provider/README.md b/dnn-providers/hip-kernel-provider/README.md index b0b71fc820e0..116aa0dfc9b4 100644 --- a/dnn-providers/hip-kernel-provider/README.md +++ b/dnn-providers/hip-kernel-provider/README.md @@ -78,7 +78,7 @@ Beyond the shared third-party set, the prefixes must provide HIP/HIPRTC, `hipdnn Descriptor packaging (`HIPDNN_ENABLE_KERNEL_INGESTOR=ON`) additionally requires a supplied `Python3_EXECUTABLE` with pip, `msgpack`, and `zstandard`, a kpack source tree, and local rocKE wheels. The `hkp_rocke_wheel_python_interp` target installs those wheels only into build-owned storage with no index access or dependency resolution, then uses the supplied interpreter with a subprocess-scoped private import path. It does not install rocKE into the parent Python environment. See [Kernel packing](../../projects/hipdnn/docs/Building.md#kernel-packing-rocm_kpack) for wheel supply modes and prerequisites. -Packaging probes check that the production descriptors pack for an architecture the build does not target. They are opt-in per architecture (one `hkp_add_packaging_probe(ARCH )` line in `descriptor-packaging/probes/probes.cmake`; a new pack under an already probed architecture in the production root is covered automatically) and superbuild-only: the `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` option is `OFF` by default. A probe packs one instance per compile group; the lane's own architecture is covered in full by the normal build. They do not cover ASM SDPA, C++/runtime gfx950 paths, install staging, or standalone-UKD references (probe derive fails configure on those). See the [descriptor-packaging README](descriptor-packaging/README.md#packaging-probes). +Packaging probes pack descriptors with the real packer for one explicit architecture, independent of the build's `GPU_TARGETS`, and assert the output. Each integration declares its own probe in `descriptor-packaging/probes/probes.cmake`, `hkp_add_packaging_probe(ARCH NAME UKDS ...)`, naming a representative set of UKDs; without `UKDS` a probe packs one UKD per compile group. Probes are superbuild-only: the `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` option is `OFF` by default. They do not cover device behavior, install staging, or standalone-UKD references (probe derive fails configure on those). See the [descriptor-packaging README](descriptor-packaging/README.md#packaging-probes). ### Testing diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 275641cb213b..27fcdbb8c037 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -265,153 +265,176 @@ its default. The reason is in the `hkp_wire_pack_target()` header in ## Packaging probes -A packaging probe proves that one integration's descriptors **pack for one explicit -architecture**, independent of the architectures the build itself targets. The packer's -architecture list is `GPU_TARGETS` and nothing else, so a lane that builds gfx942 (Linux) -or gfx1151 (Windows) never exercises the gfx950 production bundle. A probe closes that -gap: it runs the real packer through `hkp_wire_pack_target()` with its own architecture, -then a ctest entry asserts the packed output. +A packaging probe packs one descriptor root for one explicit architecture through the real +packer (`hkp_wire_pack_target()`), independent of the architectures the build targets, and +a ctest entry then asserts the packed output. The packer's architecture list is +`GPU_TARGETS` and nothing else, so without a probe a lane never packs an architecture it +does not build. A probe may use any architecture, built by the lane or not. + +Probes are superbuild-only and exist only under `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` +(default `OFF`). They are declared in `probes/probes.cmake`. With the option `ON`, the +probe pack targets are part of `all`, and the aggregate target `hkp_packaging_probes` +builds them alone. Probe output is written under `/hkp-probes//`, outside both +shipped descriptor trees, and is never installed. With the option `OFF` the only difference +is that cache entry; the probe module is never read. **What a probe proves** -- The real production descriptors (UUID ids, the specialization contract, the real - KMD/UED/UDD/UHD) load, pass the pack-time agreement check and compile through comgr. -- The rocKE sources come from the wheel the packer is given, not from the in-tree source - tree the pytest suite imports. +- The descriptors it packs, taken from the real root (ids, the specialization contract, + the real KMD/UED/UDD/UHD), load, pass the pack-time agreement check and go through the + packer for that architecture, including the compile for compiled kinds. - The architecture and output wiring of the CMake pack target: `--arches`, the stamp, the - interpreter and wheel directory, comgr forwarding (the last is asserted via `provenance-comgr` only; no local mutation measures it). -- The output: a kpack archive for exactly that architecture, with the expected UKD count - and shipped provenance. + interpreter and wheel directory, comgr forwarding (the last is asserted via + `provenance-comgr` only; no local mutation measures it). +- The output: exactly one architecture directory, exactly the expected UKDs, an archive + entry for each kpack-output UKD, and the provenance each producer kind ships. +- rocKE only: the packer records a wheel (`provenance-wheel`). The wheel mutation check + below shows that the rocKE sources come from that wheel, not from the in-tree source the + pytest suite imports. **What it does not prove** -- Variants within a compile group: one instance per group is packed (see below). The - lane's own architecture is covered in full by the normal build. -- ASM SDPA (prebuilt `.co` files that kpack only stores: nothing compiles and nothing - reads the archive), the C++/runtime gfx950 paths, and install staging. +- UKDs it does not pack: the other variants of a compile group in the default sample, or + the UKDs not listed in `UKDS`. +- Anything outside the descriptor root and the packer, for example the gfx950 ASM SDPA + (prebuilt `.co` files) and C++/runtime paths, and install staging. - Device behavior: no kernel is launched, no numerics are checked. -- Anything about integrations without a probe. -- That the CI job runs: that is the workflow's job (`--no-tests=error`, the manifest check - below). - -Probes are **opt-in per architecture and superbuild-only**: they exist only under -`HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` (default `OFF`, CI-only), declared one -`hkp_add_packaging_probe(ARCH ...)` line per architecture. A new pack under an already -probed architecture in the production root is covered automatically. With the option -`ON`, the pack targets and `hkp_packaging_probes` are part of `all`; the CI -"Build packaging probes" step builds them explicitly for labelling. Probe output is never -installed and is written under `/hkp-probes//`, outside both shipped -descriptor trees. With the option `OFF` the only difference is that cache entry; the probe -module is never read. +- Integrations and architectures without a probe. +- That the CI job runs: that is the job of `--no-tests=error` and the junit check below. -### Running the probes locally +### How to use -From the repository root. The paths below are the workstation's; substitute your ROCm and -rocm_kpack locations. The `GPU_TARGETS` value must differ from every probe architecture -(see "Adding a probe"). +Probing is author-controlled: each integration declares what to probe, one +`hkp_add_packaging_probe()` line in `probes/probes.cmake`. -```bash -cmake --preset hipdnn-dev-all -GNinja -B build-probe \ - -DROCM_LIBS_ENABLE_COMPONENTS="hipdnn;hip-kernel-provider" \ - -DGPU_TARGETS=gfx942 \ - -DHIPKERNELPROVIDER_ENABLE_ROCKE=ON -DHIPDNN_ENABLE_KERNEL_INGESTOR=ON \ - -DHIPDNN_ENABLE_SDPA=ON \ - -DHIPKERNELPROVIDER_KPACK_PYTHON_DIR=/opt/rocm-kpack/python \ - -DCMAKE_PREFIX_PATH=/opt/rocm -DROCM_PATH=/opt/rocm \ - -DPython3_EXECUTABLE=/usr/bin/python3 -DPython_EXECUTABLE=/usr/bin/python3 \ - -DENABLE_CLANG_FORMAT=OFF -DENABLE_CLANG_TIDY=OFF -DROCM_LIBS_ENABLE_ROOT_CTEST=ON \ - -DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON -cmake --build build-probe --target hkp_packaging_probes -ctest --test-dir build-probe -R '^hkp-probe-' --no-tests=error --output-on-failure \ - --output-junit probe-junit.xml -python3 .github/scripts/check_probe_junit.py \ - --junit build-probe/probe-junit.xml --manifest build-probe/hkp-probes/manifest.txt +```cmake +# Default sample: one UKD per compile group of every KDP in the root shipping for gfx950. +hkp_add_packaging_probe(ARCH gfx950) +# One integration, a chosen representative set. +hkp_add_packaging_probe(ARCH gfx950 NAME attention_dense + UKDS attention_dense.bf16_d64_hq8_kv1_c1_bm128_bn64.gfx950 + attention_dense.bf16_d64_hq8_kv1_c1_bm256_bn32.gfx950) ``` -The selector is `^hkp-probe-`, anchored: `-R` is an unanchored regex. It matches every -probe (`hkp-probe-`) and `hkp-probe-tools`, the pytest suite of the probe tooling -(`probes/tests/`). `--no-tests=error` makes an empty selection a failure instead of a -green run that proved nothing. `manifest.txt` is written at configure and lists the -ctest names that must run; the junit check fails unless every one ran with no failure, -error or skip. Configuring without the option and running the same build and ctest lines -fails (unknown target, then "No tests were found"). - -Locally on Linux the build is quick (the wheel environment plus a one-instance-per-group -pack) and the per-arch ctest entries a few seconds; `hkp-probe-tools` (the probe tooling's own pytest suite, which really compiles hip and rocke fixtures) takes longer. There is no local Windows reproduction. - -### `hkp_add_packaging_probe` arguments - -Declared in `probes/probes.cmake`, defined in `cmake/HkpPackagingProbes.cmake`. The -normal declaration is one line per architecture: `hkp_add_packaging_probe(ARCH gfx950)`. +- **With `UKDS`**, the probe packs exactly the listed UKDs, selected by UKD `name`. Pick a + minimal representative set: one UKD per compile path you want checked on every PR. Probe + as much as you consider necessary; every listed UKD is packed, and compiled for a + compiled kind, in every CI run. A KDP that keeps none of the listed UKDs is left out of + the derived root. Give the line a `NAME`, since the default name is shared with the + default-sample probe of the same architecture. +- **Without `UKDS`**, the probe packs the default sample: one UKD per compile group of + every KDP in the root that ships for `ARCH` (see the compile-group table below). A new + pack under that architecture is then covered with no new line. + +A listed name that is not kept fails configure: no KDP shipping for `ARCH` holds an inline +UKD with that name that itself ships for `ARCH` (a misspelled name, a UKD for another +architecture, or one reached only through a standalone-UKD reference). Listing a name twice +also fails configure. | Argument | Meaning | |---|---| -| `ARCH` | Required. The one architecture to pack, e.g. `gfx950`. Passed to the packer as `--arches`. | -| `ROOT` | Optional descriptor root to probe. Defaults to the production root (`HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT`). | -| `NAME` | Optional probe name. Defaults to `` for the production root and `_` for another `ROOT`. Pack target `hkp_packaging_probe_`, ctest entry `hkp-probe-` (e.g. `hkp-probe-gfx950`). Declaring a name twice is a configure error. | +| `ARCH` | Required. The one architecture to pack, e.g. `gfx950`; must match `^gfx[0-9a-z]+$`. Passed to the packer as `--arches`. | +| `ROOT` | Optional descriptor root to probe. Defaults to the production root (`HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT`). A relative path is resolved against `probes/`; a trailing `/` is ignored. | +| `NAME` | Optional probe name; must match `^[A-Za-z0-9_.+-]+$`, must not be dots only, and must not be `tools`. Defaults to `` for the production root and `_` for another `ROOT`. Pack target `hkp_packaging_probe_`, ctest entry `hkp-probe-`. Declaring a name twice is a configure error. | +| `UKDS` | Optional UKD `name`s to pack instead of the default sample. Each entry is non-empty and listed once. | | `PACK_JOBS` | Optional prewarm worker count; defaults to `1`. | -A probe has no producer-kind or per-KDP argument: the producer kinds present in the root -are discovered from its descriptors, and the assertion expects each UKD's provenance by -its authored `kernel_source.kind`. Probes cover `hip`, `rocke`, `hsaco` and -`embedded_source`, the kinds registered in `tools/hkp_probe_kinds.py`. Each entry gives the -`kernel_source` fields that define a compile group, the provenance checks a shipped UKD must -satisfy, and the packed **output type**: `kpack` (the packer packs the UKD into the archive -and ships a `kind: kpack` UKD: `hip`, `rocke`, `hsaco`) or `passthrough` (the packer ships the -UKD as authored and nothing enters the archive: `embedded_source`). A kind whose output is -one of these types, using provenance checks that already exist, is probeable with one entry -there plus one end-to-end test case; the kinds list in this paragraph is edited with it. A -kind the packer does not support yet needs packer work first. Any kind not registered, in a -UKD kept for the probed arch, makes configure fail and fails `no-rules-for-kind` in the -assertion; such a UKD must live in a KDP that does not ship for a probed arch, or the probe -needs a design decision. - -The archive rule: the archive checks (`kpack-missing`, `kpack-empty`, `kpack-toc`, `sha256`, -`symbol`, `signature`) run only when the expected list holds at least one kpack-output UKD. -A root of only pass-through UKDs ships descriptors and no `kpack/` directory; the archive is -then not required and an archive that happens to exist is ignored. - -Non-goals: a pass-through kind probes one UKD per KDP (`group_by` is empty), not every -`source_file` and `entry_point`; standalone-UKD references are rejected by derive; a further -output type (for example a runtime-compiled kind) is a new decision when it lands, not an -entry. - -Configuration fails, never skips, when: `ARCH` is missing; `ROOT` is not a directory; -no KDP under the root ships for `ARCH`; a -KDP shipping for `ARCH` references a standalone UKD (a `kernelDescriptors` entry that is -not an object; derive supports inline UKDs only); the probe output root would sit under a -shipped descriptor tree; a UKD kept for `ARCH` has a kind not registered in `tools/hkp_probe_kinds.py`; no probe is declared; or pytest is not importable by -`Python3_EXECUTABLE`. - -### The derived-root compile-group rule +A probe has no producer-kind argument: the kinds present are discovered from the +descriptors, and the assertion expects each UKD's provenance by its authored +`kernel_source.kind`. Probes cover `hip`, `rocke`, `hsaco` and `embedded_source`, the kinds +registered in `tools/hkp_probe_kinds.py`. Each entry gives the `kernel_source` fields that +define a compile group, the provenance checks a shipped UKD must satisfy, and the packed +**output type**: `kpack` (the packer packs the UKD into the archive and ships a +`kind: kpack` UKD: `hip`, `rocke`, `hsaco`) or `passthrough` (the packer ships the UKD as +authored and nothing enters the archive: `embedded_source`). + +The archive checks (`kpack-missing`, `kpack-empty`, `kpack-toc`, `sha256`, `symbol`) run +only when the expected list holds at least one kpack-output UKD. A probe of only +pass-through UKDs ships descriptors and no `kpack/` directory; the archive is then not +required and an archive that happens to exist is ignored. + +Configuration fails, never skips, when: `ARCH` is missing or malformed; `NAME` is malformed, +`tools` or declared twice; `ROOT` is not a directory; no KDP under the root ships for +`ARCH`; a `UKDS` entry is empty, repeated or not kept; a KDP the probe keeps references a +standalone UKD (a `kernelDescriptors` entry that is not an object; derive supports inline +UKDs only); a kept UKD has a kind not registered in `tools/hkp_probe_kinds.py`; two kept +UKDs of one KDP share a name; in the default sample, a UKD's `kernel_source` lacks one of +its kind's compile-group fields; the probe output root would sit under a shipped +descriptor tree; no probe is declared; or pytest is not importable by `Python3_EXECUTABLE`. + +### The derived root and the default sample At configure, `tools/hkp_probe_derive_root.py` copies the root into -`/hkp-probes//root` unchanged, except that every KDP shipping for `ARCH` has -its `kernelDescriptors` reduced to one UKD per **compile group**, among the UKDs that -themselves ship for `ARCH`. The group is the part of a UKD that selects its compile path: +`/hkp-probes//root`. Every KDP shipping for `ARCH` has its +`kernelDescriptors` reduced to the probed UKDs, among the UKDs that themselves ship for +`ARCH`: the `UKDS` list (one `--ukd` per entry), or else one UKD per **compile group**. +With `UKDS`, a KDP that keeps nothing is left out; other files are copied unchanged. The +compile group is the part of a UKD that selects its compile path: | Producer kind | Compile group | |---|---| -| `rocke` | `(kind, builder)` | +| `rocke` | `(kind, source, builder)` | | `hip` | `(kind, source, build)` | -| `hsaco` | one per KDP (compiles nothing) | -| `embedded_source` | one per KDP (compiles nothing; ships as authored, no archive entry) | +| `hsaco` | one per KDP (list more with `UKDS`) | +| `embedded_source` | one per KDP (list more with `UKDS`; ships as authored, no archive entry) | -The kept UKD is the first by sorted `name`. The probe therefore packs the real -descriptors and compiles every distinct compile path once instead of every variant. -A production KDP whose UKDs share one builder keeps one UKD. +The kept UKD is the first of its group by sorted `name`, so a KDP whose UKDs share one +compile path keeps one UKD. -- **Variants inside a group are not compiled.** Breakage specific to one spec of a shared - builder is not caught; that is the accepted gap of the design. +- **Variants inside a group are not compiled** by the default sample. Breakage specific to + one spec of a shared builder is caught only if that UKD is listed in `UKDS`. - **The key is the UKD `name`, not its `id`.** Ids are `uuid4` values regenerated on every ingestor run; names encode the specialization and are unique. - **Arch matching follows the packer** (`arch_matches`, `_arch_subset_ok`): an empty or absent `arch` list is a wildcard. -- Derive also writes `expect.json` (kept KDP, UKD name and kind; pass-through kinds also carry the authored `kernel_source`) beside the root; the - assertion expects exactly that set. +- Derive also writes `expect.json` (kept KDP, UKD name and kind; pass-through kinds also + carry the authored `kernel_source`) beside the root; the assertion expects exactly that + set. - The derive step re-runs when any file under the root changes, and rewrites only files whose content differs, so a reconfigure leaves the pack stamp fresh. +### Running the probes locally + +From the repository root. The paths below are the workstation's; substitute your ROCm and +rocm_kpack locations. + +```bash +cmake --preset hipdnn-dev-all -GNinja -B build-probe \ + -DROCM_LIBS_ENABLE_COMPONENTS="hipdnn;hip-kernel-provider" \ + -DGPU_TARGETS=gfx942 \ + -DHIPKERNELPROVIDER_ENABLE_ROCKE=ON -DHIPDNN_ENABLE_KERNEL_INGESTOR=ON \ + -DHIPDNN_ENABLE_SDPA=ON \ + -DHIPKERNELPROVIDER_KPACK_PYTHON_DIR=/opt/rocm-kpack/python \ + -DCMAKE_PREFIX_PATH=/opt/rocm -DROCM_PATH=/opt/rocm \ + -DPython3_EXECUTABLE=/usr/bin/python3 -DPython_EXECUTABLE=/usr/bin/python3 \ + -DENABLE_CLANG_FORMAT=OFF -DENABLE_CLANG_TIDY=OFF -DROCM_LIBS_ENABLE_ROOT_CTEST=ON \ + -DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON +cmake --build build-probe --target hkp_packaging_probes # or the full build +ctest --test-dir build-probe -R '^hkp-probe-' --no-tests=error --output-on-failure \ + --output-junit probe-junit.xml +python3 .github/scripts/check_probe_junit.py \ + --junit build-probe/probe-junit.xml --manifest build-probe/hkp-probes/manifest.txt +``` + +The selector is `^hkp-probe-`, anchored: `-R` is an unanchored regex. It matches every +probe (`hkp-probe-`) and `hkp-probe-tools`, the pytest suite of the probe tooling +(`probes/tests/`). `--no-tests=error` makes an empty selection a failure instead of a +green run that proved nothing. `manifest.txt` is written at configure and lists the ctest +names that must run; the junit check fails unless every one ran and passed (a skipped, +disabled or missing entry fails), and ignores testcases not in the manifest, so it also +accepts the junit of a full ctest run. Configuring without the option and running the same +build and ctest lines fails (unknown target, then "No tests were found"). + +In CI both superbuild lanes configure with the option `ON`. The probe packs run in the +Build step as part of `all`; the probe ctest entries run once, inside the lane's normal +"Run tests" ctest, which writes `build/ctest-junit.xml`. The "Check packaging probes ran" +step then runs only `check_probe_junit.py` on that junit against the manifest. It runs +whenever "Run tests" ran, pass or fail. + +Locally on Linux the build is quick (the wheel environment plus the probed UKDs) and each +probe ctest entry takes a few seconds; `hkp-probe-tools` takes longer, since it really +compiles hip and rocke fixtures. There is no local Windows reproduction. + ### Assertion ids `tools/hkp_probe_assert.py` prints one line per failure, `hkp_probe_assert: FAIL : @@ -419,48 +442,52 @@ A production KDP whose UKDs share one builder keeps one UKD. | Id | Fails when | |---|---| -| `out-root-missing` | The probe output root does not exist: build `hkp_packaging_probes` first. | +| `out-root-missing` | The probe output root does not exist: build the probe pack target first. | | `stamp-missing` | The pack stamp is absent. | | `arch-dir-missing` / `extra-arch-dir` | `//` is absent, or another `gfx*` directory is present. | | `kpack-missing` / `kpack-empty` | Kpack output is expected and `//kpack/hip_kernel_provider_.kpack` is absent or empty. | -| `no-kdp` | No `*.kdp.json` under `/`. | +| `no-kdp` | No `*.kdp.json` under `/`, or one is unreadable or not a JSON object. | | `ukd-kind` | A shipped UKD's `kernel_source.kind` differs from what its expected kind's output type requires: `kpack` for kpack output, the authored kind for pass-through output. | -| `arch-field` | A KDP or UKD `arch` is not exactly the probe architecture (always required for a pass-through UKD). | +| `arch-field` | A shipped UKD's `arch` is not exactly `[]` (an absent `arch` fails too), or a KDP's `arch`, when present, is not exactly `[]`. | | `passthrough-source` | A pass-through UKD's `kernel_source` differs from the authored one recorded in `expect.json`. | | `passthrough-provenance` | A pass-through UKD's `provenance.source_label` is empty, or `provenance.source_file` differs from `kernel_source.source_file`. | -| `kpack-toc` | The archive has no entry for a UKD's `toc_key` and architecture. | +| `kpack-toc` | The archive is unreadable, or has no entry for a UKD's `toc_key` and architecture. | | `sha256` | The blob's sha256 differs from `kernel_source.sha256`. | -| `signature` | A UKD has an empty `signature`. | +| `signature` | A kpack UKD's `kernel_source.signature` is empty or not a list. | | `symbol` | A UKD's `symbol` does not appear in its blob. | | `provenance-origin` | `origin_kind` differs from the UKD's expected kind (the authored `kernel_source.kind`). | | `no-rules-for-kind` | A UKD's expected kind has no entry in `tools/hkp_probe_kinds.py`: a new producer kind must add its entry. | -| `provenance-wheel` | `rocke_wheel_sha256` is missing or empty: the wheel did not supply rocKE. | +| `provenance-wheel` | `rocke_wheel_sha256` is missing or empty: the packer recorded no rocKE wheel. | | `provenance-comgr` | The comgr recorded at pack time is not the expected library (or is empty). Catches a stale system comgr shadowing the intended one. | | `ukd-count` | The number of shipped UKDs found in the expected list (`expect.json`, written by derive) differs from its length, is zero, or a shipped UKD is not in the list. | -### Adding a probe - -Required for every new architecture or integration. **No CMake guard enforces this:** a -new integration without a probe configures, builds and passes CI, and its packaging for -an architecture the lanes do not build is then unchecked. - -1. Add one `hkp_add_packaging_probe(ARCH )` line to `probes/probes.cmake`. No - workflow edit: the superbuild lanes build `hkp_packaging_probes` and select - `^hkp-probe-`, and the manifest counts every declared probe. A new pack under an - architecture that is already probed needs no new line. -2. **The probe architecture must differ from the lane's `GPU_TARGETS`** (gfx942 on the - Linux job, gfx1151 on the Windows job). If they are equal, the architecture-wiring - mutation below becomes unobservable on that job. If a probe must use a lane's - architecture, change the job's `GPU_TARGETS` first. -3. Reconfigure and run the local block above; the new `hkp-probe-` entry and the - manifest line must appear. -4. Run the mutation checks below. Each must turn the probe red, and each edit must be - reverted afterwards. +### How to extend + +No CMake guard requires a probe: an integration without one configures, builds and passes +CI, and its packaging for an architecture the lanes do not build is then unchecked. + +- **New architecture:** one `hkp_add_packaging_probe(ARCH )` line. +- **New integration:** one line with `NAME ` and `UKDS` naming its + representative UKDs. +- **Another descriptor root:** one line with `ROOT `. +- **New producer kind:** the packer must support the `kernel_source.kind` first. Then one + `KINDS` entry in `tools/hkp_probe_kinds.py` (compile-group fields, provenance checks by + name, output type), one end-to-end test in `probes/tests/`, and the kinds list above. +- **New provenance check:** a function in `_PROVENANCE_CHECKS` (`tools/hkp_probe_assert.py`), + its name in the kind's `provenance`, a row in the assertion-id table and a test showing + the check fails on a mutated output. +- **New output type** (for example a runtime-compiled kind): an `_OUTPUT_CHECKS` entry plus + the derive and assert sites that branch on the output type (`expect.json` content, the + archive rule). This is a design decision, not an entry. + +After adding a probe line, reconfigure and run the local block above: the new +`hkp-probe-` entry and its manifest line must appear. Then run the mutation checks +below. ### Mutation checks when adding a probe -After each mutation: reconfigure, then build `hkp_packaging_probes`, then run the probe -ctest entry, then revert the edit. +Each mutation must turn the probe red. After each one: reconfigure, build the probe, run +its ctest entry, then revert the edit. ```bash cmake build-probe @@ -471,19 +498,21 @@ ctest --test-dir build-probe -R '^hkp-probe-$' --output-on-failure 1. **Derived descriptors reach the compiler.** - Replace the `ARCH` of the probe with an architecture no KDP in the root ships for: configure must fail (no KDP under the root ships for it). - - Add a standalone-UKD reference (a non-object `kernelDescriptors` entry) to a KDP that - ships for the probe architecture: configure must fail with + - Add a standalone-UKD reference (a non-object `kernelDescriptors` entry) to a KDP the + probe keeps: configure must fail with `standalone UKD references unsupported by probe derive`. - - Set a compile-affecting spec field of that UKD to a value the builder rejects (for - dense attention: `block_n` in `kernel_source.spec` from `64` to `7`): the build must - fail in the packer (`invalid spec ...`). - - Make a UKD `metadata` field disagree with the compiled specialization (`block_n` in - `metadata` from `64` to `128`): the build must fail with the pack-time agreement - error (`compiled specialization disagrees with metadata`). + - For a compiled kind, set a compile-affecting spec field of a probed UKD to a value + the compiler rejects: the build must fail in the packer. rocKE dense attention, for + example: `block_n` in `kernel_source.spec` from `64` to `7` fails with + `invalid spec ...`. + - Make a probed UKD's `metadata` disagree with the compiled specialization: the build + must fail with the pack-time agreement error. rocKE dense attention, for example: + `block_n` in `metadata` from `64` to `128` fails with + `compiled specialization disagrees with metadata`. - Also run `hip-kernel-provider-hkp-pack` on the mutated tree and record whether it stays green. For the gfx950 dense-attention probe it does, which is what makes the probe more than the existing test. -2. **The wheel supplies rocKE.** In `rocke/platform/pyproject.toml` change +2. **rocKE only: the wheel supplies rocKE.** In `rocke/platform/pyproject.toml` change `rocke = ["**/*.json"]` to `rocke = ["analysis/**/*.json"]`, so the built wheel lacks `core/arch/data/arch_specs.json`. The probe build must fail with `FileNotFoundError ... arch_specs.json` from the private wheel directory, while @@ -500,16 +529,17 @@ ctest --test-dir build-probe -R '^hkp-probe-$' --output-on-failure A fresh checkout (CI) has neither directory. 3. **The architecture and outputs are wired.** In `cmake/HkpPackagingProbes.cmake`: - - Replace the `ARCHES` argument of the pack target with the lane's architecture - (`gfx942`): the build must fail with `produced nothing: no KDP survived arch pruning`. + - Replace the `ARCHES` argument of the pack target with an architecture no KDP in the + root ships for: the build must fail with + `produced nothing: no KDP survived arch pruning`. - Replace it with an empty string: the build succeeds, and the ctest entry must fail `hkp_probe_assert: FAIL arch-dir-missing`. - After a good build, delete `build-probe/hkp-probes//out/.hkp-packed.stamp`: the ctest entry must fail `hkp_probe_assert: FAIL stamp-missing`. The wheel, architecture-wiring, spec and metadata mutations above turned the gfx950 probe -red when run locally on Linux. The Windows half of the wheel check, and comgr forwarding (covered only by -`provenance-comgr`), are verified by CI only. +red when run locally on Linux. The Windows half of the wheel check, and comgr forwarding +(covered only by `provenance-comgr`), are verified by CI only. ## Running the tests diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake index 825e9ccf3172..f2db8b7ba962 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -34,25 +34,34 @@ function(_hkp_probe_check_out_root name out_root) endfunction() # --------------------------------------------------------------------------- -# _hkp_probe_derive_root( ) +# _hkp_probe_derive_root( [...]) # Derive the probe root at configure time (FATAL with the tool's stderr when the root -# cannot be derived, e.g. a KDP for references a standalone UKD) and re-run -# configure when a source file changes. The tool writes /../expect.json, the -# list of kept UKDs the assertion expects; receives its path. FATAL when -# that list is empty: no KDP under ships for , so the pack would -# prune everything. Deriving is idempotent, so a reconfigure leaves the derived root's -# mtimes alone and does not re-pack. +# cannot be derived, e.g. a kept KDP references a standalone UKD, or a listed UKD is +# kept nowhere) and re-run configure when a source file or the derive tooling +# changes. Each is passed as one --ukd; with none, derive keeps one UKD per +# compile group. The tool writes /../expect.json, the list of kept UKDs the +# assertion expects; receives its path. FATAL when that list is empty: no +# KDP under ships for , so the pack would prune everything. Deriving +# is idempotent, so a reconfigure leaves the derived root's mtimes alone and does not +# re-pack. # --------------------------------------------------------------------------- function(_hkp_probe_derive_root name from_dir arch out_dir expect_var) file(GLOB_RECURSE _derive_inputs CONFIGURE_DEPENDS "${from_dir}/*") set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND - PROPERTY CMAKE_CONFIGURE_DEPENDS ${_derive_inputs}) + PROPERTY CMAKE_CONFIGURE_DEPENDS ${_derive_inputs} + "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" + "${HKP_PKG_DIR}/tools/hkp_probe_kinds.py") + set(_ukd_args "") + foreach(_ukd IN LISTS ARGN) + list(APPEND _ukd_args --ukd "${_ukd}") + endforeach() execute_process( COMMAND "${Python3_EXECUTABLE}" "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" --from "${from_dir}" --arch "${arch}" --out "${out_dir}" + ${_ukd_args} RESULT_VARIABLE _derive_rc OUTPUT_VARIABLE _derive_out ERROR_VARIABLE _derive_err) @@ -76,26 +85,82 @@ function(_hkp_probe_derive_root name from_dir arch out_dir expect_var) endfunction() # --------------------------------------------------------------------------- -# hkp_add_packaging_probe(ARCH [ROOT ] [NAME ] [PACK_JOBS ]) +# _hkp_probe_check_name() +# FATAL unless is usable as a target, ctest and directory name, is not the +# reserved `tools`, is not declared yet, and the call comes from a probes file loaded +# by hkp_load_packaging_probes(). +# --------------------------------------------------------------------------- +function(_hkp_probe_check_name name) + if(NOT name MATCHES "^[A-Za-z0-9_.+-]+$" OR name MATCHES "^\\.+$") + message(FATAL_ERROR + "hkp probe '${name}': NAME must match [A-Za-z0-9_.+-]+ and not be " + "dots only (it names a target, a ctest entry and a build directory); " + "pass NAME explicitly.") + endif() + if(name STREQUAL "tools") + message(FATAL_ERROR + "hkp probe '${name}': NAME 'tools' is reserved for the probe " + "tooling test hkp-probe-tools.") + endif() + get_property(_toolchain_set GLOBAL PROPERTY HKP_PROBE_TOOLCHAIN_SET) + if(NOT _toolchain_set) + message(FATAL_ERROR + "hkp probe '${name}': hkp_add_packaging_probe is only valid from a " + "probes file loaded by hkp_load_packaging_probes().") + endif() + get_property(_names GLOBAL PROPERTY HKP_PROBE_NAMES) + if(name IN_LIST _names) + message(FATAL_ERROR "hkp probe '${name}' is declared twice.") + endif() +endfunction() + +# --------------------------------------------------------------------------- +# _hkp_probe_check_ukds( [...]) +# FATAL when a UKDS entry is empty or listed twice. +# --------------------------------------------------------------------------- +function(_hkp_probe_check_ukds name) + set(_seen "") + foreach(_ukd IN LISTS ARGN) + if(_ukd STREQUAL "") + message(FATAL_ERROR "hkp probe '${name}': UKDS holds an empty UKD name.") + endif() + if(_ukd IN_LIST _seen) + message(FATAL_ERROR + "hkp probe '${name}': UKDS lists '${_ukd}' more than once.") + endif() + list(APPEND _seen "${_ukd}") + endforeach() +endfunction() + +# --------------------------------------------------------------------------- +# hkp_add_packaging_probe(ARCH [ROOT ] [NAME ] [UKDS ...] +# [PACK_JOBS ]) # Declare one probe: pack a descriptor root for ARCH and assert the output. # # ROOT defaults to the production descriptors, HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_ -# SOURCE_ROOT. It is copied at configure time into the build tree with every KDP that -# ships for ARCH trimmed to one UKD per compile group (see hkp_probe_derive_root.py), -# so the probe packs the real descriptors but compiles each distinct compile path -# once. The assertion expects exactly the kept UKDs, each with the provenance of its -# producer kind (see hkp_probe_assert.py). Adding a pack under an already probed ARCH -# needs no new declaration. +# SOURCE_ROOT; a relative ROOT is resolved against the directory of the probes file. +# It is copied at configure time into the build tree with every KDP that ships for +# ARCH trimmed (see hkp_probe_derive_root.py). Without UKDS, each such KDP keeps one +# UKD per compile group, so the probe packs the real descriptors but compiles each +# distinct compile path once. With UKDS, the probe packs exactly the listed UKDs (by +# UKD name): KDPs keeping none of them are left out of the derived root, and +# configure fails when a listed name is kept by no KDP shipping for ARCH. The +# assertion expects exactly the kept UKDs, each with the provenance of its producer +# kind (see hkp_probe_assert.py). Adding a pack under an already probed ARCH needs no +# new declaration unless the probe lists UKDS. # -# NAME defaults to for the production root and _ for -# another ROOT. Creates pack target hkp_packaging_probe_ (stamp and output under +# ARCH must match gfx[0-9a-z]+. NAME defaults to for the production root and +# _ for another ROOT; it must match [A-Za-z0-9_.+-]+, must not be +# dots only, and must not be `tools` (hkp-probe-tools is the probe tooling test). +# UKDS entries must be non-empty and unique. Creates pack target +# hkp_packaging_probe_ (stamp and output under # ${CMAKE_BINARY_DIR}/hkp-probes//out) and ctest entry hkp-probe-. The -# pack target is part of `all` (hkp_wire_pack_target declares it so), as is the -# aggregate hkp_packaging_probes. Only callable from a probes file loaded by -# hkp_load_packaging_probes(). +# pack targets are part of `all` (hkp_wire_pack_target declares them so); the +# aggregate hkp_packaging_probes builds every probe's pack. Only callable from a +# probes file loaded by hkp_load_packaging_probes(). # --------------------------------------------------------------------------- function(hkp_add_packaging_probe) - cmake_parse_arguments(PARSE_ARGV 0 ARG "" "ARCH;ROOT;NAME;PACK_JOBS" "") + cmake_parse_arguments(PARSE_ARGV 0 ARG "" "ARCH;ROOT;NAME;PACK_JOBS" "UKDS") if(ARG_UNPARSED_ARGUMENTS) message(FATAL_ERROR @@ -104,31 +169,34 @@ function(hkp_add_packaging_probe) if(NOT ARG_ARCH) message(FATAL_ERROR "hkp probe: hkp_add_packaging_probe requires ARCH.") endif() + if(NOT ARG_ARCH MATCHES "^gfx[0-9a-z]+$") + message(FATAL_ERROR + "hkp probe: ARCH '${ARG_ARCH}' is not one architecture of the form " + "gfx[0-9a-z]+.") + endif() if(NOT ARG_ROOT) set(ARG_ROOT "${HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT}") set(_default_name "${ARG_ARCH}") else() + # A function sees the CMAKE_CURRENT_LIST_DIR of its caller: the probes file. + get_filename_component(ARG_ROOT "${ARG_ROOT}" ABSOLUTE + BASE_DIR "${CMAKE_CURRENT_LIST_DIR}") get_filename_component(_root_base "${ARG_ROOT}" NAME) set(_default_name "${_root_base}_${ARG_ARCH}") endif() if(NOT ARG_NAME) set(ARG_NAME "${_default_name}") endif() + _hkp_probe_check_name("${ARG_NAME}") if(NOT IS_DIRECTORY "${ARG_ROOT}") message(FATAL_ERROR "hkp probe '${ARG_NAME}': ROOT is not a directory: '${ARG_ROOT}'") endif() - - get_property(_toolchain_set GLOBAL PROPERTY HKP_PROBE_TOOLCHAIN_SET) - if(NOT _toolchain_set) + if("UKDS" IN_LIST ARG_KEYWORDS_MISSING_VALUES) message(FATAL_ERROR - "hkp probe '${ARG_NAME}': hkp_add_packaging_probe is only valid from a " - "probes file loaded by hkp_load_packaging_probes().") - endif() - get_property(_names GLOBAL PROPERTY HKP_PROBE_NAMES) - if(ARG_NAME IN_LIST _names) - message(FATAL_ERROR "hkp probe '${ARG_NAME}' is declared twice.") + "hkp probe '${ARG_NAME}': UKDS needs at least one UKD name.") endif() + _hkp_probe_check_ukds("${ARG_NAME}" "${ARG_UKDS}") get_property(_kpack_dir GLOBAL PROPERTY HKP_PROBE_ROCM_KPACK_DIR) get_property(_hipcc GLOBAL PROPERTY HKP_PROBE_HIPCC) @@ -142,7 +210,7 @@ function(hkp_add_packaging_probe) set(_root "${_probe_dir}/root") _hkp_probe_derive_root("${ARG_NAME}" "${ARG_ROOT}" "${ARG_ARCH}" "${_root}" - _expect_file) + _expect_file ${ARG_UKDS}) set(_pack_jobs 1) if(ARG_PACK_JOBS) @@ -193,6 +261,8 @@ function(hkp_load_packaging_probes rocm_kpack_dir hipcc rocke_comgr_lib) set_property(GLOBAL PROPERTY HKP_PROBE_TOOLCHAIN_SET TRUE) include("${HKP_PKG_DIR}/probes/probes.cmake") + # Calls from anywhere but the probes file hit the guard in hkp_add_packaging_probe. + set_property(GLOBAL PROPERTY HKP_PROBE_TOOLCHAIN_SET FALSE) get_property(_names GLOBAL PROPERTY HKP_PROBE_NAMES) if(NOT _names) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake index 7e91b2791d0d..f84c9fed75f3 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake @@ -1,13 +1,14 @@ # Copyright © Advanced Micro Devices, Inc., or its affiliates. # SPDX-License-Identifier: MIT # -# Packaging probes, one line per architecture. Loaded by hkp_load_packaging_probes() -# under HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES; see HkpPackagingProbes.cmake for the -# arguments. +# Packaging probes. Loaded by hkp_load_packaging_probes() under +# HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES; see HkpPackagingProbes.cmake for the +# arguments and descriptor-packaging/README.md, "Packaging probes", for usage. # -# A probe packs the production descriptors for ARCH, one instance per compile group, -# independent of the build's GPU_TARGETS. Probe an architecture the lane does not build -# itself: the lane's own architectures are covered in full by the normal build. A new -# architecture is covered by adding one line here. +# A probe packs a descriptor root for ARCH, independent of the build's GPU_TARGETS; ARCH +# may be any architecture. Without UKDS it packs one UKD per compile group of every KDP +# shipping for ARCH, so a new pack under ARCH needs no new line. An integration adds its +# own line, hkp_add_packaging_probe(ARCH NAME UKDS ...), +# naming a representative set of UKDs that is packed on every PR. hkp_add_packaging_probe(ARCH gfx950) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py similarity index 71% rename from dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py rename to dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py index cd0d6666fc4f..8408b2f84334 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/conftest.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py @@ -1,5 +1,14 @@ +"""Paths, helpers and fixtures shared by the probe tooling tests. + +There is no conftest.py here: tests/ imports its own conftest by module name, and a +second conftest.py would replace it when both directories are collected in one run. +Test modules import the fixtures they use from this module. +""" + import hashlib +import json import os +import shutil import subprocess import sys from pathlib import Path @@ -102,6 +111,13 @@ def pack_root(src, work, rocm_kpack_dir, hipcc, comgr_lib): return out +@pytest.fixture(scope="module") +def packed_root(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): + """The rocke fixture packed once for ARCH.""" + work = tmp_path_factory.mktemp("probe_pack") + return pack_root(ROCKE_FIXTURE, work, rocm_kpack_dir, hipcc, comgr_lib) + + def run_assert(out_root, expect_path, rocm_kpack_dir, expect_comgr=None): cmd = [ sys.executable, @@ -122,8 +138,38 @@ def run_assert(out_root, expect_path, rocm_kpack_dir, expect_comgr=None): return subprocess.run(cmd, capture_output=True, text=True) -@pytest.fixture(scope="module") -def packed_root(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): - """The rocke fixture packed once for ARCH.""" - work = tmp_path_factory.mktemp("probe_pack") - return pack_root(ROCKE_FIXTURE, work, rocm_kpack_dir, hipcc, comgr_lib) +def run_derive(src, arch, out, ukds=()): + """Run the derive tool; one --ukd per entry of `ukds`.""" + cmd = [ + sys.executable, + str(PROBE_DERIVE), + "--from", + str(src), + "--arch", + arch, + "--out", + str(out), + ] + for name in ukds: + cmd += ["--ukd", name] + return subprocess.run(cmd, capture_output=True, text=True, check=False) + + +def hip_source(dst): + """The main fixture without its standalone UKD, which derive refuses.""" + shutil.copytree(MAIN_FIXTURE, dst) + (dst / "pointwise_add_b128.ukd.json").unlink() + kdp = json.loads((dst / "pointwise.kdp.json").read_text()) + kdp["kernelDescriptors"] = [ + u for u in kdp["kernelDescriptors"] if isinstance(u, dict) + ] + (dst / "pointwise.kdp.json").write_text(json.dumps(kdp, indent=2)) + return dst + + +def derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib, ukds=()): + """Derive `src` for ARCH, pack the derived root; return (out root, expect path).""" + r = run_derive(src, ARCH, work / "probe" / "root", ukds) + assert r.returncode == 0, r.stderr + out = pack_root(work / "probe" / "root", work, rocm_kpack_dir, hipcc, comgr_lib) + return out, work / "probe" / "expect.json" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py index 4f3f15c80af7..4046c0474a7d 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py @@ -3,7 +3,15 @@ import pytest -from conftest import ARCH, STAMP_NAME, run_assert +from probe_support import ( # noqa: F401 (pytest fixtures) + ARCH, + STAMP_NAME, + comgr_lib, + hipcc, + packed_root, + rocm_kpack_dir, + run_assert, +) KDP = f"{ARCH}/attention.kdp.json" KDP_IN_ARCH_DIR = "attention.kdp.json" @@ -128,6 +136,14 @@ def test_ukd_count_mismatch(tree): _assert_fails(tree.run(), "ukd-count") +def test_ukd_shipped_twice_fails_count(tree): + # Every shipped UKD is in the expect list, but one ships twice: matched > expected. + tree.mutate_kdp( + lambda kdp: kdp["kernelDescriptors"].append(kdp["kernelDescriptors"][0]) + ) + _assert_fails(tree.run(), "ukd-count") + + def test_ukd_count_zero_is_failure(tree): tree.mutate_kdp(lambda kdp: kdp.update(kernelDescriptors=[])) tree.write_expect([]) @@ -152,6 +168,11 @@ def test_arch_field_ukd(tree): _assert_fails(tree.run(), "arch-field") +def test_arch_field_ukd_absent(tree): + tree.mutate_ukd(lambda u: u.pop("arch")) + _assert_fails(tree.run(), "arch-field") + + def test_arch_field_kdp(tree): tree.mutate_kdp(lambda kdp: kdp.update(arch=[ARCH, "gfx942"])) _assert_fails(tree.run(), "arch-field") @@ -243,6 +264,31 @@ def test_corrupt_kpack_reports_toc_failure(tree): assert "Traceback" not in result.stderr +def test_non_object_kdp_reports_no_kdp(tree): + (tree.root / KDP).write_text("[]", encoding="utf-8") + result = tree.run() + _assert_fails(result, "no-kdp") + assert "Traceback" not in result.stderr + + +def test_duplicate_expect_entry_exits_2(tree): + tree.write_expect([tree.entry(), tree.entry()]) + result = tree.run() + assert result.returncode == 2, result.stderr + assert "Traceback" not in result.stderr + + +def test_rocm_kpack_not_importable_exits_2(tree, tmp_path): + # A rocm_kpack package without the kpack module: the import fails whatever the + # interpreter has installed, because --kpack-python-dir is searched first. + (tmp_path / "fake" / "rocm_kpack").mkdir(parents=True) + (tmp_path / "fake" / "rocm_kpack" / "__init__.py").write_text("") + result = run_assert(tree.root, tree.expect_path, str(tmp_path / "fake")) + assert result.returncode == 2, result.stderr + assert "cannot import rocm_kpack" in result.stderr + assert "Traceback" not in result.stderr + + def test_malformed_kdp_json_reports_no_kdp(tree): (tree.root / KDP).write_text("{ not json", encoding="utf-8") result = tree.run() diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py index 974704dc812a..ab806be8966f 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py @@ -11,40 +11,26 @@ import pytest -from conftest import MAIN_FIXTURE, PROBE_DERIVE - - -def _run(src: Path, arch: str, out: Path): - return subprocess.run( - [ - sys.executable, - str(PROBE_DERIVE), - "--from", - str(src), - "--arch", - arch, - "--out", - str(out), - ], - capture_output=True, - text=True, - check=False, - ) +from probe_support import MAIN_FIXTURE, PROBE_DERIVE, run_derive + +def _run(src: Path, arch: str, out: Path, ukds=()): + return run_derive(src, arch, out, ukds) -def _derive(src: Path, arch: str, out: Path): - r = _run(src, arch, out) + +def _derive(src: Path, arch: str, out: Path, ukds=()): + r = _run(src, arch, out, ukds) assert r.returncode == 0, r.stderr return json.loads((out.parent / "expect.json").read_text()) -def _rocke(name, builder="b1", arch=None, **extra): +def _rocke(name, builder="b1", arch=None, source="s.py"): ukd = { "id": f"id-{name}", "name": name, "kernel_source": { "kind": "rocke", - "source": "s.py", + "source": source, "builder": builder, "spec": {"n": len(name)}, }, @@ -113,6 +99,17 @@ def test_one_ukd_per_compile_group(tmp_path): assert sorted(_kept(tmp_path / "w" / "root")) == sorted(e["name"] for e in expect) +def test_rocke_group_key_includes_source(tmp_path): + # The same builder name in two modules is two compile paths. + ukds = [ + _rocke("r_a", "b1", source="common.py"), + _rocke("r_b", "b1", source="arch.py"), + ] + _write_root(tmp_path / "src", ukds) + expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") + assert sorted(e["name"] for e in expect) == ["r_a", "r_b"] + + def test_hsaco_compiles_nothing_so_one_per_kdp(tmp_path): ukds = [_other("hsaco", "z_hsaco"), _other("hsaco", "y_hsaco"), _rocke("r")] _write_root(tmp_path / "src", ukds) @@ -156,6 +153,9 @@ def test_passthrough_kind_expect_carries_authored_kernel_source(tmp_path): def test_pick_is_independent_of_authored_order(tmp_path): ukds = [_rocke(n) for n in ("m", "c", "x", "a2", "a1")] + # Ids sort opposite to names, so a pick by id would choose differently. + for i, u in enumerate(sorted(ukds, key=lambda u: u["name"])): + u["id"] = f"id-{len(ukds) - i}" for i, order in enumerate(itertools.permutations(ukds, 3)): src, out = tmp_path / f"src{i}", tmp_path / f"w{i}" / "root" _write_root(src, order) @@ -214,6 +214,18 @@ def test_expect_records_kdp_relative_path(tmp_path): assert expect == [{"kdp": "sub/dir/x.kdp.json", "name": "a", "kind": "rocke"}] +def test_dot_prefixed_kdp_is_copied_but_not_expected(tmp_path): + # The packer skips dot-prefixed paths, so derive must not expect their UKDs. + src = tmp_path / "src" + _write_root(src, [_rocke("a")]) + _write_root(src, [_rocke("hidden")], kdp_file=".hidden/x.kdp.json") + expect = _derive(src, "gfx950", tmp_path / "w" / "root") + assert [e["name"] for e in expect] == ["a"] + assert (tmp_path / "w/root/.hidden/x.kdp.json").read_bytes() == ( + src / ".hidden/x.kdp.json" + ).read_bytes() + + # --- configure-time refusals (exit 2, nothing written) ---------------------- def _refused(tmp_path, ukds, message, kdp_arch=("gfx950",)): _write_root(tmp_path / "src", ukds, kdp_arch=kdp_arch) @@ -266,6 +278,19 @@ def test_unsupported_kind_for_other_arch_is_ignored(tmp_path): ] == ["a"] +@pytest.mark.parametrize( + "ukd, field", + [ + ({**_hip("h"), "kernel_source": {"kind": "hip", "source": "a.cpp"}}, "build"), + ({**_rocke("r"), "kernel_source": {"kind": "rocke", "builder": "b"}}, "source"), + ], + ids=["hip-build", "rocke-source"], +) +def test_group_key_field_absent_exits_2(tmp_path, ukd, field): + # Absent is not null: a typo in a group_by field would otherwise merge groups. + _refused(tmp_path, [ukd], f"has no kernel_source field {field!r}") + + def test_ukd_arch_outside_kdp_arch_exits_2(tmp_path): # The packer rejects this root; derive must not hide it by dropping the UKD. _refused(tmp_path, [_rocke("a", arch=["gfx942"])], "not a subset of the KDP arch") @@ -285,6 +310,104 @@ def test_malformed_kdp_exits_2(tmp_path): assert "Traceback" not in r.stderr +# --- --ukd: keep exactly the listed UKDs ------------------------------------- +def test_ukd_list_keeps_exactly_the_listed_ukds(tmp_path): + # Several of one compile group, and a pick the default mode would not make. + ukds = [_rocke("r_a"), _rocke("r_b"), _rocke("r_c"), _hip("h_a"), _hip("h_b")] + _write_root(tmp_path / "src", ukds) + out = tmp_path / "w" / "root" + expect = _derive(tmp_path / "src", "gfx950", out, ukds=["r_c", "r_b", "h_b"]) + assert [(e["kind"], e["name"]) for e in expect] == [ + ("rocke", "r_b"), + ("rocke", "r_c"), + ("hip", "h_b"), + ] + assert _kept(out) == ["r_b", "r_c", "h_b"] + + +def test_ukd_list_omits_kdps_keeping_nothing(tmp_path): + src, out = tmp_path / "src", tmp_path / "w" / "root" + _write_root(src, [_rocke("a")], kdp_file="keep.kdp.json") + _write_root(src, [_rocke("b")], kdp_file="drop.kdp.json") + # Derived once without --ukd, so the omission must also remove the stale copy. + _derive(src, "gfx950", out) + expect = _derive(src, "gfx950", out, ukds=["a"]) + assert [(e["kdp"], e["name"]) for e in expect] == [("keep.kdp.json", "a")] + assert not (out / "drop.kdp.json").exists() + assert (out / "x.kmd.json").is_file() + + +def test_ukd_list_copies_kdps_not_shipping_for_arch(tmp_path): + src, out = tmp_path / "src", tmp_path / "w" / "root" + _write_root(src, [_rocke("a")], kdp_file="keep.kdp.json") + _write_root(src, [_rocke("b")], kdp_arch=("gfx942",), kdp_file="other.kdp.json") + _derive(src, "gfx950", out, ukds=["a"]) + copied = (out / "other.kdp.json").read_bytes() + assert copied == (src / "other.kdp.json").read_bytes() + + +def _ukd_refused(tmp_path, ukds, listed, message): + _write_root(tmp_path / "src", ukds, kdp_arch=("gfx942", "gfx950")) + r = _run(tmp_path / "src", "gfx950", tmp_path / "w" / "root", ukds=listed) + assert r.returncode == 2, r.stderr + assert r.stderr.startswith("hkp_probe_derive:") + assert message in r.stderr + assert not (tmp_path / "w").exists() + + +def test_ukd_list_unknown_name_exits_2(tmp_path): + _ukd_refused(tmp_path, [_rocke("a")], ["a", "nope"], "['nope'] matched no") + + +def test_ukd_list_name_only_for_other_arch_exits_2(tmp_path): + ukds = [_rocke("a"), _rocke("b", arch=["gfx942"])] + _ukd_refused(tmp_path, ukds, ["a", "b"], "['b'] matched no") + + +def test_ukd_list_name_only_in_kdp_for_other_arch_exits_2(tmp_path): + src = tmp_path / "src" + _write_root(src, [_rocke("a")], kdp_file="keep.kdp.json") + _write_root(src, [_rocke("b")], kdp_arch=("gfx942",), kdp_file="other.kdp.json") + r = _run(src, "gfx950", tmp_path / "w" / "root", ukds=["a", "b"]) + assert r.returncode == 2, r.stderr + assert "['b'] matched no" in r.stderr + + +def test_ukd_list_duplicate_entry_exits_2(tmp_path): + _ukd_refused(tmp_path, [_rocke("a")], ["a", "a"], "['a'] more than once") + + +def test_ukd_list_ignores_standalone_ref_in_kdp_keeping_nothing(tmp_path): + src = tmp_path / "src" + _write_root(src, [_rocke("a")], kdp_file="keep.kdp.json") + _write_root(src, [_rocke("b"), "ukd-standalone-id"], kdp_file="drop.kdp.json") + expect = _derive(src, "gfx950", tmp_path / "w" / "root", ukds=["a"]) + assert [e["name"] for e in expect] == ["a"] + + +def test_ukd_list_standalone_ref_in_kept_kdp_exits_2(tmp_path): + _ukd_refused( + tmp_path, + [_rocke("a"), "ukd-standalone-id"], + ["a"], + "standalone UKD references unsupported by probe derive", + ) + + +def test_ukd_list_unregistered_kind_of_kept_ukd_exits_2(tmp_path): + _ukd_refused( + tmp_path, [_rocke("a"), _other("mystery", "m")], ["m"], "has no probe support" + ) + + +def test_ukd_list_ignores_unlisted_ukds_of_kept_kdp(tmp_path): + # Neither the kind nor the group key of an unlisted UKD matters. + ukds = [_rocke("a"), _other("mystery", "m"), _other("hip", "h_no_build")] + _write_root(tmp_path / "src", ukds) + expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root", ukds=["a"]) + assert [e["name"] for e in expect] == ["a"] + + # --- idempotence ------------------------------------------------------------ def _stamp_old(out: Path): for p in [*out.rglob("*"), out.parent / "expect.json"]: @@ -324,12 +447,12 @@ def test_rederive_follows_source_changes(tmp_path): (out / "stale.txt").write_text("stale") (out / "stale_dir").mkdir() (out / "stale_dir" / "f.json").write_text("{}") - (src / "x.kmd.json").write_text('{"id": "kmd2"}\n') _write_root(src, [_rocke("b"), _rocke("0first")]) + (src / "x.kmd.json").write_text('{"id": "kmd2"}\n') expect = _derive(src, "gfx950", out) assert [e["name"] for e in expect] == ["0first"] assert _kept(out) == ["0first"] - assert (out / "x.kmd.json").read_text() == '{"id": "kmd"}\n' + assert (out / "x.kmd.json").read_text() == '{"id": "kmd2"}\n' assert sorted(p.name for p in out.rglob("*")) == ["x.kdp.json", "x.kmd.json"] diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py index 1dad2341b253..16470c04467d 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py @@ -7,19 +7,19 @@ import json import shutil -import subprocess -import sys import pytest -from conftest import ( +from probe_support import ( # noqa: F401 (pytest fixtures) ARCH, EMPTY_ARCH_FIXTURE, HSACO_FIXTURES, - MAIN_FIXTURE, - PROBE_DERIVE, ROCKE_FIXTURE, - pack_root, + comgr_lib, + derive_and_pack, + hip_source, + hipcc, + rocm_kpack_dir, run_assert, ) @@ -36,18 +36,6 @@ ] -def _hip_source(dst): - """The main fixture without its standalone UKD, which derive refuses.""" - shutil.copytree(MAIN_FIXTURE, dst) - (dst / "pointwise_add_b128.ukd.json").unlink() - kdp = json.loads((dst / "pointwise.kdp.json").read_text()) - kdp["kernelDescriptors"] = [ - u for u in kdp["kernelDescriptors"] if isinstance(u, dict) - ] - (dst / "pointwise.kdp.json").write_text(json.dumps(kdp, indent=2)) - return dst - - def _hsaco_source(dst, arch=ARCH): """empty_arch's single UKD rewritten to an authored hsaco for `arch`.""" shutil.copytree(EMPTY_ARCH_FIXTURE, dst) @@ -65,32 +53,11 @@ def _hsaco_source(dst, arch=ARCH): return dst -def _derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib): - derived = work / "probe" / "root" - r = subprocess.run( - [ - sys.executable, - str(PROBE_DERIVE), - "--from", - str(src), - "--arch", - "gfx950", - "--out", - str(derived), - ], - capture_output=True, - text=True, - ) - assert r.returncode == 0, r.stderr - out = pack_root(derived, work, rocm_kpack_dir, hipcc, comgr_lib) - return out, work / "probe" / "expect.json" - - @pytest.fixture(scope="module") def hip_probe(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): work = tmp_path_factory.mktemp("hip_probe") - return _derive_and_pack( - _hip_source(work / "src"), work, rocm_kpack_dir, hipcc, comgr_lib + return derive_and_pack( + hip_source(work / "src"), work, rocm_kpack_dir, hipcc, comgr_lib ) @@ -98,9 +65,9 @@ def hip_probe(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): def mixed_probe(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): work = tmp_path_factory.mktemp("mixed_probe") src = work / "src" - _hip_source(src / "hip") + hip_source(src / "hip") shutil.copytree(ROCKE_FIXTURE, src / "rocke") - return _derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib) + return derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib) def test_hip_root_passes(hip_probe, rocm_kpack_dir): @@ -110,29 +77,6 @@ def test_hip_root_passes(hip_probe, rocm_kpack_dir): assert result.returncode == 0, result.stderr -def test_hip_root_with_broken_hip_source_does_not_pack( - tmp_path, rocm_kpack_dir, hipcc, comgr_lib -): - src = _hip_source(tmp_path / "src") - (src / "PointwiseAdd.cpp").write_text("this is not C++\n") - derived = tmp_path / "probe" / "root" - subprocess.run( - [ - sys.executable, - str(PROBE_DERIVE), - "--from", - str(src), - "--arch", - "gfx950", - "--out", - str(derived), - ], - check=True, - ) - with pytest.raises(pytest.fail.Exception, match="hkp_pack failed"): - pack_root(derived, tmp_path, rocm_kpack_dir, hipcc, comgr_lib) - - def test_mixed_root_passes_with_per_ukd_kinds(mixed_probe, rocm_kpack_dir, comgr_lib): out, expect = mixed_probe entries = json.loads(expect.read_text()) @@ -160,7 +104,7 @@ def test_mixed_root_origin_is_checked_per_ukd(mixed_probe, rocm_kpack_dir, tmp_p def test_hsaco_root_passes(tmp_path, rocm_kpack_dir, hipcc, comgr_lib): src = _hsaco_source(tmp_path / "src") - out, expect = _derive_and_pack(src, tmp_path, rocm_kpack_dir, hipcc, comgr_lib) + out, expect = derive_and_pack(src, tmp_path, rocm_kpack_dir, hipcc, comgr_lib) entries = json.loads(expect.read_text()) assert [(e["kdp"], e["kind"]) for e in entries] == [("solo.kdp.json", "hsaco")] result = run_assert(out, expect, rocm_kpack_dir) @@ -169,9 +113,9 @@ def test_hsaco_root_passes(tmp_path, rocm_kpack_dir, hipcc, comgr_lib): def test_hip_and_hsaco_mixed_root_passes(tmp_path, rocm_kpack_dir, hipcc, comgr_lib): src = tmp_path / "src" - _hip_source(src / "hip") + hip_source(src / "hip") _hsaco_source(src / "hsaco") - out, expect = _derive_and_pack(src, tmp_path, rocm_kpack_dir, hipcc, comgr_lib) + out, expect = derive_and_pack(src, tmp_path, rocm_kpack_dir, hipcc, comgr_lib) kinds = {e["kdp"]: e["kind"] for e in json.loads(expect.read_text())} assert kinds == { "hip/pointwise.kdp.json": "hip", @@ -188,9 +132,29 @@ def test_hsaco_ukd_for_other_arch_is_not_packed_or_expected( # gfx942-only hsaco beside a gfx950 hip root: derive ignores it, the pack ships # nothing for it, and the assertion still passes on the hip UKDs alone. src = tmp_path / "src" - _hip_source(src / "hip") + hip_source(src / "hip") _hsaco_source(src / "hsaco", arch="gfx942") - out, expect = _derive_and_pack(src, tmp_path, rocm_kpack_dir, hipcc, comgr_lib) + out, expect = derive_and_pack(src, tmp_path, rocm_kpack_dir, hipcc, comgr_lib) assert {e["kind"] for e in json.loads(expect.read_text())} == {"hip"} result = run_assert(out, expect, rocm_kpack_dir) assert result.returncode == 0, result.stderr + + +# --- UKDS: the probe packs exactly the listed UKDs ---------------------------- +# Not the default pick of its group (that is "PointwiseAdd f32 block64"), and both in +# pointwise.kdp.json, so pointwise_wild.kdp.json keeps nothing and is left out. +_LISTED = ["PointwiseMul f32 block64", "PointwiseAdd f32 block64"] + + +def test_listed_ukds_pack_and_pass(tmp_path, rocm_kpack_dir, hipcc, comgr_lib): + src = hip_source(tmp_path / "src") + out, expect = derive_and_pack( + src, tmp_path, rocm_kpack_dir, hipcc, comgr_lib, ukds=_LISTED + ) + entries = json.loads(expect.read_text()) + assert sorted((e["kdp"], e["name"]) for e in entries) == sorted( + ("pointwise.kdp.json", n) for n in _LISTED + ) + assert not (out / ARCH / "pointwise_wild.kdp.json").exists() + result = run_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 0, result.stderr diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_passthrough.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_passthrough.py index 05b0d86e5cae..215e313312f7 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_passthrough.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_passthrough.py @@ -6,17 +6,17 @@ import json import shutil -import subprocess -import sys import pytest -from conftest import ( +from probe_support import ( # noqa: F401 (pytest fixtures) ARCH, EMPTY_ARCH_FIXTURE, - MAIN_FIXTURE, - PROBE_DERIVE, - pack_root, + comgr_lib, + derive_and_pack, + hip_source, + hipcc, + rocm_kpack_dir, run_assert, ) @@ -37,38 +37,6 @@ def _embedded_source(dst): return dst -def _hip_source(dst): - shutil.copytree(MAIN_FIXTURE, dst) - (dst / "pointwise_add_b128.ukd.json").unlink() - kdp = json.loads((dst / "pointwise.kdp.json").read_text()) - kdp["kernelDescriptors"] = [ - u for u in kdp["kernelDescriptors"] if isinstance(u, dict) - ] - (dst / "pointwise.kdp.json").write_text(json.dumps(kdp, indent=2)) - return dst - - -def _derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib): - derived = work / "probe" / "root" - r = subprocess.run( - [ - sys.executable, - str(PROBE_DERIVE), - "--from", - str(src), - "--arch", - ARCH, - "--out", - str(derived), - ], - capture_output=True, - text=True, - ) - assert r.returncode == 0, r.stderr - out = pack_root(derived, work, rocm_kpack_dir, hipcc, comgr_lib) - return out, work / "probe" / "expect.json" - - class _Tree: def __init__(self, root, expect, rocm_kpack_dir, kdp): self.root, self.expect_path, self.rocm_kpack_dir, self.kdp_rel = ( @@ -105,7 +73,7 @@ def _fails(result, assertion_id): def emb_packed(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): work = tmp_path_factory.mktemp("emb_probe") _embedded_source(work / "src" / "emb") - return _derive_and_pack(work / "src", work, rocm_kpack_dir, hipcc, comgr_lib) + return derive_and_pack(work / "src", work, rocm_kpack_dir, hipcc, comgr_lib) @pytest.fixture(scope="module") @@ -113,8 +81,8 @@ def mixed_packed(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): work = tmp_path_factory.mktemp("emb_hip_probe") src = work / "src" _embedded_source(src / "emb") - _hip_source(src / "hip") - return _derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib) + hip_source(src / "hip") + return derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib) @pytest.fixture @@ -249,3 +217,29 @@ def test_expect_entry_of_passthrough_kind_without_kernel_source_exits_2(emb): def test_expected_passthrough_but_shipped_ukd_missing_fails_count(emb): emb.mutate_expect(lambda entries: entries.append(dict(entries[0], name="another"))) _fails(emb.run(), "ukd-count") + + +def test_two_listed_passthrough_ukds_in_one_kdp_pack_and_pass( + tmp_path, rocm_kpack_dir, hipcc, comgr_lib +): + # UKDS may keep several pass-through UKDs of one KDP; default mode keeps one. + src = _embedded_source(tmp_path / "src" / "emb") + kdp = json.loads((src / "solo.kdp.json").read_text()) + first = kdp["kernelDescriptors"][0] + second = json.loads(json.dumps(first)) + second.update(id="ukd-solo-second", name="second") + second["kernel_source"]["entry_point"] = "PointwiseMul" + kdp["kernelDescriptors"].append(second) + (src / "solo.kdp.json").write_text(json.dumps(kdp, indent=2)) + listed = [first["name"], "second"] + out, expect = derive_and_pack( + tmp_path / "src", tmp_path, rocm_kpack_dir, hipcc, comgr_lib, ukds=listed + ) + entries = json.loads(expect.read_text()) + assert sorted(e["name"] for e in entries) == sorted(listed) + assert {e["kernel_source"]["entry_point"] for e in entries} == { + "PointwiseAdd", + "PointwiseMul", + } + result = run_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 0, result.stderr diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py index c529757fb942..b96d1b6cf65d 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py @@ -34,8 +34,8 @@ ukd-kind a UKD's shipped kernel_source.kind is not what its expected kind's output type requires ("kpack", or the authored kind for a pass-through) - arch-field a KDP `arch`, or a UKD `arch` (always required for a - pass-through UKD), is not [] + arch-field a KDP `arch` present and not [], or a UKD `arch` absent + or not [] passthrough-source a pass-through UKD's kernel_source differs from the authored one passthrough-provenance a pass-through UKD's provenance.source_label is empty or its @@ -200,12 +200,7 @@ def _check_comgr(args, label, prov, failures): def _check_provenance(args, label, ukd, kind, failures): - entry = KINDS.get(kind) - if entry is None: - failures.add( - "no-rules-for-kind", f"{label} expected kind {kind!r} has no rules" - ) - return + entry = KINDS[kind] prov = ukd.get("provenance") or {} origin = prov.get("origin_kind") if origin != kind: @@ -227,8 +222,8 @@ def _check_kpack_ukd(args, label, ukd, expected, archive, failures): ) return - if "arch" in ukd and ukd["arch"] != [args.arch]: - failures.add("arch-field", f"{label} arch is {ukd['arch']!r}") + if ukd.get("arch") != [args.arch]: + failures.add("arch-field", f"{label} arch is {ukd.get('arch')!r}") signature = source.get("signature") if not isinstance(signature, list) or not signature: diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py index c28029b509a4..78a6464cb8f1 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py @@ -2,15 +2,27 @@ """Derive a minimal descriptor root for a packaging probe. Copies every file under `--from` to `--out`, except that each KDP shipping for -`--arch` has its inline `kernelDescriptors` reduced to one UKD per compile group -among the UKDs that themselves ship for `--arch`: +`--arch` has its inline `kernelDescriptors` reduced to the UKDs the probe packs, +among the UKDs that themselves ship for `--arch`. - one entry per kind in hkp_probe_kinds.py: rocke (kind, builder), hip (kind, source, - build), hsaco and embedded_source (kind: compile nothing, so one UKD per KDP) +Default mode (no `--ukd`): one UKD per compile group, where the group key is the +kind plus the kernel_source fields hkp_probe_kinds.KINDS names for it: -The pick within a group is the first by sorted UKD `name`. Packing the derived -root therefore exercises every compile path once instead of every variant. -KDPs that do not ship for `--arch` are copied unchanged; the packer prunes them. + rocke (kind, source, builder), hip (kind, source, build), hsaco and + embedded_source (kind: compile nothing, so one UKD per KDP) + +Every field of the key must be present in kernel_source. The pick within a group is +the first by sorted UKD `name`. Packing the derived root therefore exercises every +compile path once instead of every variant. Authors who want more than one UKD of a +group list them with `--ukd`. + +`--ukd ` mode (repeatable): each KDP keeps exactly the UKDs whose `name` is +listed. A KDP shipping for `--arch` that keeps nothing is omitted from the derived +root, so the packer cannot pack it in full. KDPs that keep nothing are not inspected +for standalone references or kinds. + +In both modes KDPs that do not ship for `--arch` are copied unchanged; the packer +prunes them. `expect.json` is written beside `--out` (`/expect.json`): a list of {"kdp": , "name": , "kind": source Path or bytes.""" +def _plan(src, arch, ukds=None): + """Return (files, expect): files maps relative posix path -> source Path or bytes. + + `ukds` is the list of UKD names to keep, or None for one UKD per compile group. + """ + if ukds is not None: + dupes = sorted({n for n in ukds if ukds.count(n) > 1}) + if dupes: + raise DeriveError(f"--ukd lists {dupes} more than once") + wanted = set(ukds) + files = {} for p in sorted(src.rglob("*")): if p.is_file(): @@ -137,9 +204,16 @@ def _plan(src, arch): raise DeriveError(f"{rel} is not a JSON object") if not arch_matches(kdp, arch): continue - kept = _derive_kdp(kdp, rel, arch) + if ukds is None: + kept = _derive_kdp(kdp, rel, arch) + else: + kept = _derive_kdp_listed(kdp, rel, arch, wanted) if not kept: - # Every UKD filters out for this arch: the packer drops the whole KDP. + if ukds is not None: + # Only listed UKDs are packed, so this KDP must not ship in full. + del files[rel] + # Default mode: every UKD filters out for this arch, so the packer drops + # the whole KDP. continue kdp["kernelDescriptors"] = kept files[rel] = (json.dumps(kdp, indent=2) + "\n").encode("utf-8") @@ -149,6 +223,15 @@ def _plan(src, arch): # The packer ships the block as authored; the assertion compares it. entry["kernel_source"] = u["kernel_source"] expect.append(entry) + + if ukds is not None: + missing = sorted(wanted - {e["name"] for e in expect}) + if missing: + raise DeriveError( + f"--ukd {missing} matched no inline UKD shipping for {arch} in a KDP " + f"shipping for {arch} (standalone UKD references are unsupported by " + "probe derive)" + ) return files, expect @@ -190,12 +273,17 @@ def _sync(out, files): shutil.rmtree(p) -def derive_root(src: Path, arch: str, out: Path) -> str | None: - """Write the derived root to `out`; return an error message, or None on success.""" +def derive_root( + src: Path, arch: str, out: Path, ukds: list[str] | None = None +) -> str | None: + """Write the derived root to `out`; return an error message, or None on success. + + `ukds` lists the UKD names to keep (`--ukd`); None keeps one per compile group. + """ if not src.is_dir(): return f"{_PREFIX} {src} is not a directory" try: - files, expect = _plan(src, arch) + files, expect = _plan(src, arch, ukds) except DeriveError as exc: return f"{_PREFIX} {exc}" @@ -217,9 +305,16 @@ def main(argv: list[str] | None = None) -> int: "--arch", required=True, help="architecture to probe, e.g. gfx950" ) parser.add_argument("--out", type=Path, required=True) + parser.add_argument( + "--ukd", + dest="ukds", + action="append", + metavar="NAME", + help="keep the UKD with this name (repeatable); default: one per compile group", + ) args = parser.parse_args(argv) - error = derive_root(args.src, args.arch, args.out) + error = derive_root(args.src, args.arch, args.out, args.ukds) if error is not None: print(error, file=sys.stderr) return 2 diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py index 08c5106af8a2..51aa85b52f03 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py @@ -27,8 +27,10 @@ @dataclass(frozen=True) class Kind: # kernel_source fields that define a compile group within a KDP: UKDs of one kind - # agreeing on all of them share a compile path, so one stands for the group. Empty - # means the kind compiles nothing and one UKD per KDP stands for it. + # agreeing on all of them share a compile path, so by default one stands for the + # group. Every field must be present in kernel_source. Empty means the kind compiles + # nothing and one UKD per KDP stands for it; authors who want more UKDs of a group + # packed list them by name (hkp_add_packaging_probe UKDS). group_by: tuple[str, ...] # Names of provenance checks (hkp_probe_assert._PROVENANCE_CHECKS) a shipped UKD of # this kind must satisfy, beyond provenance.origin_kind == the kind itself. @@ -38,7 +40,7 @@ class Kind: KINDS = { - "rocke": Kind(group_by=("builder",), provenance=("wheel", "comgr")), + "rocke": Kind(group_by=("source", "builder"), provenance=("wheel", "comgr")), "hip": Kind(group_by=("source", "build"), provenance=()), "hsaco": Kind(group_by=(), provenance=()), "embedded_source": Kind(group_by=(), provenance=(), output="passthrough"), diff --git a/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/descriptors/README.md b/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/descriptors/README.md index 688ff6a59a7d..08d8404efb3a 100644 --- a/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/descriptors/README.md +++ b/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/descriptors/README.md @@ -44,14 +44,13 @@ stale product tree is removed — not an error. A KDP that *is* present but prun architecture is a hard failure for a root the build NAMED, and dormancy for this root reached as the built-in default. -Packaging probes read this root too. They are opt-in per architecture (one -`hkp_add_packaging_probe(ARCH )` line) and superbuild-only -(`HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES`, `OFF` by default); a new bundle under an -already probed architecture is covered automatically. A probe packs one instance per -compile group, so the lane's own architecture is still covered in full by the normal -build. A KDP shipping for a probed architecture must keep its `kernelDescriptors` -inline: a standalone-UKD reference fails probe configure. See "Packaging probes" in -`descriptor-packaging/README.md`. +Packaging probes read this root too (superbuild-only, +`HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES`, `OFF` by default). A bundle's integration +declares its probe with `hkp_add_packaging_probe(ARCH NAME UKDS +...)`, naming UKDs of this root by `name`; a probe without `UKDS` packs one UKD +per compile group of every KDP shipping for its architecture. A KDP a probe keeps must +keep its `kernelDescriptors` inline: a standalone-UKD reference fails probe configure. +See "Packaging probes" in `descriptor-packaging/README.md`. Do not delete this README: git tracks no empty directory, and the cache variable's set-but-not-a-directory check is fatal. diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md index e49c75476463..bb778a8ab8b9 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md @@ -329,7 +329,10 @@ rocKE is likewise resolved once for **every** root, test roots included, so an unresolvable comgr is fatal at configure even in a hip-only build. There is **no per-producer production switch**: producer selection is per-UKD on -`kernel_source.kind`, so one root feeds every producer. +`kernel_source.kind`, so one root feeds every producer. Whoever adds a new +`kernel_source.kind` to the packer also adds its entry to +`descriptor-packaging/tools/hkp_probe_kinds.py`; a probe keeping a UKD of an unregistered +kind fails configure. Production packaging is wired on exactly one condition — the root named by `HIPKERNELPROVIDER_PRODUCTION_SOURCE_ROOT` holds at least one **non-hidden @@ -354,19 +357,6 @@ native pack whose symbols a bundle's UKDs must name before it serves. The packag dependencies are documented from the repository root in `dnn-providers/hip-kernel-provider/descriptor-packaging/README.md`. -**Required: add a packaging probe for every new architecture.** CI builds only a few -architectures, so packaging for the others breaks silently unless a probe packs them. -Probes are opt-in per architecture: declare one `hkp_add_packaging_probe(ARCH )` line -in `dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake`, then run -the mutation checks listed under "Packaging probes" in the descriptor-packaging README. A -new pack under an architecture that is already probed, in the production root, is covered -automatically and needs no new line. Probes are superbuild-only -(`HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES`, OFF by default) and pack one instance per -compile group; the lane's own architecture is covered in full by the normal build. They -do not cover ASM SDPA, C++/runtime gfx950 paths, install staging, or standalone-UKD -references (derive fails configure on those). No CMake guard detects a missing probe, so -this step is the only enforcement; no workflow edit is needed. - Build the provider, validator and required test targets through the configured superbuild. For packaged engines, run `hkp_packaging_product` after the full build and after any reconfigure; require the final staged descriptors, not a packaging stamp or an @@ -691,6 +681,13 @@ accounting. ambiguous, erroneous or unexplained in-scope outcomes. Changed installed artifacts invalidate old evidence and return to stages 3–5. +**Final step: packaging probe.** Add +`hkp_add_packaging_probe(ARCH NAME UKDS )` +to `dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake`, configure +with `-DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON`, and run +`ctest -R '^hkp-probe-' --no-tests=error`; `hkp-probe-` must pass. See "Packaging +probes" in the descriptor-packaging README. + ## 8. Handoff Report [SKILL.md](SKILL.md)'s completion evidence and exact limitations. Keep experiment diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md index 4b2123f48437..5bfafeec1b41 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md @@ -69,12 +69,9 @@ Completion requires: - Exact-engine quick/standard numerics and required negative cases, with selected, served, skipped and failed counts. Zero selected, all-skipped or another engine's work is not correctness evidence. An extension must dispatch its new candidate. -- For a new architecture, one `hkp_add_packaging_probe(ARCH )` line (opt-in per - architecture, superbuild-only, option `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` OFF by - default; one instance per compile group; excludes ASM SDPA, C++/runtime gfx950 paths, - install staging and standalone-UKD references), with the probe ctest entry - `hkp-probe-` run. A new pack under an already probed architecture is covered - automatically. +- A packaging probe: `hkp_add_packaging_probe(ARCH NAME UKDS + )` in `descriptor-packaging/probes/probes.cmake`, with its + ctest entry `hkp-probe-` run and passing (RUNBOOK's final step). - Rebuilt/reinstalled artifacts after tuning or regeneration, then fresh final gates. Every final corpus input has an attributable runtime outcome and the complete join required by [workloads.md](workloads.md); missing, ambiguous or error outcomes block diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md index b323a16c7ab7..73ebb78ee757 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md @@ -100,12 +100,9 @@ through that suite's ordinary host run. [native-pack.md](native-pack.md) owns eligibility. Device proof must explicitly select and numerically verify the new candidate; passing the unchanged default is not extension acceptance. -An addition under an architecture that already has a packaging probe is covered by it -automatically; a new architecture needs one `hkp_add_packaging_probe(ARCH )` line -(opt-in per architecture, superbuild-only, option `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` -OFF by default). The probe packs one instance per compile group and does not cover ASM -SDPA, C++/runtime gfx950 paths, install staging or standalone-UKD references (derive fails -configure on those); the lane's own architecture is covered in full by the normal build. +When the addition adds a compile path or an architecture that its integration's packaging +probe does not pack, add a representative UKD name to that probe's `UKDS` (or a probe line +for the new architecture) and run `hkp-probe-`, as in RUNBOOK's final step. The handoff identifies retained IDs/references, changed/new files, baseline/final installations, whole-engine results and the addition's actual dispatch. RUNBOOK stage 5 diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild-test/SKILL.md b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild-test/SKILL.md index c640ac40d943..07002ec9d20d 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild-test/SKILL.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild-test/SKILL.md @@ -120,11 +120,18 @@ proving a named engine; a generic `command:` line is not that proof. ## Packaging probes When the build was configured with `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON` -(superbuild-only, OFF by default), the probe ctest entries are `hkp-probe-` plus -`hkp-probe-tools`; select them with the anchored `-R '^hkp-probe-'` and -`--no-tests=error`. Probes are opt-in per architecture and pack one instance per compile -group; the lane's own architecture is covered in full by the normal build. A pass does not -cover ASM SDPA, C++/runtime gfx950 paths, install staging or standalone-UKD references. +(superbuild-only, OFF by default), the probe ctest entries are `hkp-probe-`, one per +`hkp_add_packaging_probe()` line, plus `hkp-probe-tools`. A full ctest run includes them; +to run them alone, select with the anchored `-R '^hkp-probe-'` and `--no-tests=error`. +`/hkp-probes/manifest.txt` lists the entries that must run. Check a ctest junit +(`--output-junit`) against it with `.github/scripts/check_probe_junit.py --junit +--manifest `, which fails on any listed entry missing, not run, failed or +errored and ignores the other testcases. + +In the superbuild CI lanes the probes run once, inside the "Run tests" ctest, which writes +`build/ctest-junit.xml`; the "Check packaging probes ran" step then runs only the junit +check, whenever "Run tests" ran, pass or fail. A pass does not cover device behavior, +install staging, UKDs the probe does not pack, or standalone-UKD references. ## Ingestor proof boundaries diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md index ba7f4c6bad0f..7221f6bd9cdb 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md @@ -79,7 +79,7 @@ on Windows the wheel venv's `_rocm_sdk_devel/bin`. | `HIPKERNELPROVIDER_PRODUCTION_SOURCE_ROOT` | the in-tree `.../kernel_ingestor_engine/descriptors` | `CACHE PATH` naming the authored tree production packaging packages from. Packaging requires at least one non-hidden `*.kdp.json`; with none it is dormant. The default root is also dormant when no KDP declares any selected packaging architecture. Dormancy removes any stale product tree and is not an error. Set but not a directory is fatal. | | `HIPKERNELPROVIDER_KPACK_PYTHON_DIR` | unset | Directory **containing** `rocm_kpack/`; this locates a package, not a compiler interpreter. | | `Python3_EXECUTABLE` | system | Explicit environment for packaging dependencies such as `msgpack` and `zstandard`; production compilation retains its selected hermetic wheel interpreter. | -| `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` | OFF | CI-only packaging probes, opt-in per architecture (one `hkp_add_packaging_probe(ARCH )` line in `descriptor-packaging/probes/probes.cmake`); superbuild-only. Packs one instance per compile group for an architecture the lane does not build, so use an architecture other than `GPU_TARGETS`. When ON, the pack targets are part of `all`; `cmake --build --target hkp_packaging_probes` builds them alone and `ctest -R '^hkp-probe-' --no-tests=error` runs them (ctest entry `hkp-probe-`). Excludes ASM SDPA, C++/runtime gfx950 paths, install staging and standalone-UKD references (derive fails configure on those). | + | `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` | OFF | Packaging probes declared in `descriptor-packaging/probes/probes.cmake` (`hkp_add_packaging_probe(ARCH [NAME ] [UKDS ...])`); superbuild-only, ON in the superbuild CI lanes. Each probe packs its descriptors for `ARCH` (any architecture) independent of `GPU_TARGETS`. When ON, the probe pack targets are part of `all`; `cmake --build --target hkp_packaging_probes` builds them alone. The ctest entries are `hkp-probe-` plus `hkp-probe-tools`. | These flags only do anything on a preset that actually builds hip-kernel-provider. The default `hipdnn-providers` preset does **not** include it; the presets that do are From 15f0d9094dc359b9f664c952d657aee427775103 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 5 Oct 2026 23:03:39 +0000 Subject: [PATCH 19/52] hkp probes: address second review sweep - UKDS "" (e.g. an empty variable) fails configure instead of falling back to the default sample. - ARCH must be one concrete target, gfx[0-9a-f]+, as the packer selects them. - --ukd mode reports a listed UKD whose arch is not a subset of its KDP's instead of dropping it; tests for that and for equal kept names. - Runbook final step builds hkp_packaging_probes before ctest; README credits the junit check for CI; unused test import and wrapper removed. --- .../descriptor-packaging/README.md | 4 +-- .../cmake/HkpPackagingProbes.cmake | 17 +++++++----- .../tests/test_hkp_probe_derive_root.py | 27 +++++++++++-------- .../tools/hkp_probe_derive_root.py | 24 ++++++++++------- .../skills/hipdnn-ingestor-engine/RUNBOOK.md | 7 ++--- 5 files changed, 46 insertions(+), 33 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 27fcdbb8c037..4d236a6e8b1a 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -300,7 +300,7 @@ is that cache entry; the probe module is never read. (prebuilt `.co` files) and C++/runtime paths, and install staging. - Device behavior: no kernel is launched, no numerics are checked. - Integrations and architectures without a probe. -- That the CI job runs: that is the job of `--no-tests=error` and the junit check below. +- That the CI job runs: that is the job of the junit check below. ### How to use @@ -333,7 +333,7 @@ also fails configure. | Argument | Meaning | |---|---| -| `ARCH` | Required. The one architecture to pack, e.g. `gfx950`; must match `^gfx[0-9a-z]+$`. Passed to the packer as `--arches`. | +| `ARCH` | Required. The one architecture to pack, e.g. `gfx950`; one concrete target matching `^gfx[0-9a-f]+$`. Passed to the packer as `--arches`. | | `ROOT` | Optional descriptor root to probe. Defaults to the production root (`HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT`). A relative path is resolved against `probes/`; a trailing `/` is ignored. | | `NAME` | Optional probe name; must match `^[A-Za-z0-9_.+-]+$`, must not be dots only, and must not be `tools`. Defaults to `` for the production root and `_` for another `ROOT`. Pack target `hkp_packaging_probe_`, ctest entry `hkp-probe-`. Declaring a name twice is a configure error. | | `UKDS` | Optional UKD `name`s to pack instead of the default sample. Each entry is non-empty and listed once. | diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake index f2db8b7ba962..f7945008d3cf 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -149,9 +149,10 @@ endfunction() # kind (see hkp_probe_assert.py). Adding a pack under an already probed ARCH needs no # new declaration unless the probe lists UKDS. # -# ARCH must match gfx[0-9a-z]+. NAME defaults to for the production root and -# _ for another ROOT; it must match [A-Za-z0-9_.+-]+, must not be -# dots only, and must not be `tools` (hkp-probe-tools is the probe tooling test). +# ARCH must be one concrete target, gfx[0-9a-f]+. NAME defaults to for the +# production root and _ for another ROOT; it must match +# [A-Za-z0-9_.+-]+, must not be dots only, and must not be `tools` (hkp-probe-tools +# is the probe tooling test). # UKDS entries must be non-empty and unique. Creates pack target # hkp_packaging_probe_ (stamp and output under # ${CMAKE_BINARY_DIR}/hkp-probes//out) and ctest entry hkp-probe-. The @@ -169,10 +170,10 @@ function(hkp_add_packaging_probe) if(NOT ARG_ARCH) message(FATAL_ERROR "hkp probe: hkp_add_packaging_probe requires ARCH.") endif() - if(NOT ARG_ARCH MATCHES "^gfx[0-9a-z]+$") + if(NOT ARG_ARCH MATCHES "^gfx[0-9a-f]+$") message(FATAL_ERROR - "hkp probe: ARCH '${ARG_ARCH}' is not one architecture of the form " - "gfx[0-9a-z]+.") + "hkp probe: ARCH '${ARG_ARCH}' is not one concrete architecture of the " + "form gfx[0-9a-f]+.") endif() if(NOT ARG_ROOT) set(ARG_ROOT "${HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT}") @@ -192,7 +193,9 @@ function(hkp_add_packaging_probe) message(FATAL_ERROR "hkp probe '${ARG_NAME}': ROOT is not a directory: '${ARG_ROOT}'") endif() - if("UKDS" IN_LIST ARG_KEYWORDS_MISSING_VALUES) + # `UKDS ""` (e.g. an empty variable) parses as a present but empty list. + if("UKDS" IN_LIST ARG_KEYWORDS_MISSING_VALUES + OR ("UKDS" IN_LIST ARGV AND ARG_UKDS STREQUAL "")) message(FATAL_ERROR "hkp probe '${ARG_NAME}': UKDS needs at least one UKD name.") endif() diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py index ab806be8966f..2ebcd40419ef 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py @@ -5,7 +5,6 @@ import itertools import json import os -import subprocess import sys from pathlib import Path @@ -14,12 +13,8 @@ from probe_support import MAIN_FIXTURE, PROBE_DERIVE, run_derive -def _run(src: Path, arch: str, out: Path, ukds=()): - return run_derive(src, arch, out, ukds) - - def _derive(src: Path, arch: str, out: Path, ukds=()): - r = _run(src, arch, out, ukds) + r = run_derive(src, arch, out, ukds) assert r.returncode == 0, r.stderr return json.loads((out.parent / "expect.json").read_text()) @@ -229,7 +224,7 @@ def test_dot_prefixed_kdp_is_copied_but_not_expected(tmp_path): # --- configure-time refusals (exit 2, nothing written) ---------------------- def _refused(tmp_path, ukds, message, kdp_arch=("gfx950",)): _write_root(tmp_path / "src", ukds, kdp_arch=kdp_arch) - r = _run(tmp_path / "src", "gfx950", tmp_path / "w" / "root") + r = run_derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") assert r.returncode == 2, r.stderr assert r.stderr.startswith("hkp_probe_derive:") assert message in r.stderr @@ -247,7 +242,7 @@ def test_standalone_ukd_reference_exits_2(tmp_path): def test_standalone_reference_in_real_fixture_exits_2(tmp_path): # tests/fixtures/main: pointwise.kdp.json ships for gfx950 and references # ukd-pointwise-add-f32-b128 by id. - r = _run(MAIN_FIXTURE, "gfx950", tmp_path / "w" / "root") + r = run_derive(MAIN_FIXTURE, "gfx950", tmp_path / "w" / "root") assert r.returncode == 2, r.stderr assert "pointwise.kdp.json" in r.stderr assert "ukd-pointwise-add-f32-b128" in r.stderr @@ -304,7 +299,7 @@ def test_malformed_kdp_exits_2(tmp_path): src = tmp_path / "src" src.mkdir() (src / "bad.kdp.json").write_text("{ not json") - r = _run(src, "gfx950", tmp_path / "w" / "root") + r = run_derive(src, "gfx950", tmp_path / "w" / "root") assert r.returncode == 2 assert "bad.kdp.json is unreadable" in r.stderr assert "Traceback" not in r.stderr @@ -348,7 +343,7 @@ def test_ukd_list_copies_kdps_not_shipping_for_arch(tmp_path): def _ukd_refused(tmp_path, ukds, listed, message): _write_root(tmp_path / "src", ukds, kdp_arch=("gfx942", "gfx950")) - r = _run(tmp_path / "src", "gfx950", tmp_path / "w" / "root", ukds=listed) + r = run_derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root", ukds=listed) assert r.returncode == 2, r.stderr assert r.stderr.startswith("hkp_probe_derive:") assert message in r.stderr @@ -368,7 +363,7 @@ def test_ukd_list_name_only_in_kdp_for_other_arch_exits_2(tmp_path): src = tmp_path / "src" _write_root(src, [_rocke("a")], kdp_file="keep.kdp.json") _write_root(src, [_rocke("b")], kdp_arch=("gfx942",), kdp_file="other.kdp.json") - r = _run(src, "gfx950", tmp_path / "w" / "root", ukds=["a", "b"]) + r = run_derive(src, "gfx950", tmp_path / "w" / "root", ukds=["a", "b"]) assert r.returncode == 2, r.stderr assert "['b'] matched no" in r.stderr @@ -394,6 +389,16 @@ def test_ukd_list_standalone_ref_in_kept_kdp_exits_2(tmp_path): ) +def test_ukd_list_arch_outside_kdp_arch_exits_2(tmp_path): + ukds = [_rocke("a", arch=["gfx90a", "gfx950"])] + _ukd_refused(tmp_path, ukds, ["a"], "not a subset of the KDP arch") + + +def test_ukd_list_kept_ukds_with_equal_names_exit_2(tmp_path): + ukds = [_rocke("same", "b1"), _rocke("same", "b2")] + _ukd_refused(tmp_path, ukds, ["same"], "share a name") + + def test_ukd_list_unregistered_kind_of_kept_ukd_exits_2(tmp_path): _ukd_refused( tmp_path, [_rocke("a"), _other("mystery", "m")], ["m"], "has no probe support" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py index 78a6464cb8f1..9a4e82276088 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py @@ -121,6 +121,14 @@ def _check_unique_names(rel, kept): ) +def _check_arch_subset(ukd, kdp, where): + if not arch_subset_ok(ukd.get("arch") or [], kdp.get("arch") or []): + raise DeriveError( + f"{where} arch {ukd.get('arch')} is not a subset of the KDP " + f"arch {kdp.get('arch')}" + ) + + def _derive_kdp(kdp, rel, arch): """Return the kept UKDs of a KDP shipping for `arch`, in authored order.""" entries = kdp.get("kernelDescriptors") @@ -133,11 +141,7 @@ def _derive_kdp(kdp, rel, arch): where = f"{rel}: UKD '{entry.get('id', '?')}'" if not isinstance(entry.get("name"), str): raise DeriveError(f"{where} has no string 'name'") - if not arch_subset_ok(entry.get("arch") or [], kdp.get("arch") or []): - raise DeriveError( - f"{where} arch {entry.get('arch')} is not a subset of the KDP " - f"arch {kdp.get('arch')}" - ) + _check_arch_subset(entry, kdp, where) if arch_matches(entry, arch): candidates.append((_group_key(entry, where), entry)) @@ -157,10 +161,7 @@ def _derive_kdp_listed(kdp, rel, arch, wanted): kept = [ e for e in entries - if isinstance(e, dict) - and e.get("name") in wanted - and arch_matches(e, arch) - and arch_subset_ok(e.get("arch") or [], kdp.get("arch") or []) + if isinstance(e, dict) and e.get("name") in wanted and arch_matches(e, arch) ] if not kept: return kept @@ -168,7 +169,10 @@ def _derive_kdp_listed(kdp, rel, arch, wanted): if not isinstance(entry, dict): raise _standalone_error(rel, entry) for entry in kept: - _check_kind(entry, f"{rel}: UKD '{entry.get('id', '?')}'") + where = f"{rel}: UKD '{entry.get('id', '?')}'" + # The packer rejects this root; a listed UKD must not be dropped silently. + _check_arch_subset(entry, kdp, where) + _check_kind(entry, where) _check_unique_names(rel, kept) return kept diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md index bb778a8ab8b9..33c01b466c9c 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md @@ -684,9 +684,10 @@ invalidate old evidence and return to stages 3–5. **Final step: packaging probe.** Add `hkp_add_packaging_probe(ARCH NAME UKDS )` to `dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake`, configure -with `-DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON`, and run -`ctest -R '^hkp-probe-' --no-tests=error`; `hkp-probe-` must pass. See "Packaging -probes" in the descriptor-packaging README. +with `-DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON`, build +(`cmake --build --target hkp_packaging_probes`), and run +`ctest --test-dir -R '^hkp-probe-' --no-tests=error`; `hkp-probe-` must +pass. See "Packaging probes" in the descriptor-packaging README. ## 8. Handoff From ef126b6355f6bace0ef18daf5b8b6e60fc48600d Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 5 Oct 2026 22:14:06 +0000 Subject: [PATCH 20/52] feat(hipdnn): load-time generic target tiers in the ingestor and kpack loader Tuple uniqueness competes per arch tier, admitted kernels sharing a tuple are shadowed by the best tier per device, the loader warns on generic names absent from the table, and KpackModuleCache::selectArch prefers the exact device key before a generic archive key containing the device. --- .../src/compilation/KpackModuleCache.hpp | 45 ++- .../compilation/TestKpackModuleCache.cpp | 54 ++++ .../ingestor/DescriptorLoader.hpp | 11 + .../ingestor/KernelIngestorStateManager.hpp | 89 +++++- .../tests/ingestor/TestDescriptorLoader.cpp | 164 ++++++++++ .../TestKernelIngestorStateManager.cpp | 296 +++++++++++++++++- 6 files changed, 631 insertions(+), 28 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp b/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp index 3d3cf35ec3c9..58b41ddb1f3f 100644 --- a/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp +++ b/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp @@ -14,11 +14,14 @@ #include #include #include +#include #include #include #include #include +#include +#include namespace hip_kernel_provider::compilation { @@ -101,6 +104,37 @@ class KpackModuleCache : public ModuleCache& archiveArches, + std::string_view deviceArch) + { + const auto baseDeviceId = hipdnn_plugin_sdk::stripArchFeatures(deviceArch); + for(const auto& candidate : archiveArches) + { + if(!hipdnn_plugin_sdk::isGenericShapedArchName(candidate) + && hipdnn_plugin_sdk::archMatches( + deviceArch, candidate, hipdnn_plugin_sdk::ArchMatchMode::PREFIX)) + { + return &candidate; + } + } + for(const auto& candidate : archiveArches) + { + if(hipdnn_plugin_sdk::genericTargetContains(candidate, baseDeviceId)) + { + HIPDNN_PLUGIN_LOG_INFO("kpack: device arch '" + << deviceArch << "' is served by generic archive key '" + << candidate << "'"); + return &candidate; + } + } + return nullptr; + } + /// @throws KpackModuleLoadFailure on any stage that fails. Never returns a null /// module: ModuleCache would decline to cache it, but with no message, and /// the caller could not tell which stage gave up. @@ -145,16 +179,7 @@ class KpackModuleCache : public ModuleCache #include +#include #include @@ -156,6 +157,59 @@ TEST(TestKpackModuleCacheLoad, ReportsAnArchTheArchiveDoesNotHold) } } +TEST(TestKpackModuleCacheSelectArch, PrefersTheExactDeviceArchOverAGenericKey) +{ + // The generic key sorts first, so a first-match-wins over mixed keys would pick it. + const std::vector keys{"gfx11-generic", "gfx1151"}; + + const auto* selected = KpackModuleCache::selectArch(keys, "gfx1151"); + + ASSERT_NE(selected, nullptr); + EXPECT_EQ(*selected, "gfx1151"); + EXPECT_EQ(selected, &keys[1]); +} + +TEST(TestKpackModuleCacheSelectArch, FallsBackToAGenericKeyContainingTheDevice) +{ + const std::vector keys{"gfx942", "gfx12-generic", "gfx11-generic"}; + + const auto* selected = KpackModuleCache::selectArch(keys, "gfx1100"); + + ASSERT_NE(selected, nullptr); + EXPECT_EQ(*selected, "gfx11-generic"); +} + +TEST(TestKpackModuleCacheSelectArch, RefusesAGenericKeyThatDoesNotContainTheDevice) +{ + // gfx1250 is not a gfx12-generic member and gfx1170 is not a gfx11-generic member, + // however close their names are to the generic's family. + EXPECT_EQ(KpackModuleCache::selectArch({"gfx12-generic"}, "gfx1250"), nullptr); + EXPECT_EQ(KpackModuleCache::selectArch({"gfx11-generic"}, "gfx1170"), nullptr); + EXPECT_EQ(KpackModuleCache::selectArch({"gfx11-generic", "gfx12-generic"}, "gfx942"), nullptr); + EXPECT_EQ(KpackModuleCache::selectArch({}, "gfx1100"), nullptr); +} + +TEST(TestKpackModuleCacheSelectArch, IgnoresFeatureDecorationOnTheDevice) +{ + const std::vector generic{"gfx9-4-generic", "gfx11-generic"}; + const auto* viaGeneric = KpackModuleCache::selectArch(generic, "gfx1151:xnack-"); + ASSERT_NE(viaGeneric, nullptr); + EXPECT_EQ(*viaGeneric, "gfx11-generic"); + + const std::vector exact{"gfx11-generic", "gfx942"}; + const auto* viaExact = KpackModuleCache::selectArch(exact, "gfx942:sramecc+:xnack-"); + ASSERT_NE(viaExact, nullptr); + EXPECT_EQ(*viaExact, "gfx942"); +} + +TEST(TestKpackModuleCacheSelectArch, NeverMatchesAnUnknownGenericShapedKey) +{ + // Shape never implies membership: a table-absent name admits no device, not even + // the one it spells. + EXPECT_EQ(KpackModuleCache::selectArch({"gfx99-generic"}, "gfx99-generic"), nullptr); + EXPECT_EQ(KpackModuleCache::selectArch({"gfx99-generic"}, "gfx1100"), nullptr); +} + TEST(TestKpackModuleCacheLoad, ASecondOrdinalDoesNotAnswerFromTheFirstOrdinalsEntry) { SKIP_IF_NO_DEVICES(); diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp index bedadcb4fb15..6767a9181117 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -712,6 +713,16 @@ inline std::vector requireArchList(const nlohmann::json& object, } } requireNoDuplicates(values, "arch entry", where); + for(const auto& value : values) + { + if(isGenericShapedArchName(value) && findGenericTarget(value) == nullptr) + { + HIPDNN_PLUGIN_LOG_WARN("ingestor: arch entry '" + << value << "' in " << where + << " is a generic target name absent from the generic target " + "table; it matches no device"); + } + } return values; } diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp index 47a2951650c7..af4f10d9034a 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp @@ -387,9 +387,10 @@ class KernelIngestorStateManager /// than completing each kernel's metadata again on every graph. void validateAndIndexPacks() { - // Two kernels may share a tuple when no single device can see both -- that is - // exactly the per-arch shard layout. Uniqueness is therefore per overlapping-arch - // group, not per engine: the tuple is the catalog key, and a catalog is built for + // Two kernels may share a tuple when no single device can see both at the same arch + // tier -- the per-arch shard layout, and an explicit or generic kernel beside a + // less specific one, which buildCatalog shadows per device. Uniqueness is therefore + // per competing-arch group, not per engine: the tuple is the catalog key, and a catalog is built for // one device. Keyed by the tuple (an ordered map, so it already orders) rather // than scanned, which would be quadratic. std::map>> archesClaimingTuple; @@ -443,14 +444,14 @@ class KernelIngestorStateManager = archesClaimingTuple.try_emplace(key).first->second; for(const auto& claimed : claimants) { - if(archOverlaps(claimed, kernelArch)) + if(archesCompete(claimed, kernelArch)) { throw std::invalid_argument( "kernel '" + toString(kernel.id) + "' duplicates the metadata tuple of another kernel under schema '" + _schema.name + "' on an arch both reach; the tuple is the catalog key " - + "and must be unique per device"); + + "and must be unique per device at the same arch tier"); } } claimants.push_back(kernelArch); @@ -672,12 +673,90 @@ class KernelIngestorStateManager << " kernel(s) after kernel-scoped matching"); } + shadowByArchTier(catalog, context); + HIPDNN_PLUGIN_LOG_INFO("ingestor: catalog for device " << context.deviceId << " holds " << catalog.entries.size() << " kernel(s) from " << _packs.size() << " pack(s)"); return catalog; } + /// Among admitted entries sharing a metadata tuple, keeps only those whose arch list + /// ranks best for the device (EXPLICIT over GENERIC over UNRESTRICTED), preserving + /// order. Runs after matcher admission, so a kernel a matcher declined never hides a + /// lower-tier one. Uniqueness at construction guarantees at most one entry per tuple + /// per tier on any device. + static void shadowByArchTier(Catalog& catalog, const MatchContext& context) + { + auto& entries = catalog.entries; + if(entries.size() < 2) + { + return; + } + + struct ByMetadata + { + bool operator()(const MetadataValues* lhs, const MetadataValues* rhs) const + { + return *lhs < *rhs; + } + }; + struct Best + { + int tier; + const KernelDefinition* winner; + }; + const auto tierOf = [&context](const KernelDefinition& definition) { + return static_cast(archTier(definition.arch, context.deviceProperties.gcnArchName) + .value_or(ArchTier::UNRESTRICTED)); + }; + + std::map bestByTuple; + for(const auto& entry : entries) + { + const int tier = tierOf(entry); + const auto [it, inserted] + = bestByTuple.try_emplace(&entry.metadata, Best{tier, &entry}); + if(!inserted && tier < it->second.tier) + { + it->second = Best{tier, &entry}; + } + } + if(bestByTuple.size() == entries.size()) + { + return; + } + + // Decide and log before moving anything: the map and winners point into entries. + std::vector keep(entries.size(), true); + for(size_t i = 0; i < entries.size(); ++i) + { + const auto& best = bestByTuple.at(&entries[i].metadata); + if(tierOf(entries[i]) != best.tier) + { + keep[i] = false; + HIPDNN_PLUGIN_LOG_INFO("ingestor: kernel " << toString(entries[i].kernelId) + << " is shadowed by kernel " + << toString(best.winner->kernelId) + << " at a better arch tier on device " + << context.deviceProperties.gcnArchName); + } + } + size_t out = 0; + for(size_t i = 0; i < entries.size(); ++i) + { + if(keep[i]) + { + if(out != i) + { + entries[out] = std::move(entries[i]); + } + ++out; + } + } + entries.erase(entries.begin() + static_cast(out), entries.end()); + } + bool graphLevelMatchersPass(const KernelDescriptorPack& pack, const MatchContext& context, GraphMatcherMemo& graphVerdicts, diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp index 292ec8926585..d16599f54790 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp @@ -2000,6 +2000,170 @@ TEST(TestDescriptorLoader, AcceptsGenericTargetIds) (std::vector{"gfx942", "gfx9-4-generic"})); } +/// A generic-shaped name the table does not list is not an error -- a newer table may know +/// it -- but it admits no device, and the author is told so at load. +TEST(TestDescriptorLoader, WarnsAndMatchesNothingForAGenericNameAbsentFromTheTable) +{ + auto recorder + = hipdnn_test_sdk::utilities::SharedLogRecorder::withOverrideLevel(HIPDNN_SEV_WARN); + const ScopedSymbols symbols; + const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("arch_unknown_generic")); + auto documents = makeSetDocuments('1', "test:unknown_generic"); + documentOfType(documents, ".kdp.json")["arch"] = nlohmann::json::array({"gfx99-generic"}); + writeDocuments(dir.path(), documents); + + const auto sets = loadFrom(dir.path()); + + ASSERT_EQ(sets.size(), 1u); + ASSERT_EQ(sets.front().packs.size(), 1u); + const auto& packArch = sets.front().packs.front().arch; + EXPECT_EQ(packArch, std::vector{"gfx99-generic"}); + for(const auto* device : {"gfx942", "gfx1100", "gfx99-generic"}) + { + EXPECT_FALSE(archSupports(packArch, device)) << device; + } + EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_WARN, "arch entry 'gfx99-generic' in ")) + << recorder.getRecordedLogsAsString(); + EXPECT_TRUE(recorder.hasLogContaining( + HIPDNN_SEV_WARN, + "is a generic target name absent from the generic target table; it matches no device")) + << recorder.getRecordedLogsAsString(); +} + +/// A known generic does not warn. +TEST(TestDescriptorLoader, DoesNotWarnForAGenericNameInTheTable) +{ + auto recorder + = hipdnn_test_sdk::utilities::SharedLogRecorder::withOverrideLevel(HIPDNN_SEV_WARN); + const ScopedSymbols symbols; + const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("arch_known_generic")); + auto documents = makeSetDocuments('1', "test:known_generic"); + documentOfType(documents, ".kdp.json")["arch"] = nlohmann::json::array({"gfx11-generic"}); + writeDocuments(dir.path(), documents); + + ASSERT_EQ(loadFrom(dir.path()).size(), 1u); + EXPECT_FALSE(recorder.hasLogContaining(HIPDNN_SEV_WARN, "absent from the generic target table")) + << recorder.getRecordedLogsAsString(); +} + +/// Loader leniency: coverage is over expanded member sets, so an inline kernel naming one +/// member sits within a generic pack. The authoring tools are stricter; this is the last +/// line. +TEST(TestDescriptorLoader, AcceptsAnInlineKernelNamingAMemberUnderAGenericPack) +{ + const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("inline_member")); + auto documents = makeSetDocuments('1', "test:inline_member"); + auto& pack = documentOfType(documents, ".kdp.json"); + pack["arch"] = nlohmann::json::array({"gfx11-generic"}); + pack.at("kernelDescriptors")[0]["arch"] = nlohmann::json::array({"gfx1151"}); + writeDocuments(dir.path(), documents); + + const auto sets = loadFrom(dir.path()); + + ASSERT_EQ(sets.size(), 1u); + ASSERT_EQ(sets.front().packs.size(), 1u); + EXPECT_EQ(sets.front().packs.front().kernels.front().arch, std::vector{"gfx1151"}); +} + +/// An inline kernel that names one member and one device outside the generic reaches past +/// the pack. +TEST(TestDescriptorLoader, RejectsAnInlineKernelReachingOutsideTheGenericsMembers) +{ + auto recorder + = hipdnn_test_sdk::utilities::SharedLogRecorder::withOverrideLevel(HIPDNN_SEV_ERROR); + const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("inline_outside")); + auto documents = makeSetDocuments('1', "test:inline_outside"); + auto& pack = documentOfType(documents, ".kdp.json"); + pack["arch"] = nlohmann::json::array({"gfx11-generic"}); + pack.at("kernelDescriptors")[0]["arch"] = nlohmann::json::array({"gfx1151", "gfx942"}); + writeDocuments(dir.path(), documents); + + EXPECT_TRUE(loadFrom(dir.path()).empty()); + EXPECT_TRUE(recorder.hasLogContaining( + HIPDNN_SEV_ERROR, + "declares arch [gfx1151, gfx942], which reaches past the pack's [gfx11-generic]")) + << recorder.getRecordedLogsAsString(); +} + +/// S1 is unchanged: a standalone kernel id defined for the generic and for one of its +/// members is two covered definitions under a generic pack, and nothing ranks them. +TEST(TestDescriptorLoader, StillRejectsAStandaloneKernelDefinedForAGenericAndForOneOfItsMembers) +{ + auto recorder + = hipdnn_test_sdk::utilities::SharedLogRecorder::withOverrideLevel(HIPDNN_SEV_ERROR); + const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("generic_and_member")); + auto documents = makeSetDocuments('1', "test:generic_and_member"); + referenceLastKernel(documents); + documentOfType(documents, ".kdp.json")["arch"] = nlohmann::json::array({"gfx11-generic"}); + documentOfType(documents, ".ukd.json")["arch"] = nlohmann::json::array({"gfx11-generic"}); + + auto member = documentOfType(documents, ".ukd.json"); + member["arch"] = nlohmann::json::array({"gfx1151"}); + member["name"] = "the gfx1151 build"; + writeDocuments(dir.path(), documents); + writeDocuments(dir.path() / "gfx1151", {TestDocument{".ukd.json", member}}); + + EXPECT_TRUE(loadFrom(dir.path()).empty()); + EXPECT_TRUE(recorder.hasLogContaining( + HIPDNN_SEV_ERROR, "which several descriptors define within the pack's arch")) + << recorder.getRecordedLogsAsString(); +} + +namespace +{ + +/// A generic pack of one referenced kernel, authored as a generic UKD under the pack's +/// generic spelling. +Documents makeGenericPackDocuments(char tag, const std::string& engineName) +{ + auto documents = makeSetDocuments(tag, engineName); + referenceLastKernel(documents); + auto& pack = documentOfType(documents, ".kdp.json"); + pack["arch"] = nlohmann::json::array({"gfx11-generic"}); + const auto referenced = documentOfType(documents, ".ukd.json").at("id"); + pack["kernelDescriptors"] = nlohmann::json::array({referenced}); + documentOfType(documents, ".ukd.json")["arch"] = nlohmann::json::array({"gfx11-generic"}); + return documents; +} + +} // namespace + +/// Regression guard for the packer's per-member-folder layout, which writes one identical +/// copy of a generic tree under every selected member's directory. Passes on the existing +/// identical-duplicate collapse; it does not prove new loader work. +TEST(TestDescriptorLoader, CollapsesIdenticalGenericCopiesAcrossMemberDirectories) +{ + const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("generic_copies")); + const auto documents = makeGenericPackDocuments('1', "test:generic_copies"); + writeDocuments(dir.path() / "gfx1100", documents); + writeDocuments(dir.path() / "gfx1151", documents); + + const auto sets = loadFrom(dir.path()); + + ASSERT_EQ(sets.size(), 1u); + ASSERT_EQ(sets.front().packs.size(), 1u); + EXPECT_EQ(sets.front().packs.front().arch, std::vector{"gfx11-generic"}); + EXPECT_EQ(sets.front().packs.front().kernels.size(), 1u); +} + +/// Regression guard, same status: two member directories whose generic copies differ are +/// an ordinary same-id-and-arch conflict and drop together. +TEST(TestDescriptorLoader, DropsBothWhenGenericCopiesInMemberDirectoriesDiffer) +{ + auto recorder + = hipdnn_test_sdk::utilities::SharedLogRecorder::withOverrideLevel(HIPDNN_SEV_ERROR); + const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("generic_copies_differ")); + auto first = makeGenericPackDocuments('1', "test:generic_copies_differ"); + auto second = first; + documentOfType(second, ".ukd.json")["kernel_source"]["source_file"] = "Other.cpp"; + writeDocuments(dir.path() / "gfx1100", first); + writeDocuments(dir.path() / "gfx1151", second); + + EXPECT_TRUE(loadFrom(dir.path()).empty()); + EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_ERROR, "both define")) + << recorder.getRecordedLogsAsString(); +} + /// The default: a pack naming no architecture applies everywhere, so absence must parse /// as empty rather than as a constraint nothing satisfies. TEST(TestDescriptorLoader, APackWithNoDeclaredArchIsArchIndependent) diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp index ad4d2d970725..038407bfadde 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp @@ -314,28 +314,298 @@ TEST(TestKernelIngestorStateManager, OffersAnUnstampedKernelEverywhereItsPackRea } } -/// An arch-independent pack claims every device, so it overlaps a per-arch one and the -/// two cannot share a tuple: on a gfx942 device both would apply and the catalog key -/// would name two kernels. This is the empty-list arm of archOverlaps, which the disjoint -/// case above never reaches. -TEST(TestKernelIngestorStateManager, RejectsATupleSharedByAnArchIndependentAndAPerArchPack) +/// Two packs share a tuple whenever no device ranks them at the same arch tier: an +/// arch-independent pack is UNRESTRICTED everywhere, a per-arch pack is EXPLICIT on its own +/// devices, so on those devices the explicit kernel shadows the unrestricted one and +/// elsewhere the unrestricted one is the only candidate. Both pack orders are exercised so +/// a keep-the-first-inserted shortcut fails. +TEST(TestKernelIngestorStateManager, AdmitsATupleSharedByAnArchIndependentAndAPerArchPack) +{ + const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); + const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); + + for(const bool explicitFirst : {true, false}) + { + auto anywhere = makePack({GRAPH_MATCHER_ID}); + anywhere.kernels = {makeKernel(testId(0x93), "kernel_anywhere", 64, "FLOAT")}; + auto pinned = makePack({GRAPH_MATCHER_ID}, {"gfx942"}); + pinned.id = testId(0x94); + pinned.kernels = {makeKernel(testId(0x95), "kernel_gfx942", 64, "FLOAT")}; + + const StateManager manager(makeSchema(), + makeTestMatchers(), + makeTestDispatches(), + explicitFirst ? std::vector{pinned, anywhere} + : std::vector{anywhere, pinned}, + std::make_shared(SCORE_SYMBOL), + "test.graph"); + + const TestGraph graph(makeGraphId(0x25)); + const auto definitionsFor = [&](int deviceId, const char* deviceArch) { + auto properties = testDeviceProperties(); + properties.gcnArchName = deviceArch; + return manager.unsortedDefinitions(MatchContext{graph, deviceId, properties}); + }; + + const auto onGfx942 = definitionsFor(0, "gfx942:sramecc+"); + ASSERT_EQ(onGfx942.size(), 1U) << "explicitFirst=" << explicitFirst; + EXPECT_EQ(onGfx942.front().kernelId, testId(0x95)) << "explicitFirst=" << explicitFirst; + + const auto onGfx90a = definitionsFor(1, "gfx90a:sramecc+:xnack-"); + ASSERT_EQ(onGfx90a.size(), 1U) << "explicitFirst=" << explicitFirst; + EXPECT_EQ(onGfx90a.front().kernelId, testId(0x93)) << "explicitFirst=" << explicitFirst; + } +} + +/// A generic pack and a pack naming one of its members never rank the same on any device: +/// the member's own device sees EXPLICIT against GENERIC, the other members see only the +/// generic one. +TEST(TestKernelIngestorStateManager, AdmitsATupleSharedByAGenericPackAndAnExplicitMemberPack) +{ + const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); + const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); + + for(const bool explicitFirst : {true, false}) + { + auto generic = makePack({GRAPH_MATCHER_ID}, {"gfx11-generic"}); + generic.kernels = {makeKernel(testId(0x93), "kernel_generic", 64, "FLOAT")}; + auto member = makePack({GRAPH_MATCHER_ID}, {"gfx1151"}); + member.id = testId(0x94); + member.kernels = {makeKernel(testId(0x95), "kernel_gfx1151", 64, "FLOAT")}; + + const StateManager manager(makeSchema(), + makeTestMatchers(), + makeTestDispatches(), + explicitFirst ? std::vector{member, generic} + : std::vector{generic, member}, + std::make_shared(SCORE_SYMBOL), + "test.graph"); + + const TestGraph graph(makeGraphId(0x26)); + const auto definitionsFor = [&](int deviceId, const char* deviceArch) { + auto properties = testDeviceProperties(); + properties.gcnArchName = deviceArch; + return manager.unsortedDefinitions(MatchContext{graph, deviceId, properties}); + }; + + const auto onMember = definitionsFor(0, "gfx1151"); + ASSERT_EQ(onMember.size(), 1U) << "explicitFirst=" << explicitFirst; + EXPECT_EQ(onMember.front().kernelId, testId(0x95)) << "explicitFirst=" << explicitFirst; + + const auto onOtherMember = definitionsFor(1, "gfx1100"); + ASSERT_EQ(onOtherMember.size(), 1U) << "explicitFirst=" << explicitFirst; + EXPECT_EQ(onOtherMember.front().kernelId, testId(0x93)) + << "explicitFirst=" << explicitFirst; + + EXPECT_TRUE(definitionsFor(2, "gfx942").empty()) << "explicitFirst=" << explicitFirst; + } +} + +/// Same generic, same tuple: every member device would rank both at the GENERIC tier, so +/// nothing could break the tie. +TEST(TestKernelIngestorStateManager, RejectsATupleSharedByTwoPacksAtTheSameGenericTier) +{ + const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); + const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); + + auto first = makePack({GRAPH_MATCHER_ID}, {"gfx11-generic"}); + first.kernels = {makeKernel(testId(0x93), "kernel_generic_a", 64, "FLOAT")}; + auto second = makePack({GRAPH_MATCHER_ID}, {"gfx11-generic"}); + second.id = testId(0x94); + second.kernels = {makeKernel(testId(0x95), "kernel_generic_b", 64, "FLOAT")}; + + try + { + const StateManager manager(makeSchema(), + makeTestMatchers(), + makeTestDispatches(), + {first, second}, + std::make_shared(SCORE_SYMBOL), + "test.graph"); + FAIL() << "two packs at the same generic tier must not construct"; + } + catch(const std::invalid_argument& error) + { + EXPECT_NE(std::string(error.what()).find("at the same arch tier"), std::string::npos) + << error.what(); + } +} + +/// Shadowing runs on the admitted catalog: a generic kernel and an unrestricted kernel +/// of one tuple coexist at load, and on a member device only the generic one survives. +/// The drop is logged at INFO, naming the shadowing kernel. +TEST(TestKernelIngestorStateManager, ShadowsTheLowerTierKernelAfterMatcherAdmission) +{ + auto recorder + = hipdnn_test_sdk::utilities::SharedLogRecorder::withOverrideLevel(HIPDNN_SEV_INFO); + + const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); + const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); + + for(const bool betterFirst : {true, false}) + { + auto generic = makePack({GRAPH_MATCHER_ID}, {"gfx11-generic"}); + generic.kernels = {makeKernel(testId(0x93), "kernel_generic", 64, "FLOAT")}; + auto anywhere = makePack({GRAPH_MATCHER_ID}); + anywhere.id = testId(0x94); + anywhere.kernels = {makeKernel(testId(0x95), "kernel_anywhere", 64, "FLOAT")}; + + const StateManager manager(makeSchema(), + makeTestMatchers(), + makeTestDispatches(), + betterFirst ? std::vector{generic, anywhere} + : std::vector{anywhere, generic}, + std::make_shared(SCORE_SYMBOL), + "test.graph"); + + const TestGraph graph(makeGraphId(0x27)); + auto properties = testDeviceProperties(); + properties.gcnArchName = "gfx1100"; + const auto definitions = manager.unsortedDefinitions(MatchContext{graph, 0, properties}); + + ASSERT_EQ(definitions.size(), 1U) << "betterFirst=" << betterFirst; + EXPECT_EQ(definitions.front().kernelId, testId(0x93)) << "betterFirst=" << betterFirst; + } + + EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_INFO, + "kernel " + toString(testId(0x95)) + + " is shadowed by kernel " + toString(testId(0x93)) + + " at a better arch tier")) + << recorder.getRecordedLogsAsString(); +} + +/// Kernel matchers that decline the explicit kernel by name, so the better-tier kernel is +/// never admitted. +inline bool declineTheExplicitKernel(const MatchContext& context, + const BoundTokens& bound, + const KernelDefinition& kernel) +{ + return kernel.name != "kernel_gfx942" && acceptFloatKernels(context, bound, kernel); +} + +/// S2: shadowing compares only entries a matcher admitted. A better-tier kernel the +/// matcher declined must not hide the lower-tier one that remains. +TEST(TestKernelIngestorStateManager, ADeclinedBetterTierKernelDoesNotHideALowerTierOne) +{ + const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", declineTheExplicitKernel); + const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); + + for(const bool explicitFirst : {true, false}) + { + auto anywhere = makePack({GRAPH_MATCHER_ID, KERNEL_MATCHER_ID}); + anywhere.kernels = {makeKernel(testId(0x93), "kernel_anywhere", 64, "FLOAT")}; + auto pinned = makePack({GRAPH_MATCHER_ID, KERNEL_MATCHER_ID}, {"gfx942"}); + pinned.id = testId(0x94); + pinned.kernels = {makeKernel(testId(0x95), "kernel_gfx942", 64, "FLOAT")}; + + const StateManager manager(makeSchema(), + makeTestMatchers(), + makeTestDispatches(), + explicitFirst ? std::vector{pinned, anywhere} + : std::vector{anywhere, pinned}, + std::make_shared(SCORE_SYMBOL), + "test.graph"); + + const TestGraph graph(makeGraphId(0x28)); + auto properties = testDeviceProperties(); + properties.gcnArchName = "gfx942:sramecc+"; + const auto definitions = manager.unsortedDefinitions(MatchContext{graph, 0, properties}); + + ASSERT_EQ(definitions.size(), 1U) << "explicitFirst=" << explicitFirst; + EXPECT_EQ(definitions.front().kernelId, testId(0x93)) << "explicitFirst=" << explicitFirst; + } +} + +/// The winner cache is read back through the catalog, so a record naming a shadowed kernel +/// cannot resurrect it: the record does not cover the surviving explicit kernel, so it is +/// declined and the heuristic orders the catalog. +TEST(TestKernelIngestorStateManager, AWinnerRecordNamingAShadowedKernelFallsBackToHeuristicOrdering) { const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); auto anywhere = makePack({GRAPH_MATCHER_ID}); - anywhere.kernels = {makeKernel(testId(0x93), "kernel_anywhere", 64, "FLOAT")}; + anywhere.kernels = {makeKernel(testId(0x93), "kernel_anywhere_64", 64, "FLOAT"), + makeKernel(testId(0x96), "kernel_anywhere_256", 256, "FLOAT")}; auto pinned = makePack({GRAPH_MATCHER_ID}, {"gfx942"}); pinned.id = testId(0x94); pinned.kernels = {makeKernel(testId(0x95), "kernel_gfx942", 64, "FLOAT")}; - EXPECT_THROW(StateManager(makeSchema(), - makeTestMatchers(), - makeTestDispatches(), - {anywhere, pinned}, - std::make_shared(SCORE_SYMBOL), - "test.graph"), - std::invalid_argument); + const StateManager manager(makeSchema(), + makeTestMatchers(), + makeTestDispatches(), + {anywhere, pinned}, + std::make_shared(SCORE_SYMBOL), + "test.graph"); + + const TestGraph graph(makeGraphId(0x29)); + auto properties = testDeviceProperties(); + properties.gcnArchName = "gfx942:sramecc+"; + const MatchContext context{graph, 0, properties}; + + const auto baseline = manager.sortedCatalog(context); + ASSERT_EQ(baseline.entries.size(), 2U); + ASSERT_FALSE(baseline.orderedFromRecord); + + // The shadowed kernel is the record's fastest; the surviving explicit kernel is absent. + const WinnerRecord record{ + RankedEntry{testId(0x93), anywhere.id, DISPATCH_ID, 0.1}, + RankedEntry{testId(0x96), anywhere.id, DISPATCH_ID, 0.2}, + }; + manager.recordWinner( + WinnerKey{hipdnn_flatbuffers_sdk::flatbuffer_utilities::GraphContentKey{graph}, + DeviceKey{properties}}, + record, + WinnerWriteCause::FRESH_MISS); + + const auto after = manager.sortedCatalog(context); + EXPECT_FALSE(after.orderedFromRecord); + ASSERT_EQ(after.entries.size(), baseline.entries.size()); + for(size_t i = 0; i < after.entries.size(); ++i) + { + EXPECT_EQ(after.entries[i].kernelId, baseline.entries[i].kernelId); + EXPECT_NE(after.entries[i].kernelId, testId(0x93)) << "a shadowed kernel was resurrected"; + } +} + +/// One manager, one graph, two devices: the per-(graph, device) catalog cache must keep the +/// member device's shadowed catalog from answering for the device the better tier does not +/// reach. +TEST(TestKernelIngestorStateManager, KeepsTheLowerTierKernelOnDevicesTheBetterTierDoesNotReach) +{ + const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); + const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); + + auto generic = makePack({GRAPH_MATCHER_ID}, {"gfx11-generic"}); + generic.kernels = {makeKernel(testId(0x93), "kernel_generic", 64, "FLOAT")}; + auto anywhere = makePack({GRAPH_MATCHER_ID}); + anywhere.id = testId(0x94); + anywhere.kernels = {makeKernel(testId(0x95), "kernel_anywhere", 64, "FLOAT")}; + + const StateManager manager(makeSchema(), + makeTestMatchers(), + makeTestDispatches(), + {anywhere, generic}, + std::make_shared(SCORE_SYMBOL), + "test.graph"); + + const TestGraph graph(makeGraphId(0x2A)); + auto member = testDeviceProperties(); + member.gcnArchName = "gfx1151"; + auto nonMember = testDeviceProperties(); + nonMember.gcnArchName = "gfx942:sramecc+"; + + // Queried twice each, interleaved, so the second answer of each comes from the cache. + for(int round = 0; round < 2; ++round) + { + const auto onMember = manager.unsortedDefinitions(MatchContext{graph, 0, member}); + ASSERT_EQ(onMember.size(), 1U) << "round " << round; + EXPECT_EQ(onMember.front().kernelId, testId(0x93)) << "round " << round; + + const auto onNonMember = manager.unsortedDefinitions(MatchContext{graph, 1, nonMember}); + ASSERT_EQ(onNonMember.size(), 1U) << "round " << round; + EXPECT_EQ(onNonMember.front().kernelId, testId(0x95)) << "round " << round; + } } TEST(TestKernelIngestorStateManager, APackIsPrunedWhenAnyOfItsCriteriaFails) From b98800b4eee3c9eedd5b79a9b32c204b899b20a8 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 5 Oct 2026 22:21:26 +0000 Subject: [PATCH 21/52] feat(hkp): pack generic targets and copy them into each member folder The packer validates generic arch entries (S5/S6/unknown/rocKE), compiles a generic entry once under its literal spelling and materialises it in every selected member target's folder. The arch probe, desk check and descriptor context follow the same tier rule via the shared table. --- .../descriptor-packaging/README.md | 46 +- .../cmake/HkpPackaging.cmake | 88 +-- .../examples/descriptors/README.md | 10 +- .../python/hkp_pack/__init__.py | 5 +- .../python/hkp_pack/descriptor_context.py | 29 +- .../python/hkp_pack/descriptors.py | 153 +++- .../python/hkp_pack/desk_check.py | 58 +- .../python/hkp_pack/pipeline.py | 168 ++++- .../descriptor-packaging/tests/conftest.py | 7 + .../tests/test_desk_check_invariants.py | 115 ++- .../tests/test_hkp_arch_probe.py | 57 ++ .../tests/test_hkp_pack_generic.py | 679 ++++++++++++++++++ .../tests/test_hkp_pack_hip.py | 42 +- .../tests/test_hkp_pack_hsaco.py | 24 +- .../tests/test_hkp_pack_layout.py | 57 +- .../tests/test_hkp_pack_parallel.py | 65 +- .../tests/test_hkp_pack_rocke.py | 21 +- .../tests/test_hkp_python_environment.py | 2 + .../tests/test_specialization_agreement.py | 23 +- .../tools/hkp_arch_probe.py | 65 ++ .../tools/hkp_desk_check.py | 16 +- .../descriptor-packaging/tools/hkp_pack.py | 9 + 22 files changed, 1540 insertions(+), 199 deletions(-) create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index edaf694e0f84..031b170e8a59 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -4,6 +4,43 @@ Build-time UKD/KMD/KDP -> kpack packaging. Provider-internal (`tools/hkp_pack.py see `python/hkp_pack/` for the pipeline and `examples/descriptors/` for a real, minimal authored source root. +## Generic GPU targets + +`arch` may name a LLVM generic target (`gfx11-generic`) listed in the shared table +`projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json`; the packer reads it through +the required `--generic-targets-json` (CMake passes +`HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON`). `GPU_TARGETS` stays concrete. After the +concrete passes, every table generic that a KDP lists and that contains a selected arch +is compiled and packed once under its own spelling (`kpack/hip_kernel_provider_.kpack`, +every emitted document `arch: []`), and that one tree is copied into the folder of each +selected member whose concrete pass succeeded. No folder is named for the generic, and an +empty-`arch` KDP is never emitted into a generic copy. The copy is a pure function of the +source tree, the generic's spelling, hipcc and the table, so the copies are byte-identical +across member folders and across separate per-arch builds. + +Authoring rules (packer errors, stable substrings in parentheses): + +- A name ending `-generic` that the table does not list is an error (`generic target name + absent`). +- One `arch` list may not hold a generic with a member it contains, nor two generics that + share a member (`lists '`). Author two KDPs instead: an explicit override plus a generic + fallback are separate packs. +- Under a KDP whose list holds a generic, every UKD with its own `arch` must list every + generic of the KDP and only entries the KDP lists, and a standalone UKD must carry such an + `arch` (`must list every generic of the KDP`). An inline UKD with no `arch` inherits the + pack. +- A UKD may not name a generic its KDP does not list, including under an empty-`arch` KDP + (`ship in no shard`). +- rocKE does not support generics yet (`does not support generic targets yet`). +- Two shards whose member sets intersect may not write one path with different bytes + (`would be written by shard`); identical shared descriptors merge. + +Duplicate matcher tuples are judged per arch tier (`hkp_desk_check`): an explicit arch +beats a generic beats an arch-less kernel on a device, so an arch-less kernel and an +explicit one do not collide, while two arch-less kernels, or two naming the same +generic, do. `tools/hkp_arch_probe.py` answers the CMake configure's "does this KDP reach +any selected arch" with the same rule. + ## Source roots and what the walk accepts Each wired root is walked recursively and packs into **its own** `OUT_ROOT`; no two @@ -52,8 +89,9 @@ object's AMDGPU metadata as for a compiled object, and the UKD ships as `kind: k The toc key derives from the file's resolved root-relative path, so one file serving several symbols is one archive entry, and one key claimed by two different files is a hard error. The packer does not check the object's format or target processor. An -`hsaco` UKD must list the arch(es) its object runs on in `arch` (a generic-target -object lists every arch it runs on); an absent or empty `arch` is rejected. The author's own load test on the target arch +`hsaco` UKD must list the arch(es) its object runs on in `arch` (a generic spelling such +as `gfx11-generic` is allowed and packs the authored bytes under that key, with the +generic rules above and no ELF check); an absent or empty `arch` is rejected. The author's own load test on the target arch is the only check; no in-tree load test covers `hsaco`. The shipped provenance records `origin_kind: "hsaco"`, the root-relative `file`, its `sha256` and the `symbol`, and makes no toolchain claim. As for `hip`, the specialization contract must declare @@ -284,8 +322,8 @@ Set `HIPKERNELPROVIDER_KPACK_REQUIRE_ROCM_KPACK=1` (mirroring `_REQUIRE_HIPCC` / ### Desk-check a variant set (`hkp_pack.desk_check`, `tools/hkp_desk_check.py`) ``` -tools/hkp_desk_check.py --mode {full,structural} [--kpack-python-dir D] - [--field F] [--drift-field F] +tools/hkp_desk_check.py --mode {full,structural} --generic-targets-json J + [--kpack-python-dir D] [--field F] [--drift-field F] ``` The desk check resolves KDP engine → UED metadata → KMD UUID within the selected diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake index ba4c3848d517..776ab3fe9641 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake @@ -12,6 +12,7 @@ include_guard(GLOBAL) set(HKP_PKG_DIR "${CMAKE_CURRENT_LIST_DIR}/..") set(HKP_PYTHON_ROOT "${HKP_PKG_DIR}/python") set(HKP_TOOL "${HKP_PKG_DIR}/tools/hkp_pack.py") +set(HKP_ARCH_PROBE_TOOL "${HKP_PKG_DIR}/tools/hkp_arch_probe.py") set(HKP_WHEEL_DIGEST_TOOL "${HKP_PKG_DIR}/tools/hkp_wheel_digest.py") set(HKP_FIXTURES "${HKP_PKG_DIR}/tests/fixtures") @@ -313,11 +314,12 @@ function(hkp_wire_pack_target) --inter-root "${_inter_root}" --kpack-python-dir "${ARG_ROCM_KPACK_DIR}" --source-label "${ARG_NAME}" + --generic-targets-json "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}" ${_wheel_stamp_arg} COMMAND "${CMAKE_COMMAND}" -E make_directory "${ARG_OUT_ROOT}" COMMAND "${CMAKE_COMMAND}" -E touch "${_stamp}" DEPENDS "${HKP_TOOL}" ${_source_inputs} ${_tool_sources} - "${_input_manifest}" + "${_input_manifest}" "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}" ${_interp_dep} ${_wheel_dep} COMMENT "hkp: packing root '${ARG_NAME}' for ${ARG_ARCHES}" VERBATIM) @@ -1024,13 +1026,12 @@ endfunction() # --------------------------------------------------------------------------- # _hkp_root_covers_any_arch( ) # TRUE when at least one non-hidden *.kdp.json under would survive -# arch_matches() for at least one arch in . Mirrors that predicate exactly: an -# absent `arch` key and an empty `arch` array are both wildcards, anything else is -# exact string membership in the wired arch list. Consulted for the default root alone -# (hkp_add_packaging below). +# admit at least one arch in (_hkp_kdp_arch_matches: an absent or empty `arch` +# is a wildcard; an entry admits a target it names or a table generic containing it). +# Consulted for the default root alone (hkp_add_packaging below). # -# Only FALSE is authoritative. kdp_survives() tests arch_matches() first, so a root no -# arch matches is provably empty; TRUE claims nothing beyond "not provably empty", +# Only FALSE is authoritative. kdp_survives() tests the KDP-level arch rule first, so a +# root no arch reaches is provably empty; TRUE claims nothing beyond "not provably empty", # since a matching KDP can still prune on its UKD entries. Every ambiguous case # therefore resolves to TRUE -- an unparseable KDP, or an `arch` that is not an array, # counts as covering, so the root stays wired and the packer reports what is wrong @@ -1059,8 +1060,7 @@ function(_hkp_root_covers_any_arch out_var root arches) DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_kdp}") - file(READ "${_kdp}" _kdp_json) - _hkp_kdp_arch_matches(_matches "${_kdp_json}" "${arches}") + _hkp_kdp_arch_matches(_matches "${_kdp}" "${arches}") if(_matches) set(${out_var} TRUE PARENT_SCOPE) return() @@ -1069,42 +1069,41 @@ function(_hkp_root_covers_any_arch out_var root arches) endfunction() # --------------------------------------------------------------------------- -# _hkp_kdp_arch_matches( ) -# TRUE when ships for any architecture in . +# _hkp_kdp_arch_matches( ) +# TRUE when the KDP at ships for any architecture in . # -# The packer's own arch_matches(): a KDP naming no architecture, or an empty list, -# wildcards and ships everywhere. +# Answered by tools/hkp_arch_probe.py, which runs the packer's own tier rule +# (hkp_pack.generic_targets.admits_target): a KDP naming no architecture, or an empty +# list, wildcards; an entry admits a target when it names it or is a generic target +# of the shared table that contains it. The generic table is the SDK's +# HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON, so the verdict and the packer read the +# same data. # # Every ambiguous case also resolves TRUE, deliberately: only FALSE is authoritative. # Resolving ambiguity the other way would let a malformed declaration read as a clean # absence and silently withdraw the packaging whose validation would have reported it. +# A probe that exits non-zero (an unreadable table) is a configure error. # --------------------------------------------------------------------------- -function(_hkp_kdp_arch_matches out_var kdp_json arches) - # cmake-lint: disable=E1120 - # cmake-lint carries no argument spec for foreach(... RANGE ...) and reports - # every spelling of it as missing a positional argument. The index loop below - # is valid CMake. - set(${out_var} TRUE PARENT_SCOPE) - - string(JSON _arch_type ERROR_VARIABLE _type_err TYPE "${kdp_json}" arch) - if(_type_err OR NOT _arch_type STREQUAL "ARRAY") - return() +function(_hkp_kdp_arch_matches out_var kdp_path arches) + execute_process( + COMMAND "${Python3_EXECUTABLE}" "${HKP_ARCH_PROBE_TOOL}" + --generic-targets-json "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}" + --arches "${arches}" + --kdp "${kdp_path}" + RESULT_VARIABLE _probe_rc + OUTPUT_VARIABLE _probe_out + ERROR_VARIABLE _probe_err + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT _probe_rc EQUAL 0) + message(FATAL_ERROR + "hkp: the arch probe failed (exit ${_probe_rc}) on '${kdp_path}': " + "${_probe_err}") endif() - - string(JSON _arch_len ERROR_VARIABLE _len_err LENGTH "${kdp_json}" arch) - if(_len_err OR _arch_len EQUAL 0) - return() + if(_probe_out STREQUAL "FALSE") + set(${out_var} FALSE PARENT_SCOPE) + else() + set(${out_var} TRUE PARENT_SCOPE) endif() - - math(EXPR _arch_last "${_arch_len} - 1") - foreach(_i RANGE ${_arch_last}) - string(JSON _declared ERROR_VARIABLE _get_err GET "${kdp_json}" arch ${_i}) - if(_get_err OR _declared IN_LIST arches) - return() - endif() - endforeach() - - set(${out_var} FALSE PARENT_SCOPE) endfunction() # --------------------------------------------------------------------------- @@ -1205,7 +1204,7 @@ function(_hkp_root_declares_engine_for_arch out_var root engine arch) continue() endif() - _hkp_kdp_arch_matches(_matches "${_kdp_json}" "${arch}") + _hkp_kdp_arch_matches(_matches "${_kdp}" "${arch}") if(_matches) set(${out_var} TRUE PARENT_SCOPE) return() @@ -1324,6 +1323,15 @@ endfunction() # regardless. # --------------------------------------------------------------------------- function(hkp_add_packaging) + if(NOT HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON + OR NOT EXISTS "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}") + message(FATAL_ERROR + "hkp: HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON is empty or names a missing " + "file ('${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}'). The packer reads " + "the generic GPU target table that hipdnn_plugin_sdk ships " + "(gpu_generic_targets.json); an older installed hipdnn_plugin_sdk lacks it. " + "Build against a hipdnn_plugin_sdk that provides the table.") + endif() find_package(Python3 COMPONENTS Interpreter REQUIRED) hkp_resolve_kpack(_rocm_kpack_dir "${Python3_EXECUTABLE}") @@ -1345,8 +1353,8 @@ function(hkp_add_packaging) # Arch coverage is consulted for the DEFAULT root alone, which builds inherit without # asking for it; a named root reaches the packer and fails there. Safe in one - # direction only: arch_matches() runs first inside kdp_survives(), so "no KDP - # declares an arch this build packs for" proves no KDP survives, and a root this + # direction only: the KDP-level arch rule runs first inside kdp_survives(), so "no KDP + # reaches an arch this build packs for" proves no KDP survives, and a root this # misses stays wired for the packer to report. if(_product_has_content AND _source_root_is_default) _hkp_root_covers_any_arch(_product_covers_arch "${_source_root}" "${_arches}") diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/examples/descriptors/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/examples/descriptors/README.md index a4c66924575d..a4ff48b13d0c 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/examples/descriptors/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/examples/descriptors/README.md @@ -79,12 +79,14 @@ compiled artifact. Authoring it would let the descriptor disagree with the kerne authored.** `file` resolves relative to the descriptor that names it, must stay inside the source root, and has no root-relative fallback, like a hip `source`. The object is packed as-is, with no compile. This tree contains no hsaco example. -Each hsaco UKD must list in `arch` the arch(es) its object runs on (a -generic-target object lists every arch it runs on); an absent or empty `arch` -is rejected. +Each hsaco UKD must list in `arch` the arch(es) its object runs on (it may +be a generic spelling such as `gfx11-generic`, packed under that key); an absent +or empty `arch` is rejected. **`arch` filters which shard a descriptor ships in.** It does not select a -builder: naming `gfx942` does not make a gfx950 builder produce gfx942 code. +builder: naming `gfx942` does not make a gfx950 builder produce gfx942 code. It +may name a generic target (`gfx11-generic`): that pack is built once and copied +into the folder of every selected member of the generic. **`library` is relative to the descriptor that declared it**, and the archive is one per arch at the arch root — so a descriptor in a child folder climbs back out diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/__init__.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/__init__.py index a6ac9cfae4bd..d18fdf347720 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/__init__.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/__init__.py @@ -15,8 +15,9 @@ its descriptor-relative file is packed as-is, with no compile, into the same archive, and its signature is read from the object's AMDGPU metadata, as for a compiled object. The packer does not check the object's format or target -processor. Each such UKD must list the arch(es) its object runs on (a generic-target -object lists every arch it runs on); a wildcard is rejected. The author's own load test +processor. Each such UKD must list the arch(es) its object runs on, which may be a +generic target spelling of the generic target table (the object is then packed under +that spelling); a wildcard is rejected. The author's own load test on the target arch is the only check; no in-tree load test covers hsaco. """ diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py index 33392497494a..c3f8911dc8cf 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py @@ -8,6 +8,7 @@ import os from . import agreement +from . import generic_targets as gtmod from .errors import HkpPackError _DESCRIPTOR_TYPES = ("kdp", "ukd", "kmd", "ued", "umd", "udd", "uhd") @@ -207,9 +208,29 @@ def declarations(bundles: list[Bundle], schemas: dict) -> dict: return found -def consumer_records(bundles: list[Bundle], schemas: dict, arch: str) -> dict: +def archive_arch(entry_arch: list, target: str, generic_targets) -> str: + """The spelling to read a kpack archive with for @target. + + @target itself when @entry_arch lists it; otherwise the first generic in + @entry_arch that contains it (the archive of a generic copy is keyed by the + generic's name); otherwise @target. + """ + if target in entry_arch: + return target + for entry in entry_arch: + if gtmod.entry_tier(entry, target, generic_targets) == gtmod.TIER_GENERIC: + return entry + return target + + +def consumer_records( + bundles: list[Bundle], schemas: dict, arch: str, generic_targets +) -> dict: """UKD id -> canonical records over EVERY consumer inside the caller root. + An entry is a consumer of @arch when its arch is empty, lists @arch, or + names a generic of the table that contains @arch. + Shared standalone UKDs carry all KDP/engine/KMD bindings, not just the selected bundle's. Agreement stays the sole record-construction authority. """ @@ -218,7 +239,11 @@ def consumer_records(bundles: list[Bundle], schemas: dict, arch: str) -> dict: header = {k: v for k, v in bundle.kdp_doc.items() if k != "kernelDescriptors"} header["arch"] = [arch] for entry in bundle.entries: - if entry.arch and arch not in entry.arch: + if ( + entry.arch + and arch not in entry.arch + and not gtmod.admits_target(entry.arch, arch, generic_targets) + ): continue enclosing = bundle.kdp_doc if entry.inline else None if agreement.resolved_contract(entry.ukd, enclosing) is None: diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py index cebab6220c07..7220565ba1c9 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py @@ -3,6 +3,7 @@ from dataclasses import dataclass, field from pathlib import Path, PureWindowsPath +from . import generic_targets as gtmod from .errors import HkpPackError KDP_TYPE = "kdp" @@ -65,6 +66,9 @@ def id(self): @dataclass class FlatInput: descriptors: list = field(default_factory=list) + # The generic target table the root was validated against; every arch rule + # downstream (kdp_survives, the pipeline's selection) reads it from here. + generic_targets: object = None def by_type(self, dtype): return [d for d in self.descriptors if d.type == dtype] @@ -127,16 +131,30 @@ def arch_matches(kdp_doc, arch): return arch in archs -def _arch_subset_ok(ukd_arch, kdp_arch): +def kdp_arch_matches(kdp_doc, arch, generic_targets): + """Whether a KDP is emitted into the pass for @arch. + + A generic pass (@arch is a table generic) takes literal membership only: an + empty-arch KDP is a concrete-shard wildcard and is NOT emitted into the + generic copy. Any other arch follows arch_matches. + """ + if generic_targets.has(arch): + return arch in (kdp_doc.get("arch") or []) + return arch_matches(kdp_doc, arch) + + +def _arch_subset_ok(ukd_arch, kdp_arch, generic_targets): """A UKD's arch is admissible under a referencing KDP's arch. An empty list on either side is a wildcard: a wildcard KDP admits any UKD, and a wildcard UKD is admissible under any KDP. Two explicit lists require - the UKD's arches to be a subset of the KDP's. + the UKD's expanded device set to lie within the KDP's (a generic stands for + its members). The stricter literal rules for generic KDPs live in + validate_generic_arch. """ if not ukd_arch or not kdp_arch: return True - return set(ukd_arch) <= set(kdp_arch) + return gtmod.covers(kdp_arch, ukd_arch, generic_targets) def kdp_survives(kdp_doc, flat, arch): @@ -146,7 +164,7 @@ def kdp_survives(kdp_doc, flat, arch): (an inline dict or a standalone resolved by id) also applies to that arch. A KDP whose UKDs all filter out for this arch is dropped from the shard. """ - if not arch_matches(kdp_doc, arch): + if not kdp_arch_matches(kdp_doc, arch, flat.generic_targets): return False ukd_by_id = flat.ukd_by_id() for entry in kdp_doc.get("kernelDescriptors", []): @@ -502,7 +520,7 @@ def _validate_shape(desc, log=print): _validate_kmd(desc) -def load_flat_input(root, log=print): +def load_flat_input(root, generic_targets, log=print): """Load and structurally validate every *.json descriptor under a root. Walks the root recursively: a descriptor's authored subpath is meaningful @@ -514,7 +532,9 @@ def load_flat_input(root, log=print): is not one of ours: warn and skip it rather than aborting the pack, so an incidental file in the source folder is tolerated. A hidden path -- any dot-prefixed segment, or a dot-prefixed filename -- is warned and skipped - the same way, so nothing the walk passes over is invisible. Raises + the same way, so nothing the walk passes over is invisible. `generic_targets` + is the loaded GenericTargets table the arch rules are checked against and + that the returned FlatInput carries. Raises HkpPackError on any malformed / missing-field / unknown-type / dangling-reference descriptor that IS type-tagged. @@ -570,14 +590,129 @@ def load_flat_input(root, log=print): _validate_shape(desc, log) descriptors.append(desc) - flat = FlatInput(descriptors=descriptors) + flat = FlatInput(descriptors=descriptors, generic_targets=generic_targets) _reject_inline_standalone_collision(flat) _reject_duplicate_ids(flat) + validate_generic_arch(flat) _validate_references(flat) _warn_orphan_standalone_ukds(flat, log) return flat +def _arch_entries(doc): + """The `arch` list of a descriptor document (empty when absent).""" + return list(doc.get("arch") or []) + + +def _reject_unknown_generics(archs, file_name, table): + for entry in archs: + if gtmod.is_generic_shaped(entry) and not table.has(entry): + raise HkpPackError( + f"{file_name}: arch entry '{entry}' is a generic target name absent " + f"from the generic target table {table.path} " + f"(known: {', '.join(table.names())})" + ) + + +def _reject_mixed_generic_list(archs, file_name, table): + """S5: one list may not hold a generic together with a member it contains, nor + two generics that share a member.""" + advice = ( + "list one, or author two KDPs (an explicit override plus a generic " + "fallback are separate packs)" + ) + for i, x in enumerate(archs): + for y in archs[i + 1 :]: + if table.has(x) and table.has(y): + shared = [m for m in table.members(x) if m in table.members(y)] + if shared: + raise HkpPackError( + f"{file_name}: 'arch' lists '{x}' and '{y}': the two " + f"generics share member {shared[0]}; {advice}" + ) + continue + generic, member = (x, y) if table.has(x) else (y, x) + if table.has(generic) and member in table.members(generic): + raise HkpPackError( + f"{file_name}: 'arch' lists '{x}' and '{y}': {generic} contains " + f"{member}; {advice}" + ) + + +def validate_generic_arch(flat): + """Generic-target rules over every KDP/UKD `arch` in the root. + + Per list: names that look generic but are not in the table are errors, and a + list may not mix a generic with a member it contains or two generics sharing + a member (S5). Per KDP holding a generic: a UKD with its own arch must list + every generic of the KDP and only entries the KDP lists (S6), and a + standalone UKD must carry such an arch. Per KDP of any shape: a UKD may not + name a generic the KDP does not list. rocKE does not support generics yet. + The loader accepts the lenient forms; this is the stricter packer-side layer. + """ + table = flat.generic_targets + ukd_by_id = flat.ukd_by_id() + for desc in flat.descriptors: + if desc.type not in (KDP_TYPE, UKD_TYPE): + continue + name = desc.path.name + _reject_unknown_generics(_arch_entries(desc.doc), name, table) + _reject_mixed_generic_list(_arch_entries(desc.doc), name, table) + if desc.type == KDP_TYPE: + for entry in desc.doc.get("kernelDescriptors", []): + if isinstance(entry, dict): + _reject_unknown_generics(_arch_entries(entry), name, table) + _reject_mixed_generic_list(_arch_entries(entry), name, table) + + for kdp in flat.kdps(): + file_name = kdp.path.name + kdp_arch = _arch_entries(kdp.doc) + kdp_generics = [a for a in kdp_arch if gtmod.is_generic_shaped(a)] + for entry in kdp.doc.get("kernelDescriptors", []): + if isinstance(entry, str): + sdesc = ukd_by_id.get(entry) + if sdesc is None: + continue # reported by _validate_references + ukd, standalone = sdesc.doc, sdesc + else: + ukd, standalone = entry, None + ukd_arch = _arch_entries(ukd) + ukd_generics = [a for a in ukd_arch if gtmod.is_generic_shaped(a)] + kind = ukd.get("kernel_source", {}).get("kind") + if kind == "rocke" and (kdp_generics or ukd_generics): + generic = (kdp_generics or ukd_generics)[0] + raise HkpPackError( + f"{file_name}: rocKE does not support generic targets yet " + f"(arch '{generic}')" + ) + ukd_label = f"UKD '{ukd.get('id', '?')}'" + for generic in ukd_generics: + if generic not in kdp_arch: + raise HkpPackError( + f"{file_name}: {ukd_label} declares generic target " + f"'{generic}' but the KDP does not list '{generic}'; it " + "would ship in no shard" + ) + if not kdp_generics: + continue + if standalone is not None: + who = f"standalone UKD '{ukd.get('id')}' ({standalone.path.name})" + else: + who = f"inline UKD '{ukd.get('name', ukd.get('id', '?'))}'" + if standalone is None and not ukd_arch: + continue # an inline UKD with no arch inherits the pack + lists_all = all(g in ukd_arch for g in kdp_generics) + only_listed = all(a in kdp_arch for a in ukd_arch) + if not ukd_arch or not lists_all or not only_listed: + declared = f"arch {ukd_arch}" if ukd_arch else "no arch" + raise HkpPackError( + f"{file_name}: KDP lists generic target(s) {kdp_generics} " + f"(arch {kdp_arch}) but {who} declares {declared}; a UKD under " + "a generic KDP must list every generic of the KDP and only " + "entries the KDP lists" + ) + + def _reject_inline_standalone_collision(flat): """An inline UKD id colliding with a standalone UKD is ambiguous by-id. @@ -666,7 +801,9 @@ def _validate_references(flat): udoc = ukd_by_id[entry].doc else: udoc = entry - if not _arch_subset_ok(udoc.get("arch") or [], kdp_arch): + if not _arch_subset_ok( + udoc.get("arch") or [], kdp_arch, flat.generic_targets + ): raise HkpPackError( f"UKD '{udoc.get('id')}' arch {udoc.get('arch')} is not a " f"subset of KDP {kdp.path.name} arch {doc.get('arch')}" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py index e399d02db95b..2327b39eb5dd 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py @@ -12,6 +12,7 @@ from pathlib import Path from . import agreement, descriptor_context +from . import generic_targets as gtmod from .errors import HkpPackError from .kpack_resolver import load_kpack @@ -212,7 +213,7 @@ def _payload( def compiled_agreement( - kdp_path: Path, kpack_python_dir=None + kdp_path: Path, generic_targets, kpack_python_dir=None ) -> tuple[list[str], list[str], int]: """Compiled-specialization agreement over one shipped KDP. @@ -249,7 +250,9 @@ def compiled_agreement( 0, ) arch = arches[0] - all_records = descriptor_context.consumer_records(bundles, schemas, arch) + all_records = descriptor_context.consumer_records( + bundles, schemas, arch, generic_targets + ) failures: list[str] = [] unclaimed: list[str] = [] verified = 0 @@ -291,7 +294,13 @@ def compiled_agreement( f"verified against a binary" ) continue - payload = _payload(entry, arch, kpack_python_dir) + payload = _payload( + entry, + descriptor_context.archive_arch( + entry.arch or [arch], arch, generic_targets + ), + kpack_python_dir, + ) agreement.verify(kernel, records, payload) verified += 1 except HkpPackError as exc: @@ -366,25 +375,41 @@ def metadata_spec_drift(kernels: list[dict], fields=None) -> list[tuple[str, str return bad -def _reachable_together(group: list[dict]) -> int: - """The largest number of kernels in `group` one device reaches. +def _reachable_together(group: list[dict], generic_targets) -> int: + """The largest number of kernels in `group` that tie for one device. - A tuple shared across disjoint arches is no collision. An absent or empty - `arch` is a wildcard and counts against every arch in the group. + Per candidate device (every explicit id and every member of a table generic in + the group's lists; one synthetic device when every list is empty) the kernels + whose arch tier equals the group's best tier on that device are counted: an + arch-less kernel and an explicit one do not collide (the explicit one wins), + two arch-less ones do, and so do two kernels naming the same generic. """ - sets = [frozenset(k.get("arch") or ()) for k in group] - named = frozenset().union(*sets) if sets else frozenset() - if not named: + lists = [list(k.get("arch") or ()) for k in group] + devices: set[str] = set() + for entries in lists: + for entry in entries: + if gtmod.is_generic_shaped(entry): + if generic_targets.has(entry): + devices.update(generic_targets.members(entry)) + else: + devices.add(entry) + if not devices: return len(group) - return max(sum(1 for s in sets if not s or arch in s) for arch in named) + best = 0 + for device in devices: + tiers = [gtmod.list_tier(e, device, generic_targets) for e in lists] + reached = [t for t in tiers if t is not None] + if reached: + best = max(best, reached.count(min(reached))) + return best def duplicate_matcher_tuples( - kernels: list[dict], fields=DEFAULT_MATCHER_FIELDS + kernels: list[dict], fields, generic_targets ) -> dict[tuple, int]: """Invariant 2: no two kernels may share a matcher tuple on the same arch -- one is unreachable. Returns {tuple: count} for every tuple two kernels reach - one device with, the scope the runtime refuses in. + one device at the same arch tier with, the scope the runtime refuses in. The compared set is the union of `fields` present in any kernel's metadata, never ``kernels[0]``'s, which would make the identity list-order dependent. A @@ -395,7 +420,7 @@ def duplicate_matcher_tuples( for kernel in kernels: key = tuple(kernel.get("metadata", {}).get(f, _ABSENT) for f in present) groups[key].append(kernel) - counts = {t: _reachable_together(g) for t, g in groups.items()} + counts = {t: _reachable_together(g, generic_targets) for t, g in groups.items()} return {t: c for t, c in counts.items() if c > 1} @@ -454,6 +479,7 @@ def __init__( drift_fields=None, *, mode: str, + generic_targets, agreement_failures=None, agreement_unclaimed=None, agreement_verified=0, @@ -486,7 +512,9 @@ def __init__( self.drift = metadata_spec_drift(kernels, self.drift_fields) except DeskCheckNoSpecFound as exc: self.spec_drift_error = str(exc) - self.duplicate_tuples = duplicate_matcher_tuples(kernels, self.fields) + self.duplicate_tuples = duplicate_matcher_tuples( + kernels, self.fields, generic_targets + ) self.toc_applicable = _field_applicable(kernels, "toc_key") self.toc_distinct, self.toc_total = ( diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py index 2ad7f0913d14..7a7bf5ce85d3 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py @@ -18,11 +18,14 @@ from .descriptors import ( KPACK_DIR_NAME, arch_matches, + kdp_arch_matches, kdp_survives, load_flat_input, reachable_generic_ids, + type_from_filename, ) from .errors import HkpPackError +from .generic_targets import GenericTargets from .kernel_signature import kernel_signature from .kpack_resolver import load_kpack @@ -124,6 +127,8 @@ class ArchResult: out_dir: Path kpack_path: Path skipped: bool = False + # Archives of the generic passes copied into this member's folder. + generic_kpack_paths: tuple = () _EXC_TEXT_LIMIT = 200 @@ -406,7 +411,7 @@ class _VariantJob: requests: dict = field(default_factory=dict) -def _selected_entries(doc, arch, ukd_by_id): +def _selected_entries(doc, arch, ukd_by_id, generic_targets): """Yield the entries of doc that ship for arch. Yields `(entry_id, ukd_doc, sdesc)`: for a standalone-UKD id ref, the id @@ -415,13 +420,14 @@ def _selected_entries(doc, arch, ukd_by_id): because the walk needs its `path.name` for the error context and for the shipped filename as well as its `rel_dir` for the variant key. - All three arch filters live here and nowhere else, so the prewarm and the + All three arch filters live here and nowhere else (the KDP-level one is + `kdp_arch_matches`, the two UKD-level ones `arch_matches`), so the prewarm and the serial walk cannot select different variant sets. `ukd_by_id` arrives as a parameter rather than being reached for through `flat`, which leaves the generator no way to enumerate a standalone UKD no KDP references: an orphan is legal input the walk never compiles. """ - if not arch_matches(doc, arch): + if not kdp_arch_matches(doc, arch, generic_targets): return for entry in doc["kernelDescriptors"]: if isinstance(entry, str): @@ -457,7 +463,9 @@ def _agreement_inputs(flat, arch): ) # The KDP declares nothing (just checked), so there is nothing for a # kernel to inherit and each speaks only for itself. - for _sid, ukd, _sdesc in _selected_entries(kdp.doc, arch, ukd_by_id): + for _sid, ukd, _sdesc in _selected_entries( + kdp.doc, arch, ukd_by_id, flat.generic_targets + ): if agreement.resolved_contract(ukd) is not None: raise HkpPackError( f"UKD {ukd['id']} in {kdp.path.name}: a specialization " @@ -477,7 +485,9 @@ def _agreement_inputs(flat, arch): kmd = schemas[kmd_id] header = _kdp_header(kdp.doc) header["arch"] = [arch] - for sid, ukd, sdesc in _selected_entries(kdp.doc, arch, ukd_by_id): + for sid, ukd, sdesc in _selected_entries( + kdp.doc, arch, ukd_by_id, flat.generic_targets + ): # Completion is checked against the KMD the chain actually resolved to, # so a mis-typed or missing mandatory value fails before a compile. agreement.complete_metadata(ukd["metadata"], kmd) @@ -528,7 +538,9 @@ def _prewarm_jobs(flat, source_root, arch, observation_requests=None): jobs = [] seen = set() for kdp in flat.kdps(): - for sid, ukd, sdesc in _selected_entries(kdp.doc, arch, ukd_by_id): + for sid, ukd, sdesc in _selected_entries( + kdp.doc, arch, ukd_by_id, flat.generic_targets + ): rel_dir = sdesc.rel_dir if sid is not None else kdp.rel_dir vk = _variant_key_for(ukd, rel_dir) if vk is None or vk in seen: @@ -903,7 +915,7 @@ def compile_intermediate(flat, source_root, arch, hipcc, inter_arch_dir, log=pri for kdp in flat.kdps(): doc = kdp.doc - if not arch_matches(doc, arch): + if not kdp_arch_matches(doc, arch, flat.generic_targets): _write_bytes_at( inter_arch_dir, kdp.rel_dir, kdp.path.name, kdp.path.read_bytes() ) @@ -913,7 +925,9 @@ def compile_intermediate(flat, source_root, arch, hipcc, inter_arch_dir, log=pri ukds = [] entries = [] new_kds = [] - for sid, entry, sdesc in _selected_entries(new_doc, arch, ukd_by_id): + for sid, entry, sdesc in _selected_entries( + new_doc, arch, ukd_by_id, flat.generic_targets + ): if sid is not None: # A reference to a standalone UKD: compile it once per arch and # keep the string in the KDP; it ships as its own file. Listing @@ -1447,11 +1461,71 @@ def _all_ukds(): return ArchResult(arch=arch, out_dir=out_arch_dir, kpack_path=kpack_path) +def _merge_generic_into_member(generic_dir, member_dir, staging_dir): + """Merge one generic pass's tree into a member shard folder, atomically. + + Builds `staging_dir` as a copy of `member_dir` (when it exists) plus every + file of `generic_dir`, then swaps it over `member_dir`. A destination that + already exists is skipped when its bytes equal the source's (the shared + engine/dispatch/matcher/KMD descriptors both passes emit verbatim) and + refused when they differ: two shards whose member sets intersect would then + write one path with different content. Any failure removes the staging dir + and leaves `member_dir` exactly as it was. + """ + generic_dir, member_dir, staging_dir = ( + Path(generic_dir), + Path(member_dir), + Path(staging_dir), + ) + if staging_dir.exists(): + shutil.rmtree(staging_dir) + try: + if member_dir.exists(): + shutil.copytree(member_dir, staging_dir, copy_function=shutil.copyfile) + else: + staging_dir.mkdir(parents=True) + for src in sorted(p for p in generic_dir.rglob("*") if p.is_file()): + rel = src.relative_to(generic_dir) + dest = staging_dir / rel + if dest.exists(): + if dest.read_bytes() == src.read_bytes(): + continue + described = "" + if src.suffix == ".json": + try: + doc = json.loads(src.read_text(encoding="utf-8")) + token = type_from_filename(src) + if isinstance(doc, dict) and doc.get("id") is not None: + described = f" ({str(token).upper()} id '{doc['id']}')" + except ValueError: + pass + raise HkpPackError( + f"{rel.as_posix()}{described} would be written by shard " + f"'{member_dir.name}' ({member_dir / rel}) and shard " + f"'{generic_dir.name}' ({src}) with different bytes; the " + "shards' member sets intersect" + ) + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(src, dest) + if member_dir.exists(): + shutil.rmtree(member_dir) + staging_dir.rename(member_dir) + except HkpPackError: + shutil.rmtree(staging_dir, ignore_errors=True) + raise + except OSError as exc: + shutil.rmtree(staging_dir, ignore_errors=True) + raise HkpPackError( + f"cannot merge '{generic_dir.name}' into '{member_dir}': {exc}" + ) from exc + + def run_pipeline( source_root, arches, out_root, hipcc, + generic_targets_json, rocm_kpack_dir=None, inter_root=None, expected_sha256=None, @@ -1474,6 +1548,13 @@ def run_pipeline( arch with no surviving KDP is skipped cleanly (no folder, no kpack) and logged with 'no kernels for , skipping'; every arch skipping is a failure, not a pack. Empty arch list installs nothing (exit 0). + + `generic_targets_json` names the generic target table. After the concrete + passes, every table generic that some KDP lists and that contains a selected + arch is compiled and packed ONCE (a pure function of the source tree, the + generic's spelling, hipcc and the table), and that one tree is merged into + the folder of every selected member whose concrete pass did not fail. A + generic never gets a folder of its own under `out_root`. """ out_root = Path(out_root) results = {} @@ -1481,7 +1562,8 @@ def run_pipeline( return results kpack_mod, comp = load_kpack(rocm_kpack_dir) - flat = load_flat_input(source_root, log=log) + generic_targets = GenericTargets.load(generic_targets_json) + flat = load_flat_input(source_root, generic_targets, log=log) if inter_root is None: raise HkpPackError( @@ -1563,15 +1645,75 @@ def run_pipeline( arch=arch, out_dir=out_arch_dir, kpack_path=None, skipped=True ) + for generic in generic_targets.names(): + members = [a for a in arches if a in generic_targets.members(generic)] + if not members: + continue + if not any(kdp_survives(k.doc, flat, generic) for k in flat.kdps()): + continue + generic_out = inter_root / ".generic-out" / generic + try: + inter = compile_intermediate( + flat, source_root, generic, hipcc, inter_root / generic, log=log + ) + if generic_out.exists(): + shutil.rmtree(generic_out) + generic_result = pack_arch( + flat, + inter, + generic_out, + kpack_mod, + comp, + expected_sha256=expected_sha256, + hipcc=hipcc, + rocke_wheel_stamp=rocke_wheel_stamp, + group=group, + source_label=source_label, + ) + except HkpPackError as exc: + failures[generic] = str(exc) + log(f"ERROR: {generic} failed: {exc}") + if generic_out.exists(): + shutil.rmtree(generic_out) + continue + for member in members: + if member in failures: + continue + member_dir = out_root / member + try: + _merge_generic_into_member( + generic_out, member_dir, out_root / f".{member}.merge.staging" + ) + except HkpPackError as exc: + failures[member] = str(exc) + log(f"ERROR: {member} failed merging {generic}: {exc}") + break + previous = results.get(member) + kpack_path = None if previous is None else previous.kpack_path + copied = () + if generic_result.kpack_path is not None: + copied = ( + member_dir / KPACK_DIR_NAME / _kpack_filename(generic, group), + ) + results[member] = ArchResult( + arch=member, + out_dir=member_dir, + kpack_path=kpack_path, + skipped=False, + generic_kpack_paths=( + (() if previous is None else previous.generic_kpack_paths) + copied + ), + ) + if failures: # Non-zero exit with partial output: the build fails loudly, but a # developer can still inspect what did succeed. Exiting 0 here would # resurrect the silent-empty-package class of defect. detail = "; ".join(f"{a}: {r}" for a, r in sorted(failures.items())) raise HkpPackError( - f"packing failed for {len(failures)} of {len(arches)} arch(es) " - f"[{detail}]. Arches that succeeded were written; the failed arches' " - "output was discarded." + f"packing failed for {len(failures)} failure(s) across {len(arches)} " + f"requested arch(es) [{detail}]. Arches that succeeded were written; " + "the failed arches' output was discarded." ) # The archive clause keys on what the root holds rather than on what survived @@ -1595,7 +1737,7 @@ def run_pipeline( ) if _root_holds_compiling_source(flat) and not any( - r.kpack_path for r in results.values() + r.kpack_path or r.generic_kpack_paths for r in results.values() ): raise HkpPackError( f"packing '{source_root}' wrote descriptors but no archive, while " diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/conftest.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/conftest.py index a3a261dfb032..ff8b912c14b2 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/conftest.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/conftest.py @@ -12,6 +12,13 @@ if str(_PKG_ROOT) not in sys.path: sys.path.insert(0, str(_PKG_ROOT)) +# The in-repo generic GPU target table every pack in these tests is validated against. +# Exported so a test names it once; a test needing a different table writes its own. +from hkp_pack.generic_targets import DEFAULT_TABLE_PATH, GenericTargets # noqa: E402 + +GENERIC_TARGETS_JSON = DEFAULT_TABLE_PATH +GENERIC_TARGETS = GenericTargets.load(GENERIC_TARGETS_JSON) + # rocm_kpack location: CMake passes HIPKERNELPROVIDER_ROCM_KPACK_DIR; otherwise # rely on an installed rocm_kpack already importable. No skip on absence — the # compiler and kpack are load-bearing; a missing dependency is a hard failure. diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py index 4e2a1bb0a37e..a6e2b02aaddb 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py @@ -20,6 +20,7 @@ import pytest +from conftest import GENERIC_TARGETS, GENERIC_TARGETS_JSON from hkp_pack.desk_check import ( DEFAULT_MATCHER_FIELDS, MODES, @@ -64,6 +65,7 @@ def packed_desk_check(tmp_path_factory, desk_check_fixture, hipcc, rocm_kpack_di attention_dense variants: head_size 64 and 128, both batch=1).""" tmp_path = tmp_path_factory.mktemp("desk_check_pack") run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=desk_check_fixture, arches=[ARCH], out_root=tmp_path / "out", @@ -84,6 +86,7 @@ def _pack_mutated(tmp_path, desk_check_fixture, hipcc, rocm_kpack_dir, mutate): mutate(doc) kdp_path.write_text(json.dumps(doc), encoding="utf-8") run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=src, arches=[ARCH], out_root=tmp_path / "out", @@ -162,7 +165,10 @@ def test_corrected_check_also_works_on_the_authored_tree(self, desk_check_fixtur class TestInvariant2DuplicateMatcherTuples: def test_distinct_variants_report_no_duplicates(self, packed_desk_check): assert ( - duplicate_matcher_tuples(_kernels(packed_desk_check), _MATCHER_FIELDS) == {} + duplicate_matcher_tuples( + _kernels(packed_desk_check), _MATCHER_FIELDS, GENERIC_TARGETS + ) + == {} ) @staticmethod @@ -178,19 +184,34 @@ def test_one_tuple_on_disjoint_arches_is_not_a_duplicate(self): unreachable. The runtime refuses a duplicate only on an arch both kernels reach.""" kernels = self._twins(["gfx942"], ["gfx950"]) - assert duplicate_matcher_tuples(kernels, ("dtype",)) == {} + assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == {} def test_one_tuple_on_a_shared_arch_is_still_a_duplicate(self): """Control for the case above: arch scoping narrows the check rather than switching it off. A single overlapping arch is enough.""" kernels = self._twins(["gfx942", "gfx950"], ["gfx950"]) - assert duplicate_matcher_tuples(kernels, ("dtype",)) == {("FLOAT",): 2} + assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == { + ("FLOAT",): 2 + } - def test_an_arch_less_kernel_collides_with_every_arch(self): - """An absent arch is the wildcard `arch_matches` reads it as, so it - reaches the other kernel's device and the two are a real collision.""" + def test_an_arch_less_kernel_and_an_explicit_one_do_not_collide(self): + """On the explicit kernel's device the explicit kernel outranks the + arch-less one, and everywhere else only the arch-less one reaches: the + runtime never has two candidates at one tier, so neither is unreachable.""" kernels = self._twins(None, ["gfx942"]) - assert duplicate_matcher_tuples(kernels, ("dtype",)) == {("FLOAT",): 2} + assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == {} + + def test_two_arch_less_kernels_collide(self): + kernels = self._twins(None, None) + assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == { + ("FLOAT",): 2 + } + + def test_two_kernels_naming_the_same_generic_collide(self): + kernels = self._twins(["gfx11-generic"], ["gfx11-generic"]) + assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == { + ("FLOAT",): 2 + } def test_real_pack_of_two_identical_matcher_tuples_is_detected( self, tmp_path, desk_check_fixture, hipcc, rocm_kpack_dir @@ -211,7 +232,9 @@ def mutate(doc): shipped = _pack_mutated( tmp_path, desk_check_fixture, hipcc, rocm_kpack_dir, mutate ) - dupes = duplicate_matcher_tuples(_kernels(shipped), _MATCHER_FIELDS) + dupes = duplicate_matcher_tuples( + _kernels(shipped), _MATCHER_FIELDS, GENERIC_TARGETS + ) assert dupes == {(1, 64): 2}, dupes @@ -300,7 +323,15 @@ def _run_cli(*args, mode="structural"): """The CLI as an agent runs it. The mode is always explicit, because the tool requires it.""" return subprocess.run( - [sys.executable, str(_TOOL), "--mode", mode, *args], + [ + sys.executable, + str(_TOOL), + "--mode", + mode, + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), + *args, + ], capture_output=True, text=True, ) @@ -340,7 +371,13 @@ def test_the_mode_is_required(self, packed_desk_check, tmp_path): json.dumps({"kernelDescriptors": _kernels(packed_desk_check)}) ) proc = subprocess.run( - [sys.executable, str(_TOOL), str(kdp_path)], + [ + sys.executable, + str(_TOOL), + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), + str(kdp_path), + ], capture_output=True, text=True, ) @@ -517,7 +554,10 @@ def _two_variants_with_a_translated_field(self): def test_narrowing_drift_fields_silences_drift_but_keeps_the_tuple(self): kernels = self._two_variants_with_a_translated_field() coupled = DeskCheckReport( - kernels, fields=("layout", "head_size"), mode="structural" + kernels, + fields=("layout", "head_size"), + mode="structural", + generic_targets=GENERIC_TARGETS, ) # The premise: with one shared list, `layout` false-positives. assert coupled.drift == [("nhwc", "layout"), ("nchw", "layout")] @@ -527,6 +567,7 @@ def test_narrowing_drift_fields_silences_drift_but_keeps_the_tuple(self): fields=("layout", "head_size"), drift_fields=("head_size",), mode="structural", + generic_targets=GENERIC_TARGETS, ) assert narrowed.drift == [], "drift comparison should have dropped layout" assert narrowed.duplicate_tuples == {}, ( @@ -543,6 +584,7 @@ def test_narrowing_drift_fields_does_not_narrow_the_matcher_tuple(self): fields=("dtype", "head_size"), drift_fields=("head_size",), mode="structural", + generic_targets=GENERIC_TARGETS, ) assert report.duplicate_tuples == {}, ( "dtype was dropped from the DRIFT comparison only -- it must " @@ -559,6 +601,7 @@ def test_a_real_duplicate_is_still_caught_with_narrowed_drift_fields(self): fields=("dtype", "head_size"), drift_fields=("head_size",), mode="structural", + generic_targets=GENERIC_TARGETS, ) assert report.duplicate_tuples == {("BFLOAT16", 64): 2} assert not report.ok @@ -571,7 +614,12 @@ def test_the_drift_default_is_wider_than_the_matcher_field_list(self): -> `return tuple(fields[:1])`.""" kernels = self._two_variants_differing_only_in_dtype() kernels[0]["metadata"]["head_size"] = 999 # real drift - report = DeskCheckReport(kernels, fields=("dtype",), mode="structural") + report = DeskCheckReport( + kernels, + fields=("dtype",), + mode="structural", + generic_targets=GENERIC_TARGETS, + ) assert report.fields == ("dtype",) assert report.drift_fields == ("dtype", "head_size") assert report.drift == [("bf16", "head_size")] @@ -598,12 +646,19 @@ def _mixed(self): ] def test_absent_field_is_distinguishing_not_a_collision(self): - assert duplicate_matcher_tuples(self._mixed(), ("head_size", "block_n")) == {} + assert ( + duplicate_matcher_tuples( + self._mixed(), ("head_size", "block_n"), GENERIC_TARGETS + ) + == {} + ) def test_result_is_independent_of_kernel_order(self): fields = ("head_size", "block_n") - forward = duplicate_matcher_tuples(self._mixed(), fields) - reverse = duplicate_matcher_tuples(list(reversed(self._mixed())), fields) + forward = duplicate_matcher_tuples(self._mixed(), fields, GENERIC_TARGETS) + reverse = duplicate_matcher_tuples( + list(reversed(self._mixed())), fields, GENERIC_TARGETS + ) assert forward == reverse == {} def test_two_kernels_both_missing_the_field_still_collide(self): @@ -611,7 +666,9 @@ def test_two_kernels_both_missing_the_field_still_collide(self): two kernels are indistinguishable to the matcher and must collide.""" kernels = self._mixed() del kernels[0]["metadata"]["block_n"] - assert duplicate_matcher_tuples(kernels, ("head_size", "block_n")) == {(64,): 2} + assert duplicate_matcher_tuples( + kernels, ("head_size", "block_n"), GENERIC_TARGETS + ) == {(64,): 2} def test_absent_marker_distinguishes_only_when_some_kernel_declares_it(self): """Complement of the case above: once any kernel declares the field, @@ -625,9 +682,9 @@ def test_absent_marker_distinguishes_only_when_some_kernel_declares_it(self): } ] # Two kernels share (64, absent); the block_n=64 one stands alone. - assert duplicate_matcher_tuples(kernels, ("head_size", "block_n")) == { - (64, ""): 2 - } + assert duplicate_matcher_tuples( + kernels, ("head_size", "block_n"), GENERIC_TARGETS + ) == {(64, ""): 2} @pytest.mark.quick @@ -722,10 +779,12 @@ def test_declared_fields_distinguish_what_the_generic_list_collapses( kdp = self._bundle(tmp_path, ("head_size", "waves_per_eu")) kernels, fields = load_variant_set(kdp) assert fields == ("head_size", "waves_per_eu") - assert duplicate_matcher_tuples(kernels, fields) == {} + assert duplicate_matcher_tuples(kernels, fields, GENERIC_TARGETS) == {} # The premise: waves_per_eu is not in the generic list, so the same # two kernels are indistinguishable under it. - assert duplicate_matcher_tuples(kernels, DEFAULT_MATCHER_FIELDS) == {(64,): 2} + assert duplicate_matcher_tuples( + kernels, DEFAULT_MATCHER_FIELDS, GENERIC_TARGETS + ) == {(64,): 2} proc = _run_cli(str(kdp)) assert proc.returncode == 0, proc.stdout + proc.stderr assert "duplicate matcher tuples: none" in proc.stdout @@ -754,9 +813,9 @@ def test_a_kernel_carrying_no_metadata_derives_an_empty_identity(self): """ assert metadata_identity_fields([{"name": "k"}]) == () assert duplicate_matcher_tuples( - [{"name": "a"}, {"name": "b"}], () + [{"name": "a"}, {"name": "b"}], (), GENERIC_TARGETS ) == duplicate_matcher_tuples( - [{"name": "a"}, {"name": "b"}], ("dtype", "batch") + [{"name": "a"}, {"name": "b"}], ("dtype", "batch"), GENERIC_TARGETS ) def test_an_explicit_field_still_outranks_the_declaration(self, tmp_path): @@ -841,7 +900,9 @@ def test_the_generic_fallback_would_also_miss_this_field(self, tmp_path): assert "block_m" not in DEFAULT_MATCHER_FIELDS assert metadata_spec_drift(kernels, DEFAULT_MATCHER_FIELDS) == [] assert metadata_spec_drift(kernels, declared) == [] - report = DeskCheckReport(kernels, fields=declared, mode="structural") + report = DeskCheckReport( + kernels, fields=declared, mode="structural", generic_targets=GENERIC_TARGETS + ) assert report.drift == [("narrow", "block_m")] def test_drift_field_still_narrows_deliberately(self, tmp_path): @@ -875,7 +936,9 @@ def test_widening_the_drift_list_does_not_widen_the_matcher_tuple(self, tmp_path kdp.write_text(json.dumps(doc)) kernels, declared = load_variant_set(kdp) - report = DeskCheckReport(kernels, fields=declared, mode="structural") + report = DeskCheckReport( + kernels, fields=declared, mode="structural", generic_targets=GENERIC_TARGETS + ) assert report.fields == self._NARROW_CONTRACT assert "block_m" in report.drift_fields assert report.drift == [] @@ -1034,7 +1097,7 @@ def test_structural_lookup_raises_a_named_finding(self, tmp_path): def test_full_mode_lookup_raises_a_named_finding(self, tmp_path): typo = self._typo_beside_a_real_shard(tmp_path) with pytest.raises(HkpPackError, match="typo.kdp.json"): - compiled_agreement(typo) + compiled_agreement(typo, GENERIC_TARGETS) @pytest.mark.parametrize("mode", MODES) def test_the_cli_prints_the_finding_rather_than_a_traceback(self, tmp_path, mode): diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py new file mode 100644 index 000000000000..3a4fceaed3ab --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py @@ -0,0 +1,57 @@ +"""`tools/hkp_arch_probe.py`: the packer's arch rule, as the CMake configure asks it.""" + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +from conftest import GENERIC_TARGETS_JSON + +pytestmark = pytest.mark.quick + +_TOOL = Path(__file__).resolve().parent.parent / "tools" / "hkp_arch_probe.py" + + +def _probe(tmp_path, arch, arches, raw=None): + kdp = tmp_path / "x.kdp.json" + kdp.write_text(raw if raw is not None else json.dumps({"arch": arch})) + proc = subprocess.run( + [ + sys.executable, + str(_TOOL), + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), + "--arches", + arches, + "--kdp", + str(kdp), + ], + capture_output=True, + text=True, + ) + assert proc.returncode == 0, proc.stderr + return proc.stdout.strip() + + +def test_probe_true_when_a_generic_entry_contains_a_selected_target(tmp_path): + assert _probe(tmp_path, ["gfx11-generic"], "gfx942;gfx1151") == "TRUE" + + +def test_probe_false_when_no_entry_reaches_any_selected_target(tmp_path): + assert _probe(tmp_path, ["gfx11-generic", "gfx950"], "gfx942;gfx1250") == "FALSE" + assert _probe(tmp_path, ["gfx12-generic"], "gfx1100") == "FALSE" + + +def test_probe_true_for_empty_arch(tmp_path): + assert _probe(tmp_path, [], "gfx942") == "TRUE" + + +@pytest.mark.parametrize( + "raw", + ['{"arch": "gfx942"}', "{not json", '{"arch": [1]}', "[]", '{"name": "x"}'], + ids=["string", "unparseable", "non_string_entry", "not_an_object", "absent"], +) +def test_probe_true_for_an_unparseable_or_non_array_arch(tmp_path, raw): + assert _probe(tmp_path, None, "gfx1100", raw=raw) == "TRUE" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py new file mode 100644 index 000000000000..fb1dd25e8bd4 --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py @@ -0,0 +1,679 @@ +"""Generic GPU targets at pack time. + +A KDP whose `arch` names a table generic (`gfx11-generic`) is compiled and packed +once, under the generic's spelling, and that one tree is copied into the folder of +every selected member of the generic. The rules that keep that sound (S5 / S6 / +reverse direction / unknown name / rocKE) are validation errors with stable +substrings; the merge into a member folder is atomic and refuses two shards that +would write one path with different bytes. + +The quick tier packs `embedded_source` and authored `hsaco` UKDs, which need no +compiler. The hipcc tier at the end compiles for real and is not quick. +""" + +import copy +import json +import shutil +from pathlib import Path + +import pytest + +from conftest import GENERIC_TARGETS, GENERIC_TARGETS_JSON +from hkp_pack import pipeline +from hkp_pack.descriptors import load_flat_input +from hkp_pack.errors import HkpPackError +from hkp_pack.generic_targets import GenericTargets +from hkp_pack.pipeline import _merge_generic_into_member, run_pipeline + +GENERIC = "gfx11-generic" +MEMBER_A = "gfx1100" +MEMBER_B = "gfx1151" +UNSELECTED_MEMBER = "gfx1101" +CO_NAME = "HsacoFixture.co" + +_SHARED_SUFFIXES = (".kmd.json", ".ued.json", ".udd.json", ".uhd.json", ".umd.json") + + +def _read(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def _write(path, doc): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(doc, indent=2), encoding="utf-8") + + +def _load_kpack(rocm_kpack_dir): + from hkp_pack.kpack_resolver import load_kpack + + kpack, _comp = load_kpack(rocm_kpack_dir) + return kpack + + +def _embedded(uid, arch=None): + ukd = { + "version": "0.1", + "id": uid, + "name": uid, + "kernel_source": { + "kind": "embedded_source", + "source_file": "kernels/PointwiseAdd.cpp", + "entry_point": "PointwiseAdd", + }, + "metadata": {"dtype": "FLOAT", "block_size": 64}, + "priority": 0, + } + if arch is not None: + ukd["arch"] = arch + return ukd + + +# What a producing UKD must carry: the consumer declaration its engine's KMD checks. +_CONTRACT = { + "specialization_contract": { + "schema_version": 1, + "consumers": [ + { + "engine_id": "ued-solo", + "kmd_id": "kmd-solo", + "metadata_fields": [], + "matcher_only_fields": ["block_size", "dtype"], + "bindings": {}, + "vocabulary": {}, + } + ], + } +} + + +def _kdp(kid, arch, ukds): + return { + "version": "0.1", + "id": kid, + "name": kid, + "arch": arch, + "matchers": ["umd-solo"], + "engine": "ued-solo", + "dispatch": "udd-solo", + "kernelDescriptors": ukds, + } + + +def _root(tmp_path, empty_arch_fixture, kdps, standalone=()): + """A source root: the fixture's shared descriptors plus the given KDPs. + + `kdps` maps a file stem to a KDP document, `standalone` maps a file stem to a + standalone UKD document. + """ + root = tmp_path / "src" + root.mkdir() + for path in empty_arch_fixture.iterdir(): + if path.name.endswith(_SHARED_SUFFIXES): + shutil.copyfile(path, root / path.name) + for stem, doc in dict(kdps).items(): + _write(root / f"{stem}.kdp.json", doc) + for stem, doc in dict(standalone).items(): + _write(root / f"{stem}.ukd.json", doc) + return root + + +def _pack(root, tmp_path, arches, rocm_kpack_dir, table=GENERIC_TARGETS_JSON): + return run_pipeline( + source_root=root, + arches=list(arches), + out_root=tmp_path / "out", + hipcc="hipcc-not-invoked", + generic_targets_json=table, + rocm_kpack_dir=rocm_kpack_dir, + inter_root=tmp_path / "inter", + source_label="generic-test", + ) + + +def _files(folder): + """Relative path -> bytes of every file under `folder`.""" + return { + p.relative_to(folder).as_posix(): p.read_bytes() + for p in sorted(folder.rglob("*")) + if p.is_file() + } + + +def _load(root, table=GENERIC_TARGETS): + return load_flat_input(root, table, log=lambda *_: None) + + +def _generic_kdp_root(tmp_path, empty_arch_fixture): + return _root( + tmp_path, + empty_arch_fixture, + {"g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-g")])}, + ) + + +# --- layout ------------------------------------------------------------------ + + +@pytest.mark.quick +def test_generic_entry_layout_is_one_copy_per_selected_member_folder( + tmp_path, empty_arch_fixture, rocm_kpack_dir +): + root = _generic_kdp_root(tmp_path, empty_arch_fixture) + results = _pack(root, tmp_path, [MEMBER_A, MEMBER_B], rocm_kpack_dir) + + out = tmp_path / "out" + # One folder per selected member: none for the generic or an unselected member, + # and no staging directory left beside them. + assert sorted(p.name for p in out.iterdir()) == [MEMBER_A, MEMBER_B] + assert not (out / UNSELECTED_MEMBER).exists() + assert not (out / GENERIC).exists() + assert {a: r.out_dir for a, r in results.items()} == { + MEMBER_A: out / MEMBER_A, + MEMBER_B: out / MEMBER_B, + } + + first, second = _files(out / MEMBER_A), _files(out / MEMBER_B) + assert first == second + assert "g.kdp.json" in first + + documents = { + name: json.loads(data) + for name, data in first.items() + if name.endswith((".kdp.json", ".ukd.json")) + } + assert documents + kdp = documents["g.kdp.json"] + assert kdp["arch"] == [GENERIC] + assert [u["arch"] for u in kdp["kernelDescriptors"]] == [[GENERIC]] + + +@pytest.mark.quick +def test_a_member_with_only_generic_content_still_gets_a_folder( + tmp_path, empty_arch_fixture, rocm_kpack_dir +): + root = _root( + tmp_path, + empty_arch_fixture, + { + "c": _kdp("kdp-c", ["gfx942"], [_embedded("ukd-c")]), + "g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-g")]), + }, + ) + results = _pack(root, tmp_path, ["gfx942", MEMBER_A], rocm_kpack_dir) + + out = tmp_path / "out" + assert not results[MEMBER_A].skipped + assert (out / MEMBER_A / "g.kdp.json").is_file() + assert not (out / MEMBER_A / "c.kdp.json").exists() + assert (out / "gfx942" / "c.kdp.json").is_file() + assert not (out / "gfx942" / "g.kdp.json").exists() + + +@pytest.mark.quick +def test_generic_entry_is_not_materialized_when_no_member_is_selected( + tmp_path, empty_arch_fixture, rocm_kpack_dir +): + root = _root( + tmp_path, + empty_arch_fixture, + { + "c": _kdp("kdp-c", ["gfx942"], [_embedded("ukd-c")]), + "g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-g")]), + }, + ) + _pack(root, tmp_path, ["gfx942"], rocm_kpack_dir) + + out = tmp_path / "out" + assert sorted(p.name for p in out.iterdir()) == ["gfx942"] + assert not (out / "gfx942" / "g.kdp.json").exists() + assert not (tmp_path / "inter" / ".generic-out").exists() + assert not (tmp_path / "inter" / GENERIC).exists() + + +@pytest.mark.quick +def test_empty_arch_kdp_is_not_emitted_into_the_generic_copy( + tmp_path, empty_arch_fixture, rocm_kpack_dir +): + root = _root( + tmp_path, + empty_arch_fixture, + { + "wild": _kdp("kdp-wild", [], [_embedded("ukd-wild")]), + "g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-g")]), + }, + ) + _pack(root, tmp_path, [MEMBER_A], rocm_kpack_dir) + + generic_tree = tmp_path / "inter" / ".generic-out" / GENERIC + assert (generic_tree / "g.kdp.json").is_file() + assert not (generic_tree / "wild.kdp.json").exists() + # The member folder holds the wildcard through the concrete pass, narrowed to it. + member = tmp_path / "out" / MEMBER_A + assert _read(member / "wild.kdp.json")["arch"] == [MEMBER_A] + assert _read(member / "g.kdp.json")["arch"] == [GENERIC] + + +@pytest.mark.quick +def test_explicit_and_generic_packs_coexist_in_one_member_folder( + tmp_path, empty_arch_fixture, rocm_kpack_dir +): + root = _root( + tmp_path, + empty_arch_fixture, + { + "explicit": _kdp("kdp-explicit", [MEMBER_B], [_embedded("ukd-e")]), + "generic": _kdp("kdp-generic", [GENERIC], [_embedded("ukd-g")]), + }, + ) + _pack(root, tmp_path, [MEMBER_A, MEMBER_B], rocm_kpack_dir) + + out = tmp_path / "out" + both = _files(out / MEMBER_B) + assert json.loads(both["explicit.kdp.json"])["arch"] == [MEMBER_B] + assert json.loads(both["generic.kdp.json"])["arch"] == [GENERIC] + only_generic = _files(out / MEMBER_A) + assert "explicit.kdp.json" not in only_generic + assert only_generic["generic.kdp.json"] == both["generic.kdp.json"] + + +@pytest.mark.quick +def test_shared_engine_dispatch_and_kmd_files_merge_cleanly( + tmp_path, empty_arch_fixture, rocm_kpack_dir +): + root = _root( + tmp_path, + empty_arch_fixture, + { + "explicit": _kdp("kdp-explicit", [MEMBER_B], [_embedded("ukd-e")]), + "generic": _kdp("kdp-generic", [GENERIC], [_embedded("ukd-g")]), + }, + ) + _pack(root, tmp_path, [MEMBER_B], rocm_kpack_dir) + + member = _files(tmp_path / "out" / MEMBER_B) + shared = [p for p in root.iterdir() if p.name.endswith(_SHARED_SUFFIXES)] + assert shared + for path in shared: + assert member[path.name] == path.read_bytes() + assert sorted(member) == sorted( + ["explicit.kdp.json", "generic.kdp.json", *(p.name for p in shared)] + ) + + +# --- merge guard and failure policy ------------------------------------------ + + +@pytest.mark.quick +def test_merge_collision_with_differing_bytes_names_the_id_and_both_paths_and_leaves_no_partial_member_folder( + tmp_path, +): + generic_dir = tmp_path / "inter" / ".generic-out" / GENERIC + member_dir = tmp_path / "out" / MEMBER_A + staging = tmp_path / "out" / f".{MEMBER_A}.merge.staging" + _write(generic_dir / "only_generic.kdp.json", {"id": "kdp-only"}) + _write(generic_dir / "dup.ukd.json", {"id": "ukd-dup", "side": "generic"}) + _write(member_dir / "dup.ukd.json", {"id": "ukd-dup", "side": "concrete"}) + _write(member_dir / "concrete_only.kdp.json", {"id": "kdp-c"}) + before = _files(member_dir) + + with pytest.raises(HkpPackError) as err: + _merge_generic_into_member(generic_dir, member_dir, staging) + + message = str(err.value) + assert "would be written by shard" in message + assert "ukd-dup" in message + assert str(member_dir / "dup.ukd.json") in message + assert str(generic_dir / "dup.ukd.json") in message + assert _files(member_dir) == before + assert not staging.exists() + + +@pytest.mark.quick +def test_a_member_whose_concrete_pass_failed_gets_no_generic_copy( + tmp_path, empty_arch_fixture, rocm_kpack_dir, monkeypatch +): + root = _root( + tmp_path, + empty_arch_fixture, + { + "c": _kdp("kdp-c", [MEMBER_A, MEMBER_B], [_embedded("ukd-c")]), + "g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-g")]), + }, + ) + real = pipeline.compile_intermediate + + def failing(flat, source_root, arch, *args, **kwargs): + if arch == MEMBER_A: + raise HkpPackError("synthetic compile failure") + return real(flat, source_root, arch, *args, **kwargs) + + monkeypatch.setattr(pipeline, "compile_intermediate", failing) + with pytest.raises(HkpPackError, match="synthetic compile failure"): + _pack(root, tmp_path, [MEMBER_A, MEMBER_B], rocm_kpack_dir) + + out = tmp_path / "out" + assert not (out / MEMBER_A).exists() + assert (out / MEMBER_B / "g.kdp.json").is_file() + assert (out / MEMBER_B / "c.kdp.json").is_file() + assert not [p for p in out.iterdir() if p.name.startswith(".")] + + +@pytest.mark.quick +def test_a_failed_generic_pass_leaves_no_member_copy( + tmp_path, empty_arch_fixture, rocm_kpack_dir, monkeypatch +): + root = _root( + tmp_path, + empty_arch_fixture, + { + "c": _kdp("kdp-c", [MEMBER_A, MEMBER_B], [_embedded("ukd-c")]), + "g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-g")]), + }, + ) + real = pipeline.pack_arch + + def failing(flat, inter, out_dir, *args, **kwargs): + if inter.arch == GENERIC: + raise HkpPackError("synthetic generic pack failure") + return real(flat, inter, out_dir, *args, **kwargs) + + monkeypatch.setattr(pipeline, "pack_arch", failing) + with pytest.raises(HkpPackError) as err: + _pack(root, tmp_path, [MEMBER_A, MEMBER_B], rocm_kpack_dir) + + assert f"{GENERIC}: synthetic generic pack failure" in str(err.value) + out = tmp_path / "out" + for member in (MEMBER_A, MEMBER_B): + assert (out / member / "c.kdp.json").is_file() + assert not (out / member / "g.kdp.json").exists() + assert not (tmp_path / "inter" / ".generic-out" / GENERIC).exists() + + +# --- authored hsaco under a generic ------------------------------------------ + + +@pytest.mark.quick +def test_hsaco_under_a_generic_packs_under_the_generic_key_into_every_member_folder( + tmp_path, empty_arch_fixture, hsaco_fixture_dir, rocm_kpack_dir +): + authored = (hsaco_fixture_dir / "gfx942" / CO_NAME).read_bytes() + ukd = _embedded("ukd-hsaco", [GENERIC]) + ukd["kernel_source"] = { + "kind": "hsaco", + "file": CO_NAME, + "symbol": "HsacoFixtureAdd", + } + ukd["provenance"] = copy.deepcopy(_CONTRACT) + root = _root(tmp_path, empty_arch_fixture, {"g": _kdp("kdp-g", [GENERIC], [ukd])}) + shutil.copyfile(hsaco_fixture_dir / "gfx942" / CO_NAME, root / CO_NAME) + results = _pack(root, tmp_path, [MEMBER_A, MEMBER_B], rocm_kpack_dir) + + out = tmp_path / "out" + shipped = _read(out / MEMBER_A / "g.kdp.json")["kernelDescriptors"][0] + ks = shipped["kernel_source"] + assert ks["kind"] == "kpack" + assert ks["library"] == f"kpack/hip_kernel_provider_{GENERIC}.kpack" + assert shipped["arch"] == [GENERIC] + for member in (MEMBER_A, MEMBER_B): + archive_path = out / member / ks["library"] + assert results[member].generic_kpack_paths == (archive_path,) + archive = _load_kpack(rocm_kpack_dir).PackedKernelArchive.read(archive_path) + assert bytes(archive.get_kernel(ks["toc_key"], GENERIC)) == authored + assert archive.get_kernel(ks["toc_key"], member) is None + assert _files(out / MEMBER_A) == _files(out / MEMBER_B) + + +# --- S5 ----------------------------------------------------------------------- + + +@pytest.mark.quick +def test_s5_list_with_a_generic_and_its_member_is_an_error( + tmp_path, empty_arch_fixture +): + root = _root( + tmp_path, + empty_arch_fixture, + {"g": _kdp("kdp-g", [GENERIC, MEMBER_B], [_embedded("ukd-g")])}, + ) + with pytest.raises(HkpPackError) as err: + _load(root) + message = str(err.value) + assert "g.kdp.json" in message + assert f"lists '{GENERIC}' and '{MEMBER_B}'" in message + assert f"{GENERIC} contains {MEMBER_B}" in message + + +@pytest.mark.quick +def test_s5_two_generics_sharing_a_member_is_an_error(tmp_path, empty_arch_fixture): + table_path = tmp_path / "overlap.json" + _write( + table_path, + { + "schemaVersion": 1, + "generics": { + "gfx11-generic": ["gfx1100", "gfx1101"], + "gfx11-b-generic": ["gfx1101", "gfx1102"], + }, + }, + ) + root = _root( + tmp_path, + empty_arch_fixture, + {"g": _kdp("kdp-g", ["gfx11-generic", "gfx11-b-generic"], [_embedded("u")])}, + ) + with pytest.raises(HkpPackError) as err: + _load(root, GenericTargets.load(table_path)) + message = str(err.value) + assert "lists 'gfx11-generic' and 'gfx11-b-generic'" in message + assert "share member gfx1101" in message + + +# --- S6 ----------------------------------------------------------------------- + +_S6_RULE = "must list every generic of the KDP" + + +def _standalone(uid, arch=None): + ukd = _embedded(uid, arch) + return ukd + + +@pytest.mark.quick +@pytest.mark.parametrize( + "ukd_arch, accepted", + [([GENERIC], True), ([MEMBER_B], False), (None, False)], + ids=["exact_generic", "narrower", "unrestricted"], +) +def test_s6_standalone_ukd_under_a_generic_kdp( + tmp_path, empty_arch_fixture, ukd_arch, accepted +): + root = _root( + tmp_path, + empty_arch_fixture, + {"g": _kdp("kdp-g", [GENERIC], ["ukd-s"])}, + {"s": _standalone("ukd-s", ukd_arch)}, + ) + if accepted: + assert _load(root).generic_targets is GENERIC_TARGETS + return + with pytest.raises(HkpPackError) as err: + _load(root) + message = str(err.value) + assert _S6_RULE in message + assert "ukd-s" in message and "s.ukd.json" in message and "g.kdp.json" in message + + +@pytest.mark.quick +def test_s6_inline_ukd_with_a_narrower_arch_under_a_generic_kdp_is_an_error( + tmp_path, empty_arch_fixture +): + root = _root( + tmp_path, + empty_arch_fixture, + {"g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-i", [MEMBER_B])])}, + ) + with pytest.raises(HkpPackError, match=_S6_RULE): + _load(root) + + +@pytest.mark.quick +def test_inline_ukd_without_its_own_arch_under_a_generic_kdp_is_accepted( + tmp_path, empty_arch_fixture +): + _load(_generic_kdp_root(tmp_path, empty_arch_fixture)) + + +@pytest.mark.quick +def test_s6_mixed_list_ukd_listing_every_entry_is_accepted( + tmp_path, empty_arch_fixture +): + both = ["gfx942", GENERIC] + root = _root( + tmp_path, + empty_arch_fixture, + {"m": _kdp("kdp-m", both, [_embedded("ukd-m", both)])}, + ) + _load(root) + + +@pytest.mark.quick +def test_s6_mixed_list_ukd_missing_the_generic_is_an_error( + tmp_path, empty_arch_fixture +): + root = _root( + tmp_path, + empty_arch_fixture, + {"m": _kdp("kdp-m", ["gfx942", GENERIC], [_embedded("ukd-m", ["gfx942"])])}, + ) + with pytest.raises(HkpPackError, match=_S6_RULE): + _load(root) + + +# --- reverse direction, unknown names, rocKE --------------------------------- + + +@pytest.mark.quick +@pytest.mark.parametrize("kdp_arch", [[], [MEMBER_B]], ids=["empty", "explicit_member"]) +def test_a_generic_ukd_under_a_kdp_that_does_not_list_it_is_an_error( + tmp_path, empty_arch_fixture, kdp_arch +): + root = _root( + tmp_path, + empty_arch_fixture, + {"k": _kdp("kdp-k", kdp_arch, [_embedded("ukd-k", [GENERIC])])}, + ) + with pytest.raises(HkpPackError) as err: + _load(root) + message = str(err.value) + assert f"declares generic target '{GENERIC}'" in message + assert "ship in no shard" in message + assert "k.kdp.json" in message + + +@pytest.mark.quick +def test_unknown_generic_name_is_an_error_naming_the_file(tmp_path, empty_arch_fixture): + root = _root( + tmp_path, + empty_arch_fixture, + {"u": _kdp("kdp-u", ["gfx99-generic"], [_embedded("ukd-u")])}, + ) + with pytest.raises(HkpPackError) as err: + _load(root) + message = str(err.value) + assert message.startswith("u.kdp.json:") + assert "generic target name absent" in message + assert "'gfx99-generic'" in message and str(GENERIC_TARGETS.path) in message + + +@pytest.mark.quick +def test_rocke_with_a_generic_arch_is_an_error(tmp_path, empty_arch_fixture): + ukd = _embedded("ukd-r", [GENERIC]) + ukd["kernel_source"] = { + "kind": "rocke", + "source": "pkg/kernels/attention.py", + "builder": "build_attention", + "spec": {"tile": 64}, + } + root = _root(tmp_path, empty_arch_fixture, {"r": _kdp("kdp-r", [GENERIC], [ukd])}) + with pytest.raises(HkpPackError) as err: + _load(root) + assert "does not support generic targets yet" in str(err.value) + assert GENERIC in str(err.value) + + +# --- hipcc tier (not quick) --------------------------------------------------- + + +def _hip_generic_root(dest, empty_arch_fixture): + """The empty_arch hip fixture with its KDP rewritten to the generic.""" + shutil.copytree(empty_arch_fixture, dest) + kdp_path = dest / "solo.kdp.json" + doc = _read(kdp_path) + doc["arch"] = [GENERIC] + _write(kdp_path, doc) + return dest + + +def _hip_pack(root, out, inter, arches, hipcc, rocm_kpack_dir): + return run_pipeline( + source_root=root, + arches=list(arches), + out_root=out, + hipcc=hipcc, + generic_targets_json=GENERIC_TARGETS_JSON, + rocm_kpack_dir=rocm_kpack_dir, + inter_root=inter, + source_label="generic-test", + ) + + +def test_generic_hip_archive_is_keyed_by_the_generic_spelling( + tmp_path, empty_arch_fixture, hipcc, rocm_kpack_dir +): + root = _hip_generic_root(tmp_path / "src", empty_arch_fixture) + results = _hip_pack( + root, tmp_path / "out", tmp_path / "inter", [MEMBER_A], hipcc, rocm_kpack_dir + ) + + shipped = _read(tmp_path / "out" / MEMBER_A / "solo.kdp.json") + ks = shipped["kernelDescriptors"][0]["kernel_source"] + assert shipped["arch"] == [GENERIC] + assert ks["library"] == f"kpack/hip_kernel_provider_{GENERIC}.kpack" + archive = _load_kpack(rocm_kpack_dir).PackedKernelArchive.read( + tmp_path / "out" / MEMBER_A / ks["library"] + ) + assert archive.get_kernel(ks["toc_key"], GENERIC) is not None + assert archive.get_kernel(ks["toc_key"], MEMBER_A) is None + assert results[MEMBER_A].kpack_path is None + + +def test_generic_pack_is_byte_identical_across_separate_per_arch_builds( + tmp_path, empty_arch_fixture, hipcc, rocm_kpack_dir, monkeypatch +): + """Three separate builds of one hip root -- GPU_TARGETS gfx1100 only, gfx1151 + only, and both -- each with its own out root, inter root, cwd and absolute copy + of the source. Every file of the generic entry is identical across all of them + and across the member folders of the two-target build: the copies a real + per-arch shard build produces must collapse to one.""" + builds = {} + for label, arches in ( + ("only_a", [MEMBER_A]), + ("only_b", [MEMBER_B]), + ("both", [MEMBER_A, MEMBER_B]), + ): + base = tmp_path / label + base.mkdir() + root = _hip_generic_root(base / "deeper" / label / "src", empty_arch_fixture) + (base / "cwd").mkdir() + monkeypatch.chdir(base / "cwd") + _hip_pack( + root, base / "out", base / "inter" / label, arches, hipcc, rocm_kpack_dir + ) + for member in arches: + builds[(label, member)] = _files(base / "out" / member) + + reference_key, reference = next(iter(builds.items())) + assert any(name.endswith(".kpack") for name in reference) + assert any(name.endswith(".kdp.json") for name in reference) + for key, files in builds.items(): + assert files == reference, f"{key} differs from {reference_key}" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py index 07dd0f6312ab..c99a75d8c70e 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py @@ -4,6 +4,7 @@ import pytest +from conftest import GENERIC_TARGETS, GENERIC_TARGETS_JSON from hkp_pack import agreement from hkp_pack.hip_compile import hip_variant_key as variant_key from hkp_pack.descriptors import load_flat_input, reachable_generic_ids @@ -37,6 +38,7 @@ def built(tmp_path_factory, main_fixture, hipcc, rocm_kpack_dir): """Compile + prune + pack the main fixture once for the 3-arch matrix.""" base = tmp_path_factory.mktemp("built") results = run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=main_fixture, arches=ARCHES, out_root=base / "out", @@ -188,6 +190,7 @@ def test_prn3_exact_post_prune_set(built): def test_prn4_empty_arch_skip(tmp_path, empty_arch_fixture, hipcc, rocm_kpack_dir): logs = [] results = run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=empty_arch_fixture, arches=["gfx942", "gfx950"], out_root=tmp_path / "out", @@ -356,6 +359,7 @@ def test_self_describing_ukd(built, rocm_kpack_dir): # --- D. Negatives: compile-spec -------------------------------------------- def _run(source_root, tmp_path, hipcc, rocm_kpack_dir, arches=("gfx942",)): return run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=source_root, arches=list(arches), out_root=tmp_path / "out", @@ -447,7 +451,7 @@ def test_neg_kpack_source_missing_field(tmp_path, main_fixture, field): src = _copy_fixture(tmp_path, main_fixture) _author_kpack_source(src, drop=field) with pytest.raises(HkpPackError, match=f"missing required field '{field}'"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -456,7 +460,7 @@ def test_kpack_source_complete_is_accepted(tmp_path, main_fixture): # rejected for some reason of its own would read as the drop being caught. src = _copy_fixture(tmp_path, main_fixture) _author_kpack_source(src) - flat = load_flat_input(src) + flat = load_flat_input(src, GENERIC_TARGETS) kdp = next(d for d in flat.kdps() if d.path.name == "copy.kdp.json") assert kdp.doc["kernelDescriptors"][0]["kernel_source"]["kind"] == "kpack" @@ -485,6 +489,7 @@ def test_neg_sha256_mismatch(tmp_path, main_fixture, hipcc, rocm_kpack_dir): with pytest.raises(HkpPackError, match="sha256 mismatch"): # An expected digest that cannot match the freshly compiled blob. run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=main_fixture, arches=["gfx942"], out_root=tmp_path / "out", @@ -527,6 +532,7 @@ def test_cli1_single_arch(tmp_path, main_fixture, hipcc, rocm_kpack_dir): @pytest.mark.quick def test_cli2_empty_gpu_targets(tmp_path, main_fixture, hipcc, rocm_kpack_dir): results = run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=main_fixture, arches=[], out_root=tmp_path / "out", @@ -541,7 +547,7 @@ def test_cli2_empty_gpu_targets(tmp_path, main_fixture, hipcc, rocm_kpack_dir): # --- Unit: pruning reachability + wildcard --------------------------------- @pytest.mark.quick def test_wildcard_arch_matches(main_fixture): - flat = load_flat_input(main_fixture) + flat = load_flat_input(main_fixture, GENERIC_TARGETS) wild = next(k for k in flat.kdps() if k.id == "kdp-pointwise-wild") from hkp_pack.descriptors import arch_matches @@ -601,6 +607,7 @@ def _shas(out): return result run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=main_fixture, arches=["gfx942"], out_root=tmp_path / "out1", @@ -609,6 +616,7 @@ def _shas(out): inter_root=tmp_path / "inter1", ) run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=main_fixture, arches=["gfx942"], out_root=tmp_path / "out2", @@ -1020,7 +1028,7 @@ def test_neg_duplicate_standalone_ukd_ids(tmp_path, main_fixture): dup = _read(src / _STANDALONE_UKD_FILE) (src / "dup.ukd.json").write_text(json.dumps(dup), encoding="utf-8") with pytest.raises(HkpPackError, match="duplicate"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -1031,7 +1039,7 @@ def test_neg_duplicate_kdp_ids(tmp_path, main_fixture): doc["id"] = "kdp-pointwise" # already the id of pointwise.kdp.json p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="duplicate"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -1042,7 +1050,7 @@ def test_neg_duplicate_generic_ids(tmp_path, main_fixture): doc["id"] = "kmd-pointwise" # already the id of pointwise.kmd.json p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="duplicate"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -1055,7 +1063,7 @@ def test_neg_cross_type_id_reuse_rejected(tmp_path, main_fixture): doc["id"] = "kmd-pointwise" # a KMD id, different type p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="duplicate"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) # --- H. Version enforcement -------------------------- @@ -1067,7 +1075,7 @@ def test_neg_file_backed_missing_version(tmp_path, main_fixture): del doc["version"] p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="missing required field 'version'"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -1079,7 +1087,7 @@ def test_neg_malformed_version(tmp_path, main_fixture, bad): doc["version"] = bad p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="invalid version"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -1091,7 +1099,7 @@ def test_neg_inline_ukd_missing_version(tmp_path, main_fixture): del doc["kernelDescriptors"][0]["version"] p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="missing required field 'version'"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) # --- I. UED scoped name ------------------------------ @@ -1103,13 +1111,13 @@ def test_neg_ued_name_not_scoped(tmp_path, main_fixture): doc["name"] = "Pointwise engine" # unscoped p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="scoped"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick def test_scoped_ued_name_loads_clean(main_fixture): # The renamed fixture UED (test_fixture:pointwise) validates without error. - flat = load_flat_input(main_fixture) + flat = load_flat_input(main_fixture, GENERIC_TARGETS) ued = next(d for d in flat.by_type("ued") if d.id == "ued-pointwise") assert ued.doc["name"] == "test_fixture:pointwise" @@ -1151,7 +1159,7 @@ def test_non_object_provenance_is_refused_at_load(tmp_path, main_fixture): doc["provenance"] = "HIJACKED" p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="provenance must be an object"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) def _share_contract_onto_kdp(kdp_path): @@ -1210,7 +1218,7 @@ def test_effective_spec_on_a_kdp_is_refused_at_load(tmp_path, main_fixture): doc["provenance"] = {"effective_spec": {"schema_version": 1}} p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="effective_spec is reserved"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -1223,7 +1231,7 @@ def test_a_malformed_kdp_contract_is_refused_at_load(tmp_path, main_fixture): doc["provenance"] = {"specialization_contract": {"schema_version": 1}} p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="specialization_contract must be"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) # --- K. Drop diagnostics + arch warning -------------- @@ -1235,7 +1243,7 @@ def test_orphan_standalone_ukd_warns(tmp_path, main_fixture): orphan["id"] = "ukd-orphan" (src / "orphan.ukd.json").write_text(json.dumps(orphan), encoding="utf-8") logs = [] - load_flat_input(src, log=logs.append) + load_flat_input(src, GENERIC_TARGETS, log=logs.append) assert any("orphan.ukd.json" in m and "not referenced" in m for m in logs) @@ -1249,6 +1257,7 @@ def test_empty_pruned_kdp_is_logged(tmp_path, main_fixture, hipcc, rocm_kpack_di p.write_text(json.dumps(doc), encoding="utf-8") logs = [] run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=src, arches=["gfx950"], out_root=tmp_path / "out", @@ -1284,6 +1293,7 @@ def test_nonbare_arch_is_rejected(tmp_path, main_fixture, hipcc, rocm_kpack_dir) p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="feature suffix"): run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=src, arches=["gfx942"], out_root=tmp_path / "out", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hsaco.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hsaco.py index cd78b58b4356..52706c5c2eb4 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hsaco.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hsaco.py @@ -16,6 +16,7 @@ import pytest +from conftest import GENERIC_TARGETS, GENERIC_TARGETS_JSON from hkp_pack import pipeline, toolchain from hkp_pack.descriptors import load_flat_input from hkp_pack.errors import HkpPackError @@ -46,6 +47,7 @@ def _load_kpack(rocm_kpack_dir): def _run(source_root, tmp_path, rocm_kpack_dir, arches=(ARCH,), **kwargs): return run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=source_root, arches=list(arches), out_root=tmp_path / "out", @@ -256,7 +258,7 @@ def test_missing_file_has_no_root_fallback( and a path leaving the root is refused.""" root = _hsaco_root(tmp_path, empty_arch_fixture, hsaco_fixture_dir) shutil.move(root / "solo" / CO_NAME, root / CO_NAME) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) with pytest.raises(HkpPackError) as excinfo: compile_intermediate(flat, root, ARCH, NO_HIPCC, tmp_path / "inter") @@ -269,7 +271,7 @@ def test_missing_file_has_no_root_fallback( doc = _read(kdp_path) doc["kernelDescriptors"][0]["kernel_source"]["file"] = f"../../{CO_NAME}" _write(kdp_path, doc) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) with pytest.raises(HkpPackError, match="hsaco file escapes the source root"): compile_intermediate(flat, root, ARCH, NO_HIPCC, tmp_path / "inter") @@ -292,7 +294,7 @@ def two_files(doc, template): shutil.copyfile( hsaco_fixture_dir / ARCH / CO_NAME, root / "solo" / "other" / CO_NAME ) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) monkeypatch.setattr(pipeline, "hsaco_variant_key", lambda *a, **k: "COLLIDE") with pytest.raises(HkpPackError) as excinfo: @@ -386,7 +388,7 @@ def bound_contract(doc, template): consumer["bindings"] = {"block_size": {"field": "block_size"}} root = _hsaco_root(tmp_path, empty_arch_fixture, hsaco_fixture_dir, bound_contract) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) with pytest.raises(HkpPackError, match="a 'hsaco' source cannot fulfil"): compile_intermediate(flat, root, ARCH, NO_HIPCC, tmp_path / "inter") @@ -458,7 +460,7 @@ def test_symlink_leaving_the_root_is_refused( outside.mkdir() shutil.move(root / "solo" / CO_NAME, outside / CO_NAME) (root / "solo" / CO_NAME).symlink_to(outside / CO_NAME) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) with pytest.raises(HkpPackError, match="hsaco file escapes the source root"): compile_intermediate(flat, root, ARCH, NO_HIPCC, tmp_path / "inter") @@ -477,7 +479,11 @@ def set_file(doc, template): with pytest.raises(HkpPackError, match="hsaco file cannot be resolved"): compile_intermediate( - load_flat_input(root), root, ARCH, NO_HIPCC, tmp_path / "inter" + load_flat_input(root, GENERIC_TARGETS), + root, + ARCH, + NO_HIPCC, + tmp_path / "inter", ) @@ -506,7 +512,7 @@ def set_field(doc, template): root = _hsaco_root(tmp_path, empty_arch_fixture, hsaco_fixture_dir, set_field) with pytest.raises(HkpPackError, match="ukd-solo-add-f32-b64"): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -517,7 +523,7 @@ def set_symbol(doc, template): root = _hsaco_root(tmp_path, empty_arch_fixture, hsaco_fixture_dir, set_symbol) with pytest.raises(HkpPackError, match="ukd-solo-add-f32-b64"): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -535,4 +541,4 @@ def drop_arch(doc, template): root = _hsaco_root(tmp_path, empty_arch_fixture, hsaco_fixture_dir, drop_arch) with pytest.raises(HkpPackError, match="ukd-solo-add-f32-b64"): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py index 6b45406d4b8a..22e61ad2e083 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py @@ -20,6 +20,7 @@ import pytest +from conftest import GENERIC_TARGETS, GENERIC_TARGETS_JSON from hkp_pack.descriptors import load_flat_input from hkp_pack.errors import HkpPackError from hkp_pack.hip_compile import hip_variant_key @@ -43,6 +44,7 @@ def _load_kpack(rocm_kpack_dir): def _run(source_root, tmp_path, hipcc, rocm_kpack_dir, arches, source_label=None): """Pack one root. A root holding an `embedded_source` descriptor needs a label.""" return run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=source_root, arches=list(arches), out_root=tmp_path / "out", @@ -79,7 +81,7 @@ def test_discovery_is_recursive(tmp_path, main_fixture): root = tmp_path / "root" _nest(root, "hip/pointwise", main_fixture) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) assert flat.descriptors, "nested descriptors must be discovered" for d in flat.descriptors: assert d.rel_dir.as_posix() == "hip/pointwise" @@ -91,7 +93,7 @@ def test_rel_dir_is_root_relative_parent(tmp_path, main_fixture, empty_arch_fixt _nest(root, "hip/a", main_fixture) _nest(root, "rocKE/b", empty_arch_fixture) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) by_rel = {} for d in flat.descriptors: by_rel.setdefault(d.rel_dir.as_posix(), set()).add(d.path.name) @@ -121,7 +123,7 @@ def test_same_filename_in_two_folders_both_survive( src.rename(b / src.name.replace("solob.", "solo.", 1)) assert {p.name for p in a.glob("solo.*")} == {p.name for p in b.glob("solo.*")} - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) kdp_paths = {(d.rel_dir.as_posix(), d.path.name) for d in flat.kdps()} assert ("hip/a", "solo.kdp.json") in kdp_paths assert ("hip/b", "solo.kdp.json") in kdp_paths @@ -136,7 +138,7 @@ def test_duplicate_id_across_folders_rejected(tmp_path, empty_arch_fixture): _nest(root, "hip/b", empty_arch_fixture) with pytest.raises(HkpPackError, match="duplicate"): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -153,7 +155,7 @@ def test_a_hidden_folder_is_skipped_and_logged(tmp_path, empty_arch_fixture): _rename_ids(hidden, "solo", "vendor") logs = [] - flat = load_flat_input(root, log=logs.append) + flat = load_flat_input(root, GENERIC_TARGETS, log=logs.append) assert {d.rel_dir.as_posix() for d in flat.descriptors} == {"hip/a"} assert not [d for d in flat.descriptors if d.path.name.startswith("vendor.")] @@ -180,7 +182,7 @@ def test_a_non_descriptor_json_is_skipped_and_logged(tmp_path, empty_arch_fixtur incidental.write_text("[]", encoding="utf-8") logs = [] - flat = load_flat_input(root, log=logs.append) + flat = load_flat_input(root, GENERIC_TARGETS, log=logs.append) assert {d.rel_dir.as_posix() for d in flat.descriptors} == {"hip/a"} assert not [d for d in flat.descriptors if d.path.name == incidental.name] @@ -313,6 +315,7 @@ def _pack_from(parent_name): root = parent / "src" _nest(root, "hip/solo_add", empty_arch_fixture) run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=parent / "out", @@ -433,6 +436,7 @@ def crash(flat, inter, out_arch_dir, *a, **kw): out_root = tmp_path / "out" with pytest.raises(RuntimeError, match="uncaught failure"): pipeline.run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=out_root, @@ -482,6 +486,7 @@ def flaky_pack(flat, inter, out_arch_dir, *a, **kw): out_root = tmp_path / "out" with pytest.raises(HkpPackError, match="gfx950"): pipeline.run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=["gfx942", "gfx950"], out_root=out_root, @@ -521,6 +526,7 @@ def test_failed_arch_removes_its_previous_good_output( out_root = tmp_path / "out" pipeline.run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=out_root, @@ -538,6 +544,7 @@ def always_fail(flat, inter, out_arch_dir, *a, **kw): with pytest.raises(HkpPackError, match=ARCH): pipeline.run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=out_root, @@ -567,6 +574,7 @@ def always_fail(flat, inter, out_arch_dir, *a, **kw): with pytest.raises(HkpPackError) as exc: pipeline.run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=["gfx942", "gfx950"], out_root=tmp_path / "out", @@ -577,7 +585,7 @@ def always_fail(flat, inter, out_arch_dir, *a, **kw): message = str(exc.value) assert "gfx942" in message and "gfx950" in message - assert "2 of 2" in message + assert "2 failure(s) across 2" in message # --- E. The shipped example tree ------------------------------------------- @@ -595,6 +603,7 @@ def test_example_tree_packs_both_producers( what keeps it honest: if the example rots, this fails. """ results = run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=EXAMPLE_ROOT, arches=[ARCH], out_root=tmp_path / "out", @@ -633,6 +642,7 @@ def test_example_tree_keeps_both_shared_filenames( both. """ run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=EXAMPLE_ROOT, arches=[ARCH], out_root=tmp_path / "out", @@ -653,7 +663,7 @@ def test_example_tree_is_self_consistent(): Catches a broken example on any box, including one with neither hipcc nor comgr, so the tree cannot rot silently between full runs. """ - flat = load_flat_input(EXAMPLE_ROOT) + flat = load_flat_input(EXAMPLE_ROOT, GENERIC_TARGETS) ids = [d.id for d in flat.descriptors] assert len(ids) == len(set(ids)), "duplicate descriptor ids in the example" @@ -675,6 +685,7 @@ def test_provenance_records_the_toolchain_that_built_each_kernel( stamp.write_text("deadbeefcafe\n", encoding="utf-8") run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=EXAMPLE_ROOT, arches=[ARCH], out_root=tmp_path / "out", @@ -786,6 +797,7 @@ def test_library_resolves_from_a_nested_descriptor( _nest(root, "hip/deep/deeper", main_fixture) run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=tmp_path / "out", @@ -815,6 +827,7 @@ def test_library_resolves_for_a_flat_descriptor( shutil.copytree(empty_arch_fixture, root) run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=tmp_path / "out", @@ -842,7 +855,7 @@ def test_authored_kpack_folder_is_rejected(tmp_path, empty_arch_fixture): _nest(root, "kpack", empty_arch_fixture) with pytest.raises(HkpPackError, match="reserved"): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -852,7 +865,7 @@ def test_kpack_folder_rejected_only_at_the_arch_root(tmp_path, empty_arch_fixtur root = tmp_path / "root" _nest(root, "hip/kpack", empty_arch_fixture) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) assert {d.rel_dir.as_posix() for d in flat.kdps()} == {"hip/kpack"} @@ -979,6 +992,7 @@ def test_library_resolves_for_a_nested_standalone_ukd( _nest(root, "hip/deep", main_fixture) run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=tmp_path / "out", @@ -1030,6 +1044,7 @@ def test_standalone_ukd_anchors_on_its_own_dir_not_the_kdps( ) run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=tmp_path / "out", @@ -1088,7 +1103,7 @@ def test_generic_descriptors_are_validated_against_the_loader_schema( target.write_text(json.dumps(doc, indent=2), encoding="utf-8") with pytest.raises(HkpPackError, match=expected): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -1108,7 +1123,7 @@ def test_reserved_kpack_folder_is_case_insensitive( _nest(root, spelling, empty_arch_fixture) with pytest.raises(HkpPackError, match="reserved"): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -1267,7 +1282,7 @@ def test_embedded_source_root_loads(tmp_path, empty_arch_fixture): """The walk accepts the kind and leaves the block unmodified.""" root = _embedded_source_root(tmp_path, empty_arch_fixture, dict(_EMBEDDED_SOURCE)) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) kdps = list(flat.kdps()) assert len(kdps) == 1 ukd = kdps[0].doc["kernelDescriptors"][0] @@ -1284,7 +1299,7 @@ def test_embedded_source_requires_source_file_and_entry_point( root = _embedded_source_root(tmp_path, empty_arch_fixture, kernel_source) with pytest.raises(HkpPackError, match=missing): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -1302,7 +1317,7 @@ def test_unhandled_kind_aborts_the_walk_and_lists_the_accepted_kinds( ) with pytest.raises(HkpPackError) as excinfo: - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) message = str(excinfo.value) assert "unsupported kind 'embedded_sources'" in message @@ -1338,7 +1353,7 @@ def test_a_kind_no_producer_handles_fails_the_compile( root = _embedded_source_root( tmp_path, empty_arch_fixture, _UNPRODUCED_SOURCES[kind] ) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) with pytest.raises(HkpPackError) as excinfo: compile_intermediate(flat, root, ARCH, "hipcc-not-invoked", tmp_path / "inter") @@ -1370,7 +1385,7 @@ def test_a_passthrough_kind_carries_no_specialization_obligation( root = _inline_ukd_root( tmp_path, empty_arch_fixture, _embedded_source_without_contract ) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) assert _agreement_inputs(flat, ARCH) == ({}, {}) @@ -1391,7 +1406,7 @@ def test_a_compiling_kind_without_a_contract_is_still_refused( refused before a compiler is reached. """ root = _inline_ukd_root(tmp_path, empty_arch_fixture, _drop_specialization_contract) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) with pytest.raises(HkpPackError, match="missing/invalid specialization_contract"): compile_intermediate(flat, root, ARCH, "hipcc-not-invoked", tmp_path / "inter") @@ -1415,7 +1430,7 @@ def test_embedded_source_rejects_a_parent_segment( ) with pytest.raises(HkpPackError, match=re.escape(source_file)): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -1435,7 +1450,7 @@ def test_embedded_source_rejects_an_absolute_path( ) with pytest.raises(HkpPackError, match=re.escape(source_file)): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) # --- J. Emitting embedded_source through pass-through and pruning ----------- @@ -1560,6 +1575,7 @@ def _add_sharing_embedded_kdp(folder): def _pack_embedded(root, tmp_path, rocm_kpack_dir, arches, log=print, out="out"): """Pack a root that compiles nothing, so hipcc must never be invoked.""" return run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=list(arches), out_root=tmp_path / out, @@ -1848,6 +1864,7 @@ def test_packing_without_a_source_label_is_refused( with pytest.raises(HkpPackError) as excinfo: run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=tmp_path / "out", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py index 83aa098b30f9..e039bf9b5f67 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py @@ -24,6 +24,7 @@ import pytest +from conftest import GENERIC_TARGETS from hkp_pack import agreement, pipeline, rocke_compile from hkp_pack.descriptors import load_flat_input from hkp_pack.errors import HkpPackError @@ -459,11 +460,13 @@ def _entry_identity(entry_id, ukd_doc, sdesc): def _observed_sequence(corpus_dir): - flat = load_flat_input(corpus_dir, log=_silent) + flat = load_flat_input(corpus_dir, GENERIC_TARGETS, log=_silent) ukd_by_id = flat.ukd_by_id() observed = [] for kdp in flat.kdps(): - for tup in pipeline._selected_entries(kdp.doc, TARGET_ARCH, ukd_by_id): + for tup in pipeline._selected_entries( + kdp.doc, TARGET_ARCH, ukd_by_id, GENERIC_TARGETS + ): observed.append(_entry_identity(*tup)) return observed @@ -509,17 +512,17 @@ def test_prewarm_jobs_are_deduped_on_variant_key(corpus): one standalone UKD from two KDPs, so a job list that failed to dedup would be longer than the set of keys it carries. """ - flat = load_flat_input(corpus, log=_silent) + flat = load_flat_input(corpus, GENERIC_TARGETS, log=_silent) jobs = pipeline._prewarm_jobs(flat, corpus, TARGET_ARCH) assert jobs, "the corpus selects variants, so the job list cannot be empty" assert len({j.vk for j in jobs}) == len(jobs) -def _arch_matches_call_sites(): - """`arch_matches` call counts in pipeline.py, keyed by enclosing function. +def _call_sites(callee): + """`callee` call counts in pipeline.py, keyed by enclosing function. - Parsed rather than counted as strings: an explanatory comment naming - `arch_matches` is not a call. + Parsed rather than counted as strings: an explanatory comment naming the + callee is not a call. """ tree = ast.parse(inspect.getsource(pipeline)) counts = {} @@ -536,7 +539,7 @@ def _arch_matches_call_sites(): if isinstance(func, ast.Name) else func.attr if isinstance(func, ast.Attribute) else None ) - if name == "arch_matches": + if name == callee: found += 1 if found: counts[node.name] = found @@ -545,16 +548,26 @@ def _arch_matches_call_sites(): @pytest.mark.quick def test_arch_matches_call_sites_are_pinned(): - """All three selection filters live in the generator and nowhere else. + """The two UKD-level selection filters live in the generator and nowhere else. - `compile_intermediate` keeps exactly one call, and it is not a filter: it - decides KDP disposition -- copy the authored KDP through verbatim -- before - the deepcopy the generator would consume. A call in any other function is a - fourth selection site, which is the divergence a single shared generator - exists to make impossible. + `arch_matches` is the empty-inherits filter on a UKD. `compile_intermediate` + no longer calls it: its KDP disposition call is `kdp_arch_matches`. """ - assert _arch_matches_call_sites() == { - "_selected_entries": 3, + assert _call_sites("arch_matches") == {"_selected_entries": 2} + + +@pytest.mark.quick +def test_kdp_arch_matches_call_sites_are_pinned(): + """The KDP-level filter is in the generator; `compile_intermediate` keeps one call. + + That call is not a filter: it decides KDP disposition -- copy the authored KDP + through verbatim -- before the deepcopy the generator would consume. A call in + any other pipeline function is a further selection site, which is the + divergence a single shared generator exists to make impossible. The + `kdp_survives` call lives in descriptors.py, not pipeline.py. + """ + assert _call_sites("kdp_arch_matches") == { + "_selected_entries": 1, "compile_intermediate": 1, } @@ -755,7 +768,7 @@ def test_prewarm_skips_hsaco_kind(hsaco_corpus): `_variant_key_for` declines the kind, so the prewarm drops it and only the compilable hip sibling is scheduled; the walk keys the hsaco UKD itself. """ - flat = load_flat_input(hsaco_corpus, log=_silent) + flat = load_flat_input(hsaco_corpus, GENERIC_TARGETS, log=_silent) hsaco_ukd = flat.kdps()[0].doc["kernelDescriptors"][0] assert pipeline._variant_key_for(hsaco_ukd, Path(".")) is None @@ -814,7 +827,7 @@ def test_prewarm_pool_stops_at_first_failure(tmp_path, monkeypatch): jobs = [_synthetic_job(corpus, 64 + i) for i in range(24)] monkeypatch.setattr(pipeline, "_prewarm_jobs", lambda *_a, **_k: list(jobs)) - flat = load_flat_input(corpus, log=_silent) + flat = load_flat_input(corpus, GENERIC_TARGETS, log=_silent) tally = tmp_path / "tally" tally.mkdir() @@ -961,7 +974,7 @@ def test_prewarm_failure_names_variant(failing_corpus, tmp_path, monkeypatch): for. """ monkeypatch.setenv("HKP_PACK_JOBS", "2") - flat = load_flat_input(failing_corpus, log=_silent) + flat = load_flat_input(failing_corpus, GENERIC_TARGETS, log=_silent) jobs = pipeline._prewarm_jobs(flat, failing_corpus, TARGET_ARCH) assert len(jobs) >= 2, "a single job returns before starting a pool" @@ -1122,7 +1135,7 @@ def test_prewarm_pool_populates_both_caches(tmp_path, monkeypatch): variant_symbol = {} variant_observations = {} pipeline._prewarm_variants( - load_flat_input(corpus, log=_silent), + load_flat_input(corpus, GENERIC_TARGETS, log=_silent), corpus, TARGET_ARCH, _stub_hipcc(tmp_path), @@ -1135,7 +1148,7 @@ def test_prewarm_pool_populates_both_caches(tmp_path, monkeypatch): ) jobs = pipeline._prewarm_jobs( - load_flat_input(corpus, log=_silent), corpus, TARGET_ARCH + load_flat_input(corpus, GENERIC_TARGETS, log=_silent), corpus, TARGET_ARCH ) expected_symbol = {job.vk: job.ukd["kernel_source"]["entry"] for job in jobs} assert len(expected_symbol) == HIP_ONLY_EXPECTED_CO_COUNT @@ -1158,7 +1171,7 @@ def test_prewarm_pool_populates_both_caches(tmp_path, monkeypatch): def _staged_tree(corpus, out_dir): """Every staged file under `out_dir`, keyed by path relative to it.""" pipeline.compile_intermediate( - load_flat_input(corpus, log=_silent), + load_flat_input(corpus, GENERIC_TARGETS, log=_silent), corpus, TARGET_ARCH, _stub_hipcc(out_dir.parent), @@ -1253,7 +1266,7 @@ def _no_pool(*_args, **_kwargs): variant_co = {} pipeline._prewarm_variants( - load_flat_input(corpus, log=_silent), + load_flat_input(corpus, GENERIC_TARGETS, log=_silent), corpus, TARGET_ARCH, _stub_hipcc(tmp_path), @@ -1416,7 +1429,7 @@ def _serial_pool(monkeypatch, between=None): def _editable_jobs(corpus, out_dir): - flat = load_flat_input(corpus, log=_silent) + flat = load_flat_input(corpus, GENERIC_TARGETS, log=_silent) jobs = pipeline._prewarm_jobs(flat, corpus, TARGET_ARCH) return flat, [replace(job, out_dir=str(out_dir), hipcc="hipcc") for job in jobs] @@ -1470,7 +1483,7 @@ def _edit_producer(): with pytest.raises(HkpPackError) as excinfo: pipeline.compile_intermediate( - load_flat_input(corpus, log=_silent), + load_flat_input(corpus, GENERIC_TARGETS, log=_silent), corpus, TARGET_ARCH, "hipcc", @@ -1496,7 +1509,7 @@ def test_pool_accepts_variants_built_by_one_producer_revision( _serial_pool(monkeypatch) inter = pipeline.compile_intermediate( - load_flat_input(corpus, log=_silent), + load_flat_input(corpus, GENERIC_TARGETS, log=_silent), corpus, TARGET_ARCH, "hipcc", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_rocke.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_rocke.py index a84404ee13dd..da64d951fdf2 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_rocke.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_rocke.py @@ -8,7 +8,14 @@ import pytest -from conftest import _arg, _kernel, _object, requires_msgpack +from conftest import ( + GENERIC_TARGETS, + GENERIC_TARGETS_JSON, + _arg, + _kernel, + _object, + requires_msgpack, +) from hkp_pack.descriptors import load_flat_input from hkp_pack.errors import HkpPackError from hkp_pack.pipeline import run_pipeline @@ -33,6 +40,7 @@ def _copy_fixture(tmp_path, fixture): def _run(source_root, tmp_path, hipcc, rocm_kpack_dir, arches=(ARCH,)): return run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=source_root, arches=list(arches), out_root=tmp_path / "out", @@ -460,7 +468,7 @@ def test_variant_key_stem_is_source_stem(): # --- D. rocke validation accept/reject (quick, comgr-free) ------------------ @pytest.mark.quick def test_validation_accepts_rocke_ukd(tmp_path, rocke_fixture, rocke_ukd): - flat = load_flat_input(_copy_fixture(tmp_path, rocke_fixture)) + flat = load_flat_input(_copy_fixture(tmp_path, rocke_fixture), GENERIC_TARGETS) kdp = next(k for k in flat.kdps() if k.id == "kdp-attention") ks = kdp.doc["kernelDescriptors"][0]["kernel_source"] assert ks["kind"] == "rocke" @@ -475,7 +483,7 @@ def test_validation_rejects_missing_builder(tmp_path, rocke_fixture): del doc["kernelDescriptors"][0]["kernel_source"]["builder"] p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="missing required field 'builder'"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -486,7 +494,7 @@ def test_validation_rejects_missing_spec(tmp_path, rocke_fixture): del doc["kernelDescriptors"][0]["kernel_source"]["spec"] p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="missing required field 'spec'"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -497,7 +505,7 @@ def test_validation_rejects_non_object_spec(tmp_path, rocke_fixture): doc["kernelDescriptors"][0]["kernel_source"]["spec"] = "not-an-object" p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="invalid spec"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -511,7 +519,7 @@ def test_validation_standalone_rocke_admitted(tmp_path, rocke_fixture): doc["kernelDescriptors"] = [standalone_id] kdp_path.write_text(json.dumps(doc), encoding="utf-8") (src / "attention.ukd.json").write_text(json.dumps(inline), encoding="utf-8") - flat = load_flat_input(src) + flat = load_flat_input(src, GENERIC_TARGETS) assert standalone_id in flat.ukd_by_id() @@ -751,6 +759,7 @@ def test_rocke_toc_key_collision_is_detected(tmp_path, monkeypatch, rocm_kpack_d with pytest.raises(HkpPackError, match="toc_key collision"): pipeline.run_pipeline( + generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=tmp_path / "out", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_python_environment.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_python_environment.py index 8af2a9b0200b..1988b354e033 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_python_environment.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_python_environment.py @@ -17,6 +17,7 @@ from pathlib import Path import pytest +from conftest import GENERIC_TARGETS_JSON pytestmark = pytest.mark.quick PKG = Path(__file__).resolve().parent.parent @@ -172,6 +173,7 @@ def __init__(self, root, *, cmake, make_program, pip=True, user_site=False): list(APPEND CMAKE_MODULE_PATH "{(PKG.parent / 'cmake').as_posix()}") include("{MODULE.as_posix()}") set(ROCKE_WHEEL_VERSION 0.1.0) +set(HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON "{GENERIC_TARGETS_JSON.as_posix()}") hkp_rocke_wheel_stamp(wheel_stamp) hkp_rocke_wheel_python_interp(interp ready python_dir "${{wheel_stamp}}") set(HKP_TOOL "${{CMAKE_CURRENT_SOURCE_DIR}}/consumer.py") diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_specialization_agreement.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_specialization_agreement.py index 6724d73e1f94..3c36b4198687 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_specialization_agreement.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_specialization_agreement.py @@ -25,6 +25,7 @@ import pytest +from conftest import GENERIC_TARGETS, GENERIC_TARGETS_JSON from hkp_pack import agreement, pipeline from hkp_pack.errors import HkpPackError from hkp_pack.desk_check import compiled_agreement, load_kernels @@ -1014,7 +1015,7 @@ def retire_evidence(self, origin_kind): def assert_qualified_waiver(self): """Both readers pass, and both say out loud that they bound nothing.""" failures, unclaimed, verified = compiled_agreement( - self.kdp_path, self.python_dir + self.kdp_path, GENERIC_TARGETS, self.python_dir ) assert failures == [] and len(unclaimed) == 1 and verified == 0 for result in self.run_clis(): @@ -1032,6 +1033,8 @@ def run_clis(self): "full", "--field", "head_size", + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), ] variant = [ sys.executable, @@ -1053,14 +1056,18 @@ def run_clis(self): ] def assert_agreement(self): - assert compiled_agreement(self.kdp_path, self.python_dir) == ([], [], 1) + assert compiled_agreement(self.kdp_path, GENERIC_TARGETS, self.python_dir) == ( + [], + [], + 1, + ) for result in self.run_clis(): assert result.returncode == 0, result.stdout + result.stderr assert "NOT VERIFIED HERE" not in result.stdout def assert_failure(self, reason): failures, unclaimed, verified = compiled_agreement( - self.kdp_path, self.python_dir + self.kdp_path, GENERIC_TARGETS, self.python_dir ) assert failures and not unclaimed and verified == 0 assert any(reason in failure for failure in failures), failures @@ -1124,7 +1131,9 @@ def test_shared_standalone_requires_every_consumer( artifact = reader_artifact(shared=True, distinct_kmd=distinct_kmd) artifact.assert_agreement() sibling_path = artifact.root / "sibling.kdp.json" - assert compiled_agreement(sibling_path, artifact.python_dir) == ([], [], 1) + assert compiled_agreement( + sibling_path, GENERIC_TARGETS, artifact.python_dir + ) == ([], [], 1) if mutation == "missing-record": artifact.ukd["provenance"]["effective_spec"]["consumers"].pop() else: @@ -1147,7 +1156,7 @@ def test_nested_standalone_reads_its_own_named_archive( wrong_origin.parent.mkdir(parents=True, exist_ok=True) artifact.archive_path.rename(wrong_origin) failures, unclaimed, verified = compiled_agreement( - artifact.kdp_path, artifact.python_dir + artifact.kdp_path, GENERIC_TARGETS, artifact.python_dir ) assert failures and not unclaimed and verified == 0 for result in artifact.run_clis(): @@ -1214,7 +1223,7 @@ def test_standalone_requires_own_declaration_despite_kdp_contracts( kdp["provenance"] = {"specialization_contract": contract} artifact.save() failures, unclaimed, verified = compiled_agreement( - artifact.kdp_path, artifact.python_dir + artifact.kdp_path, GENERIC_TARGETS, artifact.python_dir ) assert failures and not unclaimed and verified == 0 assert any("missing/invalid specialization_contract" in f for f in failures) @@ -1236,7 +1245,7 @@ def test_own_override_cannot_borrow_kdp_consumer(self, reader_artifact): ) artifact.save() with pytest.raises(HkpPackError, match="0 specialization consumers"): - compiled_agreement(artifact.kdp_path, artifact.python_dir) + compiled_agreement(artifact.kdp_path, GENERIC_TARGETS, artifact.python_dir) for result in artifact.run_clis(): assert result.returncode == 1, result.stdout + result.stderr assert "0 specialization consumers" in result.stdout + result.stderr diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py new file mode 100644 index 000000000000..e49d7216610e --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py @@ -0,0 +1,65 @@ +"""Does a KDP ship for any of the selected arches? Prints `TRUE` or `FALSE`. + + python3 tools/hkp_arch_probe.py --generic-targets-json \\ + --arches "gfx1100;gfx1151" --kdp + +The CMake configure step asks this to decide whether a root is dormant, so it +must give the packer's own answer: it applies `generic_targets.admits_target` +(an empty `arch` is a wildcard; an entry admits a target it names or a table +generic containing it) to each selected arch. + +Only FALSE is authoritative. A KDP that does not parse, or whose `arch` is not +an array of strings, prints TRUE so the packer reports what is wrong with it. +Exits 0 whenever it answered; non-zero only when the table cannot be read. +""" + +import argparse +import json +import sys +from pathlib import Path + +# Same shadowing hazard hkp_pack.py's own tool guards against. +_PKG_ROOT = str(Path(__file__).resolve().parent.parent / "python") +while _PKG_ROOT in sys.path: + sys.path.remove(_PKG_ROOT) +sys.path.insert(0, _PKG_ROOT) + +from hkp_pack.errors import HkpPackError # noqa: E402 +from hkp_pack.generic_targets import GenericTargets, admits_target # noqa: E402 + + +def _covers_any(kdp_path, arches, table): + try: + doc = json.loads(Path(kdp_path).read_text(encoding="utf-8")) + except (OSError, ValueError): + return True + if not isinstance(doc, dict): + return True + if "arch" not in doc: + return True + entries = doc["arch"] + if not isinstance(entries, list) or not all(isinstance(e, str) for e in entries): + return True + if not entries: + return True + return any(admits_target(entries, arch, table) for arch in arches) + + +def main(argv=None): + p = argparse.ArgumentParser(prog="hkp_arch_probe", description=__doc__) + p.add_argument("--generic-targets-json", required=True) + p.add_argument("--arches", required=True, help="';'- or ','-separated arch list") + p.add_argument("--kdp", required=True) + args = p.parse_args(sys.argv[1:] if argv is None else argv) + try: + table = GenericTargets.load(args.generic_targets_json) + except HkpPackError as exc: + print(f"hkp_arch_probe: error: {exc}", file=sys.stderr) + return 1 + arches = [a for a in args.arches.replace(",", ";").split(";") if a] + print("TRUE" if _covers_any(args.kdp, arches, table) else "FALSE") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_desk_check.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_desk_check.py index 2918c0730346..84d553df4634 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_desk_check.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_desk_check.py @@ -53,6 +53,7 @@ metadata_identity_fields, ) from hkp_pack.errors import HkpPackError # noqa: E402 +from hkp_pack.generic_targets import GenericTargets # noqa: E402 def _parse_args(argv): @@ -79,6 +80,13 @@ def _parse_args(argv): "reports compiled agreement as NOT CHECKED. Required: a default would " "let a structural run read as a full one.", ) + p.add_argument( + "--generic-targets-json", + required=True, + help="The generic GPU target table (gpu_generic_targets.json). Duplicate " + "matcher tuples are judged per arch tier, and a generic stands for its " + "table members.", + ) p.add_argument( "--kpack-python-dir", default=None, @@ -120,6 +128,11 @@ def main(argv=None): args = _parse_args(sys.argv[1:] if argv is None else argv) drift_fields = tuple(args.drift_fields) if args.drift_fields else None kdp = Path(args.kdp) + try: + generic_targets = GenericTargets.load(args.generic_targets_json) + except HkpPackError as exc: + print(f"FAIL: {exc}", file=sys.stderr) + return 1 failures = None unclaimed: list = [] verified = 0 @@ -129,7 +142,7 @@ def main(argv=None): # question with no. try: failures, unclaimed, verified = compiled_agreement( - kdp, args.kpack_python_dir + kdp, generic_targets, args.kpack_python_dir ) except HkpPackError as exc: failures = [str(exc)] @@ -150,6 +163,7 @@ def main(argv=None): fields, drift_fields, mode=args.mode, + generic_targets=generic_targets, agreement_failures=failures, agreement_unclaimed=unclaimed, agreement_verified=verified, diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_pack.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_pack.py index 58f5737af8bb..254ab33db69c 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_pack.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_pack.py @@ -112,6 +112,14 @@ def _parse_args(argv): "scratch inside whatever tree the output is written to, which is now a " "staged, installable directory.", ) + p.add_argument( + "--generic-targets-json", + required=True, + help="The generic GPU target table (gpu_generic_targets.json) shared with " + "the loader. GPU_TARGETS stays concrete: every table generic that a KDP " + "lists and that contains a selected arch is packed once and copied into " + "each selected member's folder.", + ) p.add_argument( "--kpack-python-dir", default=None, @@ -152,6 +160,7 @@ def main(argv=None): arches=arches, out_root=Path(args.out_root), hipcc=args.hipcc, + generic_targets_json=Path(args.generic_targets_json), rocm_kpack_dir=args.kpack_python_dir, inter_root=Path(args.inter_root), rocke_wheel_stamp=args.rocke_wheel_stamp, From 15b4b58369f81c3e8e95af2cd3b0d532d4fc3cac Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 5 Oct 2026 22:50:44 +0000 Subject: [PATCH 22/52] feat(hipdnn): generic target tiers in IngestorGenerator and verifier tools Config validation, dedup and the generated census follow the loader's tier rule; verify_variant_sets reads the shared table through hkp_pack. agreement.overlap is removed with its last caller. --- .../python/hkp_pack/agreement.py | 4 - .../rfcs/0017_UniversalKernelDescriptor.md | 8 +- .../hipdnn/tools/IngestorGenerator/README.md | 35 +++- .../codegen/config_loader.py | 105 +++++++++- .../IngestorGenerator/codegen/generator.py | 51 +++-- .../codegen/generic_targets.py | 179 ++++++++++++++++++ .../templates/test_packs.cpp.j2 | 12 +- .../tests/test_config_loader.py | 93 +++++++++ .../IngestorGenerator/tests/test_generator.py | 105 +++++++++- .../tests/test_generic_targets.py | 50 +++++ .../tests/test_native_stub.py | 27 ++- .../tests/test_verify_variant_sets.py | 78 +++++++- .../tools/verify_variant_sets.py | 87 +++++++-- 13 files changed, 769 insertions(+), 65 deletions(-) create mode 100644 projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py create mode 100644 projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/agreement.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/agreement.py index fe3db3b5bcc5..46b87ebca97a 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/agreement.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/agreement.py @@ -52,10 +52,6 @@ def complete_metadata(metadata, kmd): return result -def overlap(left, right): - return not left or not right or bool(set(left) & set(right)) - - def validate_consumer(consumer, kmd): required = { "engine_id", diff --git a/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md b/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md index 22e172a2dd8d..96525e9d4f74 100644 --- a/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md +++ b/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md @@ -1422,8 +1422,12 @@ provider's descriptor cache, reused by every later graph. **4. Resolve the kernel packs that name this engine, and apply the arch gate.** Each pack (**KDP**) contributes a matcher set, one dispatch-descriptor (**UDD**) id, and a kernel vector with each -kernel's metadata values. A pack whose `arch` list excludes this device is dropped here. The -dispatch descriptor is named but **not** loaded; nothing dispatches yet. +kernel's metadata values. A pack whose `arch` list excludes this device is dropped here. An `arch` +list ranks per device in tiers: an explicit entry (the device's own base id) beats an LLVM generic +target containing it (e.g. `gfx11-generic`, whose members come from the shared generic target +table), which beats an empty list. Two kernels of one engine collide on a tuple only where they +select a device at the same tier, so a generic fallback and an explicit override of one tuple +coexist. The dispatch descriptor is named but **not** loaded; nothing dispatches yet. *Stored:* the parsed packs, matchers, and kernel metadata, in the same descriptor cache. **5. Run the engine's `graph_match`, lazily, on the first pack that cleared the gate.** It either diff --git a/projects/hipdnn/tools/IngestorGenerator/README.md b/projects/hipdnn/tools/IngestorGenerator/README.md index 57bd556287d0..d3f6f0092f8d 100644 --- a/projects/hipdnn/tools/IngestorGenerator/README.md +++ b/projects/hipdnn/tools/IngestorGenerator/README.md @@ -295,7 +295,7 @@ machine. | Tool | Answers | Invocation | |---|---|---| -| `tools/verify_variant_sets.py` | Structural nesting/runtime tuple identity, sentinels and vocabulary; artifact-bound compiler agreement is the distinct, stronger mode | `verify_variant_sets.py --mode {full,structural} [--arch A] [--profile P] [--kpack-python-dir D] LABEL ROOT...`. `--mode` is required and has no default: `structural` reports compiled specialization agreement as NOT CHECKED by name and still exits 0 on the rest; `full` fails on a missing, unsupported or mismatched producing-build record **and** on any check that could not run (`GATE FAILED (N check(s) NOT RUN: ...)`). `--profile` supplies the bundle to gate and the matcher vocabulary, which full mode requires over string fields no declaration spells out | +| `tools/verify_variant_sets.py` | Structural nesting/runtime tuple identity, sentinels and vocabulary; artifact-bound compiler agreement is the distinct, stronger mode | `verify_variant_sets.py --mode {full,structural} [--arch A] [--profile P] [--kpack-python-dir D] [--generic-targets-json T] LABEL ROOT...`. `--mode` is required and has no default: `structural` reports compiled specialization agreement as NOT CHECKED by name and still exits 0 on the rest; `full` fails on a missing, unsupported or mismatched producing-build record **and** on any check that could not run (`GATE FAILED (N check(s) NOT RUN: ...)`). `--profile` supplies the bundle to gate and the matcher vocabulary, which full mode requires over string fields no declaration spells out | | `tools/variant_reachability.py` | Can any shape in the corpus actually select each variant, or is one dead weight? | `variant_reachability.py --kdp K --shapes S [--profile P]` | | `tools/launch_surface.py` | Is every surface the C++ restates from the kernel's Python declared, guarded and tested? | `launch_surface.py PROFILE --check [--allow-unguarded]` | | `tools/coverage_gate.py` | Structural, loading and serving obligations, reported separately; an unmet required obligation cannot pass | `coverage_gate.py --tree T --mode {full,structural} [--arch A] [--validator V] [--expect-engine E] [--min-served N]`; `--mode` is required and governs what rung 1 may claim. A missing `--validator` makes rung 2 `loads-not-run`, a failure rather than a skip; an offline result is not serving evidence | @@ -496,11 +496,36 @@ Run, in this order, **before any UUID is minted**: plan-build time against a real device. 3. Every kernel's `metadata` type-checks against the KMD, with no mandatory field (one with no `default_value`) omitted; the real loader drops the whole pack instead. -4. Every kernel's `arch` is a subset of its pack's `arch`. +4. Every kernel's `arch` is a subset of its pack's `arch`, compared as expanded device + sets (a generic stands for its table members), and the generic-target rules below hold. 5. Every `arch` entry is a plausible `gfx`-prefixed base id (lowercase, no feature suffix): an error if malformed, a **warning** if well-formed but unrecognized (e.g. `gfx94` for `gfx942`), since either looks like an ordinary INFO decline at match time and this tool - keeps no exhaustive arch list. + keeps no exhaustive arch list. A generic named in the table is recognized. + +### Generic targets and tiered `arch` matching + +An `arch` entry may be an LLVM generic target (`gfx11-generic`). Membership is data in +`plugin_sdk/data/gpu_generic_targets.json`; `codegen/generic_targets.py` reads that file +(no flag) and mirrors the loader's and the packer's tier rules, held equal by the golden +vectors in `plugin_sdk/tests/data/arch_tier_vectors.json` (`tests/test_generic_targets.py`). +A device is matched per `arch` list at one tier: an explicit entry (the device's own base +id) beats a generic containing it, which beats an empty list. Consequences for a config: + +- De-duplication (`build_kdp`) refuses a tuple shared by two kernels only where they select + a device at the **same** tier. A generic kernel and an explicit member kernel with one + tuple coexist (the explicit one wins on its own device), as do an empty-`arch` kernel and + a concrete one; two kernels under the same generic, or two empty lists, still collide. +- A list may not hold a generic with one of its members, nor two generics sharing a member; + an unknown generic-shaped name is an error. The packer enforces the same rules. +- In a pack whose `arch` lists a generic, a kernel with its own non-empty `arch` must list + every generic of the pack and only entries the pack lists; a kernel with no `arch` + inherits the pack. A kernel may not name a generic its pack does not list. An `hsaco` + kernel is stamped with its own `arch`, else the whole pack list, which satisfies this. +- The emitted inventory (and so the generated census) gives every member of a generic row + the generic's descriptors, and the census checks coverage with the SDK's `archSupports`. + An `hsaco` object packed under a generic must really be generic-compatible; neither the + generator nor the packer checks this (a mismatch fails at module load). ## Source adapters (`codegen/sources/`) @@ -518,8 +543,8 @@ descriptor that names it and `symbol` its kernel. `file` must stay inside the so with no root-relative fallback. `hkp_pack` packs that object as-is, without compiling, so like `hip` the specialization declares `metadata_fields: []`. The packer does not check the object's format or target processor: every hsaco kernel must carry a non-empty per-kernel `arch` listing the -arch(es) its object runs on (a generic-target object lists each one), and the loader -rejects one without. +arch(es) its object runs on (a generic-target object lists the generic, or each member +under an explicit pack), and the loader rejects one without. `hsaco_file` is rejected explicitly, naming `supportsSourceKind()` as the missing prerequisite on `IKernelDispatchHandler`. diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py b/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py index aa0e34fa2bf0..f978a7e772dc 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py @@ -19,6 +19,7 @@ import yaml +from . import generic_targets as gtmod from .models import ( ARCH_BASE_ID_PATTERN, AUTHORED_TEST_SETS, @@ -1364,19 +1365,110 @@ def _check_kernel_priority(config: IngestorConfig) -> None: ) +def _generic_table() -> "gtmod.GenericTargets": + """The in-repo generic target table, with a read failure surfaced as a ConfigError.""" + try: + return gtmod.default_table() + except ValueError as exc: + raise ConfigError(str(exc)) from exc + + +def _check_arch_list_generics(archs: list, where: str, table) -> None: + """Per-list generic rules: an unknown generic-shaped name is an error, and one + list may not hold a generic with a member it contains or two generics sharing a + member (the packer's S5).""" + for entry in archs: + if gtmod.is_generic_shaped(entry) and not table.has(entry): + raise ConfigError( + f"{where}: arch entry '{entry}' is a generic target name absent " + f"from the generic target table {table.path} " + f"(known: {', '.join(table.names())})" + ) + advice = ( + "list one, or author two packs (an explicit override plus a generic " + "fallback are separate packs)" + ) + for i, x in enumerate(archs): + for y in archs[i + 1 :]: + if table.has(x) and table.has(y): + shared = [m for m in table.members(x) if m in table.members(y)] + if shared: + raise ConfigError( + f"{where}: 'arch' lists '{x}' and '{y}': the two generics " + f"share member {shared[0]}; {advice}" + ) + continue + generic, member = (x, y) if table.has(x) else (y, x) + if table.has(generic) and member in table.members(generic): + raise ConfigError( + f"{where}: 'arch' lists '{x}' and '{y}': {generic} contains " + f"{member}; {advice}" + ) + + +def _check_generic_arch(config: IngestorConfig) -> None: + """Pre-mint check #4a: the generic-target rules, the packer's own (``hkp_pack`` + ``validate_generic_arch``) so a config the generator accepts is one the packer + accepts. + + Per list: unknown generic names and a generic listed beside its member (or beside + a generic sharing a member) are errors. Per pack of any shape: a kernel may not + name a generic the pack does not list, an empty pack list included, since it + would ship in no shard. In a pack that lists a generic, a kernel with its own + non-empty ``arch`` must list every generic of the pack and only entries the pack + lists; a kernel with no own ``arch`` inherits the pack and is unaffected. A hsaco + kernel is checked on the list it is stamped with (its own, else the pack's). + """ + table = _generic_table() + for pack in config.packs: + where = f"pack '{pack.name}'" + _check_arch_list_generics(pack.arch, where, table) + pack_generics = [a for a in pack.arch if gtmod.is_generic_shaped(a)] + for kernel in pack.kernels: + kernel_where = f"{where} kernel '{kernel.name}'" + _check_arch_list_generics(kernel.arch, kernel_where, table) + stamped = ( + list(kernel.arch or pack.arch) + if kernel.kernel_source.kind == KERNEL_SOURCE_KIND_HSACO + else list(kernel.arch) + ) + for generic in stamped: + if gtmod.is_generic_shaped(generic) and generic not in pack.arch: + raise ConfigError( + f"{kernel_where}: declares generic target '{generic}' but " + f"the pack does not list '{generic}'; it would ship in no " + f"shard" + ) + if not pack_generics or not stamped: + continue + lists_all = all(g in stamped for g in pack_generics) + only_listed = all(a in pack.arch for a in stamped) + if not lists_all or not only_listed: + raise ConfigError( + f"{where}: pack lists generic target(s) {pack_generics} (arch " + f"{pack.arch}) but kernel '{kernel.name}' declares arch " + f"{stamped}; a kernel under a generic pack must list every " + f"generic of the pack and only entries the pack lists" + ) + + def _check_kernel_arch_subset_of_pack(config: IngestorConfig) -> None: """Pre-mint check #4: a kernel's arch must be a subset of its pack's. - Mirrors ``DescriptorLoader.hpp``'s ``archCovers(pack.arch, kernel.arch)``. - An empty pack.arch covers everything; an empty kernel.arch inherits it. + Mirrors ``DescriptorLoader.hpp``'s ``archCovers(pack.arch, kernel.arch)`` over + the expanded device sets (a generic stands for its table members). An empty + pack.arch covers everything; an empty kernel.arch inherits it. """ + table = _generic_table() for pack in config.packs: if not pack.arch: continue for kernel in pack.kernels: if not kernel.arch: continue - reaching = [a for a in kernel.arch if a not in pack.arch] + reaching = [ + a for a in kernel.arch if not gtmod.covers(pack.arch, [a], table) + ] if reaching: raise ConfigError( f"pack '{pack.name}' kernel '{kernel.name}' declares arch " @@ -1392,10 +1484,12 @@ def _check_arch_shape(config: IngestorConfig) -> list[str]: A shape violation (``GFX942``, ``" gfx942"``, a feature suffix) is a ``ConfigError``, mirroring ``isPlausibleArchBaseId``; a well-formed but - unrecognized id (``gfx94``) is a warning. Returns the warnings emitted, + unrecognized id (``gfx94``) is a warning; a generic named in the table is + recognized. Returns the warnings emitted, also raised via ``warnings.warn``. """ messages: list[str] = [] + table = _generic_table() def check_list(archs: list, where: str) -> None: for arch in archs: @@ -1409,7 +1503,7 @@ def check_list(archs: list, where: str) -> None: f"parses fine and then declines on every device, logging " f"exactly what a healthy cross-arch install logs." ) - if arch not in KNOWN_ARCH_BASE_IDS: + if arch not in KNOWN_ARCH_BASE_IDS and not table.has(arch): message = ( f"{where} arch entry '{arch}' is well-formed but not a " f"recognized gfx target id (e.g. a typo like 'gfx94' for " @@ -1954,6 +2048,7 @@ def _validate_config(config: IngestorConfig) -> list[str]: # KMD_FIELD_TYPES; the default_value check indexes _METADATA_TYPE_CHECKS by it. _check_kmd_default_values(config) _check_kernel_priority(config) + _check_generic_arch(config) _check_kernel_arch_subset_of_pack(config) # #4 warnings_out = _check_arch_shape(config) # #5 diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py b/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py index 8f2f5f5ae63a..14c6129b1ac8 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py @@ -14,6 +14,7 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined +from . import generic_targets as gtmod from .models import ( KERNEL_SOURCE_KIND_HSACO, KERNEL_SOURCE_KIND_KPACK, @@ -336,7 +337,7 @@ def _dedup_key(metadata: dict, config: IngestorConfig) -> str: """Identity of a descriptor as the matcher sees it: the completed tuple, not the emitted document. - Architecture is not part of this key; `build_kdp` decides overlap against + Architecture is not part of this key; `build_kdp` decides competition against the arch coverage recorded beside each key. """ return json.dumps(_completed_metadata(metadata, config), sort_keys=True) @@ -356,11 +357,13 @@ def _candidate_identity(kernel: KernelSpec) -> str: ) -def _arch_overlaps(left: list, right: list) -> bool: - """Whether two arch coverages can select on the same device. An empty list - is the loader's wildcard and overlaps everything. Mirrors - ``hkp_pack.agreement.overlap``.""" - return not left or not right or bool(set(left) & set(right)) +def _arch_competes(left: list, right: list) -> bool: + """Whether two arch coverages tie on some device: both select it at the same + tier (explicit, generic, or unrestricted), so the matcher would see one tuple + twice there. A device where one outranks the other is not a collision: an + explicit entry beats a generic containing it, which beats an empty list. + Mirrors ``generic_targets.compete`` of the packer.""" + return gtmod.compete(left, right, gtmod.default_table()) def _pack_index(config: IngestorConfig, pack: PackSpec) -> int: @@ -451,9 +454,11 @@ def build_kdp( if config.is_multi_pack: matchers.insert(0, ids[("operation_umd", pack_index)]) # Generation expressions targeting one engine may overlap, so a shared - # tuple has four outcomes: disjoint arch coverage is not a duplicate; - # overlapping coverage with the same candidate and equal arch de-duplicates - # here; a different candidate, or unequal coverage, is refused. + # tuple has four outcomes: coverage that never ties on a device (disjoint, + # or an explicit entry against a generic or empty list it outranks) is not + # a duplicate; competing coverage with the same candidate and equal arch + # de-duplicates here; a different candidate, or unequal coverage, is + # refused. kernel_descriptors = [] if seen_metadata is None: seen_metadata = {} @@ -475,7 +480,7 @@ def build_kdp( identity = _candidate_identity(kernel) already = None for prior in seen_metadata.setdefault(key, []): - if not _arch_overlaps(arch, prior["arch"]): + if not _arch_competes(arch, prior["arch"]): continue if prior["identity"] == identity: if sorted(prior["arch"]) == sorted(arch): @@ -531,7 +536,9 @@ def build_kdp( "priority": kernel.priority, } # hkp_pack validates the kernel's own arch for hsaco and rejects a - # wildcard, so the inherited pack arch is stated on the descriptor. + # wildcard, so the inherited pack arch is stated on the descriptor. The + # whole pack list is the one stamp that satisfies the generic-pack rule + # (list every generic of the pack, only entries the pack lists). if kernel.kernel_source.kind == KERNEL_SOURCE_KIND_HSACO: entry["arch"] = arch elif kernel.arch: @@ -616,8 +623,11 @@ def emitted_inventory(config: IngestorConfig, kdp_documents: list) -> dict: lowered to ``kpack`` first. A descriptor with no ``arch`` is filed under its pack's; a pack with none under `ARCH_WILDCARD`. - Each concrete arch row is unioned with the wildcard row, one way only, - because a wildcard entry ships on that device too. + A table generic's row is shared by every member of the generic: each member's + row is created if absent and holds its own descriptors, those of every + generic row containing it, and the wildcard row's. A wildcard entry ships on + every device too, so every non-wildcard row also takes the wildcard row. + These unions are one way only. """ arches: dict[str, dict] = {} @@ -635,6 +645,21 @@ def bucket(arch: str) -> dict: for arch in list(descriptor.get("arch") or []) or pack_arch: bucket(arch)["descriptors"].append(descriptor["name"]) + table = gtmod.default_table() + generic_rows = { + name: (list(arches[name]["descriptors"]), set(arches[name]["pack_names"])) + for name in table.names() + if name in arches + } + for name in generic_rows: + for member in table.members(name): + bucket(member) + for arch, entry in arches.items(): + for name, (descriptors, pack_names) in generic_rows.items(): + if arch in table.members(name): + entry["descriptors"].extend(descriptors) + entry["pack_names"].update(pack_names) + wildcard = arches.get(ARCH_WILDCARD) if wildcard is not None: for arch, entry in arches.items(): diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py b/projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py new file mode 100644 index 000000000000..c1b5c779b0ca --- /dev/null +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py @@ -0,0 +1,179 @@ +# Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT + +"""LLVM generic GPU targets as data, and the tier algebra over them, for ``codegen/``. + +An in-repo mirror of ``hkp_pack.generic_targets`` and of the C++ loader's +``archTier``: the generator may not import the kernel provider's packaging tree +(descriptor generation must not need the kernel toolchain), so the three +implementations are held equal by the shared golden vectors in +``plugin_sdk/tests/data/arch_tier_vectors.json`` (``tests/test_generic_targets.py``). +Packer-side verifier tools import ``hkp_pack.generic_targets`` instead; this module is +used only inside ``codegen/``. + +Membership is data, never name shape: a generic-shaped name the table does not list +expands to the empty set and matches no device. An ``arch`` list ranks per device: +EXPLICIT (the device's own base id) beats GENERIC (a table generic containing the +device) beats UNRESTRICTED (an empty list). +""" + +import json +from functools import cache +from pathlib import Path + +TIER_EXPLICIT = 0 +TIER_GENERIC = 1 +TIER_UNRESTRICTED = 2 + +# .../projects/hipdnn/tools/IngestorGenerator/codegen/ +DEFAULT_TABLE_PATH = ( + Path(__file__).resolve().parents[3] + / "plugin_sdk" + / "data" + / "gpu_generic_targets.json" +) + +_GENERIC_SUFFIX = "-generic" + + +class GenericTargets: + """The generic target table: generic name -> member processor names, document order.""" + + def __init__(self, path: Path, generics: dict): + self._path = Path(path) + self._generics = generics + + @classmethod + def load(cls, path) -> "GenericTargets": + """Read the table at @path; ``ValueError`` names the path on any defect.""" + path = Path(path) + try: + doc = json.loads(path.read_text(encoding="utf-8")) + except OSError as exc: + raise ValueError( + f"{path}: cannot read generic target table: {exc}" + ) from exc + except ValueError as exc: + raise ValueError( + f"{path}: generic target table is not valid JSON: {exc}" + ) from exc + raw = doc.get("generics") if isinstance(doc, dict) else None + if not isinstance(raw, dict): + raise ValueError(f"{path}: 'generics' must be an object") + generics = {} + for name, members in raw.items(): + if ( + not isinstance(members, list) + or not members + or not all(isinstance(m, str) and m for m in members) + ): + raise ValueError( + f"{path}: generics.{name} must be a non-empty array of names" + ) + generics[name] = tuple(members) + return cls(path, generics) + + @property + def path(self) -> Path: + return self._path + + def has(self, name: str) -> bool: + return name in self._generics + + def members(self, name: str) -> tuple: + """Members of generic @name in document order; KeyError when it is not listed.""" + return self._generics[name] + + def names(self) -> tuple: + return tuple(self._generics) + + +@cache +def default_table() -> GenericTargets: + """The table at ``DEFAULT_TABLE_PATH``, read once per process.""" + return GenericTargets.load(DEFAULT_TABLE_PATH) + + +def is_generic_shaped(name: str) -> bool: + """Shape only (``...-generic``); use ``GenericTargets.has`` for membership.""" + return name.endswith(_GENERIC_SUFFIX) + + +def _strip_features(arch: str) -> str: + return arch.split(":", 1)[0] + + +def entry_tier(entry: str, device: str, table: GenericTargets): + """Tier of one list entry for @device (features allowed), or None when it admits it. + + A generic-shaped entry is never EXPLICIT, so an unknown generic matches no device. + """ + if is_generic_shaped(entry): + if table.has(entry) and _strip_features(device) in table.members(entry): + return TIER_GENERIC + return None + if device == entry or device.startswith(entry + ":"): + return TIER_EXPLICIT + return None + + +def list_tier(entries, device: str, table: GenericTargets): + """Best tier of @entries for @device; empty is UNRESTRICTED; None = no match.""" + if not entries: + return TIER_UNRESTRICTED + tiers = [ + t for t in (entry_tier(e, device, table) for e in entries) if t is not None + ] + return min(tiers) if tiers else None + + +def expand(entries, table: GenericTargets): + """The set of device ids @entries admits; None (unrestricted) for an empty list.""" + if not entries: + return None + devices: set = set() + for entry in entries: + if is_generic_shaped(entry): + if table.has(entry): + devices.update(table.members(entry)) + else: + devices.add(entry) + return frozenset(devices) + + +def overlaps(a, b, table: GenericTargets) -> bool: + """Can one device satisfy both lists? Empty overlaps everything.""" + expanded_a, expanded_b = expand(a, table), expand(b, table) + if expanded_a is None or expanded_b is None: + return True + return not expanded_a.isdisjoint(expanded_b) + + +def covers(outer, inner, table: GenericTargets) -> bool: + """Is every device @inner admits also admitted by @outer?""" + expanded_outer, expanded_inner = expand(outer, table), expand(inner, table) + if expanded_outer is None or expanded_inner is None: + return True + return expanded_inner <= expanded_outer + + +def compete(a, b, table: GenericTargets) -> bool: + """Do the lists tie on some candidate device (both match it at the same tier)? + + Candidates are every explicit id in either list and every member of every table + generic in either list. Two empty lists compete. + """ + if not a and not b: + return True + candidates: set = set() + for entry in (*a, *b): + if is_generic_shaped(entry): + if table.has(entry): + candidates.update(table.members(entry)) + else: + candidates.add(entry) + for device in candidates: + tier = list_tier(a, device, table) + if tier is not None and tier == list_tier(b, device, table): + return True + return False diff --git a/projects/hipdnn/tools/IngestorGenerator/templates/test_packs.cpp.j2 b/projects/hipdnn/tools/IngestorGenerator/templates/test_packs.cpp.j2 index 32eb166ece54..7d8a655fcd10 100644 --- a/projects/hipdnn/tools/IngestorGenerator/templates/test_packs.cpp.j2 +++ b/projects/hipdnn/tools/IngestorGenerator/templates/test_packs.cpp.j2 @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -92,13 +93,6 @@ constexpr ingestor::KernelSourceKind EXPECTED_SOURCE_KIND 'kpack': 'KPACK', 'hsaco_file': 'HSACO_FILE'}[emitted.source_kind] }}; -/// A pack or kernel naming no architecture inherits/wildcards; one that names -/// architectures must name the requested one, or this is the wrong shard. -bool coversArch(const std::vector& declared, const std::string& arch) -{ - return declared.empty() || std::find(declared.begin(), declared.end(), arch) != declared.end(); -} - class Test{{ config.engine.pascal_name }}Packs : public ::testing::Test { protected: @@ -169,12 +163,12 @@ TEST_F(Test{{ config.engine.pascal_name }}Packs, EveryLoadedDescriptorCoversTheR { for(const auto& pack : _set->packs) { - EXPECT_TRUE(coversArch(pack.arch, _arch)) + EXPECT_TRUE(ingestor::archSupports(pack.arch, _arch)) << "pack '" << pack.name << "' does not cover '" << _arch << "', so this is not that architecture's shard"; for(const auto& kernel : pack.kernels) { - EXPECT_TRUE(coversArch(kernel.arch, _arch)) + EXPECT_TRUE(ingestor::archSupports(kernel.arch, _arch)) << "kernel '" << kernel.name << "' does not cover '" << _arch << "'"; } } diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py index e7665f426af4..59ce8e279463 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py @@ -189,6 +189,99 @@ def test_empty_pack_arch_covers_everything(self): _check_kernel_arch_subset_of_pack(config) # does not raise +class TestGenericArchRules: + """The packer's generic-target rules (``hkp_pack.validate_generic_arch``), applied + before a UUID is minted so a config the generator accepts is one the packer + accepts. ``gfx11-generic`` holds gfx1100..gfx1153; ``gfx12-generic`` holds + gfx1200/gfx1201.""" + + @staticmethod + def _config(pack_arch, kernel_arch): + kernel = make_kernel(arch=kernel_arch) + return make_minimal_config(packs=[make_pack(arch=pack_arch, kernels=[kernel])]) + + def test_generic_pack_with_a_kernel_naming_a_member_is_rejected(self): + from codegen.config_loader import _check_generic_arch + + config = self._config(["gfx11-generic"], ["gfx1151"]) + with pytest.raises(ConfigError, match="must list every generic of the pack"): + _check_generic_arch(config) + + def test_generic_pack_accepts_a_kernel_with_no_own_arch(self): + from codegen.config_loader import _check_generic_arch + + _check_generic_arch(self._config(["gfx11-generic"], [])) + + def test_generic_pack_accepts_a_kernel_declaring_exactly_the_generic(self): + from codegen.config_loader import _check_generic_arch + + _check_generic_arch(self._config(["gfx11-generic"], ["gfx11-generic"])) + + def test_mixed_generic_pack_kernel_listing_every_entry_is_accepted(self): + from codegen.config_loader import _check_generic_arch + + config = self._config(["gfx942", "gfx11-generic"], ["gfx942", "gfx11-generic"]) + _check_generic_arch(config) + + def test_mixed_generic_pack_kernel_missing_the_generic_is_rejected(self): + from codegen.config_loader import _check_generic_arch + + config = self._config(["gfx942", "gfx11-generic"], ["gfx942"]) + with pytest.raises(ConfigError, match="must list every generic of the pack"): + _check_generic_arch(config) + + @pytest.mark.parametrize("pack_arch", [[], ["gfx942"], ["gfx942", "gfx12-generic"]]) + def test_a_kernel_naming_a_generic_the_pack_does_not_list_is_rejected( + self, pack_arch + ): + from codegen.config_loader import _check_generic_arch + + config = self._config(pack_arch, ["gfx11-generic"]) + with pytest.raises(ConfigError, match="the pack does not list 'gfx11-generic'"): + _check_generic_arch(config) + + @pytest.mark.parametrize( + "arch, shared", + [ + (["gfx11-generic", "gfx1151"], "gfx11-generic contains gfx1151"), + (["gfx1151", "gfx11-generic"], "gfx11-generic contains gfx1151"), + ], + ) + def test_a_generic_and_its_member_in_one_arch_list_is_rejected(self, arch, shared): + from codegen.config_loader import _check_generic_arch + + with pytest.raises(ConfigError, match=shared): + _check_generic_arch(self._config(arch, [])) + # The kernel's own list is held to the same rule. + with pytest.raises(ConfigError, match=shared): + _check_generic_arch(self._config(["gfx942", "gfx11-generic"], arch)) + + def test_unknown_generic_name_is_rejected(self): + from codegen.config_loader import _check_generic_arch + + with pytest.raises( + ConfigError, match="'gfx9-4-generic' is a generic target name absent" + ): + _check_generic_arch(self._config(["gfx9-4-generic"], [])) + + def test_known_generic_produces_no_unrecognised_arch_warning(self): + from codegen.config_loader import _check_arch_shape + + config = self._config(["gfx11-generic"], []) + assert _check_arch_shape(config) == [] + + def test_an_expanded_subset_is_accepted_by_the_coverage_check(self): + from codegen.config_loader import _check_kernel_arch_subset_of_pack + + # Loader-equivalent coverage: gfx1151 is inside gfx11-generic's members, but + # gfx1250 is not. + _check_kernel_arch_subset_of_pack(self._config(["gfx11-generic"], ["gfx1151"])) + with pytest.raises(ConfigError, match="reaches past the pack's arch"): + _check_kernel_arch_subset_of_pack( + self._config(["gfx11-generic"], ["gfx1250"]) + ) + + class TestArchShapeCheck: """Pre-mint check #5: arch entries are plausible gfx-prefixed base ids (error), and unrecognized-but-well-formed ids warn rather than error.""" diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py index d3bd20a0d107..c3a8c56a7247 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py @@ -752,18 +752,52 @@ def test_one_candidate_on_unequal_overlapping_arches_names_the_coverage(self): assert "SAME candidate" in message assert "kernel_source/priority differ" not in message - def test_a_wildcard_arch_overlapping_a_concrete_one_is_refused(self): - """An absent ``arch`` is the loader's wildcard: treated as no coverage it sits - beside a concrete candidate holding the same tuple and collides on that - device.""" + def test_a_wildcard_arch_and_a_concrete_one_with_one_tuple_both_survive(self): + """An empty ``arch`` is the unrestricted tier and a concrete entry outranks it + on its own device, so the two never tie and the matcher never sees one tuple + twice.""" config, pack, left, right = self._twin_config() pack.arch = [] left.arch = [] # every device right.arch = ["gfx942"] right.kernel_source.entry_point = "ScaleAddOther" - with pytest.raises(ValueError, match="overlapping architectures"): + kdp = build_kdp(config, pack, mint_ids(config)) + assert [k["name"] for k in kdp["kernelDescriptors"]] == [ + "twin.left", + "twin.right", + ] + + def test_a_generic_and_an_explicit_member_with_one_tuple_both_survive(self): + """gfx1151 is inside gfx11-generic: on that device the explicit entry outranks + the generic, so the tuple is carried once per tier.""" + config, pack, left, right = self._twin_config() + pack.arch = ["gfx1151", "gfx11-generic"] + left.arch = ["gfx11-generic"] + right.arch = ["gfx1151"] + right.kernel_source.entry_point = "ScaleAddOther" + # Only tuple identity is under test; the config loader owns the pack rule. + kdp = build_kdp(config, pack, mint_ids(config)) + assert [k["name"] for k in kdp["kernelDescriptors"]] == [ + "twin.left", + "twin.right", + ] + + def test_two_kernels_one_tuple_under_the_same_generic_are_refused_naming_both( + self, + ): + """Both select gfx1100 at the generic tier: one tuple twice drops the engine.""" + config, pack, left, right = self._twin_config() + pack.arch = ["gfx11-generic"] + left.arch = [] + right.arch = [] + right.kernel_source.entry_point = "ScaleAddOther" + + with pytest.raises(ValueError) as excinfo: build_kdp(config, pack, mint_ids(config)) + message = str(excinfo.value) + assert "twin.left" in message and "twin.right" in message + assert "gfx11-generic" in message def test_an_omitted_field_and_the_kmd_default_are_one_key(self): """One tuple omits an optional field; the other states it at exactly the KMD @@ -1034,6 +1068,44 @@ def test_two_distinct_arches_each_count_the_descriptor_once(self, tmp_path): assert inventory["arches"]["gfx950"]["descriptor_count"] == 1 assert inventory["total_descriptor_count"] == 1 + def test_inventory_gives_every_member_the_generic_rows(self): + """A generic pack ships in every member's folder, so each member's row holds the + generic's descriptors, its own, and the wildcard's; a member of no generic row + gets only its own and the wildcard's.""" + generic = make_pack( + name="generic", + arch=["gfx11-generic"], + kernels=[make_kernel(name="generic.k", metadata={"block_size": 64})], + ) + explicit = make_pack( + name="explicit", + arch=["gfx1151"], + kernels=[make_kernel(name="explicit.k", metadata={"block_size": 128})], + ) + other = make_pack( + name="other", + arch=["gfx942"], + kernels=[make_kernel(name="other.k", metadata={"block_size": 256})], + ) + everywhere = make_pack( + name="everywhere", + arch=[], + kernels=[make_kernel(name="any.k", metadata={"block_size": 512})], + ) + config = make_minimal_config(packs=[generic, explicit, other, everywhere]) + documents = build_kdp_documents(config, mint_ids(config)) + arches = emitted_inventory(config, documents)["arches"] + + names = {a: set(row["descriptor_names"]) for a, row in arches.items()} + assert names["gfx1100"] == {"generic.k", "any.k"} + assert names["gfx1153"] == {"generic.k", "any.k"} + assert names["gfx1151"] == {"generic.k", "explicit.k", "any.k"} + assert names["gfx942"] == {"other.k", "any.k"} + assert names["gfx11-generic"] == {"generic.k", "any.k"} + assert "gfx1154" not in names + stems = {config.kdp_stem(p) for p in (generic, everywhere)} + assert set(arches["gfx1100"]["pack_names"]) == stems + def test_the_packaged_dialect_reports_the_kind_that_actually_ships(self): """hkp_pack lowers ``rocke``/``hip`` to ``kpack`` before the loader reads it, so a census expecting the AUTHORED kind expects one that never arrives.""" @@ -1717,6 +1789,29 @@ def test_an_expansion_path_is_not_double_rejected( class TestPackagedHsacoKdp: + def test_hsaco_kernel_inheriting_a_generic_pack_arch_conforms_to_the_pack_rule( + self, + ): + """hkp_pack requires an own arch on an hsaco kernel, so the generator stamps the + whole pack list; that stamp lists every generic of the pack and only entries the + pack lists, which is the pack rule the config loader enforces.""" + from codegen.config_loader import _check_generic_arch + + kernel = make_kernel( + name="test.hsaco", + kernel_source=KernelSource( + kind="hsaco", file="HsacoFixture.co", symbol="HsacoFixtureAdd" + ), + ) + config = make_minimal_config( + dialect="packaged", + kernel_source_kind="hsaco", + packs=[make_pack(kernels=[kernel], arch=["gfx942", "gfx11-generic"])], + ) + _check_generic_arch(config) + kdp = build_kdp(config, config.packs[0], mint_ids(config)) + assert kdp["kernelDescriptors"][0]["arch"] == ["gfx942", "gfx11-generic"] + def test_every_hsaco_ukd_carries_exactly_kind_file_symbol(self): """hkp_pack validates a closed field set per kind, so a UKD carrying any other kind's key is refused at pack time.""" diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py new file mode 100644 index 000000000000..66ca72d1572a --- /dev/null +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py @@ -0,0 +1,50 @@ +# Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT + +"""The codegen mirror of the tier algebra, held equal to the C++ loader and to +``hkp_pack.generic_targets`` by the shared golden vectors. Fails (never skips) outside +the monorepo: the vectors and the table are located by repo-relative path.""" + +import json +from pathlib import Path + +from codegen import generic_targets + +_SDK_DIR = Path(__file__).resolve().parents[3] / "plugin_sdk" +_VECTORS = _SDK_DIR / "tests" / "data" / "arch_tier_vectors.json" +_TIER_NAMES = { + generic_targets.TIER_EXPLICIT: "explicit", + generic_targets.TIER_GENERIC: "generic", + generic_targets.TIER_UNRESTRICTED: "unrestricted", + None: "none", +} + + +def test_matches_the_shared_golden_vectors(): + table = generic_targets.GenericTargets.load(generic_targets.DEFAULT_TABLE_PATH) + assert ( + generic_targets.DEFAULT_TABLE_PATH + == _SDK_DIR / "data" / "gpu_generic_targets.json" + ) + vectors = json.loads(_VECTORS.read_text()) + failures = [] + for case in vectors["tier"]: + got = _TIER_NAMES[ + generic_targets.list_tier(case["arch"], case["device"], table) + ] + if got != case["expect"]: + failures.append(("tier", case, got)) + for case in vectors["overlap"]: + got = generic_targets.overlaps(case["a"], case["b"], table) + if got != case["expect"]: + failures.append(("overlap", case, got)) + for case in vectors["covers"]: + got = generic_targets.covers(case["outer"], case["inner"], table) + if got != case["expect"]: + failures.append(("covers", case, got)) + for case in vectors["compete"]: + got = generic_targets.compete(case["a"], case["b"], table) + if got != case["expect"]: + failures.append(("compete", case, got)) + assert not failures + assert all(vectors[k] for k in ("tier", "overlap", "covers", "compete")) diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_native_stub.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_native_stub.py index c064b478a36d..40b096085fec 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_native_stub.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_native_stub.py @@ -4,8 +4,9 @@ """A real compiler's verdict on the emitted C++. ``packs/Native.cpp`` is what an agent or a human fills in to make an engine serve real graphs (see RUNBOOK.md).""" -import subprocess +import json import shutil +import subprocess import tempfile from pathlib import Path @@ -78,6 +79,30 @@ def compile_env(): out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(text) + # GpuGenericTargetsTable.hpp: rendered as plugin_sdk/CMakeLists.txt does, from the + # one JSON table. + table_in = (plugin_sdk / ".." / "cmake" / "GpuGenericTargetsTable.hpp.in").resolve() + table_json = (plugin_sdk / ".." / "data" / "gpu_generic_targets.json").resolve() + if not (table_in.is_file() and table_json.is_file()): + pytest.skip(f"missing generic target table inputs {table_in} / {table_json}") + generics = json.loads(table_json.read_text())["generics"] + arrays, rows = "", "" + for generic in sorted(generics): + array = generic.upper().replace("-", "_") + "_MEMBERS" + literals = "".join(f' "{m}",\n' for m in generics[generic]) + arrays += f"inline constexpr std::string_view {array}[] = {{\n{literals}}};\n\n" + rows += f' {{"{generic}", {array}, {len(generics[generic])}}},\n' + table_text = table_in.read_text() + for key, value in ( + ("HIPDNN_GENERIC_TARGET_MEMBER_ARRAYS", arrays), + ("HIPDNN_GENERIC_TARGET_ROWS", rows), + ("HIPDNN_GENERIC_TARGET_ROW_COUNT", str(len(generics))), + ): + table_text = table_text.replace(f"@{key}@", value) + table_out = gen_dir / "hipdnn_plugin_sdk" / "GpuGenericTargetsTable.hpp" + table_out.parent.mkdir(parents=True, exist_ok=True) + table_out.write_text(table_text) + for name, root, version_vals in ( ( "hipdnn_data_sdk", diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py index a7e44c385b5d..22f8cd3f649c 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py @@ -343,13 +343,34 @@ def test_equal_tuples_overlapping_on_one_arch_collide(self, gate): assert "loader-tuple collisions" in result.stdout assert "gfx950" in result.stdout - def test_a_wildcard_arch_overlaps_a_concrete_one(self, gate): - """An absent arch list is "every device", so it meets the concrete pack on that - pack's own device.""" + def test_a_wildcard_arch_and_a_concrete_one_do_not_collide(self, gate): + """An absent arch list is the unrestricted tier; the concrete pack outranks it on + its own device, so the catalog holds one candidate per device and never one + tuple twice.""" self._two_packs(gate, "wildcard", None, ["gfx950"]) result = gate.run("w", "wildcard", profiled=False) + assert result.returncode == 0, result.stdout + result.stderr + assert "loader-tuple" not in result.stdout + + def test_a_generic_and_an_explicit_member_do_not_collide(self, gate): + self._two_packs(gate, "member", ["gfx11-generic"], ["gfx1151"]) + result = gate.run("m", "member", profiled=False) + assert result.returncode == 0, result.stdout + result.stderr + assert "loader-tuple" not in result.stdout + + def test_two_packs_naming_the_same_generic_collide(self, gate): + self._two_packs(gate, "twice", ["gfx11-generic"], ["gfx11-generic"]) + result = gate.run("t", "twice", profiled=False) assert result.returncode == 1, result.stdout assert "loader-tuple collisions" in result.stdout + assert "generic tier" in result.stdout + assert "gfx1151" in result.stdout + + def test_two_wildcard_packs_collide_at_the_unrestricted_tier(self, gate): + self._two_packs(gate, "wild2", None, None) + result = gate.run("w2", "wild2", profiled=False) + assert result.returncode == 1, result.stdout + assert "unrestricted tier" in result.stdout def test_one_and_one_point_zero_are_one_tuple_on_a_float_field(self, gate): """The catalog holds a value of the field's declared type, so 1 and 1.0 on a @@ -1206,3 +1227,54 @@ def test_unclaimed_hip_must_still_be_packed(self, tmp_path, real_archive): assert "packed dialect" in result.stdout structural = self.run(root, python_dir, tmp_path, mode="structural") assert structural.returncode == 0, structural.stdout + structural.stderr + + +@pytest.mark.needs_rocm_kpack +class TestAGenericDocumentIsReadWithItsGenericArchiveKey: + """The archive of a generic copy is keyed by the generic's name, so a member being + checked reads it under that key.""" + + @staticmethod + def _entry(arch): + ukd = _packed_ukd() + ukd["arch"] = arch + return type( + "Entry", (), {"ukd": ukd, "arch": arch, "origin_dir": None, "inline": False} + ) + + def test_a_generic_document_is_read_with_its_generic_archive_key(self, tmp_path): + python_dir = _kpack_python_dir() + kpack, compression = load_kpack(python_dir) + path = tmp_path / "kpack" / "test.kpack" + path.parent.mkdir(parents=True) + archive = kpack.PackedKernelArchive( + group_name="test", + gfx_arch_family="gfx11-generic", + gfx_arches=["gfx11-generic"], + compressor=compression.ZstdCompressor(compression_level=3), + ) + archive.add_kernel( + archive.prepare_kernel( + relative_path="v0", + gfx_arch="gfx11-generic", + hsaco_data=_PAYLOAD, + metadata={}, + ) + ) + archive.finalize_archive() + archive.write(path) + + table = gate_module.gtmod.GenericTargets.load( + gate_module.gtmod.DEFAULT_TABLE_PATH + ) + payloads = gate_module.Payloads(python_dir, table) + + generic = self._entry(["gfx11-generic"]) + generic.origin_dir = str(tmp_path) + assert payloads.read(generic, "gfx1151") == _PAYLOAD + + # The control: a document naming the member has no member-keyed archive here. + member = self._entry(["gfx1151"]) + member.origin_dir = str(tmp_path) + with pytest.raises(gate_module.GateError, match="carries no member"): + payloads.read(member, "gfx1151") diff --git a/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py b/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py index fcbb0ec1bec6..b65ebb629f2e 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py +++ b/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py @@ -5,8 +5,9 @@ 1. BINARY NESTING -- the larger set can still choose every binary the smaller one could. 2. LOADER-TUPLE UNIQUENESS -- with absent keys filled from the KMD - `default_value`, each tuple is unique per device. A duplicate rejects the - whole engine at load. + `default_value`, each tuple is unique per device and tier: two entries + collide only where both select a device at the same tier (explicit, + generic, unrestricted). A duplicate rejects the whole engine at load. 3. NO SENTINEL -- `-1` never reaches a shipped descriptor. 4. METADATA MATCHES ITS BINARY -- the matcher selects on metadata, the spec decides what was built. @@ -51,6 +52,7 @@ def _agreement_python_root() -> Path: sys.path.insert(0, str(_agreement_python_root())) from hkp_pack import agreement, descriptor_context # noqa: E402 +from hkp_pack import generic_targets as gtmod # noqa: E402 from hkp_pack.errors import HkpPackError # noqa: E402 @@ -208,6 +210,37 @@ def _specialization_twins(order: list, by_label: dict, knobs: set) -> list: return violations +_TIER_NAMES = { + gtmod.TIER_EXPLICIT: "explicit", + gtmod.TIER_GENERIC: "generic", + gtmod.TIER_UNRESTRICTED: "unrestricted", +} + + +def _tie(left: list, right: list, table) -> tuple[str, str]: + """Where two competing arch lists tie: the devices and the tier name. Two empty + lists tie everywhere, at the unrestricted tier.""" + if not left and not right: + return "every arch", _TIER_NAMES[gtmod.TIER_UNRESTRICTED] + candidates = set() + for entry in (*left, *right): + if gtmod.is_generic_shaped(entry): + if table.has(entry): + candidates.update(table.members(entry)) + else: + candidates.add(entry) + ties = {} + for device in candidates: + tier = gtmod.list_tier(left, device, table) + if tier is not None and tier == gtmod.list_tier(right, device, table): + ties[device] = tier + best = min(ties.values()) + return ( + ", ".join(sorted(d for d, t in ties.items() if t == best)), + _TIER_NAMES[best], + ) + + def effective_arch( bundles: list[descriptor_context.Bundle], requested: str | None ) -> str: @@ -232,8 +265,9 @@ class Payloads: descriptor that cannot produce them has not shown its evidence is about the artifact it ships, so that is a failure rather than an unchecked property.""" - def __init__(self, kpack_python_dir: str | None = None): + def __init__(self, kpack_python_dir: str | None = None, generic_targets=None): self._dir = kpack_python_dir + self._table = generic_targets self._archives: dict = {} self._module = None @@ -269,8 +303,13 @@ def read(self, entry: descriptor_context.Entry, arch: str) -> bytes: f"{entry.ukd.get('name')}: kernel_source.library resolves to " f"{archive_path}, which does not exist." ) + # A generic document's archive is keyed by the generic's name, not by the + # member being checked. + if self._table is None: + self._table = gtmod.GenericTargets.load(gtmod.DEFAULT_TABLE_PATH) + key = descriptor_context.archive_arch(entry.arch, arch, self._table) try: - blob = self._archive(archive_path).get_kernel(toc_key, arch) + blob = self._archive(archive_path).get_kernel(toc_key, key) except HkpPackError: raise except Exception as exc: @@ -280,7 +319,7 @@ def read(self, entry: descriptor_context.Entry, arch: str) -> bytes: if blob is None: raise GateError( f"{entry.ukd.get('name')}: {archive_path} carries no member " - f"{toc_key!r} for {arch}." + f"{toc_key!r} for {key}." ) return blob @@ -303,6 +342,7 @@ def check( mode: str, arch: str | None = None, payloads: Payloads | None = None, + generic_targets=None, ): """Run every property this mode can honestly claim, and name the rest. @@ -312,6 +352,8 @@ def check( under `--mode full`; `unverified` is a check that ran and found nothing to bind, which neither fails the gate nor joins the pass line. """ + if generic_targets is None: + generic_targets = gtmod.GenericTargets.load(gtmod.DEFAULT_TABLE_PATH) index = descriptor_context.Index(root) schemas = index.schemas() all_bundles = descriptor_context.resolve_bundles(index) @@ -353,19 +395,14 @@ def check( collisions = [] for i, (left, left_key) in enumerate(completed): for right, right_key in completed[i + 1 :]: - if left_key != right_key or not agreement.overlap(left.arch, right.arch): + if left_key != right_key or not gtmod.compete( + left.arch, right.arch, generic_targets + ): continue - # A wildcard covers whatever the other side names, so the overlap it - # reports is that side's list rather than an empty intersection. - both = set(left.arch) & set(right.arch) - either = set(left.arch) | set(right.arch) - where = ( - ", ".join(sorted(both if left.arch and right.arch else either)) - or "every arch" - ) + where, tier = _tie(left.arch, right.arch, generic_targets) collisions.append( f"{left.ukd.get('name')} and {right.ukd.get('name')} complete to one " - f"tuple on {where}" + f"tuple on {where} at the {tier} tier" ) if collisions: failures.append( @@ -478,7 +515,9 @@ def check( # producing compile's evidence says what the binary was built with. Full # mode checks it against the payload bytes; structural mode names it. if mode == "full": - records = descriptor_context.consumer_records(all_bundles, schemas, arch) + records = descriptor_context.consumer_records( + all_bundles, schemas, arch, generic_targets + ) for bundle in bundles: for entry in bundle.entries: name = entry.ukd.get("name") @@ -610,6 +649,13 @@ def main(argv=None) -> int: "bytes a packed descriptor names under --mode full. Omit it to use the " "installed one.", ) + parser.add_argument( + "--generic-targets-json", + default=str(gtmod.DEFAULT_TABLE_PATH), + help="The generic GPU target table (gpu_generic_targets.json). Tuple " + "collisions are judged per arch tier and a generic stands for its table " + "members. Defaults to the table in this checkout.", + ) args = parser.parse_args(argv) if len(args.pairs) % 2: @@ -623,8 +669,13 @@ def main(argv=None) -> int: sets, by_label, bad, skipped, knobs = {}, {}, [], [], set() unverified: list[str] = [] - payloads = Payloads(args.kpack_python_dir) if args.mode == "full" else None try: + generic_targets = gtmod.GenericTargets.load(args.generic_targets_json) + payloads = ( + Payloads(args.kpack_python_dir, generic_targets) + if args.mode == "full" + else None + ) arch = None if args.mode == "full": probe = descriptor_context.resolve_bundles( @@ -633,7 +684,7 @@ def main(argv=None) -> int: arch = effective_arch(probe, args.arch) for label, root in roots: binaries, descriptors, failures, unchecked, unbound, declared = check( - label, root, profile, args.mode, arch, payloads + label, root, profile, args.mode, arch, payloads, generic_targets ) sets[label] = binaries by_label[label] = descriptors From 69a1fa0ce57008a492d6a2f42ddc4e1e3708ec32 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 5 Oct 2026 23:01:26 +0000 Subject: [PATCH 23/52] docs(hkp): note the per-member generic copy limitation --- .../hip-kernel-provider/descriptor-packaging/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 031b170e8a59..535357a2c73b 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -18,6 +18,8 @@ empty-`arch` KDP is never emitted into a generic copy. The copy is a pure functi source tree, the generic's spelling, hipcc and the table, so the copies are byte-identical across member folders and across separate per-arch builds. +Known limitation: every installed member package carries its own copy of the generic content, so disk use scales with the number of installed members. Identical copies collapse to one catalog entry at load; copies built by different compilers differ in `provenance` and are both dropped. + Authoring rules (packer errors, stable substrings in parentheses): - A name ending `-generic` that the table does not list is an error (`generic target name From db46405a8bb0a5fbdb4171b08d161bf9d16f7005 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Tue, 6 Oct 2026 00:15:02 +0000 Subject: [PATCH 24/52] fix(hipdnn): address final review of generic target support Desk check follows the loader's same-tier rule and inheritance; a standalone UKD with an empty arch is only valid under an empty-arch KDP; the table validator, its negative fixtures and ctests are tightened; generator and packer share the generic arch rules under a parity test; tests renamed and categorised to repo conventions. --- .../hip-kernel-provider/CMakeLists.txt | 3 + .../descriptor-packaging/README.md | 12 +- .../cmake/HkpPackaging.cmake | 8 +- .../python/hkp_pack/descriptor_context.py | 2 + .../python/hkp_pack/descriptors.py | 17 +- .../python/hkp_pack/desk_check.py | 48 ++++-- .../python/hkp_pack/generic_targets.py | 6 + .../python/hkp_pack/pipeline.py | 5 +- .../fixtures/main/pointwise_add_b128.ukd.json | 1 + .../tests/test_desk_check_invariants.py | 63 ++++++- .../tests/test_hkp_arch_probe.py | 6 + .../tests/test_hkp_generic_targets.py | 4 + .../tests/test_hkp_pack_generic.py | 157 ++++++++++++++++-- .../tests/test_hkp_pack_hip.py | 18 +- .../tests/test_hkp_pack_layout.py | 26 +-- .../tests/test_hkp_pack_parallel.py | 14 +- .../tools/hkp_arch_probe.py | 12 +- .../tools/hkp_desk_check.py | 8 +- .../rfcs/0017_UniversalKernelDescriptor.md | 6 +- projects/hipdnn/plugin_sdk/CMakeLists.txt | 3 +- .../cmake/GpuGenericTargetsTable.hpp.in | 2 + .../cmake/ValidateGpuGenericTargets.cmake | 10 +- .../ingestor/KernelIngestorStateManager.hpp | 6 +- .../hipdnn/plugin_sdk/tests/CMakeLists.txt | 14 +- .../tests/data/arch_tier_vectors.json | 5 +- .../generic_targets_invalid/bad_license.json | 33 ++++ .../bad_statement.json | 33 ++++ .../generic_targets_invalid/not_json.json | 1 + .../string_schema_version.json | 33 ++++ ...ericTargets.cpp => TestGenericTargets.cpp} | 24 +-- projects/hipdnn/test_categories.yaml | 1 + .../hipdnn/tools/IngestorGenerator/README.md | 1 + .../codegen/config_loader.py | 18 +- .../tools/IngestorGenerator/codegen/models.py | 11 +- .../tests/test_config_loader.py | 135 ++++++++++++++- .../IngestorGenerator/tests/test_generator.py | 15 -- .../IngestorGenerator/tests/test_models.py | 5 + 37 files changed, 654 insertions(+), 112 deletions(-) create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_license.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_statement.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/not_json.json create mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/string_schema_version.json rename projects/hipdnn/plugin_sdk/tests/ingestor/{TestGpuGenericTargets.cpp => TestGenericTargets.cpp} (91%) diff --git a/dnn-providers/hip-kernel-provider/CMakeLists.txt b/dnn-providers/hip-kernel-provider/CMakeLists.txt index 2a2680a335da..273208b7a58c 100644 --- a/dnn-providers/hip-kernel-provider/CMakeLists.txt +++ b/dnn-providers/hip-kernel-provider/CMakeLists.txt @@ -249,6 +249,9 @@ if(NOT TARGET hipdnn_flatbuffers_sdk) find_package(hipdnn_flatbuffers_sdk CONFIG REQUIRED) endif() if(NOT TARGET hipdnn_plugin_sdk) + # A superbuild of this binary dir cached the in-tree table path; an older installed + # SDK whose Config does not set it would otherwise be answered by that stale entry. + unset(HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON CACHE) find_package(hipdnn_plugin_sdk CONFIG REQUIRED) endif() diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 535357a2c73b..8287cb267f77 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -20,6 +20,10 @@ across member folders and across separate per-arch builds. Known limitation: every installed member package carries its own copy of the generic content, so disk use scales with the number of installed members. Identical copies collapse to one catalog entry at load; copies built by different compilers differ in `provenance` and are both dropped. +Known limitation: the configure-time arch probe starts one Python interpreter per KDP of the root (about 135 ms each), so a root of hundreds of KDPs adds tens of seconds to each reconfigure. + +Known limitation: a generic copy is merged into its member folder by staging then renaming; a filesystem failure part-way through the swap (a failed `rmtree` or rename) can lose that member's content, and the build fails. + Authoring rules (packer errors, stable substrings in parentheses): - A name ending `-generic` that the table does not list is an error (`generic target name @@ -28,9 +32,11 @@ Authoring rules (packer errors, stable substrings in parentheses): share a member (`lists '`). Author two KDPs instead: an explicit override plus a generic fallback are separate packs. - Under a KDP whose list holds a generic, every UKD with its own `arch` must list every - generic of the KDP and only entries the KDP lists, and a standalone UKD must carry such an - `arch` (`must list every generic of the KDP`). An inline UKD with no `arch` inherits the - pack. + generic of the KDP and only entries the KDP lists (`must list every generic of the KDP`). + An inline UKD with no `arch` inherits the pack. +- A standalone UKD (referenced by id) with an empty or absent `arch` is unrestricted, and is + valid only under a KDP whose `arch` is empty too; under a KDP listing any arch, concrete or + generic, it is an error (`has an empty 'arch' (unrestricted) but the KDP lists`). - A UKD may not name a generic its KDP does not list, including under an empty-`arch` KDP (`ship in no shard`). - rocKE does not support generics yet (`does not support generic targets yet`). diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake index 776ab3fe9641..f9a82d295552 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake @@ -1025,8 +1025,8 @@ endfunction() # --------------------------------------------------------------------------- # _hkp_root_covers_any_arch( ) -# TRUE when at least one non-hidden *.kdp.json under would survive -# admit at least one arch in (_hkp_kdp_arch_matches: an absent or empty `arch` +# TRUE when at least one non-hidden *.kdp.json under would survive: its arch +# must admit at least one arch in (_hkp_kdp_arch_matches: an absent or empty `arch` # is a wildcard; an entry admits a target it names or a table generic containing it). # Consulted for the default root alone (hkp_add_packaging below). # @@ -1332,6 +1332,10 @@ function(hkp_add_packaging) "(gpu_generic_targets.json); an older installed hipdnn_plugin_sdk lacks it. " "Build against a hipdnn_plugin_sdk that provides the table.") endif() + # The dormancy verdicts below turn on the table's contents, so an edit to it (a + # reinstalled SDK) must re-run configure, as an edit to a KDP does. + set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY + CMAKE_CONFIGURE_DEPENDS "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}") find_package(Python3 COMPONENTS Interpreter REQUIRED) hkp_resolve_kpack(_rocm_kpack_dir "${Python3_EXECUTABLE}") diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py index c3f8911dc8cf..33c4112395aa 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py @@ -239,6 +239,8 @@ def consumer_records( header = {k: v for k, v in bundle.kdp_doc.items() if k != "kernelDescriptors"} header["arch"] = [arch] for entry in bundle.entries: + # The literal-arch test is not subsumed by `admits_target`: a generic + # copy's @arch is the generic's own spelling, which names no device. if ( entry.arch and arch not in entry.arch diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py index 7220565ba1c9..fda17ea049f5 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py @@ -645,8 +645,9 @@ def validate_generic_arch(flat): Per list: names that look generic but are not in the table are errors, and a list may not mix a generic with a member it contains or two generics sharing a member (S5). Per KDP holding a generic: a UKD with its own arch must list - every generic of the KDP and only entries the KDP lists (S6), and a - standalone UKD must carry such an arch. Per KDP of any shape: a UKD may not + every generic of the KDP and only entries the KDP lists (S6). Per KDP of any + shape: a standalone UKD with an empty `arch` (unrestricted) is valid only under + a KDP whose `arch` is empty too (S7; an inline UKD with none inherits the KDP's); a UKD may not name a generic the KDP does not list. rocKE does not support generics yet. The loader accepts the lenient forms; this is the stricter packer-side layer. """ @@ -686,6 +687,13 @@ def validate_generic_arch(flat): f"(arch '{generic}')" ) ukd_label = f"UKD '{ukd.get('id', '?')}'" + if standalone is not None and not ukd_arch and kdp_arch: + raise HkpPackError( + f"{file_name}: standalone UKD '{ukd.get('id')}' " + f"({standalone.path.name}) has an empty 'arch' (unrestricted) " + f"but the KDP lists {kdp_arch}; a standalone UKD under a KDP " + "with an arch must declare an arch of its own" + ) for generic in ukd_generics: if generic not in kdp_arch: raise HkpPackError( @@ -703,11 +711,10 @@ def validate_generic_arch(flat): continue # an inline UKD with no arch inherits the pack lists_all = all(g in ukd_arch for g in kdp_generics) only_listed = all(a in kdp_arch for a in ukd_arch) - if not ukd_arch or not lists_all or not only_listed: - declared = f"arch {ukd_arch}" if ukd_arch else "no arch" + if not lists_all or not only_listed: raise HkpPackError( f"{file_name}: KDP lists generic target(s) {kdp_generics} " - f"(arch {kdp_arch}) but {who} declares {declared}; a UKD under " + f"(arch {kdp_arch}) but {who} declares arch {ukd_arch}; a UKD under " "a generic KDP must list every generic of the KDP and only " "entries the KDP lists" ) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py index 2327b39eb5dd..a9fa7af621f7 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py @@ -9,6 +9,7 @@ from __future__ import annotations import collections +import json from pathlib import Path from . import agreement, descriptor_context @@ -46,6 +47,10 @@ # identity can say so explicitly instead of silently shortening. _ABSENT = "" +#: A device no table generic contains, standing in for "any device" when a list is +#: empty (unrestricted). +_SYNTHETIC_DEVICE = "" + def _canonical_dtype(value) -> str: """A dtype spelling reduced to the token both vocabularies mean, or the @@ -167,6 +172,13 @@ def metadata_identity_fields(kernels: list[dict]) -> tuple[str, ...]: return tuple(fields) +def load_kdp_arch(kdp_path: Path) -> list[str]: + """The arch list a `.kdp.json` declares; what a kernel with no list inherits.""" + return list( + json.loads(Path(kdp_path).read_text(encoding="utf-8")).get("arch") or [] + ) + + def load_variant_set(kdp_path: Path) -> tuple[list[dict], tuple[str, ...] | None]: """A `.kdp.json`'s kernel descriptors plus the matcher fields it declares. @@ -375,16 +387,20 @@ def metadata_spec_drift(kernels: list[dict], fields=None) -> list[tuple[str, str return bad -def _reachable_together(group: list[dict], generic_targets) -> int: - """The largest number of kernels in `group` that tie for one device. +def _reachable_together(group: list[dict], generic_targets, kdp_arch=()) -> int: + """The largest number of kernels in `group` that tie at one arch tier on one + device -- the loader's rule (`archesCompete`): any two kernels sharing a tier on + some device collide, whether or not that tier is the best one there. - Per candidate device (every explicit id and every member of a table generic in - the group's lists; one synthetic device when every list is empty) the kernels - whose arch tier equals the group's best tier on that device are counted: an - arch-less kernel and an explicit one do not collide (the explicit one wins), - two arch-less ones do, and so do two kernels naming the same generic. + A kernel's arch is its own list, else the KDP's (`kdp_arch`), as + `KernelIngestorStateManager` takes it. Candidate devices are every explicit id + and every member of a table generic in those lists, plus one synthetic device + when any list is empty (an empty list is unrestricted, so it ties there at the + unrestricted tier). A list naming only unknown generics matches nothing. """ - lists = [list(k.get("arch") or ()) for k in group] + lists = [list(k.get("arch") or kdp_arch) for k in group] + if not any(lists): + return len(group) devices: set[str] = set() for entries in lists: for entry in entries: @@ -393,23 +409,24 @@ def _reachable_together(group: list[dict], generic_targets) -> int: devices.update(generic_targets.members(entry)) else: devices.add(entry) - if not devices: - return len(group) + if not all(lists): + devices.add(_SYNTHETIC_DEVICE) best = 0 for device in devices: tiers = [gtmod.list_tier(e, device, generic_targets) for e in lists] reached = [t for t in tiers if t is not None] if reached: - best = max(best, reached.count(min(reached))) + best = max(best, max(collections.Counter(reached).values())) return best def duplicate_matcher_tuples( - kernels: list[dict], fields, generic_targets + kernels: list[dict], fields, generic_targets, kdp_arch=() ) -> dict[tuple, int]: """Invariant 2: no two kernels may share a matcher tuple on the same arch -- one is unreachable. Returns {tuple: count} for every tuple two kernels reach one device at the same arch tier with, the scope the runtime refuses in. + `kdp_arch` is the arch a kernel without its own list inherits. The compared set is the union of `fields` present in any kernel's metadata, never ``kernels[0]``'s, which would make the identity list-order dependent. A @@ -420,7 +437,9 @@ def duplicate_matcher_tuples( for kernel in kernels: key = tuple(kernel.get("metadata", {}).get(f, _ABSENT) for f in present) groups[key].append(kernel) - counts = {t: _reachable_together(g, generic_targets) for t, g in groups.items()} + counts = { + t: _reachable_together(g, generic_targets, kdp_arch) for t, g in groups.items() + } return {t: c for t, c in counts.items() if c > 1} @@ -480,6 +499,7 @@ def __init__( *, mode: str, generic_targets, + kdp_arch=(), agreement_failures=None, agreement_unclaimed=None, agreement_verified=0, @@ -513,7 +533,7 @@ def __init__( except DeskCheckNoSpecFound as exc: self.spec_drift_error = str(exc) self.duplicate_tuples = duplicate_matcher_tuples( - kernels, self.fields, generic_targets + kernels, self.fields, generic_targets, kdp_arch ) self.toc_applicable = _field_applicable(kernels, "toc_key") diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py index c349638bb7e0..2492ac7d055e 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py @@ -64,6 +64,12 @@ def load(cls, path): raise HkpPackError(f"{path}: 'generics' must be an object") generics = {} for name, members in raw.items(): + if not is_generic_shaped(name): + raise HkpPackError( + f"{path}: generics.{name}: a generic target name must end in " + f"'-generic'; a concrete processor name here would turn its own " + f"pass into a generic one" + ) if not isinstance(members, list) or not members: raise HkpPackError( f"{path}: generics.{name} must be a non-empty array of names" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py index 7a7bf5ce85d3..c16393b90253 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py @@ -1546,8 +1546,9 @@ def run_pipeline( of a pass-through kind runs no producer and is emitted as authored, so a root that holds only pass-through UKDs writes descriptors and no archive. An arch with no surviving KDP is skipped cleanly (no folder, no kpack) and - logged with 'no kernels for , skipping'; every arch skipping is a - failure, not a pack. Empty arch list installs nothing (exit 0). + logged with 'no kernels for , skipping' (a selected member whose KDPs are + all generic still gets a folder, from the generic copy); every arch skipping + is a failure, not a pack. Empty arch list installs nothing (exit 0). `generic_targets_json` names the generic target table. After the concrete passes, every table generic that some KDP lists and that contains a selected diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/fixtures/main/pointwise_add_b128.ukd.json b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/fixtures/main/pointwise_add_b128.ukd.json index 8e3b0f9bb1d1..23cefcd11121 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/fixtures/main/pointwise_add_b128.ukd.json +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/fixtures/main/pointwise_add_b128.ukd.json @@ -2,6 +2,7 @@ "version": "0.1", "id": "ukd-pointwise-add-f32-b128", "name": "PointwiseAdd f32 block128", + "arch": ["gfx942", "gfx950"], "kernel_source": { "kind": "hip", "source": "PointwiseAdd.cpp", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py index a6e2b02aaddb..0a81957e32e6 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py @@ -194,11 +194,66 @@ def test_one_tuple_on_a_shared_arch_is_still_a_duplicate(self): ("FLOAT",): 2 } - def test_an_arch_less_kernel_and_an_explicit_one_do_not_collide(self): - """On the explicit kernel's device the explicit kernel outranks the - arch-less one, and everywhere else only the arch-less one reaches: the - runtime never has two candidates at one tier, so neither is unreachable.""" + def test_an_arch_less_kernel_inherits_the_kdp_arch_and_collides_with_an_explicit_one( + self, + ): + """The loader gives a kernel with no arch of its own the KDP's list, so + under a KDP listing gfx942 the arch-less kernel and the explicit gfx942 one + tie at the same tier on gfx942.""" + kernels = self._twins(None, ["gfx942"]) + assert duplicate_matcher_tuples( + kernels, ("dtype",), GENERIC_TARGETS, ["gfx942"] + ) == {("FLOAT",): 2} + + def test_an_arch_less_kernel_under_a_disjoint_kdp_arch_does_not_collide(self): kernels = self._twins(None, ["gfx942"]) + assert ( + duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS, ["gfx950"]) + == {} + ) + + def test_a_tie_at_a_worse_tier_than_the_best_is_a_duplicate(self): + """Two arch-less kernels tie at the unrestricted tier on gfx1100 even + though a third, explicit one outranks them there; the loader's pairwise + `archesCompete` refuses them.""" + kernels = [ + {"name": n, "metadata": {"dtype": "FLOAT"}, **({"arch": a} if a else {})} + for n, a in (("a", None), ("b", None), ("c", ["gfx942"])) + ] + assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == { + ("FLOAT",): 2 + } + + def test_a_tie_beaten_on_every_device_it_reaches_is_still_a_duplicate(self): + """Both gfx12-generic kernels are outranked by an explicit one on each member + (gfx1200, gfx1201), so they never tie at the BEST tier anywhere; they still + tie at the generic tier on both, which the loader refuses.""" + kernels = [ + {"name": n, "metadata": {"dtype": "FLOAT"}, "arch": a} + for n, a in ( + ("g1", ["gfx12-generic"]), + ("g2", ["gfx12-generic"]), + ("e0", ["gfx1200"]), + ("e1", ["gfx1201"]), + ) + ] + assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == { + ("FLOAT",): 2 + } + + def test_a_generic_and_an_arch_less_kernel_do_not_collide(self): + """The generic outranks the unrestricted kernel wherever both reach, and the + arch-less one is alone elsewhere: no device sees two at one tier.""" + kernels = self._twins(["gfx11-generic"], None) + assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == {} + + def test_a_generic_and_one_of_its_members_do_not_collide(self): + """The member is EXPLICIT on its own device and outranks the generic.""" + kernels = self._twins(["gfx11-generic"], ["gfx1100"]) + assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == {} + + def test_lists_naming_only_unknown_generics_match_nothing(self): + kernels = self._twins(["gfx99-generic"], ["gfx99-generic"]) assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == {} def test_two_arch_less_kernels_collide(self): diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py index 3a4fceaed3ab..1163a9c85337 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py @@ -44,6 +44,12 @@ def test_probe_false_when_no_entry_reaches_any_selected_target(tmp_path): assert _probe(tmp_path, ["gfx12-generic"], "gfx1100") == "FALSE" +def test_probe_true_for_a_generic_absent_from_the_table(tmp_path): + """The packer reports it as an error; a FALSE would hide it behind a dormant root.""" + assert _probe(tmp_path, ["gfx99-generic"], "gfx942") == "TRUE" + assert _probe(tmp_path, ["gfx99-generic", "gfx950"], "gfx942") == "TRUE" + + def test_probe_true_for_empty_arch(tmp_path): assert _probe(tmp_path, [], "gfx942") == "TRUE" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py index 3e507b112160..7b9fe92d7744 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py @@ -76,6 +76,10 @@ def test_loads_the_table_members_in_document_order(table): {"schemaVersion": 1, "generics": {"gfx11-generic": ["gfx1100", "gfx1100"]}}, "more than once", ), + ( + {"schemaVersion": 1, "generics": {"gfx1100": ["gfx1100"]}}, + "must end in '-generic'", + ), ([], "must be a JSON object"), ], ) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py index fb1dd25e8bd4..0520e6723b5c 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py @@ -389,6 +389,40 @@ def failing(flat, inter, out_dir, *args, **kwargs): assert not (tmp_path / "inter" / ".generic-out" / GENERIC).exists() +@pytest.mark.quick +def test_generic_copies_of_every_producer_are_equal_across_separate_builds( + tmp_path, empty_arch_fixture, hsaco_fixture_dir, rocm_kpack_dir +): + """An embedded and an hsaco generic entry, packed once for gfx1100 and once for + gfx1151 from separate roots: the member folders hold identical files, so the + copies per-arch shard builds produce collapse to one. Nothing of the build's + target list may leak into the generic copy.""" + hsaco = _embedded("ukd-hsaco", [GENERIC]) + hsaco["kernel_source"] = { + "kind": "hsaco", + "file": CO_NAME, + "symbol": "HsacoFixtureAdd", + } + hsaco["provenance"] = copy.deepcopy(_CONTRACT) + folders = {} + for member in (MEMBER_A, MEMBER_B): + base = tmp_path / member + base.mkdir() + root = _root( + base, + empty_arch_fixture, + { + "e": _kdp("kdp-e", [GENERIC], [_embedded("ukd-e")]), + "h": _kdp("kdp-h", [GENERIC], [copy.deepcopy(hsaco)]), + }, + ) + shutil.copyfile(hsaco_fixture_dir / "gfx942" / CO_NAME, root / CO_NAME) + _pack(root, base, [member], rocm_kpack_dir) + folders[member] = _files(base / "out" / member) + assert any(name.endswith(".kpack") for name in folders[MEMBER_A]) + assert folders[MEMBER_A] == folders[MEMBER_B] + + # --- authored hsaco under a generic ------------------------------------------ @@ -464,6 +498,7 @@ def test_s5_two_generics_sharing_a_member_is_an_error(tmp_path, empty_arch_fixtu with pytest.raises(HkpPackError) as err: _load(root, GenericTargets.load(table_path)) message = str(err.value) + assert message.startswith("g.kdp.json:") assert "lists 'gfx11-generic' and 'gfx11-b-generic'" in message assert "share member gfx1101" in message @@ -481,8 +516,8 @@ def _standalone(uid, arch=None): @pytest.mark.quick @pytest.mark.parametrize( "ukd_arch, accepted", - [([GENERIC], True), ([MEMBER_B], False), (None, False)], - ids=["exact_generic", "narrower", "unrestricted"], + [([GENERIC], True), ([MEMBER_B], False)], + ids=["exact_generic", "narrower"], ) def test_s6_standalone_ukd_under_a_generic_kdp( tmp_path, empty_arch_fixture, ukd_arch, accepted @@ -494,7 +529,7 @@ def test_s6_standalone_ukd_under_a_generic_kdp( {"s": _standalone("ukd-s", ukd_arch)}, ) if accepted: - assert _load(root).generic_targets is GENERIC_TARGETS + _load(root) return with pytest.raises(HkpPackError) as err: _load(root) @@ -503,6 +538,55 @@ def test_s6_standalone_ukd_under_a_generic_kdp( assert "ukd-s" in message and "s.ukd.json" in message and "g.kdp.json" in message +_S7_RULE = "has an empty 'arch' (unrestricted) but the KDP lists" + + +@pytest.mark.quick +@pytest.mark.parametrize( + "kdp_arch", [["gfx942"], [GENERIC]], ids=["concrete", "generic"] +) +def test_s7_standalone_ukd_without_arch_under_a_kdp_with_an_arch_is_an_error( + tmp_path, empty_arch_fixture, kdp_arch +): + root = _root( + tmp_path, + empty_arch_fixture, + {"g": _kdp("kdp-g", kdp_arch, ["ukd-s"])}, + {"s": _standalone("ukd-s")}, + ) + with pytest.raises(HkpPackError) as err: + _load(root) + message = str(err.value) + assert message.startswith("g.kdp.json:") + assert _S7_RULE in message + assert "'ukd-s'" in message and "s.ukd.json" in message + + +@pytest.mark.quick +def test_s7_standalone_ukd_without_arch_under_an_arch_less_kdp_is_accepted( + tmp_path, empty_arch_fixture +): + root = _root( + tmp_path, + empty_arch_fixture, + {"g": _kdp("kdp-g", [], ["ukd-s"])}, + {"s": _standalone("ukd-s")}, + ) + _load(root) + + +@pytest.mark.quick +def test_s7_inline_ukd_without_arch_under_a_concrete_kdp_inherits_and_is_accepted( + tmp_path, empty_arch_fixture +): + root = _root( + tmp_path, + empty_arch_fixture, + {"g": _kdp("kdp-g", ["gfx942"], [_embedded("ukd-i")])}, + ) + _load(root) + + @pytest.mark.quick def test_s6_inline_ukd_with_a_narrower_arch_under_a_generic_kdp_is_an_error( tmp_path, empty_arch_fixture @@ -512,7 +596,32 @@ def test_s6_inline_ukd_with_a_narrower_arch_under_a_generic_kdp_is_an_error( empty_arch_fixture, {"g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-i", [MEMBER_B])])}, ) - with pytest.raises(HkpPackError, match=_S6_RULE): + with pytest.raises(HkpPackError, match=_S6_RULE) as err: + _load(root) + assert str(err.value).startswith("g.kdp.json:") + assert "inline UKD 'ukd-i'" in str(err.value) + + +@pytest.mark.quick +@pytest.mark.parametrize( + "ukd_arch, rule", + [ + (["gfx99-generic"], "generic target name absent"), + ([GENERIC, MEMBER_B], f"{GENERIC} contains {MEMBER_B}"), + ], + ids=["unknown_generic", "generic_beside_member"], +) +def test_per_list_rules_apply_to_an_inline_ukd_arch( + tmp_path, empty_arch_fixture, ukd_arch, rule +): + """An inline UKD's own list is held to the per-list rules, which name the actual + defect rather than a later KDP-level rule.""" + root = _root( + tmp_path, + empty_arch_fixture, + {"g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-i", ukd_arch)])}, + ) + with pytest.raises(HkpPackError, match=rule): _load(root) @@ -545,8 +654,10 @@ def test_s6_mixed_list_ukd_missing_the_generic_is_an_error( empty_arch_fixture, {"m": _kdp("kdp-m", ["gfx942", GENERIC], [_embedded("ukd-m", ["gfx942"])])}, ) - with pytest.raises(HkpPackError, match=_S6_RULE): + with pytest.raises(HkpPackError, match=_S6_RULE) as err: _load(root) + assert str(err.value).startswith("m.kdp.json:") + assert "inline UKD 'ukd-m'" in str(err.value) # --- reverse direction, unknown names, rocKE --------------------------------- @@ -567,7 +678,8 @@ def test_a_generic_ukd_under_a_kdp_that_does_not_list_it_is_an_error( message = str(err.value) assert f"declares generic target '{GENERIC}'" in message assert "ship in no shard" in message - assert "k.kdp.json" in message + assert message.startswith("k.kdp.json:") + assert "UKD 'ukd-k'" in message @pytest.mark.quick @@ -586,8 +698,13 @@ def test_unknown_generic_name_is_an_error_naming_the_file(tmp_path, empty_arch_f @pytest.mark.quick -def test_rocke_with_a_generic_arch_is_an_error(tmp_path, empty_arch_fixture): - ukd = _embedded("ukd-r", [GENERIC]) +@pytest.mark.parametrize( + "ukd_arch", [None, [GENERIC]], ids=["no_own_arch", "own_generic"] +) +def test_rocke_under_a_generic_kdp_is_an_error(tmp_path, empty_arch_fixture, ukd_arch): + """A rocKE UKD is refused whether it carries the generic itself or merely + inherits it from the KDP.""" + ukd = _embedded("ukd-r", ukd_arch) ukd["kernel_source"] = { "kind": "rocke", "source": "pkg/kernels/attention.py", @@ -597,8 +714,10 @@ def test_rocke_with_a_generic_arch_is_an_error(tmp_path, empty_arch_fixture): root = _root(tmp_path, empty_arch_fixture, {"r": _kdp("kdp-r", [GENERIC], [ukd])}) with pytest.raises(HkpPackError) as err: _load(root) - assert "does not support generic targets yet" in str(err.value) - assert GENERIC in str(err.value) + message = str(err.value) + assert message.startswith("r.kdp.json:") + assert "does not support generic targets yet" in message + assert f"'{GENERIC}'" in message # --- hipcc tier (not quick) --------------------------------------------------- @@ -647,6 +766,24 @@ def test_generic_hip_archive_is_keyed_by_the_generic_spelling( assert results[MEMBER_A].kpack_path is None +def test_compiled_agreement_holds_on_a_generic_copy( + tmp_path, empty_arch_fixture, hipcc, rocm_kpack_dir +): + """A shipped generic copy carries the generic as its literal arch, which names no + device: `consumer_records` must still count its own entry as a consumer, or the + record lookup for the copy's kernel raises.""" + from hkp_pack.desk_check import compiled_agreement + + root = _hip_generic_root(tmp_path / "src", empty_arch_fixture) + _hip_pack( + root, tmp_path / "out", tmp_path / "inter", [MEMBER_A], hipcc, rocm_kpack_dir + ) + failures, _unclaimed, _verified = compiled_agreement( + tmp_path / "out" / MEMBER_A / "solo.kdp.json", GENERIC_TARGETS, rocm_kpack_dir + ) + assert failures == [] + + def test_generic_pack_is_byte_identical_across_separate_per_arch_builds( tmp_path, empty_arch_fixture, hipcc, rocm_kpack_dir, monkeypatch ): diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py index c99a75d8c70e..ef40d8482c88 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py @@ -772,9 +772,11 @@ def test_standalone_ukd_shared_by_two_kdps_stored_once( # is stored once (deduped by variant_key), not once per referencing KDP. src = _copy_fixture(tmp_path, main_fixture) # copy.kdp.json is gfx942-only; add the standalone ref to it too, so both it - # and pointwise.kdp.json reference the same standalone id on gfx942. + # and pointwise.kdp.json reference the same standalone id on gfx942. The + # standalone declares [gfx942, gfx950], so copy.kdp.json lists both. p = src / "copy.kdp.json" doc = _read(p) + doc["arch"] = ["gfx942", "gfx950"] doc["kernelDescriptors"].append(_STANDALONE_UKD_ID) p.write_text(json.dumps(doc), encoding="utf-8") # A second referencing KDP is a second CONSUMER: it resolves to its own engine @@ -816,6 +818,7 @@ def test_standalone_ukd_referenced_by_a_second_engine_without_declaring_it_fails src = _copy_fixture(tmp_path, main_fixture) p = src / "copy.kdp.json" doc = _read(p) + doc["arch"] = ["gfx942", "gfx950"] doc["kernelDescriptors"].append(_STANDALONE_UKD_ID) p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="expected exactly one"): @@ -825,15 +828,22 @@ def test_standalone_ukd_referenced_by_a_second_engine_without_declaring_it_fails def test_standalone_ukd_referenced_by_wildcard_kdp( tmp_path, main_fixture, hipcc, rocm_kpack_dir ): - # A standalone UKD referenced by a wildcard KDP (arch []) ships in every shard. + # An arch-less standalone UKD (unrestricted) referenced by a wildcard KDP + # (arch []) ships in every shard. The fixture's own standalone is authored for + # gfx942, so this one is a sibling without an arch. src = _copy_fixture(tmp_path, main_fixture) + wild_doc = _read(src / _STANDALONE_UKD_FILE) + wild_doc["id"] = "ukd-pointwise-add-f32-b128-wild" + del wild_doc["arch"] + wild_file = "pointwise_add_b128_wild.ukd.json" + (src / wild_file).write_text(json.dumps(wild_doc), encoding="utf-8") p = src / "pointwise_wild.kdp.json" doc = _read(p) - doc["kernelDescriptors"].append(_STANDALONE_UKD_ID) + doc["kernelDescriptors"].append(wild_doc["id"]) p.write_text(json.dumps(doc), encoding="utf-8") _run(src, tmp_path, hipcc, rocm_kpack_dir, arches=["gfx942", "gfx90a"]) for arch in ("gfx942", "gfx90a"): - assert (tmp_path / "out" / arch / _STANDALONE_UKD_FILE).exists(), arch + assert (tmp_path / "out" / arch / wild_file).exists(), arch @pytest.mark.quick diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py index 22e61ad2e083..9283ddb31161 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py @@ -585,7 +585,6 @@ def always_fail(flat, inter, out_arch_dir, *a, **kw): message = str(exc.value) assert "gfx942" in message and "gfx950" in message - assert "2 failure(s) across 2" in message # --- E. The shipped example tree ------------------------------------------- @@ -1515,19 +1514,20 @@ def _make_embedded(folder, arch=None): kdp["kernelDescriptors"] = [inline, _STANDALONE_ID] kdp_path.write_text(json.dumps(kdp, indent=2) + "\n", encoding="utf-8") + standalone = { + "version": "0.1", + "id": _STANDALONE_ID, + "name": "PointwiseMul f32 block64 (solo)", + "kernel_source": dict(_STANDALONE_SOURCE), + "metadata": {"dtype": "FLOAT", "block_size": 64}, + "priority": 0, + } + if arch: + # A standalone UKD without an arch is unrestricted, valid only under a KDP + # with none; under an arch'd KDP it declares that arch itself. + standalone["arch"] = list(arch) (folder / _STANDALONE_FILE).write_text( - json.dumps( - { - "version": "0.1", - "id": _STANDALONE_ID, - "name": "PointwiseMul f32 block64 (solo)", - "kernel_source": dict(_STANDALONE_SOURCE), - "metadata": {"dtype": "FLOAT", "block_size": 64}, - "priority": 0, - }, - indent=2, - ) - + "\n", + json.dumps(standalone, indent=2) + "\n", encoding="utf-8", ) return folder diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py index e039bf9b5f67..a514d142b813 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py @@ -222,7 +222,7 @@ def _write_corpus(dest, *, hip_only=False, with_embedded=False): _write_json( dest, "u_standalone_wild.ukd.json", - _ukd("ukd-standalone-wild", _hip_ks(_K1_SOURCE, "K1", 1024)), + _ukd("ukd-standalone-wild", _hip_ks(_K1_SOURCE, "K1", 1024), [OTHER_ARCH]), ) # Case 2 -- inline hip. @@ -271,7 +271,7 @@ def _write_corpus(dest, *, hip_only=False, with_embedded=False): _write_json( dest, "u_standalone_hip.ukd.json", - _ukd("ukd-standalone-hip", _hip_ks(_K1_SOURCE, "K1", 256)), + _ukd("ukd-standalone-hip", _hip_ks(_K1_SOURCE, "K1", 256), [TARGET_ARCH]), ) # Case 6 -- a standalone rocke UKD referenced by id. @@ -284,7 +284,7 @@ def _write_corpus(dest, *, hip_only=False, with_embedded=False): _write_json( dest, "u_standalone_rocke.ukd.json", - _ukd("ukd-standalone-rocke", _rocke_ks()), + _ukd("ukd-standalone-rocke", _rocke_ks(), [TARGET_ARCH]), ) # Case 7 -- a standalone UKD whose own arch excludes the target, listed @@ -339,7 +339,7 @@ def _write_corpus(dest, *, hip_only=False, with_embedded=False): _write_json( dest, "u_standalone_shared.ukd.json", - _ukd("ukd-standalone-shared", _hip_ks(_K2_SOURCE, "K2", 512)), + _ukd("ukd-standalone-shared", _hip_ks(_K2_SOURCE, "K2", 512), [TARGET_ARCH]), ) # Case 10 -- an orphan standalone UKD. @@ -387,7 +387,7 @@ def _write_corpus(dest, *, hip_only=False, with_embedded=False): _write_json( dest, "u_standalone_embedded.ukd.json", - _ukd("ukd-standalone-embedded", _embedded_ks("E1B")), + _ukd("ukd-standalone-embedded", _embedded_ks("E1B"), [TARGET_ARCH]), ) return dest @@ -550,8 +550,8 @@ def _call_sites(callee): def test_arch_matches_call_sites_are_pinned(): """The two UKD-level selection filters live in the generator and nowhere else. - `arch_matches` is the empty-inherits filter on a UKD. `compile_intermediate` - no longer calls it: its KDP disposition call is `kdp_arch_matches`. + `arch_matches` is the empty-inherits filter on a UKD. `compile_intermediate`'s + KDP disposition call is `kdp_arch_matches`. """ assert _call_sites("arch_matches") == {"_selected_entries": 2} diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py index e49d7216610e..94293da82525 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py @@ -9,7 +9,7 @@ generic containing it) to each selected arch. Only FALSE is authoritative. A KDP that does not parse, or whose `arch` is not -an array of strings, prints TRUE so the packer reports what is wrong with it. +an array of strings, or that names a generic absent from the table, prints TRUE so the packer reports what is wrong with it. Exits 0 whenever it answered; non-zero only when the table cannot be read. """ @@ -25,7 +25,11 @@ sys.path.insert(0, _PKG_ROOT) from hkp_pack.errors import HkpPackError # noqa: E402 -from hkp_pack.generic_targets import GenericTargets, admits_target # noqa: E402 +from hkp_pack.generic_targets import ( # noqa: E402 + GenericTargets, + admits_target, + is_generic_shaped, +) def _covers_any(kdp_path, arches, table): @@ -42,6 +46,10 @@ def _covers_any(kdp_path, arches, table): return True if not entries: return True + # An unknown generic admits nothing, which would read as a clean absence; the packer + # names it as an error, so it must be reached. + if any(is_generic_shaped(e) and not table.has(e) for e in entries): + return True return any(admits_target(entries, arch, table) for arch in arches) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_desk_check.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_desk_check.py index 84d553df4634..2650bc5e52c3 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_desk_check.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_desk_check.py @@ -1,7 +1,9 @@ """The desk-check invariants of RUNBOOK §4's host boundary, as a real, runnable CLI. - python3 tools/hkp_desk_check.py --mode structural - python3 tools/hkp_desk_check.py --mode full + python3 tools/hkp_desk_check.py --mode structural \\ + --generic-targets-json + python3 tools/hkp_desk_check.py --mode full \\ + --generic-targets-json `--mode structural` reads the descriptors against themselves and each other: drift between metadata and the authored spec, duplicate matcher tuples, toc_key @@ -49,6 +51,7 @@ MODES, DeskCheckReport, compiled_agreement, + load_kdp_arch, load_variant_set, metadata_identity_fields, ) @@ -164,6 +167,7 @@ def main(argv=None): drift_fields, mode=args.mode, generic_targets=generic_targets, + kdp_arch=load_kdp_arch(kdp), agreement_failures=failures, agreement_unclaimed=unclaimed, agreement_verified=verified, diff --git a/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md b/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md index 96525e9d4f74..941ddd2e0204 100644 --- a/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md +++ b/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md @@ -1427,7 +1427,11 @@ list ranks per device in tiers: an explicit entry (the device's own base id) bea target containing it (e.g. `gfx11-generic`, whose members come from the shared generic target table), which beats an empty list. Two kernels of one engine collide on a tuple only where they select a device at the same tier, so a generic fallback and an explicit override of one tuple -coexist. The dispatch descriptor is named but **not** loaded; nothing dispatches yet. +coexist. The tiering and the shadowing of a less specific kernel by a more specific one run after +the matcher set admits the pack, not at the arch gate, so a generic pack is dropped here only when +no tier of its list reaches the device; shadowing costs one extra pass over the pack's kernels when +a device's catalog is first built. The dispatch descriptor is named but **not** loaded; nothing +dispatches yet. *Stored:* the parsed packs, matchers, and kernel metadata, in the same descriptor cache. **5. Run the engine's `graph_match`, lazily, on the first pack that cleared the gate.** It either diff --git a/projects/hipdnn/plugin_sdk/CMakeLists.txt b/projects/hipdnn/plugin_sdk/CMakeLists.txt index 28c4e251de15..1a0d27a7ce4a 100644 --- a/projects/hipdnn/plugin_sdk/CMakeLists.txt +++ b/projects/hipdnn/plugin_sdk/CMakeLists.txt @@ -1,6 +1,5 @@ # Copyright © Advanced Micro Devices, Inc., or its affiliates. # SPDX-License-Identifier: MIT -# cmake-lint: disable=E1120 include(CMakePackageConfigHelpers) @@ -38,6 +37,8 @@ set(HIPDNN_GENERIC_TARGET_MEMBER_ARRAYS "") set(HIPDNN_GENERIC_TARGET_ROWS "") set(HIPDNN_GENERIC_TARGET_ROW_COUNT ${_generic_count}) math(EXPR _last_generic "${_generic_count} - 1") +# cmake-lint reports E1120 on foreach(... RANGE ...) arguments, which are valid CMake. +# cmake-lint: disable=E1120 foreach(_g RANGE 0 ${_last_generic}) string(JSON _generic MEMBER "${_generic_targets_json}" generics ${_g}) string(TOUPPER "${_generic}" _array_name) diff --git a/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in b/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in index 5f0b8f533465..cb94152acc16 100644 --- a/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in +++ b/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in @@ -19,12 +19,14 @@ struct GenericTargetRow std::size_t memberCount; }; +// clang-format off // Constexpr tables of string_view; arrays keep the data in static storage with no allocation. // NOLINTBEGIN(modernize-avoid-c-arrays) @HIPDNN_GENERIC_TARGET_MEMBER_ARRAYS@ inline constexpr GenericTargetRow GENERIC_TARGET_ROWS[] = { @HIPDNN_GENERIC_TARGET_ROWS@}; // NOLINTEND(modernize-avoid-c-arrays) +// clang-format on inline constexpr std::size_t GENERIC_TARGET_ROW_COUNT = @HIPDNN_GENERIC_TARGET_ROW_COUNT@; diff --git a/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake b/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake index 3360c976f99f..c76a30f3cc87 100644 --- a/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake +++ b/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake @@ -8,7 +8,7 @@ # This is the only implementation of the provenance rules; the Python readers validate # shape only. # -# cmake-lint does not model string(JSON ...) and misreads its index arguments. +# cmake-lint does not model foreach(... RANGE ...) and reports E1120 on its arguments. # cmake-lint: disable=E1120 # Reads the string at of into ; FATAL_ERROR if it is absent, @@ -128,13 +128,19 @@ function(hipdnn_validate_gpu_generic_targets json_path) endif() file(READ "${json_path}" json) + string(JSON _root_type ERROR_VARIABLE parse_error TYPE "${json}") + if(parse_error) + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: not valid JSON: ${parse_error}") + endif() + _hipdnn_gpu_generic_targets_get_string(_ "${json}" "${json_path}" "\$comment" "\$comment") string(JSON version ERROR_VARIABLE err GET "${json}" schemaVersion) if(err) message(FATAL_ERROR "gpu_generic_targets: ${json_path}: missing required key 'schemaVersion'") endif() - if(NOT version STREQUAL "1") + string(JSON version_type TYPE "${json}" schemaVersion) + if(NOT version_type STREQUAL "NUMBER" OR NOT version STREQUAL "1") message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'schemaVersion' must be 1, got '${version}'") endif() diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp index af4f10d9034a..911123973b72 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp @@ -390,9 +390,9 @@ class KernelIngestorStateManager // Two kernels may share a tuple when no single device can see both at the same arch // tier -- the per-arch shard layout, and an explicit or generic kernel beside a // less specific one, which buildCatalog shadows per device. Uniqueness is therefore - // per competing-arch group, not per engine: the tuple is the catalog key, and a catalog is built for - // one device. Keyed by the tuple (an ordered map, so it already orders) rather - // than scanned, which would be quadratic. + // per competing-arch group, not per engine: the tuple is the catalog key, and a + // catalog is built for one device. Keyed by the tuple (an ordered map, so it already + // orders) rather than scanned, which would be quadratic. std::map>> archesClaimingTuple; _definitions.reserve(_packs.size()); diff --git a/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt b/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt index d076769b64b3..76fa1f0b3679 100644 --- a/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt +++ b/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt @@ -38,7 +38,7 @@ add_executable(hipdnn_plugin_sdk_tests $<$:ingestor/TestMatchContext.cpp> $<$:ingestor/TestDescriptors.cpp> $<$:ingestor/TestArchGate.cpp> - $<$:ingestor/TestGpuGenericTargets.cpp> + $<$:ingestor/TestGenericTargets.cpp> $<$:ingestor/TestKernelDefinition.cpp> $<$:ingestor/TestKernelIngestorStateManager.cpp> $<$:ingestor/TestGenericPlan.cpp> @@ -119,7 +119,13 @@ function(_hipdnn_add_generic_targets_rejection_test case expected_text) -DGENERIC_TARGETS_JSON=${CMAKE_CURRENT_SOURCE_DIR}/data/generic_targets_invalid/${case}.json -P ${_generic_targets_check} ) - set_tests_properties(${test_name} PROPERTIES PASS_REGULAR_EXPRESSION "${expected_regex}") + # A validator that downgrades the FATAL_ERROR to a warning still reaches the + # success line; FAIL_REGULAR_EXPRESSION turns that into a failure. + set_tests_properties( + ${test_name} + PROPERTIES PASS_REGULAR_EXPRESSION "${expected_regex}" FAIL_REGULAR_EXPRESSION + "gpu_generic_targets: valid" + ) _apply_hipdnn_test_category_labels(${test_name}) endfunction() @@ -132,6 +138,10 @@ _hipdnn_add_generic_targets_rejection_test( ) _hipdnn_add_generic_targets_rejection_test(bad_retrieved_on "'source.retrievedOn' must be YYYY-MM-DD") _hipdnn_add_generic_targets_rejection_test(missing_license "missing required key 'source.license'") +_hipdnn_add_generic_targets_rejection_test(not_json "not valid JSON") +_hipdnn_add_generic_targets_rejection_test(string_schema_version "'schemaVersion' must be 1") +_hipdnn_add_generic_targets_rejection_test(bad_license "'source.license' must be") +_hipdnn_add_generic_targets_rejection_test(bad_statement "'source.statement' must state") _hipdnn_add_generic_targets_rejection_test(missing_statement "missing required key 'source.statement'") _hipdnn_add_generic_targets_rejection_test(empty_generics "'generics' must be non-empty") _hipdnn_add_generic_targets_rejection_test( diff --git a/projects/hipdnn/plugin_sdk/tests/data/arch_tier_vectors.json b/projects/hipdnn/plugin_sdk/tests/data/arch_tier_vectors.json index 4a1ba2322ce0..dc6d6bc8f8e1 100644 --- a/projects/hipdnn/plugin_sdk/tests/data/arch_tier_vectors.json +++ b/projects/hipdnn/plugin_sdk/tests/data/arch_tier_vectors.json @@ -14,6 +14,7 @@ {"arch": ["gfx12-generic"], "device": "gfx1250:sramecc+", "expect": "none"}, {"arch": ["gfx12-generic"], "device": "gfx1250-strict", "expect": "none"}, {"arch": ["gfx1250"], "device": "gfx1250-strict", "expect": "none"}, + {"arch": ["gfx1250-strict"], "device": "gfx1250-strict", "expect": "explicit"}, {"arch": ["gfx1250"], "device": "gfx1250", "expect": "explicit"}, {"arch": ["gfx9-4-generic"], "device": "gfx942", "expect": "none"}, {"arch": ["gfx9-4-generic"], "device": "gfx9-4-generic", "expect": "none"}, @@ -70,6 +71,8 @@ {"a": ["gfx9-4-generic"], "b": ["gfx9-4-generic"], "expect": false}, {"a": ["gfx9-4-generic"], "b": [], "expect": false}, {"a": ["gfx1151"], "b": ["gfx11-generic", "gfx1151"], "expect": true}, - {"a": ["gfx11-generic"], "b": ["gfx1151", "gfx1100"], "expect": false} + {"a": ["gfx11-generic"], "b": ["gfx1151", "gfx1100"], "expect": false}, + {"a": ["gfx1250"], "b": ["gfx1250-strict"], "expect": false}, + {"a": ["gfx1250-strict"], "b": ["gfx1250-strict"], "expect": true} ] } diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_license.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_license.json new file mode 100644 index 000000000000..b98092a7f52a --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_license.json @@ -0,0 +1,33 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "2026-10-05", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)", + "license": "MIT" + }, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12-generic": [ + "gfx1200", + "gfx1201" + ] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_statement.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_statement.json new file mode 100644 index 000000000000..b19573342155 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_statement.json @@ -0,0 +1,33 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": 1, + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "2026-10-05", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "copied", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)", + "license": "Apache-2.0 WITH LLVM-exception" + }, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12-generic": [ + "gfx1200", + "gfx1201" + ] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/not_json.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/not_json.json new file mode 100644 index 000000000000..928e8a7bf557 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/not_json.json @@ -0,0 +1 @@ +{ "$comment": "truncated", "schemaVersion": 1, "source": diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/string_schema_version.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/string_schema_version.json new file mode 100644 index 000000000000..ba464faf813b --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/string_schema_version.json @@ -0,0 +1,33 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "schemaVersion": "1", + "source": { + "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", + "document": "llvm/docs/AMDGPUUsage.rst", + "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", + "repository": "https://github.com/ROCm/llvm-project", + "revision": "998a696feca902c689c53383e48d29494f9edba7", + "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "retrievedOn": "2026-10-05", + "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", + "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)", + "license": "Apache-2.0 WITH LLVM-exception" + }, + "generics": { + "gfx11-generic": [ + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + "gfx1150", + "gfx1151", + "gfx1152", + "gfx1153" + ], + "gfx12-generic": [ + "gfx1200", + "gfx1201" + ] + } +} diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestGpuGenericTargets.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp similarity index 91% rename from projects/hipdnn/plugin_sdk/tests/ingestor/TestGpuGenericTargets.cpp rename to projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp index a6859104eb4d..e996b6e4a58c 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestGpuGenericTargets.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp @@ -16,7 +16,7 @@ #include /** - * @file TestGpuGenericTargets.cpp + * @file TestGenericTargets.cpp * @brief The generic GPU target table and the tier algebra over it, as C++ evaluates * them. The table is the generated constexpr form of data/gpu_generic_targets.json * and the golden vectors are shared with the Python implementation. @@ -58,7 +58,7 @@ const char* tierName(const std::optional& tier) return "unrestricted"; } -TEST(TestGpuGenericTargets, GeneratedTableMatchesTheJsonMemberForMember) +TEST(TestGenericTargets, GeneratedTableMatchesTheJsonMemberForMember) { const auto json = readJson(HIPDNN_GPU_GENERIC_TARGETS_JSON_PATH); const auto& generics = json.at("generics"); @@ -78,7 +78,7 @@ TEST(TestGpuGenericTargets, GeneratedTableMatchesTheJsonMemberForMember) } } -TEST(TestGpuGenericTargets, FindsAGenericOnlyByItsExactName) +TEST(TestGenericTargets, FindsAGenericOnlyByItsExactName) { const auto* row = findGenericTarget("gfx11-generic"); ASSERT_NE(row, nullptr); @@ -95,7 +95,7 @@ TEST(TestGpuGenericTargets, FindsAGenericOnlyByItsExactName) EXPECT_FALSE(isGenericShapedArchName("generic")); } -TEST(TestGpuGenericTargets, ContainsReportsMembership) +TEST(TestGenericTargets, ContainsReportsMembership) { EXPECT_TRUE(genericTargetContains("gfx11-generic", "gfx1100")); EXPECT_TRUE(genericTargetContains("gfx11-generic", "gfx1153")); @@ -106,7 +106,7 @@ TEST(TestGpuGenericTargets, ContainsReportsMembership) EXPECT_FALSE(genericTargetContains("gfx9-4-generic", "gfx942")); } -TEST(TestGpuGenericTargets, EntryTierRanksExplicitAboveGeneric) +TEST(TestGenericTargets, EntryTierRanksExplicitAboveGeneric) { EXPECT_EQ(archEntryTier("gfx1151", "gfx1151"), ArchTier::EXPLICIT); EXPECT_EQ(archEntryTier("gfx11-generic", "gfx1151"), ArchTier::GENERIC); @@ -116,7 +116,7 @@ TEST(TestGpuGenericTargets, EntryTierRanksExplicitAboveGeneric) EXPECT_LT(static_cast(ArchTier::GENERIC), static_cast(ArchTier::UNRESTRICTED)); } -TEST(TestGpuGenericTargets, ListTierIsTheBestOfItsEntries) +TEST(TestGenericTargets, ListTierIsTheBestOfItsEntries) { const Arch both{"gfx11-generic", "gfx1151"}; EXPECT_EQ(archTier(both, "gfx1151"), ArchTier::EXPLICIT); @@ -126,14 +126,14 @@ TEST(TestGpuGenericTargets, ListTierIsTheBestOfItsEntries) EXPECT_EQ(archTier({"gfx11-generic"}, "gfx1151:sramecc+"), ArchTier::GENERIC); } -TEST(TestGpuGenericTargets, EmptyListIsUnrestricted) +TEST(TestGenericTargets, EmptyListIsUnrestricted) { EXPECT_EQ(archTier({}, "gfx1151"), ArchTier::UNRESTRICTED); EXPECT_EQ(archTier({}, ""), ArchTier::UNRESTRICTED); EXPECT_TRUE(archSupports({}, "gfx1151")); } -TEST(TestGpuGenericTargets, OverlapIsOverExpandedMemberSets) +TEST(TestGenericTargets, OverlapIsOverExpandedMemberSets) { EXPECT_TRUE(archOverlaps({"gfx11-generic"}, {"gfx1151"})); EXPECT_TRUE(archOverlaps({"gfx1151"}, {"gfx11-generic"})); @@ -144,7 +144,7 @@ TEST(TestGpuGenericTargets, OverlapIsOverExpandedMemberSets) EXPECT_FALSE(archOverlaps({"gfx1250"}, {"gfx12-generic"})); } -TEST(TestGpuGenericTargets, CoversIsOverExpandedMemberSets) +TEST(TestGenericTargets, CoversIsOverExpandedMemberSets) { EXPECT_TRUE(archCovers({"gfx11-generic"}, {"gfx1151"})); EXPECT_TRUE(archCovers({"gfx11-generic"}, {"gfx1100", "gfx1153"})); @@ -156,7 +156,7 @@ TEST(TestGpuGenericTargets, CoversIsOverExpandedMemberSets) EXPECT_TRUE(archCovers({"gfx11-generic"}, {})); } -TEST(TestGpuGenericTargets, CompeteIsSameTierOverlap) +TEST(TestGenericTargets, CompeteIsSameTierOverlap) { EXPECT_TRUE(archesCompete({}, {})); EXPECT_TRUE(archesCompete({"gfx942"}, {"gfx942"})); @@ -168,7 +168,7 @@ TEST(TestGpuGenericTargets, CompeteIsSameTierOverlap) EXPECT_FALSE(archesCompete({"gfx11-generic"}, {"gfx12-generic"})); } -TEST(TestGpuGenericTargets, UnknownGenericExpandsToNothing) +TEST(TestGenericTargets, UnknownGenericExpandsToNothing) { const Arch unknown{"gfx9-4-generic"}; EXPECT_FALSE(archSupports(unknown, "gfx942")); @@ -180,7 +180,7 @@ TEST(TestGpuGenericTargets, UnknownGenericExpandsToNothing) EXPECT_FALSE(archesCompete(unknown, unknown)); } -TEST(TestGpuGenericTargets, MatchesGoldenTierVectors) +TEST(TestGenericTargets, MatchesGoldenTierVectors) { const auto vectors = readJson(HIPDNN_ARCH_TIER_VECTORS_JSON_PATH); diff --git a/projects/hipdnn/test_categories.yaml b/projects/hipdnn/test_categories.yaml index 77d76223c5a7..8f37002b4f56 100644 --- a/projects/hipdnn/test_categories.yaml +++ b/projects/hipdnn/test_categories.yaml @@ -45,6 +45,7 @@ test_categories: - "hipdnn_frontend_tests" - "hipdnn_test_sdk_tests" - "hipdnn_plugin_sdk_tests" + - "hipdnn_gpu_generic_targets_.*" integration: description: "Current hipDNN integration-check coverage" diff --git a/projects/hipdnn/tools/IngestorGenerator/README.md b/projects/hipdnn/tools/IngestorGenerator/README.md index d3f6f0092f8d..8c730e2063c4 100644 --- a/projects/hipdnn/tools/IngestorGenerator/README.md +++ b/projects/hipdnn/tools/IngestorGenerator/README.md @@ -522,6 +522,7 @@ id) beats a generic containing it, which beats an empty list. Consequences for a every generic of the pack and only entries the pack lists; a kernel with no `arch` inherits the pack. A kernel may not name a generic its pack does not list. An `hsaco` kernel is stamped with its own `arch`, else the whole pack list, which satisfies this. +- The generator emits every kernel inline in its pack, so each inherits the pack's `arch`; a hand-authored standalone UKD (referenced by id) with no `arch` is unrestricted, and the packer accepts it only under a KDP whose `arch` is empty too (`has an empty 'arch' (unrestricted) but the KDP lists`). - The emitted inventory (and so the generated census) gives every member of a generic row the generic's descriptors, and the census checks coverage with the SDK's `archSupports`. An `hsaco` object packed under a generic must really be generic-compatible; neither the diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py b/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py index f978a7e772dc..b5606e16c11e 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py @@ -1415,9 +1415,10 @@ def _check_generic_arch(config: IngestorConfig) -> None: a generic sharing a member) are errors. Per pack of any shape: a kernel may not name a generic the pack does not list, an empty pack list included, since it would ship in no shard. In a pack that lists a generic, a kernel with its own - non-empty ``arch`` must list every generic of the pack and only entries the pack + non-empty ``arch`` must list every generic of the KDP and only entries the pack lists; a kernel with no own ``arch`` inherits the pack and is unaffected. A hsaco - kernel is checked on the list it is stamped with (its own, else the pack's). + kernel is checked on the list it is stamped with (its own, else the pack's). A + rocKE kernel may not sit under a generic, whether the pack's or its own. """ table = _generic_table() for pack in config.packs: @@ -1427,6 +1428,17 @@ def _check_generic_arch(config: IngestorConfig) -> None: for kernel in pack.kernels: kernel_where = f"{where} kernel '{kernel.name}'" _check_arch_list_generics(kernel.arch, kernel_where, table) + if kernel.kernel_source.kind == KERNEL_SOURCE_KIND_ROCKE: + rocke_generics = [ + a + for a in (pack_generics or kernel.arch) + if gtmod.is_generic_shaped(a) + ] + if rocke_generics: + raise ConfigError( + f"{kernel_where}: rocKE does not support generic targets " + f"yet (arch '{rocke_generics[0]}')" + ) stamped = ( list(kernel.arch or pack.arch) if kernel.kernel_source.kind == KERNEL_SOURCE_KIND_HSACO @@ -1448,7 +1460,7 @@ def _check_generic_arch(config: IngestorConfig) -> None: f"{where}: pack lists generic target(s) {pack_generics} (arch " f"{pack.arch}) but kernel '{kernel.name}' declares arch " f"{stamped}; a kernel under a generic pack must list every " - f"generic of the pack and only entries the pack lists" + f"generic of the KDP and only entries the KDP lists" ) diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/models.py b/projects/hipdnn/tools/IngestorGenerator/codegen/models.py index 2f587921a4e8..591ed177832b 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/models.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/models.py @@ -7,6 +7,8 @@ from dataclasses import dataclass, field from typing import Optional +from . import generic_targets as gtmod + #: KMD field types the loader accepts (``DescriptorLoader.hpp``'s ``MetadataField``). KMD_FIELD_TYPES: tuple[str, ...] = ("bool", "int", "float", "string", "int_list") @@ -387,9 +389,14 @@ def kdp_stem(self, pack: PackSpec) -> str: def device_fixture_arch(self) -> str: """Architecture named by the emitted matcher-test device fixture: the first pack's first arch, or `DEFAULT_FIXTURE_ARCH` when the config - restricts none.""" + restricts none. A generic names no device, so it stands for its first + table member.""" first_pack_arch = self.packs[0].arch if self.packs else [] - return first_pack_arch[0] if first_pack_arch else DEFAULT_FIXTURE_ARCH + if not first_pack_arch: + return DEFAULT_FIXTURE_ARCH + first = first_pack_arch[0] + table = gtmod.default_table() + return table.members(first)[0] if table.has(first) else first @property def device_fixture_wave_size(self) -> int: diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py index 59ce8e279463..3b96250649ab 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py @@ -5,6 +5,10 @@ configs, the five pre-mint loader-mirroring checks, deprecated- and duplicate-key rejection, and the kernel_source_kind rejections.""" +import sys +from pathlib import Path +from types import SimpleNamespace + import pytest import yaml @@ -204,7 +208,7 @@ def test_generic_pack_with_a_kernel_naming_a_member_is_rejected(self): from codegen.config_loader import _check_generic_arch config = self._config(["gfx11-generic"], ["gfx1151"]) - with pytest.raises(ConfigError, match="must list every generic of the pack"): + with pytest.raises(ConfigError, match="must list every generic of the KDP"): _check_generic_arch(config) def test_generic_pack_accepts_a_kernel_with_no_own_arch(self): @@ -227,7 +231,7 @@ def test_mixed_generic_pack_kernel_missing_the_generic_is_rejected(self): from codegen.config_loader import _check_generic_arch config = self._config(["gfx942", "gfx11-generic"], ["gfx942"]) - with pytest.raises(ConfigError, match="must list every generic of the pack"): + with pytest.raises(ConfigError, match="must list every generic of the KDP"): _check_generic_arch(config) @pytest.mark.parametrize("pack_arch", [[], ["gfx942"], ["gfx942", "gfx12-generic"]]) @@ -282,6 +286,133 @@ def test_an_expanded_subset_is_accepted_by_the_coverage_check(self): ) +_PACKER_PYTHON = ( + Path(__file__).resolve().parents[5] + / "dnn-providers" + / "hip-kernel-provider" + / "descriptor-packaging" + / "python" +) + + +class TestGeneratorAgreesWithThePackerOnGenericRules: + """The generator and ``hkp_pack.descriptors.validate_generic_arch`` each carry a + hand-written copy of the generic-target rules. Each case runs one pack through + both: they must agree on accept/reject and name the same stable rule text.""" + + @staticmethod + def _packer_verdict(pack_arch, kernel_arch, kind): + if str(_PACKER_PYTHON) not in sys.path: + sys.path.insert(0, str(_PACKER_PYTHON)) + from hkp_pack.descriptors import Descriptor, validate_generic_arch + from hkp_pack.errors import HkpPackError + from hkp_pack.generic_targets import GenericTargets + + ukd = { + "id": "k", + "name": "k", + "kernel_source": {"kind": kind}, + **({"arch": kernel_arch} if kernel_arch else {}), + } + kdp = Descriptor( + Path("p.kdp.json"), + {"id": "p", "arch": pack_arch, "kernelDescriptors": [ukd]}, + ) + flat = SimpleNamespace( + descriptors=[kdp], + generic_targets=GenericTargets.load( + Path(__file__).resolve().parents[3] + / "plugin_sdk" + / "data" + / "gpu_generic_targets.json" + ), + ukd_by_id=lambda: {}, + kdps=lambda: [kdp], + ) + try: + validate_generic_arch(flat) + except HkpPackError as exc: + return str(exc) + return None + + @staticmethod + def _generator_verdict(pack_arch, kernel_arch, kind): + from codegen.config_loader import _validate_config + from codegen.models import KernelSource + + kernel = make_kernel( + arch=kernel_arch, + **({"kernel_source": KernelSource(kind=kind)} if kind == "rocke" else {}), + ) + config = make_minimal_config( + packs=[make_pack(arch=pack_arch, kernels=[kernel])] + ) + try: + _validate_config(config) + except ConfigError as exc: + return str(exc) + return None + + @pytest.mark.parametrize( + ("pack_arch", "kernel_arch", "kind", "rule"), + [ + (["gfx11-generic"], [], "embedded_source", None), + (["gfx11-generic"], ["gfx11-generic"], "embedded_source", None), + ( + ["gfx942", "gfx11-generic"], + ["gfx942", "gfx11-generic"], + "embedded_source", + None, + ), + ( + ["gfx11-generic"], + ["gfx1151"], + "embedded_source", + "must list every generic of the KDP", + ), + ( + ["gfx942", "gfx11-generic"], + ["gfx942"], + "embedded_source", + "must list every generic of the KDP", + ), + ( + ["gfx11-generic", "gfx12-generic"], + ["gfx11-generic"], + "embedded_source", + "must list every generic of the KDP", + ), + ([], ["gfx11-generic"], "embedded_source", "does not list"), + (["gfx9-4-generic"], [], "embedded_source", "generic target name absent"), + ( + ["gfx11-generic", "gfx1151"], + [], + "embedded_source", + "gfx11-generic contains gfx1151", + ), + (["gfx11-generic"], [], "rocke", "does not support generic targets yet"), + ( + ["gfx942", "gfx11-generic"], + [], + "rocke", + "does not support generic targets yet", + ), + ([], ["gfx11-generic"], "rocke", "does not support generic targets yet"), + ], + ) + def test_the_two_copies_of_the_rules_agree( + self, pack_arch, kernel_arch, kind, rule + ): + packer = self._packer_verdict(pack_arch, kernel_arch, kind) + generator = self._generator_verdict(pack_arch, kernel_arch, kind) + if rule is None: + assert packer is None + assert generator is None + else: + assert packer is not None and rule in packer + assert generator is not None and rule in generator + + class TestArchShapeCheck: """Pre-mint check #5: arch entries are plausible gfx-prefixed base ids (error), and unrecognized-but-well-formed ids warn rather than error.""" diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py index c3a8c56a7247..d65de7c98251 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py @@ -768,21 +768,6 @@ def test_a_wildcard_arch_and_a_concrete_one_with_one_tuple_both_survive(self): "twin.right", ] - def test_a_generic_and_an_explicit_member_with_one_tuple_both_survive(self): - """gfx1151 is inside gfx11-generic: on that device the explicit entry outranks - the generic, so the tuple is carried once per tier.""" - config, pack, left, right = self._twin_config() - pack.arch = ["gfx1151", "gfx11-generic"] - left.arch = ["gfx11-generic"] - right.arch = ["gfx1151"] - right.kernel_source.entry_point = "ScaleAddOther" - # Only tuple identity is under test; the config loader owns the pack rule. - kdp = build_kdp(config, pack, mint_ids(config)) - assert [k["name"] for k in kdp["kernelDescriptors"]] == [ - "twin.left", - "twin.right", - ] - def test_two_kernels_one_tuple_under_the_same_generic_are_refused_naming_both( self, ): diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_models.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_models.py index 34b861677a9e..51e913d598ea 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_models.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_models.py @@ -119,6 +119,11 @@ def test_device_fixture_arch_is_the_first_packs_first_arch(self): config = make_minimal_config(packs=[make_pack(arch=["gfx1201", "gfx1200"])]) assert config.device_fixture_arch == "gfx1201" + def test_device_fixture_arch_of_a_generic_pack_is_its_first_member(self): + """A generic names no device; a fixture claiming one matches nothing.""" + config = make_minimal_config(packs=[make_pack(arch=["gfx11-generic"])]) + assert config.device_fixture_arch == "gfx1100" + def test_device_fixture_arch_falls_back_when_no_pack_names_one(self): config = make_minimal_config(packs=[make_pack()]) assert not config.packs[0].arch From 7e5edef9c4fd88c4ccdb9be175c1df6335129e1d Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Tue, 6 Oct 2026 00:27:21 +0000 Subject: [PATCH 25/52] docs(ingestor-generator): state what the generator emits and the rocKE rule --- projects/hipdnn/tools/IngestorGenerator/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/hipdnn/tools/IngestorGenerator/README.md b/projects/hipdnn/tools/IngestorGenerator/README.md index 8c730e2063c4..47b723d639af 100644 --- a/projects/hipdnn/tools/IngestorGenerator/README.md +++ b/projects/hipdnn/tools/IngestorGenerator/README.md @@ -522,7 +522,7 @@ id) beats a generic containing it, which beats an empty list. Consequences for a every generic of the pack and only entries the pack lists; a kernel with no `arch` inherits the pack. A kernel may not name a generic its pack does not list. An `hsaco` kernel is stamped with its own `arch`, else the whole pack list, which satisfies this. -- The generator emits every kernel inline in its pack, so each inherits the pack's `arch`; a hand-authored standalone UKD (referenced by id) with no `arch` is unrestricted, and the packer accepts it only under a KDP whose `arch` is empty too (`has an empty 'arch' (unrestricted) but the KDP lists`). +- The generator emits every kernel inline in its pack and never a standalone UKD; a kernel without its own `arch` inherits the pack's (an `hsaco` kernel is stamped with the whole pack list). A `kind: rocke` kernel under a generic is a ConfigError (`does not support generic targets yet`). a hand-authored standalone UKD (referenced by id) with no `arch` is unrestricted, and the packer accepts it only under a KDP whose `arch` is empty too (`has an empty 'arch' (unrestricted) but the KDP lists`). - The emitted inventory (and so the generated census) gives every member of a generic row the generic's descriptors, and the census checks coverage with the SDK's `archSupports`. An `hsaco` object packed under a generic must really be generic-compatible; neither the From b318c0a6ae635da3f0cc5617bf6589f7a172d126 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Tue, 6 Oct 2026 02:31:27 +0000 Subject: [PATCH 26/52] docs: describe generic target arch rules in words, not plan labels --- .../python/hkp_pack/descriptors.py | 8 ++++---- .../tests/test_hkp_pack_generic.py | 13 +++++++------ .../tests/ingestor/TestDescriptorLoader.cpp | 2 +- .../ingestor/TestKernelIngestorStateManager.cpp | 2 +- .../IngestorGenerator/codegen/config_loader.py | 2 +- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py index fda17ea049f5..18f556e46f90 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py @@ -615,7 +615,7 @@ def _reject_unknown_generics(archs, file_name, table): def _reject_mixed_generic_list(archs, file_name, table): - """S5: one list may not hold a generic together with a member it contains, nor + """One list may not hold a generic together with a member it contains, nor two generics that share a member.""" advice = ( "list one, or author two KDPs (an explicit override plus a generic " @@ -644,10 +644,10 @@ def validate_generic_arch(flat): Per list: names that look generic but are not in the table are errors, and a list may not mix a generic with a member it contains or two generics sharing - a member (S5). Per KDP holding a generic: a UKD with its own arch must list - every generic of the KDP and only entries the KDP lists (S6). Per KDP of any + a member. Per KDP holding a generic: a UKD with its own arch must list + every generic of the KDP and only entries the KDP lists. Per KDP of any shape: a standalone UKD with an empty `arch` (unrestricted) is valid only under - a KDP whose `arch` is empty too (S7; an inline UKD with none inherits the KDP's); a UKD may not + a KDP whose `arch` is empty too (an inline UKD with none inherits the KDP's); a UKD may not name a generic the KDP does not list. rocKE does not support generics yet. The loader accepts the lenient forms; this is the stricter packer-side layer. """ diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py index 0520e6723b5c..a7f7a7b1dbf6 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py @@ -2,10 +2,11 @@ A KDP whose `arch` names a table generic (`gfx11-generic`) is compiled and packed once, under the generic's spelling, and that one tree is copied into the folder of -every selected member of the generic. The rules that keep that sound (S5 / S6 / -reverse direction / unknown name / rocKE) are validation errors with stable -substrings; the merge into a member folder is atomic and refuses two shards that -would write one path with different bytes. +every selected member of the generic. The rules that keep that sound (a generic and +its member in one list, a UKD's arch under a generic KDP, the reverse direction, an +unknown name, rocKE) are validation errors with stable substrings; the merge into a +member folder is atomic and refuses two shards that would write one path with +different bytes. The quick tier packs `embedded_source` and authored `hsaco` UKDs, which need no compiler. The hipcc tier at the end compiles for real and is not quick. @@ -457,7 +458,7 @@ def test_hsaco_under_a_generic_packs_under_the_generic_key_into_every_member_fol assert _files(out / MEMBER_A) == _files(out / MEMBER_B) -# --- S5 ----------------------------------------------------------------------- +# --- a generic and its member in one list ------------------------------------- @pytest.mark.quick @@ -503,7 +504,7 @@ def test_s5_two_generics_sharing_a_member_is_an_error(tmp_path, empty_arch_fixtu assert "share member gfx1101" in message -# --- S6 ----------------------------------------------------------------------- +# --- UKD arch under a generic KDP --------------------------------------------- _S6_RULE = "must list every generic of the KDP" diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp index d16599f54790..7691cecf798b 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp @@ -2085,7 +2085,7 @@ TEST(TestDescriptorLoader, RejectsAnInlineKernelReachingOutsideTheGenericsMember << recorder.getRecordedLogsAsString(); } -/// S1 is unchanged: a standalone kernel id defined for the generic and for one of its +/// Unchanged loader rule: a standalone kernel id defined for the generic and for one of its /// members is two covered definitions under a generic pack, and nothing ranks them. TEST(TestDescriptorLoader, StillRejectsAStandaloneKernelDefinedForAGenericAndForOneOfItsMembers) { diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp index 038407bfadde..846931dc4a6b 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp @@ -483,7 +483,7 @@ inline bool declineTheExplicitKernel(const MatchContext& context, return kernel.name != "kernel_gfx942" && acceptFloatKernels(context, bound, kernel); } -/// S2: shadowing compares only entries a matcher admitted. A better-tier kernel the +/// Shadowing compares only entries a matcher admitted. A better-tier kernel the /// matcher declined must not hide the lower-tier one that remains. TEST(TestKernelIngestorStateManager, ADeclinedBetterTierKernelDoesNotHideALowerTierOne) { diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py b/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py index b5606e16c11e..7ebad77c651a 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py @@ -1376,7 +1376,7 @@ def _generic_table() -> "gtmod.GenericTargets": def _check_arch_list_generics(archs: list, where: str, table) -> None: """Per-list generic rules: an unknown generic-shaped name is an error, and one list may not hold a generic with a member it contains or two generics sharing a - member (the packer's S5).""" + member, as the packer does.""" for entry in archs: if gtmod.is_generic_shaped(entry) and not table.has(entry): raise ConfigError( From 36a817130d7bde244c862fedd69e11f8a1a27e84 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Tue, 6 Oct 2026 22:02:43 +0000 Subject: [PATCH 27/52] hipdnn: drop validator rejection tests for the generic target table The provenance/shape validator already runs at configure time on the real table, so a bad table cannot build. Its 17 negative fixtures and ctests exercised CMake logic only and were never part of the installed test set. --- .../hipdnn/plugin_sdk/tests/CMakeLists.txt | 64 ------------------- .../tests/cmake/CheckGpuGenericTargets.cmake | 17 ----- .../bad_generic_name.json | 33 ---------- .../generic_targets_invalid/bad_license.json | 33 ---------- .../generic_targets_invalid/bad_member.json | 33 ---------- .../bad_retrieved_on.json | 33 ---------- .../bad_statement.json | 33 ---------- .../duplicate_member.json | 33 ---------- .../empty_generics.json | 18 ------ .../empty_members.json | 30 --------- .../missing_comment.json | 32 ---------- .../missing_license.json | 32 ---------- .../missing_permalink.json | 32 ---------- .../missing_source.json | 20 ------ .../missing_statement.json | 32 ---------- .../generic_targets_invalid/not_json.json | 1 - .../permalink_without_revision.json | 33 ---------- .../short_revision.json | 33 ---------- .../string_schema_version.json | 33 ---------- projects/hipdnn/test_categories.yaml | 1 - 20 files changed, 576 deletions(-) delete mode 100644 projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_generic_name.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_license.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_member.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_retrieved_on.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_statement.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/duplicate_member.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_generics.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_members.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_comment.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_license.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_permalink.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_source.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_statement.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/not_json.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/permalink_without_revision.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/short_revision.json delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/string_schema_version.json diff --git a/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt b/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt index 76fa1f0b3679..d5728594995d 100644 --- a/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt +++ b/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt @@ -89,70 +89,6 @@ endif() clang_tidy_check(hipdnn_plugin_sdk_tests) add_hipdnn_test(hipdnn_plugin_sdk_tests ${CMAKE_CURRENT_BINARY_DIR}) -# Table provenance (F-1): the validator must accept the real table, and reject each invalid -# fixture with the text naming that fixture's defect. Negatives match their FATAL_ERROR -# text with PASS_REGULAR_EXPRESSION, never WILL_FAIL: a validator that crashes, or cannot -# find its input, fails for an unrelated reason and would otherwise pass. CMake wraps long -# messages at spaces, so every space in the expected text matches any whitespace run. -set(_generic_targets_check "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CheckGpuGenericTargets.cmake") - -add_test( - NAME hipdnn_gpu_generic_targets_provenance - COMMAND ${CMAKE_COMMAND} -DGENERIC_TARGETS_JSON=${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE} -P - ${_generic_targets_check} -) -set_tests_properties( - hipdnn_gpu_generic_targets_provenance PROPERTIES PASS_REGULAR_EXPRESSION - "gpu_generic_targets: valid" -) -_apply_hipdnn_test_category_labels(hipdnn_gpu_generic_targets_provenance) - -# Registers ctest hipdnn_gpu_generic_targets_rejects_, which must fail with -# (spaces match any whitespace run) for data/generic_targets_invalid/.json. -function(_hipdnn_add_generic_targets_rejection_test case expected_text) - string(REPLACE " " "[ \n]+" expected_regex "${expected_text}") - set(test_name hipdnn_gpu_generic_targets_rejects_${case}) - add_test( - NAME ${test_name} - COMMAND - ${CMAKE_COMMAND} - -DGENERIC_TARGETS_JSON=${CMAKE_CURRENT_SOURCE_DIR}/data/generic_targets_invalid/${case}.json - -P ${_generic_targets_check} - ) - # A validator that downgrades the FATAL_ERROR to a warning still reaches the - # success line; FAIL_REGULAR_EXPRESSION turns that into a failure. - set_tests_properties( - ${test_name} - PROPERTIES PASS_REGULAR_EXPRESSION "${expected_regex}" FAIL_REGULAR_EXPRESSION - "gpu_generic_targets: valid" - ) - _apply_hipdnn_test_category_labels(${test_name}) -endfunction() - -_hipdnn_add_generic_targets_rejection_test(missing_source "missing required object 'source'") -_hipdnn_add_generic_targets_rejection_test(missing_comment "missing required key '.comment'") -_hipdnn_add_generic_targets_rejection_test(missing_permalink "missing required key 'source.permalink'") -_hipdnn_add_generic_targets_rejection_test(short_revision "'source.revision' must be a 40-digit") -_hipdnn_add_generic_targets_rejection_test( - permalink_without_revision "'source.permalink' must contain 'source.revision'" -) -_hipdnn_add_generic_targets_rejection_test(bad_retrieved_on "'source.retrievedOn' must be YYYY-MM-DD") -_hipdnn_add_generic_targets_rejection_test(missing_license "missing required key 'source.license'") -_hipdnn_add_generic_targets_rejection_test(not_json "not valid JSON") -_hipdnn_add_generic_targets_rejection_test(string_schema_version "'schemaVersion' must be 1") -_hipdnn_add_generic_targets_rejection_test(bad_license "'source.license' must be") -_hipdnn_add_generic_targets_rejection_test(bad_statement "'source.statement' must state") -_hipdnn_add_generic_targets_rejection_test(missing_statement "missing required key 'source.statement'") -_hipdnn_add_generic_targets_rejection_test(empty_generics "'generics' must be non-empty") -_hipdnn_add_generic_targets_rejection_test( - empty_members "generics.gfx12-generic must list at least one member" -) -_hipdnn_add_generic_targets_rejection_test( - duplicate_member "member 'gfx1200' is listed twice in gfx12-generic" -) -_hipdnn_add_generic_targets_rejection_test(bad_generic_name "generic name 'gfx12generic' does not match") -_hipdnn_add_generic_targets_rejection_test(bad_member "member 'gfx12zz' of gfx12-generic does not match") - # Cross-process coverage: one process writes a winner record, a SECOND process reads it # back. Two ctest invocations of the same binary, sequenced with # FIXTURES_SETUP/FIXTURES_REQUIRED, following the shape hipdnn/cmake/Tests.cmake's diff --git a/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake b/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake deleted file mode 100644 index dd12f8eba0a6..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright © Advanced Micro Devices, Inc., or its affiliates. -# SPDX-License-Identifier: MIT - -# Script-mode driver for hipdnn_validate_gpu_generic_targets (run with `cmake -P`): -# cmake -DGENERIC_TARGETS_JSON= -P CheckGpuGenericTargets.cmake -# Prints "gpu_generic_targets: valid" only after the table passed every rule, so a ctest -# can require that line for the real table and the validator's FATAL_ERROR text for each -# invalid fixture. A crash or a missing input matches neither. - -if(NOT GENERIC_TARGETS_JSON) - message(FATAL_ERROR "CheckGpuGenericTargets: pass -DGENERIC_TARGETS_JSON=") -endif() - -include("${CMAKE_CURRENT_LIST_DIR}/../../cmake/ValidateGpuGenericTargets.cmake") - -hipdnn_validate_gpu_generic_targets("${GENERIC_TARGETS_JSON}") -message(STATUS "gpu_generic_targets: valid") diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_generic_name.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_generic_name.json deleted file mode 100644 index d41be7e1764e..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_generic_name.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": 1, - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "2026-10-05", - "license": "Apache-2.0 WITH LLVM-exception", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" - }, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12generic": [ - "gfx1200", - "gfx1201" - ] - } -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_license.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_license.json deleted file mode 100644 index b98092a7f52a..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_license.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": 1, - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "2026-10-05", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)", - "license": "MIT" - }, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12-generic": [ - "gfx1200", - "gfx1201" - ] - } -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_member.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_member.json deleted file mode 100644 index 0cba6fe1d3a9..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_member.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": 1, - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "2026-10-05", - "license": "Apache-2.0 WITH LLVM-exception", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" - }, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12-generic": [ - "gfx1200", - "gfx12zz" - ] - } -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_retrieved_on.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_retrieved_on.json deleted file mode 100644 index 82664b576acd..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_retrieved_on.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": 1, - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "5 October 2026", - "license": "Apache-2.0 WITH LLVM-exception", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" - }, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12-generic": [ - "gfx1200", - "gfx1201" - ] - } -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_statement.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_statement.json deleted file mode 100644 index b19573342155..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/bad_statement.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": 1, - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "2026-10-05", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "copied", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)", - "license": "Apache-2.0 WITH LLVM-exception" - }, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12-generic": [ - "gfx1200", - "gfx1201" - ] - } -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/duplicate_member.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/duplicate_member.json deleted file mode 100644 index 66c6811c618c..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/duplicate_member.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": 1, - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "2026-10-05", - "license": "Apache-2.0 WITH LLVM-exception", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" - }, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12-generic": [ - "gfx1200", - "gfx1200" - ] - } -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_generics.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_generics.json deleted file mode 100644 index 8aa22e3e1605..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_generics.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": 1, - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "2026-10-05", - "license": "Apache-2.0 WITH LLVM-exception", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" - }, - "generics": {} -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_members.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_members.json deleted file mode 100644 index 59b14606c9b5..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/empty_members.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": 1, - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "2026-10-05", - "license": "Apache-2.0 WITH LLVM-exception", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" - }, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12-generic": [] - } -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_comment.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_comment.json deleted file mode 100644 index d652fa95641a..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_comment.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "schemaVersion": 1, - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "2026-10-05", - "license": "Apache-2.0 WITH LLVM-exception", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" - }, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12-generic": [ - "gfx1200", - "gfx1201" - ] - } -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_license.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_license.json deleted file mode 100644 index f891afe0d4a2..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_license.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": 1, - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "2026-10-05", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" - }, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12-generic": [ - "gfx1200", - "gfx1201" - ] - } -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_permalink.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_permalink.json deleted file mode 100644 index 305d31d91986..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_permalink.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": 1, - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "retrievedOn": "2026-10-05", - "license": "Apache-2.0 WITH LLVM-exception", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" - }, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12-generic": [ - "gfx1200", - "gfx1201" - ] - } -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_source.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_source.json deleted file mode 100644 index 121282ea5343..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_source.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": 1, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12-generic": [ - "gfx1200", - "gfx1201" - ] - } -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_statement.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_statement.json deleted file mode 100644 index 6fa4f5044151..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/missing_statement.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": 1, - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "2026-10-05", - "license": "Apache-2.0 WITH LLVM-exception", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" - }, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12-generic": [ - "gfx1200", - "gfx1201" - ] - } -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/not_json.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/not_json.json deleted file mode 100644 index 928e8a7bf557..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/not_json.json +++ /dev/null @@ -1 +0,0 @@ -{ "$comment": "truncated", "schemaVersion": 1, "source": diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/permalink_without_revision.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/permalink_without_revision.json deleted file mode 100644 index 55fa9ed5d679..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/permalink_without_revision.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": 1, - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "permalink": "https://github.com/ROCm/llvm-project/blob/main/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "2026-10-05", - "license": "Apache-2.0 WITH LLVM-exception", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" - }, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12-generic": [ - "gfx1200", - "gfx1201" - ] - } -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/short_revision.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/short_revision.json deleted file mode 100644 index f8391b35f21a..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/short_revision.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": 1, - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696", - "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "2026-10-05", - "license": "Apache-2.0 WITH LLVM-exception", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" - }, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12-generic": [ - "gfx1200", - "gfx1201" - ] - } -} diff --git a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/string_schema_version.json b/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/string_schema_version.json deleted file mode 100644 index ba464faf813b..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/generic_targets_invalid/string_schema_version.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", - "schemaVersion": "1", - "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "2026-10-05", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)", - "license": "Apache-2.0 WITH LLVM-exception" - }, - "generics": { - "gfx11-generic": [ - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153" - ], - "gfx12-generic": [ - "gfx1200", - "gfx1201" - ] - } -} diff --git a/projects/hipdnn/test_categories.yaml b/projects/hipdnn/test_categories.yaml index 8f37002b4f56..77d76223c5a7 100644 --- a/projects/hipdnn/test_categories.yaml +++ b/projects/hipdnn/test_categories.yaml @@ -45,7 +45,6 @@ test_categories: - "hipdnn_frontend_tests" - "hipdnn_test_sdk_tests" - "hipdnn_plugin_sdk_tests" - - "hipdnn_gpu_generic_targets_.*" integration: description: "Current hipDNN integration-check coverage" From afa0c1186f1e6cd79f977abf23b339ebc460978c Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 02:02:07 +0000 Subject: [PATCH 28/52] hipdnn: one entryTier for generic arch matching, loader strictness R1-R4, relocatable SDK tests --- .../src/compilation/KpackModuleCache.hpp | 36 ++- .../compilation/TestKpackModuleCache.cpp | 37 +-- .../cmake/ValidateGpuGenericTargets.cmake | 76 +----- .../plugin_sdk/data/gpu_generic_targets.json | 14 +- .../hipdnn_plugin_sdk/GpuGenericTargets.hpp | 27 +- .../ingestor/DescriptorLoader.hpp | 100 ++++++- .../ingestor/DeviceProperties.hpp | 33 +-- .../ingestor/KernelIngestorStateManager.hpp | 75 +++-- .../hipdnn/plugin_sdk/tests/CMakeLists.txt | 9 - .../tests/data/arch_tier_vectors.json | 78 ------ .../tests/ingestor/TestArchGate.cpp | 17 +- .../tests/ingestor/TestDescriptorLoader.cpp | 256 ++++++++---------- .../tests/ingestor/TestGenericTargets.cpp | 101 +------ .../TestKernelIngestorStateManager.cpp | 206 +++----------- 14 files changed, 351 insertions(+), 714 deletions(-) delete mode 100644 projects/hipdnn/plugin_sdk/tests/data/arch_tier_vectors.json diff --git a/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp b/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp index 58b41ddb1f3f..78ae473a68c3 100644 --- a/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp +++ b/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp @@ -12,6 +12,7 @@ #include "utilities/Digest.hpp" #include +#include #include #include #include @@ -104,35 +105,32 @@ class KpackModuleCache : public ModuleCache& archiveArches, std::string_view deviceArch) { - const auto baseDeviceId = hipdnn_plugin_sdk::stripArchFeatures(deviceArch); + const std::string* best = nullptr; + std::optional bestTier; for(const auto& candidate : archiveArches) { - if(!hipdnn_plugin_sdk::isGenericShapedArchName(candidate) - && hipdnn_plugin_sdk::archMatches( - deviceArch, candidate, hipdnn_plugin_sdk::ArchMatchMode::PREFIX)) + const auto tier = hipdnn_plugin_sdk::entryTier(candidate, deviceArch); + if(tier && (!bestTier || *tier < *bestTier)) { - return &candidate; + best = &candidate; + bestTier = tier; } } - for(const auto& candidate : archiveArches) + if(bestTier == hipdnn_plugin_sdk::ArchTier::GENERIC) { - if(hipdnn_plugin_sdk::genericTargetContains(candidate, baseDeviceId)) - { - HIPDNN_PLUGIN_LOG_INFO("kpack: device arch '" - << deviceArch << "' is served by generic archive key '" - << candidate << "'"); - return &candidate; - } + HIPDNN_PLUGIN_LOG_INFO("kpack: device arch '" + << deviceArch << "' is served by generic archive key '" << *best + << "'"); } - return nullptr; + return best; } /// @throws KpackModuleLoadFailure on any stage that fails. Never returns a null diff --git a/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp b/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp index 1a10ec3ebdad..7f4f8827f626 100644 --- a/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp +++ b/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp @@ -162,10 +162,9 @@ TEST(TestKpackModuleCacheSelectArch, PrefersTheExactDeviceArchOverAGenericKey) // The generic key sorts first, so a first-match-wins over mixed keys would pick it. const std::vector keys{"gfx11-generic", "gfx1151"}; - const auto* selected = KpackModuleCache::selectArch(keys, "gfx1151"); + const auto* selected = KpackModuleCache::selectArch(keys, "gfx1151:sramecc+:xnack-"); ASSERT_NE(selected, nullptr); - EXPECT_EQ(*selected, "gfx1151"); EXPECT_EQ(selected, &keys[1]); } @@ -173,41 +172,21 @@ TEST(TestKpackModuleCacheSelectArch, FallsBackToAGenericKeyContainingTheDevice) { const std::vector keys{"gfx942", "gfx12-generic", "gfx11-generic"}; - const auto* selected = KpackModuleCache::selectArch(keys, "gfx1100"); + const auto* selected = KpackModuleCache::selectArch(keys, "gfx1100:xnack-"); ASSERT_NE(selected, nullptr); - EXPECT_EQ(*selected, "gfx11-generic"); + EXPECT_EQ(selected, &keys[2]); } -TEST(TestKpackModuleCacheSelectArch, RefusesAGenericKeyThatDoesNotContainTheDevice) +TEST(TestKpackModuleCacheSelectArch, DeclinesAKeyThatDoesNotNameTheDevice) { - // gfx1250 is not a gfx12-generic member and gfx1170 is not a gfx11-generic member, - // however close their names are to the generic's family. + // gfx1250 is not a gfx12-generic member however close its name is to the family, and + // shape never implies membership: a table-absent generic admits no device, not even the + // one it spells. EXPECT_EQ(KpackModuleCache::selectArch({"gfx12-generic"}, "gfx1250"), nullptr); - EXPECT_EQ(KpackModuleCache::selectArch({"gfx11-generic"}, "gfx1170"), nullptr); EXPECT_EQ(KpackModuleCache::selectArch({"gfx11-generic", "gfx12-generic"}, "gfx942"), nullptr); - EXPECT_EQ(KpackModuleCache::selectArch({}, "gfx1100"), nullptr); -} - -TEST(TestKpackModuleCacheSelectArch, IgnoresFeatureDecorationOnTheDevice) -{ - const std::vector generic{"gfx9-4-generic", "gfx11-generic"}; - const auto* viaGeneric = KpackModuleCache::selectArch(generic, "gfx1151:xnack-"); - ASSERT_NE(viaGeneric, nullptr); - EXPECT_EQ(*viaGeneric, "gfx11-generic"); - - const std::vector exact{"gfx11-generic", "gfx942"}; - const auto* viaExact = KpackModuleCache::selectArch(exact, "gfx942:sramecc+:xnack-"); - ASSERT_NE(viaExact, nullptr); - EXPECT_EQ(*viaExact, "gfx942"); -} - -TEST(TestKpackModuleCacheSelectArch, NeverMatchesAnUnknownGenericShapedKey) -{ - // Shape never implies membership: a table-absent name admits no device, not even - // the one it spells. EXPECT_EQ(KpackModuleCache::selectArch({"gfx99-generic"}, "gfx99-generic"), nullptr); - EXPECT_EQ(KpackModuleCache::selectArch({"gfx99-generic"}, "gfx1100"), nullptr); + EXPECT_EQ(KpackModuleCache::selectArch({}, "gfx1100"), nullptr); } TEST(TestKpackModuleCacheLoad, ASecondOrdinalDoesNotAnswerFromTheFirstOrdinalsEntry) diff --git a/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake b/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake index c76a30f3cc87..254390c714b7 100644 --- a/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake +++ b/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake @@ -1,78 +1,13 @@ # Copyright © Advanced Micro Devices, Inc., or its affiliates. # SPDX-License-Identifier: MIT -# Validates data/gpu_generic_targets.json: the LLVM provenance block and the shape of the +# Validates data/gpu_generic_targets.json: the schema version and the shape of the # generic-target -> member-processor table. Every violation is a FATAL_ERROR that names # the offending key or member, so a failing configure points at the line to fix. # -# This is the only implementation of the provenance rules; the Python readers validate -# shape only. -# # cmake-lint does not model foreach(... RANGE ...) and reports E1120 on its arguments. # cmake-lint: disable=E1120 -# Reads the string at of into ; FATAL_ERROR if it is absent, -# not a string, or empty. -function(_hipdnn_gpu_generic_targets_get_string out json file key_path) - string(JSON value ERROR_VARIABLE err GET "${json}" ${ARGN}) - string(JSON type ERROR_VARIABLE type_err TYPE "${json}" ${ARGN}) - if(err OR type_err) - message(FATAL_ERROR "gpu_generic_targets: ${file}: missing required key '${key_path}'") - endif() - if(NOT type STREQUAL "STRING") - message(FATAL_ERROR "gpu_generic_targets: ${file}: key '${key_path}' must be a string") - endif() - if(value STREQUAL "") - message(FATAL_ERROR "gpu_generic_targets: ${file}: key '${key_path}' must be non-empty") - endif() - set(${out} "${value}" PARENT_SCOPE) -endfunction() - -# Checks the provenance block ('source') of the table. -function(_hipdnn_gpu_generic_targets_validate_source json json_path) - string(JSON source_type ERROR_VARIABLE err TYPE "${json}" source) - if(err OR NOT source_type STREQUAL "OBJECT") - message(FATAL_ERROR "gpu_generic_targets: ${json_path}: missing required object 'source'") - endif() - - foreach(key description document section repository revision permalink retrievedOn license - licenseNote statement pinnedBy) - _hipdnn_gpu_generic_targets_get_string(source_${key} "${json}" "${json_path}" "source.${key}" - source ${key}) - endforeach() - - if(NOT source_document MATCHES "AMDGPUUsage\\.rst$") - message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.document' must name AMDGPUUsage.rst") - endif() - if(NOT source_section MATCHES "^AMDGPU Generic Processors") - message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.section' must name the 'AMDGPU Generic Processors' table") - endif() - if(NOT source_repository STREQUAL "https://github.com/ROCm/llvm-project") - message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.repository' must be https://github.com/ROCm/llvm-project") - endif() - string(LENGTH "${source_revision}" revision_length) - if(NOT source_revision MATCHES "^[0-9a-f]+$" OR NOT revision_length EQUAL 40) - message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.revision' must be a 40-digit lowercase hex commit id, got '${source_revision}'") - endif() - if(NOT source_permalink MATCHES "^https://") - message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.permalink' must be an https:// URL") - endif() - string(FIND "${source_permalink}" "${source_revision}" revision_at) - if(revision_at EQUAL -1) - message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.permalink' must contain 'source.revision' (${source_revision}) so the link is pinned") - endif() - if(NOT source_retrievedOn MATCHES "^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$") - message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.retrievedOn' must be YYYY-MM-DD, got '${source_retrievedOn}'") - endif() - if(NOT source_license STREQUAL "Apache-2.0 WITH LLVM-exception") - message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.license' must be 'Apache-2.0 WITH LLVM-exception', got '${source_license}'") - endif() - if(NOT source_statement MATCHES "[Oo]nly factual processor-name membership") - message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'source.statement' must state that only factual processor-name membership is recorded") - endif() - -endfunction() - # Checks the 'generics' map: names, member shape, duplicates. function(_hipdnn_gpu_generic_targets_validate_generics json json_path) string(JSON generics_type ERROR_VARIABLE err TYPE "${json}" generics) @@ -119,9 +54,9 @@ function(_hipdnn_gpu_generic_targets_validate_generics json json_path) endforeach() endfunction() -# Validates the generic target table at : the comment and schema version, the -# provenance block, and every generic's member list. FATAL_ERROR naming the offending -# key or member on the first violation. +# Validates the generic target table at : the schema version and every generic's +# name and member list. FATAL_ERROR naming the offending key or member on the first +# violation. function(hipdnn_validate_gpu_generic_targets json_path) if(NOT EXISTS "${json_path}") message(FATAL_ERROR "gpu_generic_targets: cannot read '${json_path}'") @@ -133,8 +68,6 @@ function(hipdnn_validate_gpu_generic_targets json_path) message(FATAL_ERROR "gpu_generic_targets: ${json_path}: not valid JSON: ${parse_error}") endif() - _hipdnn_gpu_generic_targets_get_string(_ "${json}" "${json_path}" "\$comment" "\$comment") - string(JSON version ERROR_VARIABLE err GET "${json}" schemaVersion) if(err) message(FATAL_ERROR "gpu_generic_targets: ${json_path}: missing required key 'schemaVersion'") @@ -144,6 +77,5 @@ function(hipdnn_validate_gpu_generic_targets json_path) message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'schemaVersion' must be 1, got '${version}'") endif() - _hipdnn_gpu_generic_targets_validate_source("${json}" "${json_path}") _hipdnn_gpu_generic_targets_validate_generics("${json}" "${json_path}") endfunction() diff --git a/projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json b/projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json index 6964d10341ff..9ed9a0621d2e 100644 --- a/projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json +++ b/projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json @@ -1,17 +1,9 @@ { - "$comment": "Membership of LLVM AMDGPU generic processors, as listed in the public LLVM AMDGPU documentation. Factual processor-name membership only; no prose is copied. Edit this file (data), never a second copy: the C++ constexpr table is generated from it at CMake time and the packer reads it at pack time. Members MUST be exactly those listed at source.revision.", + "$comment": "Membership of LLVM AMDGPU generic processors; the C++ constexpr table is generated from this file at CMake time and the packer reads it at pack time. Edit this file, never a copy.", "schemaVersion": 1, "source": { - "description": "Generic processor -> supported processor names, from the 'AMDGPU Generic Processors' table.", - "document": "llvm/docs/AMDGPUUsage.rst", - "section": "AMDGPU Generic Processors (amdgpu-generic-processor-table)", - "repository": "https://github.com/ROCm/llvm-project", - "revision": "998a696feca902c689c53383e48d29494f9edba7", - "permalink": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "retrievedOn": "2026-10-05", - "license": "Apache-2.0 WITH LLVM-exception", - "licenseNote": "LLVM is licensed under Apache-2.0 with LLVM Exceptions (llvm/LICENSE.TXT at the revision above).", - "statement": "Only factual processor-name membership from public documentation is recorded here; no documentation prose is copied and nothing non-public is included.", + "document": "llvm/docs/AMDGPUUsage.rst, 'AMDGPU Generic Processors' table", + "revision": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" }, "generics": { diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp index 986bb2f0d509..3fb285dfa29c 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp @@ -17,6 +17,7 @@ #include #include +#include #include namespace hipdnn_plugin_sdk @@ -71,24 +72,22 @@ enum class ArchTier : int UNRESTRICTED = 2, ///< An empty list: any device. }; -/// Rank of one list entry for @p baseDeviceId (features already stripped): EXPLICIT when -/// the entry equals it, GENERIC when a table generic contains it, else nullopt. A -/// generic-shaped entry is never EXPLICIT, so an unknown generic matches no device. -inline std::optional archEntryTier(std::string_view entry, std::string_view baseDeviceId) +/// Rank of one `arch` list entry for the device @p rawDeviceArch as the device reports it +/// (features included): EXPLICIT when the entry is the device's own base id (PREFIX +/// match, so `gfx942` is EXPLICIT for `gfx942:sramecc+:xnack-` and never matches +/// `gfx950`), GENERIC when a table generic containing the device's base id, else nullopt. +/// A generic-shaped entry is never EXPLICIT, so an unknown generic matches no device. +inline std::optional entryTier(std::string_view entry, std::string_view rawDeviceArch) { if(isGenericShapedArchName(entry)) { - if(genericTargetContains(entry, baseDeviceId)) - { - return ArchTier::GENERIC; - } - return std::nullopt; - } - if(entry == baseDeviceId) - { - return ArchTier::EXPLICIT; + return genericTargetContains(entry, stripArchFeatures(rawDeviceArch)) + ? std::optional(ArchTier::GENERIC) + : std::nullopt; } - return std::nullopt; + return archMatches(rawDeviceArch, entry, ArchMatchMode::PREFIX) + ? std::optional(ArchTier::EXPLICIT) + : std::nullopt; } } // namespace hipdnn_plugin_sdk diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp index 6767a9181117..b23901adf95b 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp @@ -684,6 +684,77 @@ inline std::string describeArch(const std::vector& arch) return text + "]"; } +/// A device both table entries @p x and @p y cover when either is a table generic and the +/// other is a member it contains or a generic sharing a member; empty when none. +inline std::string sharedGenericMember(const std::string& x, const std::string& y) +{ + const auto* generic = findGenericTarget(x); + const std::string& other = generic != nullptr ? y : x; + if(generic == nullptr) + { + generic = findGenericTarget(y); + } + if(generic == nullptr) + { + return {}; + } + for(size_t i = 0; i < generic->memberCount; ++i) + { + if(entryTier(other, generic->members[i]).has_value()) + { + return std::string(generic->members[i]); + } + } + return {}; +} + +/// Why @p kernelArch breaks the generic rules against its pack's @p packArch, or empty. +/// A kernel may name a generic only if the pack lists it. When the pack lists generics, a +/// kernel with an arch of its own must list every one of them and only entries the pack +/// lists. A kernel with no arch inherits the pack's and is never in violation. +inline std::string genericArchViolation(const std::vector& packArch, + const std::vector& kernelArch) +{ + const auto lists = [](const std::vector& arch, const std::string& entry) { + return std::find(arch.begin(), arch.end(), entry) != arch.end(); + }; + for(const auto& entry : kernelArch) + { + if(isGenericShapedArchName(entry) && !lists(packArch, entry)) + { + return "declares generic target '" + entry + "', which its pack does not list"; + } + } + if(kernelArch.empty()) + { + return {}; + } + bool packHasGeneric = false; + for(const auto& entry : packArch) + { + if(isGenericShapedArchName(entry)) + { + packHasGeneric = true; + if(!lists(kernelArch, entry)) + { + return "declares arch " + describeArch(kernelArch) + + " without the pack's generic target '" + entry + "'"; + } + } + } + if(packHasGeneric) + { + for(const auto& entry : kernelArch) + { + if(!lists(packArch, entry)) + { + return "declares arch '" + entry + "', which its generic-target pack does not list"; + } + } + } + return {}; +} + /// `arch`: every entry must be non-empty, non-repeated, and a plausible gfx base id. /// archSupports is a case-sensitive exact compare, so `""`, `" gfx942"`, or `"gfx94"` /// would otherwise silently disable the pack everywhere with nothing louder than an @@ -717,10 +788,19 @@ inline std::vector requireArchList(const nlohmann::json& object, { if(isGenericShapedArchName(value) && findGenericTarget(value) == nullptr) { - HIPDNN_PLUGIN_LOG_WARN("ingestor: arch entry '" - << value << "' in " << where - << " is a generic target name absent from the generic target " - "table; it matches no device"); + fail("key 'arch' in " + where + " names '" + value + + "', a generic target absent from the generic target table"); + } + } + for(size_t i = 0; i < values.size(); ++i) + { + for(size_t j = i + 1; j < values.size(); ++j) + { + if(const auto shared = sharedGenericMember(values[i], values[j]); !shared.empty()) + { + fail("key 'arch' in " + where + " lists '" + values[i] + "' and '" + values[j] + + "', which both cover " + shared); + } } } return values; @@ -1091,6 +1171,11 @@ inline KernelDescriptorPack parseKernelDescriptorPack(const nlohmann::json& root + describeArch(kernel->arch) + ", which reaches past the pack's " + describeArch(pack.arch)); } + if(const auto violation = genericArchViolation(pack.arch, kernel->arch); + !violation.empty()) + { + fail("kernel '" + kernel->name + "' in " + where + " " + violation); + } pack.kernels.push_back(std::move(*kernel)); } } @@ -1903,6 +1988,13 @@ inline std::vector resolveDescriptorSets(const DescriptorCatalog& reason = match.reason; break; } + if(const auto violation + = detail::genericArchViolation(pack.arch, match.kernel->arch); + !violation.empty()) + { + reason = "names kernel " + toString(kernelId) + ", which " + violation; + break; + } pack.kernels.push_back(*match.kernel); pack.kernels.back().originDirectory = match.path.parent_path(); pack.kernels.back().treeRoot = match.treeRoot; diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp index aea6b3a4dd34..3a2facd62085 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp @@ -31,11 +31,8 @@ struct DeviceProperties }; /// How @p arch (a KDP's supported-target list) ranks for @p deviceArch: the best tier of -/// any entry, or nullopt when no entry admits the device. An empty list is UNRESTRICTED. -/// The device carries its features and entries are base ids, so an explicit entry is the -/// PREFIX match, not SUBSTRING or equality: `gfx942` is EXPLICIT for a device reporting -/// `gfx942:sramecc+:xnack-` and never matches `gfx950`. A generic entry is GENERIC when the -/// table lists the device's base id as a member. +/// any entry (see entryTier), or nullopt when no entry admits the device. An empty list is +/// UNRESTRICTED. inline std::optional archTier(const std::vector& arch, std::string_view deviceArch) { @@ -43,20 +40,11 @@ inline std::optional archTier(const std::vector& arch, { return ArchTier::UNRESTRICTED; } - const auto baseDeviceId = stripArchFeatures(deviceArch); std::optional best; for(const auto& entry : arch) { - std::optional tier; - if(!isGenericShapedArchName(entry) && archMatches(deviceArch, entry, ArchMatchMode::PREFIX)) - { - tier = ArchTier::EXPLICIT; - } - else - { - tier = archEntryTier(entry, baseDeviceId); - } - if(tier && (!best || static_cast(*tier) < static_cast(*best))) + const auto tier = entryTier(entry, deviceArch); + if(tier && (!best || *tier < *best)) { best = tier; } @@ -74,21 +62,10 @@ inline bool archSupports(const std::vector& arch, std::string_view namespace detail { -/// Does the single list entry @p entry admit the device id @p device? An explicit entry is -/// the PREFIX match; a generic admits its members; an unknown generic admits nothing. -inline bool entryAdmits(std::string_view entry, std::string_view device) -{ - if(isGenericShapedArchName(entry)) - { - return genericTargetContains(entry, stripArchFeatures(device)); - } - return archMatches(device, entry, ArchMatchMode::PREFIX); -} - inline bool listAdmits(const std::vector& list, std::string_view device) { return std::any_of(list.begin(), list.end(), [device](const std::string& entry) { - return entryAdmits(entry, device); + return entryTier(entry, device).has_value(); }); } diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp index 911123973b72..05e21988f5ca 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp @@ -683,9 +683,9 @@ class KernelIngestorStateManager /// Among admitted entries sharing a metadata tuple, keeps only those whose arch list /// ranks best for the device (EXPLICIT over GENERIC over UNRESTRICTED), preserving - /// order. Runs after matcher admission, so a kernel a matcher declined never hides a - /// lower-tier one. Uniqueness at construction guarantees at most one entry per tuple - /// per tier on any device. + /// catalog order. Runs after matcher admission, so a kernel a matcher declined never + /// hides a lower-tier one. Uniqueness at construction guarantees at most one entry per + /// tuple per tier on any device. static void shadowByArchTier(Catalog& catalog, const MatchContext& context) { auto& entries = catalog.entries; @@ -701,60 +701,53 @@ class KernelIngestorStateManager return *lhs < *rhs; } }; - struct Best - { - int tier; - const KernelDefinition* winner; - }; - const auto tierOf = [&context](const KernelDefinition& definition) { - return static_cast(archTier(definition.arch, context.deviceProperties.gcnArchName) - .value_or(ArchTier::UNRESTRICTED)); - }; - - std::map bestByTuple; - for(const auto& entry : entries) + constexpr int SHADOWED = -1; + std::vector tiers; + tiers.reserve(entries.size()); + // Index of the best-tier entry per tuple; the keys point into entries. + std::map winners; + for(size_t i = 0; i < entries.size(); ++i) { - const int tier = tierOf(entry); - const auto [it, inserted] - = bestByTuple.try_emplace(&entry.metadata, Best{tier, &entry}); - if(!inserted && tier < it->second.tier) + tiers.push_back( + static_cast(archTier(entries[i].arch, context.deviceProperties.gcnArchName) + .value_or(ArchTier::UNRESTRICTED))); + const auto [it, inserted] = winners.try_emplace(&entries[i].metadata, i); + if(!inserted && tiers[i] < tiers[it->second]) { - it->second = Best{tier, &entry}; + it->second = i; } } - if(bestByTuple.size() == entries.size()) - { - return; - } - // Decide and log before moving anything: the map and winners point into entries. - std::vector keep(entries.size(), true); + // Name every loser's winner before any entry moves. + bool anyShadowed = false; for(size_t i = 0; i < entries.size(); ++i) { - const auto& best = bestByTuple.at(&entries[i].metadata); - if(tierOf(entries[i]) != best.tier) + const size_t winner = winners.at(&entries[i].metadata); + if(tiers[i] != tiers[winner]) { - keep[i] = false; + tiers[i] = SHADOWED; + anyShadowed = true; HIPDNN_PLUGIN_LOG_INFO("ingestor: kernel " << toString(entries[i].kernelId) << " is shadowed by kernel " - << toString(best.winner->kernelId) + << toString(entries[winner].kernelId) << " at a better arch tier on device " << context.deviceProperties.gcnArchName); } } - size_t out = 0; - for(size_t i = 0; i < entries.size(); ++i) + if(!anyShadowed) { - if(keep[i]) - { - if(out != i) - { - entries[out] = std::move(entries[i]); - } - ++out; - } + return; } - entries.erase(entries.begin() + static_cast(out), entries.end()); + + // remove_if tests each element in place before any move reaches it, so its + // position indexes tiers. + entries.erase(std::remove_if(entries.begin(), + entries.end(), + [&](const KernelDefinition& entry) { + return tiers[static_cast(&entry - entries.data())] + == SHADOWED; + }), + entries.end()); } bool graphLevelMatchersPass(const KernelDescriptorPack& pack, diff --git a/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt b/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt index d5728594995d..62435ba5902b 100644 --- a/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt +++ b/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt @@ -51,15 +51,6 @@ add_executable(hipdnn_plugin_sdk_tests $<$:TestKernelIngestorConcurrency.cpp> ) -# The generic target table and the golden tier vectors are read from the source tree, so -# the C++ test compares the generated header against the very document it came from. -target_compile_definitions( - hipdnn_plugin_sdk_tests - PRIVATE - "HIPDNN_GPU_GENERIC_TARGETS_JSON_PATH=\"${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}\"" - "HIPDNN_ARCH_TIER_VECTORS_JSON_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}/data/arch_tier_vectors.json\"" -) - target_compile_options(hipdnn_plugin_sdk_tests PRIVATE ${HIPDNN_WARNING_COMPILE_OPTIONS} ${HIPDNN_NO_RTTI_OPTIONS}) target_link_libraries( diff --git a/projects/hipdnn/plugin_sdk/tests/data/arch_tier_vectors.json b/projects/hipdnn/plugin_sdk/tests/data/arch_tier_vectors.json deleted file mode 100644 index dc6d6bc8f8e1..000000000000 --- a/projects/hipdnn/plugin_sdk/tests/data/arch_tier_vectors.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "tier": [ - {"arch": [], "device": "gfx1151", "expect": "unrestricted"}, - {"arch": [], "device": "gfx942:sramecc+:xnack-", "expect": "unrestricted"}, - {"arch": ["gfx1151", "gfx11-generic"], "device": "gfx1151", "expect": "explicit"}, - {"arch": ["gfx1151", "gfx11-generic"], "device": "gfx1100", "expect": "generic"}, - {"arch": ["gfx1151", "gfx11-generic"], "device": "gfx1151:sramecc+:xnack-", "expect": "explicit"}, - {"arch": ["gfx11-generic"], "device": "gfx1151:sramecc+", "expect": "generic"}, - {"arch": ["gfx11-generic"], "device": "gfx1154", "expect": "none"}, - {"arch": ["gfx11-generic"], "device": "gfx1170", "expect": "none"}, - {"arch": ["gfx12-generic"], "device": "gfx1100", "expect": "none"}, - {"arch": ["gfx12-generic"], "device": "gfx1201", "expect": "generic"}, - {"arch": ["gfx12-generic"], "device": "gfx1250", "expect": "none"}, - {"arch": ["gfx12-generic"], "device": "gfx1250:sramecc+", "expect": "none"}, - {"arch": ["gfx12-generic"], "device": "gfx1250-strict", "expect": "none"}, - {"arch": ["gfx1250"], "device": "gfx1250-strict", "expect": "none"}, - {"arch": ["gfx1250-strict"], "device": "gfx1250-strict", "expect": "explicit"}, - {"arch": ["gfx1250"], "device": "gfx1250", "expect": "explicit"}, - {"arch": ["gfx9-4-generic"], "device": "gfx942", "expect": "none"}, - {"arch": ["gfx9-4-generic"], "device": "gfx9-4-generic", "expect": "none"}, - {"arch": ["gfx9-4-generic", "gfx942"], "device": "gfx942", "expect": "explicit"}, - {"arch": ["gfx942"], "device": "gfx942:sramecc+:xnack-", "expect": "explicit"}, - {"arch": ["gfx942"], "device": "gfx950", "expect": "none"}, - {"arch": ["gfx94"], "device": "gfx942", "expect": "none"}, - {"arch": ["gfx942", "gfx11-generic"], "device": "gfx1100", "expect": "generic"}, - {"arch": ["gfx11-generic", "gfx12-generic"], "device": "gfx1200", "expect": "generic"} - ], - "overlap": [ - {"a": [], "b": ["gfx942"], "expect": true}, - {"a": ["gfx942"], "b": [], "expect": true}, - {"a": [], "b": [], "expect": true}, - {"a": ["gfx942"], "b": ["gfx950"], "expect": false}, - {"a": ["gfx942"], "b": ["gfx942"], "expect": true}, - {"a": ["gfx11-generic"], "b": ["gfx1151"], "expect": true}, - {"a": ["gfx1151"], "b": ["gfx11-generic"], "expect": true}, - {"a": ["gfx11-generic"], "b": ["gfx1154"], "expect": false}, - {"a": ["gfx11-generic"], "b": ["gfx12-generic"], "expect": false}, - {"a": ["gfx11-generic"], "b": ["gfx11-generic"], "expect": true}, - {"a": ["gfx9-4-generic"], "b": ["gfx9-4-generic"], "expect": false}, - {"a": ["gfx9-4-generic"], "b": [], "expect": true}, - {"a": ["gfx1151", "gfx942"], "b": ["gfx11-generic"], "expect": true}, - {"a": ["gfx1250"], "b": ["gfx12-generic"], "expect": false} - ], - "covers": [ - {"outer": [], "inner": ["gfx942"], "expect": true}, - {"outer": ["gfx942"], "inner": [], "expect": true}, - {"outer": ["gfx942", "gfx950"], "inner": ["gfx942"], "expect": true}, - {"outer": ["gfx942"], "inner": ["gfx942", "gfx950"], "expect": false}, - {"outer": ["gfx11-generic"], "inner": ["gfx1151"], "expect": true}, - {"outer": ["gfx11-generic"], "inner": ["gfx1154"], "expect": false}, - {"outer": ["gfx11-generic"], "inner": ["gfx1100", "gfx1153"], "expect": true}, - {"outer": ["gfx1151"], "inner": ["gfx11-generic"], "expect": false}, - {"outer": ["gfx11-generic"], "inner": ["gfx11-generic"], "expect": true}, - {"outer": ["gfx11-generic"], "inner": ["gfx12-generic"], "expect": false}, - {"outer": ["gfx1100", "gfx1101", "gfx1102", "gfx1103", "gfx1150", "gfx1151", "gfx1152", "gfx1153"], "inner": ["gfx11-generic"], "expect": true}, - {"outer": ["gfx12-generic"], "inner": ["gfx9-4-generic"], "expect": true}, - {"outer": ["gfx9-4-generic"], "inner": ["gfx9-4-generic"], "expect": true}, - {"outer": ["gfx9-4-generic"], "inner": ["gfx1151"], "expect": false} - ], - "compete": [ - {"a": [], "b": [], "expect": true}, - {"a": [], "b": ["gfx942"], "expect": false}, - {"a": ["gfx942"], "b": ["gfx942"], "expect": true}, - {"a": ["gfx942"], "b": ["gfx950"], "expect": false}, - {"a": ["gfx11-generic"], "b": ["gfx1151"], "expect": false}, - {"a": ["gfx11-generic"], "b": ["gfx11-generic"], "expect": true}, - {"a": ["gfx11-generic"], "b": [], "expect": false}, - {"a": ["gfx11-generic"], "b": ["gfx12-generic"], "expect": false}, - {"a": ["gfx1151", "gfx11-generic"], "b": ["gfx11-generic"], "expect": true}, - {"a": ["gfx11-generic"], "b": ["gfx11-generic", "gfx942"], "expect": true}, - {"a": ["gfx9-4-generic"], "b": ["gfx9-4-generic"], "expect": false}, - {"a": ["gfx9-4-generic"], "b": [], "expect": false}, - {"a": ["gfx1151"], "b": ["gfx11-generic", "gfx1151"], "expect": true}, - {"a": ["gfx11-generic"], "b": ["gfx1151", "gfx1100"], "expect": false}, - {"a": ["gfx1250"], "b": ["gfx1250-strict"], "expect": false}, - {"a": ["gfx1250-strict"], "b": ["gfx1250-strict"], "expect": true} - ] -} diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp index b003d7a3d184..26b9c74a9773 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp @@ -67,26 +67,17 @@ TEST(TestIngestorArchGate, AdmitsWhenAnyListedArchMatches) EXPECT_FALSE(archSupports(family, "gfx90a")); } -TEST(TestIngestorArchGate, AdmitsADeviceThroughAGenericThatContainsIt) +TEST(TestIngestorArchGate, AdmitsOnlyTheMembersOfAGeneric) { EXPECT_TRUE(archSupports({"gfx11-generic"}, "gfx1151")); EXPECT_TRUE(archSupports({"gfx942", "gfx12-generic"}, "gfx1201")); -} - -TEST(TestIngestorArchGate, RefusesADeviceOutsideTheGenericsMembers) -{ + // Features on the device never change membership. + EXPECT_TRUE(archSupports({"gfx11-generic"}, "gfx1151:sramecc+:xnack-")); // gfx1154 and gfx1250 are real-looking siblings that no table row lists; membership is // data, so a name that merely shares the family stem is refused. EXPECT_FALSE(archSupports({"gfx11-generic"}, "gfx1154")); - EXPECT_FALSE(archSupports({"gfx12-generic"}, "gfx1100")); - EXPECT_FALSE(archSupports({"gfx12-generic"}, "gfx1250")); - EXPECT_FALSE(archSupports({"gfx9-4-generic"}, "gfx942")); -} - -TEST(TestIngestorArchGate, AdmitsAFeatureDecoratedDeviceThroughAGeneric) -{ - EXPECT_TRUE(archSupports({"gfx11-generic"}, "gfx1151:sramecc+:xnack-")); EXPECT_FALSE(archSupports({"gfx12-generic"}, "gfx1250:sramecc+")); + EXPECT_FALSE(archSupports({"gfx9-4-generic"}, "gfx942")); } TEST(TestIngestorArchGate, PrunesAPackWhoseArchExcludesTheDevice) diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp index 7691cecf798b..a861b78fbbda 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -1980,117 +1979,156 @@ TEST(TestDescriptorLoader, CarriesAPacksDeclaredArchitectures) EXPECT_EQ(sets.front().packs.front().arch, (std::vector{"gfx90a", "gfx942"})); } -/// The validator must admit exactly what archMatches admits of an authored entry: a bare -/// base id. LLVM generic targets are real gcnArchName values whose base id carries a '-', -/// so a shape check keyed on that character would make them unauthorable. -TEST(TestDescriptorLoader, AcceptsGenericTargetIds) +namespace { - const ScopedSymbols symbols; - const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("pack_arch_generic")); - auto documents = makeSetDocuments('1', "test:arch_generic"); - documentOfType(documents, ".kdp.json")["arch"] - = nlohmann::json::array({"gfx942", "gfx9-4-generic"}); - writeDocuments(dir.path(), documents); - const auto sets = loadFrom(dir.path()); +const std::vector INTACT_ONLY{"test:intact"}; +const std::vector BOTH{"test:candidate", "test:intact"}; - ASSERT_EQ(sets.size(), 1u); - ASSERT_EQ(sets.front().packs.size(), 1u); - EXPECT_EQ(sets.front().packs.front().arch, - (std::vector{"gfx942", "gfx9-4-generic"})); +nlohmann::json arch(std::initializer_list entries) +{ + return nlohmann::json(std::vector(entries)); } -/// A generic-shaped name the table does not list is not an error -- a newer table may know -/// it -- but it admits no device, and the author is told so at load. -TEST(TestDescriptorLoader, WarnsAndMatchesNothingForAGenericNameAbsentFromTheTable) +/// Authors the candidate's first kernel with @p kernelArch under a pack listing +/// @p packArch, spelled inline or as a standalone `.ukd.json`. +Documents candidateWithKernelArch(const nlohmann::json& packArch, + const nlohmann::json& kernelArch, + bool standalone) { - auto recorder - = hipdnn_test_sdk::utilities::SharedLogRecorder::withOverrideLevel(HIPDNN_SEV_WARN); - const ScopedSymbols symbols; - const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("arch_unknown_generic")); - auto documents = makeSetDocuments('1', "test:unknown_generic"); - documentOfType(documents, ".kdp.json")["arch"] = nlohmann::json::array({"gfx99-generic"}); - writeDocuments(dir.path(), documents); - - const auto sets = loadFrom(dir.path()); - - ASSERT_EQ(sets.size(), 1u); - ASSERT_EQ(sets.front().packs.size(), 1u); - const auto& packArch = sets.front().packs.front().arch; - EXPECT_EQ(packArch, std::vector{"gfx99-generic"}); - for(const auto* device : {"gfx942", "gfx1100", "gfx99-generic"}) + auto documents = makeSetDocuments('2', "test:candidate"); + if(standalone) { - EXPECT_FALSE(archSupports(packArch, device)) << device; + referenceLastKernel(documents); + documentOfType(documents, ".ukd.json")["arch"] = kernelArch; + documentOfType(documents, ".kdp.json")["arch"] = packArch; + return documents; } - EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_WARN, "arch entry 'gfx99-generic' in ")) - << recorder.getRecordedLogsAsString(); - EXPECT_TRUE(recorder.hasLogContaining( - HIPDNN_SEV_WARN, - "is a generic target name absent from the generic target table; it matches no device")) - << recorder.getRecordedLogsAsString(); + auto& pack = documentOfType(documents, ".kdp.json"); + pack["arch"] = packArch; + pack.at("kernelDescriptors")[0]["arch"] = kernelArch; + return documents; } -/// A known generic does not warn. -TEST(TestDescriptorLoader, DoesNotWarnForAGenericNameInTheTable) +/// The names of the engines that load when @p candidate sits beside an intact engine. +/// Sorted, so a case asserts which pack survives, not the order the catalog lists it in. +std::vector engineNames(const Documents& candidate) { - auto recorder - = hipdnn_test_sdk::utilities::SharedLogRecorder::withOverrideLevel(HIPDNN_SEV_WARN); - const ScopedSymbols symbols; - const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("arch_known_generic")); - auto documents = makeSetDocuments('1', "test:known_generic"); - documentOfType(documents, ".kdp.json")["arch"] = nlohmann::json::array({"gfx11-generic"}); - writeDocuments(dir.path(), documents); + const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("generic_arch_rule")); + writeDocuments(dir.path(), makeSetDocuments('1', "test:intact")); + writeDocuments(dir.path(), candidate); - ASSERT_EQ(loadFrom(dir.path()).size(), 1u); - EXPECT_FALSE(recorder.hasLogContaining(HIPDNN_SEV_WARN, "absent from the generic target table")) - << recorder.getRecordedLogsAsString(); + std::vector names; + for(const auto& set : loadFrom(dir.path())) + { + names.push_back(set.engine.name); + } + std::sort(names.begin(), names.end()); + return names; } -/// Loader leniency: coverage is over expanded member sets, so an inline kernel naming one -/// member sits within a generic pack. The authoring tools are stricter; this is the last -/// line. -TEST(TestDescriptorLoader, AcceptsAnInlineKernelNamingAMemberUnderAGenericPack) +} // namespace + +/// An authored generic spells a bare base id with a '-' in it, which the arch validator +/// must admit; it sits in a list beside an unrelated explicit id, and a kernel spelled +/// with the same list keeps it. +TEST(TestDescriptorLoader, AcceptsAGenericTargetBesideAnUnrelatedExplicitId) { - const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("inline_member")); - auto documents = makeSetDocuments('1', "test:inline_member"); + const ScopedSymbols symbols; + const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("pack_arch_generic")); + auto documents = makeSetDocuments('1', "test:arch_generic"); auto& pack = documentOfType(documents, ".kdp.json"); - pack["arch"] = nlohmann::json::array({"gfx11-generic"}); - pack.at("kernelDescriptors")[0]["arch"] = nlohmann::json::array({"gfx1151"}); + pack["arch"] = arch({"gfx942", "gfx11-generic"}); + pack.at("kernelDescriptors")[0]["arch"] = arch({"gfx942", "gfx11-generic"}); writeDocuments(dir.path(), documents); const auto sets = loadFrom(dir.path()); ASSERT_EQ(sets.size(), 1u); ASSERT_EQ(sets.front().packs.size(), 1u); - EXPECT_EQ(sets.front().packs.front().kernels.front().arch, std::vector{"gfx1151"}); + EXPECT_EQ(sets.front().packs.front().arch, + (std::vector{"gfx942", "gfx11-generic"})); + EXPECT_EQ(sets.front().packs.front().kernels.front().arch, + (std::vector{"gfx942", "gfx11-generic"})); } -/// An inline kernel that names one member and one device outside the generic reaches past -/// the pack. -TEST(TestDescriptorLoader, RejectsAnInlineKernelReachingOutsideTheGenericsMembers) +/// R1: a generic-shaped name the table does not list matches no device, so the file that +/// names it is dropped, whether the name is the pack's or a kernel's. +TEST(TestDescriptorLoader, DropsAPackNamingAGenericAbsentFromTheTable) { - auto recorder - = hipdnn_test_sdk::utilities::SharedLogRecorder::withOverrideLevel(HIPDNN_SEV_ERROR); - const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("inline_outside")); - auto documents = makeSetDocuments('1', "test:inline_outside"); - auto& pack = documentOfType(documents, ".kdp.json"); - pack["arch"] = nlohmann::json::array({"gfx11-generic"}); - pack.at("kernelDescriptors")[0]["arch"] = nlohmann::json::array({"gfx1151", "gfx942"}); - writeDocuments(dir.path(), documents); + EXPECT_EQ(engineNames(candidateWithKernelArch(arch({"gfx99-generic"}), arch({}), false)), + INTACT_ONLY); + EXPECT_EQ(engineNames(candidateWithKernelArch(arch({}), arch({"gfx99-generic"}), false)), + INTACT_ONLY); + EXPECT_EQ(engineNames(candidateWithKernelArch(arch({}), arch({"gfx99-generic"}), true)), + INTACT_ONLY); +} - EXPECT_TRUE(loadFrom(dir.path()).empty()); - EXPECT_TRUE(recorder.hasLogContaining( - HIPDNN_SEV_ERROR, - "declares arch [gfx1151, gfx942], which reaches past the pack's [gfx11-generic]")) - << recorder.getRecordedLogsAsString(); +/// R2: one list may not hold a generic together with a member it contains. Generics with +/// no member in common coexist. +TEST(TestDescriptorLoader, DropsAPackListingAGenericAndOneOfItsMembers) +{ + EXPECT_EQ( + engineNames(candidateWithKernelArch(arch({"gfx11-generic", "gfx1151"}), arch({}), false)), + INTACT_ONLY); + EXPECT_EQ( + engineNames(candidateWithKernelArch(arch({}), arch({"gfx1151", "gfx11-generic"}), false)), + INTACT_ONLY); + EXPECT_EQ(engineNames(candidateWithKernelArch( + arch({"gfx11-generic", "gfx12-generic", "gfx942"}), arch({}), false)), + BOTH); +} + +/// R3: a kernel names a generic only when its pack lists that generic itself, even when +/// the pack's explicit entries already cover every member. +TEST(TestDescriptorLoader, DropsAPackWhoseKernelNamesAGenericThePackDoesNotList) +{ + nlohmann::json members = nlohmann::json::array(); + for(std::size_t i = 0; i < hipdnn_plugin_sdk::findGenericTarget("gfx11-generic")->memberCount; + ++i) + { + members.push_back( + std::string(hipdnn_plugin_sdk::findGenericTarget("gfx11-generic")->members[i])); + } + for(const bool standalone : {false, true}) + { + EXPECT_EQ( + engineNames(candidateWithKernelArch(members, arch({"gfx11-generic"}), standalone)), + INTACT_ONLY) + << (standalone ? "standalone" : "inline"); + EXPECT_EQ(engineNames(candidateWithKernelArch( + arch({"gfx11-generic", "gfx942"}), arch({"gfx11-generic"}), standalone)), + BOTH) + << (standalone ? "standalone" : "inline"); + } +} + +/// R4: under a pack listing a generic, a kernel with an arch of its own lists every +/// generic of the pack and only entries the pack lists. A kernel with no arch inherits. +TEST(TestDescriptorLoader, DropsAPackWhoseKernelOmitsOrExceedsTheGenericPackArch) +{ + for(const bool standalone : {false, true}) + { + EXPECT_EQ(engineNames(candidateWithKernelArch( + arch({"gfx11-generic", "gfx942"}), arch({"gfx942"}), standalone)), + INTACT_ONLY) + << (standalone ? "standalone" : "inline"); + EXPECT_EQ(engineNames(candidateWithKernelArch( + arch({"gfx11-generic"}), arch({"gfx1151"}), standalone)), + INTACT_ONLY) + << (standalone ? "standalone" : "inline"); + EXPECT_EQ(engineNames(candidateWithKernelArch( + arch({"gfx11-generic", "gfx942"}), arch({"gfx11-generic"}), standalone)), + BOTH) + << (standalone ? "standalone" : "inline"); + } + EXPECT_EQ(engineNames(candidateWithKernelArch(arch({"gfx11-generic"}), arch({}), false)), BOTH); } /// Unchanged loader rule: a standalone kernel id defined for the generic and for one of its /// members is two covered definitions under a generic pack, and nothing ranks them. TEST(TestDescriptorLoader, StillRejectsAStandaloneKernelDefinedForAGenericAndForOneOfItsMembers) { - auto recorder - = hipdnn_test_sdk::utilities::SharedLogRecorder::withOverrideLevel(HIPDNN_SEV_ERROR); const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("generic_and_member")); auto documents = makeSetDocuments('1', "test:generic_and_member"); referenceLastKernel(documents); @@ -2104,64 +2142,6 @@ TEST(TestDescriptorLoader, StillRejectsAStandaloneKernelDefinedForAGenericAndFor writeDocuments(dir.path() / "gfx1151", {TestDocument{".ukd.json", member}}); EXPECT_TRUE(loadFrom(dir.path()).empty()); - EXPECT_TRUE(recorder.hasLogContaining( - HIPDNN_SEV_ERROR, "which several descriptors define within the pack's arch")) - << recorder.getRecordedLogsAsString(); -} - -namespace -{ - -/// A generic pack of one referenced kernel, authored as a generic UKD under the pack's -/// generic spelling. -Documents makeGenericPackDocuments(char tag, const std::string& engineName) -{ - auto documents = makeSetDocuments(tag, engineName); - referenceLastKernel(documents); - auto& pack = documentOfType(documents, ".kdp.json"); - pack["arch"] = nlohmann::json::array({"gfx11-generic"}); - const auto referenced = documentOfType(documents, ".ukd.json").at("id"); - pack["kernelDescriptors"] = nlohmann::json::array({referenced}); - documentOfType(documents, ".ukd.json")["arch"] = nlohmann::json::array({"gfx11-generic"}); - return documents; -} - -} // namespace - -/// Regression guard for the packer's per-member-folder layout, which writes one identical -/// copy of a generic tree under every selected member's directory. Passes on the existing -/// identical-duplicate collapse; it does not prove new loader work. -TEST(TestDescriptorLoader, CollapsesIdenticalGenericCopiesAcrossMemberDirectories) -{ - const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("generic_copies")); - const auto documents = makeGenericPackDocuments('1', "test:generic_copies"); - writeDocuments(dir.path() / "gfx1100", documents); - writeDocuments(dir.path() / "gfx1151", documents); - - const auto sets = loadFrom(dir.path()); - - ASSERT_EQ(sets.size(), 1u); - ASSERT_EQ(sets.front().packs.size(), 1u); - EXPECT_EQ(sets.front().packs.front().arch, std::vector{"gfx11-generic"}); - EXPECT_EQ(sets.front().packs.front().kernels.size(), 1u); -} - -/// Regression guard, same status: two member directories whose generic copies differ are -/// an ordinary same-id-and-arch conflict and drop together. -TEST(TestDescriptorLoader, DropsBothWhenGenericCopiesInMemberDirectoriesDiffer) -{ - auto recorder - = hipdnn_test_sdk::utilities::SharedLogRecorder::withOverrideLevel(HIPDNN_SEV_ERROR); - const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("generic_copies_differ")); - auto first = makeGenericPackDocuments('1', "test:generic_copies_differ"); - auto second = first; - documentOfType(second, ".ukd.json")["kernel_source"]["source_file"] = "Other.cpp"; - writeDocuments(dir.path() / "gfx1100", first); - writeDocuments(dir.path() / "gfx1151", second); - - EXPECT_TRUE(loadFrom(dir.path()).empty()); - EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_ERROR, "both define")) - << recorder.getRecordedLogsAsString(); } /// The default: a pack naming no architecture applies everywhere, so absence must parse diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp index e996b6e4a58c..35513b93f05d 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp @@ -3,14 +3,12 @@ #ifdef HIPDNN_ENABLE_KERNEL_INGESTOR -#include #include #include #include #include #include -#include #include #include @@ -18,8 +16,7 @@ /** * @file TestGenericTargets.cpp * @brief The generic GPU target table and the tier algebra over it, as C++ evaluates - * them. The table is the generated constexpr form of data/gpu_generic_targets.json - * and the golden vectors are shared with the Python implementation. + * them, on the generated constexpr table. */ namespace { @@ -29,55 +26,6 @@ using namespace hipdnn_plugin_sdk::ingestor; using Arch = std::vector; -nlohmann::json readJson(const char* path) -{ - std::ifstream stream(path); - EXPECT_TRUE(stream.is_open()) << "cannot open " << path; - return nlohmann::json::parse(stream); -} - -Arch toArch(const nlohmann::json& list) -{ - return list.get(); -} - -const char* tierName(const std::optional& tier) -{ - if(!tier) - { - return "none"; - } - if(*tier == ArchTier::EXPLICIT) - { - return "explicit"; - } - if(*tier == ArchTier::GENERIC) - { - return "generic"; - } - return "unrestricted"; -} - -TEST(TestGenericTargets, GeneratedTableMatchesTheJsonMemberForMember) -{ - const auto json = readJson(HIPDNN_GPU_GENERIC_TARGETS_JSON_PATH); - const auto& generics = json.at("generics"); - - ASSERT_EQ(generics.size(), generated::GENERIC_TARGET_ROW_COUNT); - // Rows are matched by name: object key order is not part of the contract, member - // array order is. - for(const auto& [name, members] : generics.items()) - { - const auto* row = findGenericTarget(name); - ASSERT_NE(row, nullptr) << name; - ASSERT_EQ(row->memberCount, members.size()) << name; - for(std::size_t i = 0; i < members.size(); ++i) - { - EXPECT_EQ(row->members[i], members[i].get()) << name << "[" << i << "]"; - } - } -} - TEST(TestGenericTargets, FindsAGenericOnlyByItsExactName) { const auto* row = findGenericTarget("gfx11-generic"); @@ -108,12 +56,15 @@ TEST(TestGenericTargets, ContainsReportsMembership) TEST(TestGenericTargets, EntryTierRanksExplicitAboveGeneric) { - EXPECT_EQ(archEntryTier("gfx1151", "gfx1151"), ArchTier::EXPLICIT); - EXPECT_EQ(archEntryTier("gfx11-generic", "gfx1151"), ArchTier::GENERIC); - EXPECT_EQ(archEntryTier("gfx1150", "gfx1151"), std::nullopt); - EXPECT_EQ(archEntryTier("gfx11-generic", "gfx1154"), std::nullopt); - EXPECT_LT(static_cast(ArchTier::EXPLICIT), static_cast(ArchTier::GENERIC)); - EXPECT_LT(static_cast(ArchTier::GENERIC), static_cast(ArchTier::UNRESTRICTED)); + EXPECT_EQ(entryTier("gfx1151", "gfx1151"), ArchTier::EXPLICIT); + EXPECT_EQ(entryTier("gfx1151", "gfx1151:sramecc+:xnack-"), ArchTier::EXPLICIT); + EXPECT_EQ(entryTier("gfx11-generic", "gfx1151"), ArchTier::GENERIC); + EXPECT_EQ(entryTier("gfx11-generic", "gfx1151:sramecc+:xnack-"), ArchTier::GENERIC); + EXPECT_EQ(entryTier("gfx1150", "gfx1151"), std::nullopt); + EXPECT_EQ(entryTier("gfx11-generic", "gfx1154"), std::nullopt); + EXPECT_EQ(entryTier("gfx9-4-generic", "gfx9-4-generic"), std::nullopt); + EXPECT_LT(ArchTier::EXPLICIT, ArchTier::GENERIC); + EXPECT_LT(ArchTier::GENERIC, ArchTier::UNRESTRICTED); } TEST(TestGenericTargets, ListTierIsTheBestOfItsEntries) @@ -180,38 +131,6 @@ TEST(TestGenericTargets, UnknownGenericExpandsToNothing) EXPECT_FALSE(archesCompete(unknown, unknown)); } -TEST(TestGenericTargets, MatchesGoldenTierVectors) -{ - const auto vectors = readJson(HIPDNN_ARCH_TIER_VECTORS_JSON_PATH); - - ASSERT_FALSE(vectors.at("tier").empty()); - for(const auto& v : vectors.at("tier")) - { - EXPECT_STREQ(tierName(archTier(toArch(v.at("arch")), v.at("device").get())), - v.at("expect").get().c_str()) - << v.dump(); - } - ASSERT_FALSE(vectors.at("overlap").empty()); - for(const auto& v : vectors.at("overlap")) - { - EXPECT_EQ(archOverlaps(toArch(v.at("a")), toArch(v.at("b"))), v.at("expect").get()) - << v.dump(); - } - ASSERT_FALSE(vectors.at("covers").empty()); - for(const auto& v : vectors.at("covers")) - { - EXPECT_EQ(archCovers(toArch(v.at("outer")), toArch(v.at("inner"))), - v.at("expect").get()) - << v.dump(); - } - ASSERT_FALSE(vectors.at("compete").empty()); - for(const auto& v : vectors.at("compete")) - { - EXPECT_EQ(archesCompete(toArch(v.at("a")), toArch(v.at("b"))), v.at("expect").get()) - << v.dump(); - } -} - } // namespace #endif // HIPDNN_ENABLE_KERNEL_INGESTOR diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp index 846931dc4a6b..da47c0281d5c 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp @@ -314,90 +314,53 @@ TEST(TestKernelIngestorStateManager, OffersAnUnstampedKernelEverywhereItsPackRea } } -/// Two packs share a tuple whenever no device ranks them at the same arch tier: an -/// arch-independent pack is UNRESTRICTED everywhere, a per-arch pack is EXPLICIT on its own -/// devices, so on those devices the explicit kernel shadows the unrestricted one and -/// elsewhere the unrestricted one is the only candidate. Both pack orders are exercised so -/// a keep-the-first-inserted shortcut fails. -TEST(TestKernelIngestorStateManager, AdmitsATupleSharedByAnArchIndependentAndAPerArchPack) +/// Packs share a tuple whenever no device ranks them at the same arch tier. One tuple +/// offered at all three tiers keeps, per device, only the best: EXPLICIT over GENERIC over +/// UNRESTRICTED. Every pack order is exercised so a keep-the-first-inserted shortcut fails, +/// and each device is asked twice, interleaved, so the second answer comes from the +/// per-(graph, device) catalog cache and must not be another device's. +TEST(TestKernelIngestorStateManager, KeepsOnlyTheBestArchTierOfASharedTuplePerDevice) { const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); - for(const bool explicitFirst : {true, false}) - { - auto anywhere = makePack({GRAPH_MATCHER_ID}); - anywhere.kernels = {makeKernel(testId(0x93), "kernel_anywhere", 64, "FLOAT")}; - auto pinned = makePack({GRAPH_MATCHER_ID}, {"gfx942"}); - pinned.id = testId(0x94); - pinned.kernels = {makeKernel(testId(0x95), "kernel_gfx942", 64, "FLOAT")}; - - const StateManager manager(makeSchema(), - makeTestMatchers(), - makeTestDispatches(), - explicitFirst ? std::vector{pinned, anywhere} - : std::vector{anywhere, pinned}, - std::make_shared(SCORE_SYMBOL), - "test.graph"); - - const TestGraph graph(makeGraphId(0x25)); - const auto definitionsFor = [&](int deviceId, const char* deviceArch) { - auto properties = testDeviceProperties(); - properties.gcnArchName = deviceArch; - return manager.unsortedDefinitions(MatchContext{graph, deviceId, properties}); - }; - - const auto onGfx942 = definitionsFor(0, "gfx942:sramecc+"); - ASSERT_EQ(onGfx942.size(), 1U) << "explicitFirst=" << explicitFirst; - EXPECT_EQ(onGfx942.front().kernelId, testId(0x95)) << "explicitFirst=" << explicitFirst; - - const auto onGfx90a = definitionsFor(1, "gfx90a:sramecc+:xnack-"); - ASSERT_EQ(onGfx90a.size(), 1U) << "explicitFirst=" << explicitFirst; - EXPECT_EQ(onGfx90a.front().kernelId, testId(0x93)) << "explicitFirst=" << explicitFirst; - } -} - -/// A generic pack and a pack naming one of its members never rank the same on any device: -/// the member's own device sees EXPLICIT against GENERIC, the other members see only the -/// generic one. -TEST(TestKernelIngestorStateManager, AdmitsATupleSharedByAGenericPackAndAnExplicitMemberPack) -{ - const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); - const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); - - for(const bool explicitFirst : {true, false}) + auto anywhere = makePack({GRAPH_MATCHER_ID}); + anywhere.kernels = {makeKernel(testId(0x93), "kernel_anywhere", 64, "FLOAT")}; + auto generic = makePack({GRAPH_MATCHER_ID}, {"gfx11-generic"}); + generic.id = testId(0x94); + generic.kernels = {makeKernel(testId(0x95), "kernel_generic", 64, "FLOAT")}; + auto member = makePack({GRAPH_MATCHER_ID}, {"gfx1151"}); + member.id = testId(0x96); + member.kernels = {makeKernel(testId(0x97), "kernel_gfx1151", 64, "FLOAT")}; + + std::vector> orders{ + {anywhere, generic, member}, {member, generic, anywhere}, {generic, member, anywhere}}; + uint8_t graphIndex = 0x25; + for(const auto& packs : orders) { - auto generic = makePack({GRAPH_MATCHER_ID}, {"gfx11-generic"}); - generic.kernels = {makeKernel(testId(0x93), "kernel_generic", 64, "FLOAT")}; - auto member = makePack({GRAPH_MATCHER_ID}, {"gfx1151"}); - member.id = testId(0x94); - member.kernels = {makeKernel(testId(0x95), "kernel_gfx1151", 64, "FLOAT")}; - const StateManager manager(makeSchema(), makeTestMatchers(), makeTestDispatches(), - explicitFirst ? std::vector{member, generic} - : std::vector{generic, member}, + packs, std::make_shared(SCORE_SYMBOL), "test.graph"); - const TestGraph graph(makeGraphId(0x26)); - const auto definitionsFor = [&](int deviceId, const char* deviceArch) { + const TestGraph graph(makeGraphId(graphIndex++)); + const auto winnerOn = [&](int deviceId, const char* deviceArch) { auto properties = testDeviceProperties(); properties.gcnArchName = deviceArch; - return manager.unsortedDefinitions(MatchContext{graph, deviceId, properties}); + const auto definitions + = manager.unsortedDefinitions(MatchContext{graph, deviceId, properties}); + EXPECT_EQ(definitions.size(), 1U) << deviceArch; + return definitions.empty() ? DescriptorId{} : definitions.front().kernelId; }; - const auto onMember = definitionsFor(0, "gfx1151"); - ASSERT_EQ(onMember.size(), 1U) << "explicitFirst=" << explicitFirst; - EXPECT_EQ(onMember.front().kernelId, testId(0x95)) << "explicitFirst=" << explicitFirst; - - const auto onOtherMember = definitionsFor(1, "gfx1100"); - ASSERT_EQ(onOtherMember.size(), 1U) << "explicitFirst=" << explicitFirst; - EXPECT_EQ(onOtherMember.front().kernelId, testId(0x93)) - << "explicitFirst=" << explicitFirst; - - EXPECT_TRUE(definitionsFor(2, "gfx942").empty()) << "explicitFirst=" << explicitFirst; + for(int round = 0; round < 2; ++round) + { + EXPECT_EQ(winnerOn(0, "gfx1151:sramecc+"), testId(0x97)) << "round " << round; + EXPECT_EQ(winnerOn(1, "gfx1100"), testId(0x95)) << "round " << round; + EXPECT_EQ(winnerOn(2, "gfx942:sramecc+"), testId(0x93)) << "round " << round; + } } } @@ -414,64 +377,13 @@ TEST(TestKernelIngestorStateManager, RejectsATupleSharedByTwoPacksAtTheSameGener second.id = testId(0x94); second.kernels = {makeKernel(testId(0x95), "kernel_generic_b", 64, "FLOAT")}; - try - { - const StateManager manager(makeSchema(), - makeTestMatchers(), - makeTestDispatches(), - {first, second}, - std::make_shared(SCORE_SYMBOL), - "test.graph"); - FAIL() << "two packs at the same generic tier must not construct"; - } - catch(const std::invalid_argument& error) - { - EXPECT_NE(std::string(error.what()).find("at the same arch tier"), std::string::npos) - << error.what(); - } -} - -/// Shadowing runs on the admitted catalog: a generic kernel and an unrestricted kernel -/// of one tuple coexist at load, and on a member device only the generic one survives. -/// The drop is logged at INFO, naming the shadowing kernel. -TEST(TestKernelIngestorStateManager, ShadowsTheLowerTierKernelAfterMatcherAdmission) -{ - auto recorder - = hipdnn_test_sdk::utilities::SharedLogRecorder::withOverrideLevel(HIPDNN_SEV_INFO); - - const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); - const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); - - for(const bool betterFirst : {true, false}) - { - auto generic = makePack({GRAPH_MATCHER_ID}, {"gfx11-generic"}); - generic.kernels = {makeKernel(testId(0x93), "kernel_generic", 64, "FLOAT")}; - auto anywhere = makePack({GRAPH_MATCHER_ID}); - anywhere.id = testId(0x94); - anywhere.kernels = {makeKernel(testId(0x95), "kernel_anywhere", 64, "FLOAT")}; - - const StateManager manager(makeSchema(), - makeTestMatchers(), - makeTestDispatches(), - betterFirst ? std::vector{generic, anywhere} - : std::vector{anywhere, generic}, - std::make_shared(SCORE_SYMBOL), - "test.graph"); - - const TestGraph graph(makeGraphId(0x27)); - auto properties = testDeviceProperties(); - properties.gcnArchName = "gfx1100"; - const auto definitions = manager.unsortedDefinitions(MatchContext{graph, 0, properties}); - - ASSERT_EQ(definitions.size(), 1U) << "betterFirst=" << betterFirst; - EXPECT_EQ(definitions.front().kernelId, testId(0x93)) << "betterFirst=" << betterFirst; - } - - EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_INFO, - "kernel " + toString(testId(0x95)) - + " is shadowed by kernel " + toString(testId(0x93)) - + " at a better arch tier")) - << recorder.getRecordedLogsAsString(); + EXPECT_THROW(StateManager(makeSchema(), + makeTestMatchers(), + makeTestDispatches(), + {first, second}, + std::make_shared(SCORE_SYMBOL), + "test.graph"), + std::invalid_argument); } /// Kernel matchers that decline the explicit kernel by name, so the better-tier kernel is @@ -568,46 +480,6 @@ TEST(TestKernelIngestorStateManager, AWinnerRecordNamingAShadowedKernelFallsBack } } -/// One manager, one graph, two devices: the per-(graph, device) catalog cache must keep the -/// member device's shadowed catalog from answering for the device the better tier does not -/// reach. -TEST(TestKernelIngestorStateManager, KeepsTheLowerTierKernelOnDevicesTheBetterTierDoesNotReach) -{ - const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); - const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); - - auto generic = makePack({GRAPH_MATCHER_ID}, {"gfx11-generic"}); - generic.kernels = {makeKernel(testId(0x93), "kernel_generic", 64, "FLOAT")}; - auto anywhere = makePack({GRAPH_MATCHER_ID}); - anywhere.id = testId(0x94); - anywhere.kernels = {makeKernel(testId(0x95), "kernel_anywhere", 64, "FLOAT")}; - - const StateManager manager(makeSchema(), - makeTestMatchers(), - makeTestDispatches(), - {anywhere, generic}, - std::make_shared(SCORE_SYMBOL), - "test.graph"); - - const TestGraph graph(makeGraphId(0x2A)); - auto member = testDeviceProperties(); - member.gcnArchName = "gfx1151"; - auto nonMember = testDeviceProperties(); - nonMember.gcnArchName = "gfx942:sramecc+"; - - // Queried twice each, interleaved, so the second answer of each comes from the cache. - for(int round = 0; round < 2; ++round) - { - const auto onMember = manager.unsortedDefinitions(MatchContext{graph, 0, member}); - ASSERT_EQ(onMember.size(), 1U) << "round " << round; - EXPECT_EQ(onMember.front().kernelId, testId(0x93)) << "round " << round; - - const auto onNonMember = manager.unsortedDefinitions(MatchContext{graph, 1, nonMember}); - ASSERT_EQ(onNonMember.size(), 1U) << "round " << round; - EXPECT_EQ(onNonMember.front().kernelId, testId(0x95)) << "round " << round; - } -} - TEST(TestKernelIngestorStateManager, APackIsPrunedWhenAnyOfItsCriteriaFails) { // A pack passes only if every criterion it lists passes: the first admits it, the From 1acbb24b26042d284aa8c0add7f0865bc04672be Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 02:06:34 +0000 Subject: [PATCH 29/52] ingestor-generator: arch rules R1-R4/R6, literal tier tables, drop verdict harness --- .../codegen/generic_targets.py | 13 +- .../tests/test_config_loader.py | 238 ++++-------------- .../IngestorGenerator/tests/test_generator.py | 57 ++--- .../tests/test_generic_targets.py | 150 +++++++---- .../tests/test_verify_variant_sets.py | 45 ++-- 5 files changed, 200 insertions(+), 303 deletions(-) diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py b/projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py index c1b5c779b0ca..a59e2c1858c0 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py @@ -5,9 +5,8 @@ An in-repo mirror of ``hkp_pack.generic_targets`` and of the C++ loader's ``archTier``: the generator may not import the kernel provider's packaging tree -(descriptor generation must not need the kernel toolchain), so the three -implementations are held equal by the shared golden vectors in -``plugin_sdk/tests/data/arch_tier_vectors.json`` (``tests/test_generic_targets.py``). +(descriptor generation must not need the kernel toolchain), so each implementation +tests itself with literal cases (``tests/test_generic_targets.py``). Packer-side verifier tools import ``hkp_pack.generic_targets`` instead; this module is used only inside ``codegen/``. @@ -141,14 +140,6 @@ def expand(entries, table: GenericTargets): return frozenset(devices) -def overlaps(a, b, table: GenericTargets) -> bool: - """Can one device satisfy both lists? Empty overlaps everything.""" - expanded_a, expanded_b = expand(a, table), expand(b, table) - if expanded_a is None or expanded_b is None: - return True - return not expanded_a.isdisjoint(expanded_b) - - def covers(outer, inner, table: GenericTargets) -> bool: """Is every device @inner admits also admitted by @outer?""" expanded_outer, expanded_inner = expand(outer, table), expand(inner, table) diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py index 3b96250649ab..f86c5f738eea 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py @@ -7,7 +7,6 @@ import sys from pathlib import Path -from types import SimpleNamespace import pytest import yaml @@ -194,85 +193,75 @@ def test_empty_pack_arch_covers_everything(self): class TestGenericArchRules: - """The packer's generic-target rules (``hkp_pack.validate_generic_arch``), applied - before a UUID is minted so a config the generator accepts is one the packer - accepts. ``gfx11-generic`` holds gfx1100..gfx1153; ``gfx12-generic`` holds - gfx1200/gfx1201.""" + """The packer's generic-target rules R1-R4 and R6, applied before a UUID is + minted. ``gfx11-generic`` holds gfx1100..gfx1153; ``gfx12-generic`` holds + gfx1200/gfx1201. Each case is judged by accept or reject only.""" @staticmethod - def _config(pack_arch, kernel_arch): - kernel = make_kernel(arch=kernel_arch) + def _config(pack_arch, kernel_arch, kind=None): + extra = {"kernel_source": KernelSource(kind=kind)} if kind else {} + kernel = make_kernel(arch=kernel_arch, **extra) return make_minimal_config(packs=[make_pack(arch=pack_arch, kernels=[kernel])]) - def test_generic_pack_with_a_kernel_naming_a_member_is_rejected(self): - from codegen.config_loader import _check_generic_arch - - config = self._config(["gfx11-generic"], ["gfx1151"]) - with pytest.raises(ConfigError, match="must list every generic of the KDP"): - _check_generic_arch(config) - - def test_generic_pack_accepts_a_kernel_with_no_own_arch(self): - from codegen.config_loader import _check_generic_arch - - _check_generic_arch(self._config(["gfx11-generic"], [])) - - def test_generic_pack_accepts_a_kernel_declaring_exactly_the_generic(self): - from codegen.config_loader import _check_generic_arch - - _check_generic_arch(self._config(["gfx11-generic"], ["gfx11-generic"])) - - def test_mixed_generic_pack_kernel_listing_every_entry_is_accepted(self): - from codegen.config_loader import _check_generic_arch - - config = self._config(["gfx942", "gfx11-generic"], ["gfx942", "gfx11-generic"]) - _check_generic_arch(config) - - def test_mixed_generic_pack_kernel_missing_the_generic_is_rejected(self): - from codegen.config_loader import _check_generic_arch - - config = self._config(["gfx942", "gfx11-generic"], ["gfx942"]) - with pytest.raises(ConfigError, match="must list every generic of the KDP"): - _check_generic_arch(config) - - @pytest.mark.parametrize("pack_arch", [[], ["gfx942"], ["gfx942", "gfx12-generic"]]) - def test_a_kernel_naming_a_generic_the_pack_does_not_list_is_rejected( - self, pack_arch - ): + @pytest.mark.parametrize( + ("pack_arch", "kernel_arch"), + [ + (["gfx11-generic"], []), + (["gfx11-generic"], ["gfx11-generic"]), + (["gfx942", "gfx11-generic"], ["gfx942", "gfx11-generic"]), + ], + ) + def test_accepted_shapes(self, pack_arch, kernel_arch): from codegen.config_loader import _check_generic_arch - config = self._config(pack_arch, ["gfx11-generic"]) - with pytest.raises(ConfigError, match="the pack does not list 'gfx11-generic'"): - _check_generic_arch(config) + _check_generic_arch(self._config(pack_arch, kernel_arch)) @pytest.mark.parametrize( - "arch, shared", + ("pack_arch", "kernel_arch"), [ - (["gfx11-generic", "gfx1151"], "gfx11-generic contains gfx1151"), - (["gfx1151", "gfx11-generic"], "gfx11-generic contains gfx1151"), + # R1: a generic-shaped name the table does not list. + (["gfx9-4-generic"], []), + ([], ["gfx9-4-generic"]), + # R2: a generic beside a member it contains, in either order and on + # either list (the real table's generics are disjoint). + (["gfx11-generic", "gfx1151"], []), + (["gfx1151", "gfx11-generic"], []), + (["gfx942", "gfx11-generic"], ["gfx1151", "gfx11-generic"]), + # R3: a kernel naming a generic its pack does not list. + ([], ["gfx11-generic"]), + (["gfx942"], ["gfx11-generic"]), + (["gfx942", "gfx12-generic"], ["gfx11-generic"]), + # R4: a kernel with its own arch under a generic pack must list every + # generic of the pack and only entries the pack lists. + (["gfx11-generic"], ["gfx1151"]), + (["gfx942", "gfx11-generic"], ["gfx942"]), + (["gfx11-generic", "gfx12-generic"], ["gfx11-generic"]), ], ) - def test_a_generic_and_its_member_in_one_arch_list_is_rejected(self, arch, shared): + def test_rejected_shapes(self, pack_arch, kernel_arch): from codegen.config_loader import _check_generic_arch - with pytest.raises(ConfigError, match=shared): - _check_generic_arch(self._config(arch, [])) - # The kernel's own list is held to the same rule. - with pytest.raises(ConfigError, match=shared): - _check_generic_arch(self._config(["gfx942", "gfx11-generic"], arch)) + with pytest.raises(ConfigError): + _check_generic_arch(self._config(pack_arch, kernel_arch)) - def test_unknown_generic_name_is_rejected(self): + @pytest.mark.parametrize( + ("pack_arch", "kernel_arch"), + [ + (["gfx11-generic"], []), + (["gfx942", "gfx11-generic"], []), + ([], ["gfx11-generic"]), + ], + ) + def test_rocke_under_a_generic_is_rejected(self, pack_arch, kernel_arch): from codegen.config_loader import _check_generic_arch - with pytest.raises( - ConfigError, match="'gfx9-4-generic' is a generic target name absent" - ): - _check_generic_arch(self._config(["gfx9-4-generic"], [])) + with pytest.raises(ConfigError): + _check_generic_arch(self._config(pack_arch, kernel_arch, kind="rocke")) def test_known_generic_produces_no_unrecognised_arch_warning(self): from codegen.config_loader import _check_arch_shape - config = self._config(["gfx11-generic"], []) - assert _check_arch_shape(config) == [] + assert _check_arch_shape(self._config(["gfx11-generic"], [])) == [] def test_an_expanded_subset_is_accepted_by_the_coverage_check(self): from codegen.config_loader import _check_kernel_arch_subset_of_pack @@ -280,139 +269,12 @@ def test_an_expanded_subset_is_accepted_by_the_coverage_check(self): # Loader-equivalent coverage: gfx1151 is inside gfx11-generic's members, but # gfx1250 is not. _check_kernel_arch_subset_of_pack(self._config(["gfx11-generic"], ["gfx1151"])) - with pytest.raises(ConfigError, match="reaches past the pack's arch"): + with pytest.raises(ConfigError): _check_kernel_arch_subset_of_pack( self._config(["gfx11-generic"], ["gfx1250"]) ) -_PACKER_PYTHON = ( - Path(__file__).resolve().parents[5] - / "dnn-providers" - / "hip-kernel-provider" - / "descriptor-packaging" - / "python" -) - - -class TestGeneratorAgreesWithThePackerOnGenericRules: - """The generator and ``hkp_pack.descriptors.validate_generic_arch`` each carry a - hand-written copy of the generic-target rules. Each case runs one pack through - both: they must agree on accept/reject and name the same stable rule text.""" - - @staticmethod - def _packer_verdict(pack_arch, kernel_arch, kind): - if str(_PACKER_PYTHON) not in sys.path: - sys.path.insert(0, str(_PACKER_PYTHON)) - from hkp_pack.descriptors import Descriptor, validate_generic_arch - from hkp_pack.errors import HkpPackError - from hkp_pack.generic_targets import GenericTargets - - ukd = { - "id": "k", - "name": "k", - "kernel_source": {"kind": kind}, - **({"arch": kernel_arch} if kernel_arch else {}), - } - kdp = Descriptor( - Path("p.kdp.json"), - {"id": "p", "arch": pack_arch, "kernelDescriptors": [ukd]}, - ) - flat = SimpleNamespace( - descriptors=[kdp], - generic_targets=GenericTargets.load( - Path(__file__).resolve().parents[3] - / "plugin_sdk" - / "data" - / "gpu_generic_targets.json" - ), - ukd_by_id=lambda: {}, - kdps=lambda: [kdp], - ) - try: - validate_generic_arch(flat) - except HkpPackError as exc: - return str(exc) - return None - - @staticmethod - def _generator_verdict(pack_arch, kernel_arch, kind): - from codegen.config_loader import _validate_config - from codegen.models import KernelSource - - kernel = make_kernel( - arch=kernel_arch, - **({"kernel_source": KernelSource(kind=kind)} if kind == "rocke" else {}), - ) - config = make_minimal_config( - packs=[make_pack(arch=pack_arch, kernels=[kernel])] - ) - try: - _validate_config(config) - except ConfigError as exc: - return str(exc) - return None - - @pytest.mark.parametrize( - ("pack_arch", "kernel_arch", "kind", "rule"), - [ - (["gfx11-generic"], [], "embedded_source", None), - (["gfx11-generic"], ["gfx11-generic"], "embedded_source", None), - ( - ["gfx942", "gfx11-generic"], - ["gfx942", "gfx11-generic"], - "embedded_source", - None, - ), - ( - ["gfx11-generic"], - ["gfx1151"], - "embedded_source", - "must list every generic of the KDP", - ), - ( - ["gfx942", "gfx11-generic"], - ["gfx942"], - "embedded_source", - "must list every generic of the KDP", - ), - ( - ["gfx11-generic", "gfx12-generic"], - ["gfx11-generic"], - "embedded_source", - "must list every generic of the KDP", - ), - ([], ["gfx11-generic"], "embedded_source", "does not list"), - (["gfx9-4-generic"], [], "embedded_source", "generic target name absent"), - ( - ["gfx11-generic", "gfx1151"], - [], - "embedded_source", - "gfx11-generic contains gfx1151", - ), - (["gfx11-generic"], [], "rocke", "does not support generic targets yet"), - ( - ["gfx942", "gfx11-generic"], - [], - "rocke", - "does not support generic targets yet", - ), - ([], ["gfx11-generic"], "rocke", "does not support generic targets yet"), - ], - ) - def test_the_two_copies_of_the_rules_agree( - self, pack_arch, kernel_arch, kind, rule - ): - packer = self._packer_verdict(pack_arch, kernel_arch, kind) - generator = self._generator_verdict(pack_arch, kernel_arch, kind) - if rule is None: - assert packer is None - assert generator is None - else: - assert packer is not None and rule in packer - assert generator is not None and rule in generator - - class TestArchShapeCheck: """Pre-mint check #5: arch entries are plausible gfx-prefixed base ids (error), and unrecognized-but-well-formed ids warn rather than error.""" diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py index d65de7c98251..57fba3dccf1f 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py @@ -725,33 +725,34 @@ def test_equal_tuples_on_disjoint_arches_both_survive(self): "twin.right", ] - def test_equal_tuples_on_overlapping_arches_are_refused_naming_both(self): - """Same tuple, same device, DIFFERENT binary: dropping the second discards a - binary somebody built, keeping both drops the engine at load, so the generator - names both.""" + @pytest.mark.parametrize( + ("pack_arch", "left_arch", "right_arch", "other_binary"), + [ + # Same tuple, same device, different binary. + (["gfx942"], [], [], True), + # Same tuple, same binary, unequal overlapping coverage. + (["gfx942", "gfx950"], ["gfx942"], ["gfx942", "gfx950"], False), + # Both select gfx1100 at the generic tier. + (["gfx11-generic"], [], [], True), + ], + ) + def test_equal_tuples_tied_on_a_device_are_refused_naming_both( + self, pack_arch, left_arch, right_arch, other_binary + ): + """One tuple twice on a device drops the engine at load, so the generator + refuses and names both kernels.""" config, pack, left, right = self._twin_config() - # Same matcher-visible metadata, different compiled source. - right.kernel_source.entry_point = "ScaleAddOther" + pack.arch = pack_arch + left.arch = left_arch + right.arch = right_arch + if other_binary: + right.kernel_source.entry_point = "ScaleAddOther" with pytest.raises(ValueError) as excinfo: build_kdp(config, pack, mint_ids(config)) message = str(excinfo.value) assert "twin.left" in message and "twin.right" in message - def test_one_candidate_on_unequal_overlapping_arches_names_the_coverage(self): - """Same tuple, same device, SAME binary: there is no kernel_source or priority - difference to find, so the diagnostic must name the coverage.""" - config, pack, left, right = self._twin_config() - pack.arch = ["gfx942", "gfx950"] - left.arch = ["gfx942"] - right.arch = ["gfx942", "gfx950"] - - with pytest.raises(ValueError) as excinfo: - build_kdp(config, pack, mint_ids(config)) - message = str(excinfo.value) - assert "SAME candidate" in message - assert "kernel_source/priority differ" not in message - def test_a_wildcard_arch_and_a_concrete_one_with_one_tuple_both_survive(self): """An empty ``arch`` is the unrestricted tier and a concrete entry outranks it on its own device, so the two never tie and the matcher never sees one tuple @@ -768,22 +769,6 @@ def test_a_wildcard_arch_and_a_concrete_one_with_one_tuple_both_survive(self): "twin.right", ] - def test_two_kernels_one_tuple_under_the_same_generic_are_refused_naming_both( - self, - ): - """Both select gfx1100 at the generic tier: one tuple twice drops the engine.""" - config, pack, left, right = self._twin_config() - pack.arch = ["gfx11-generic"] - left.arch = [] - right.arch = [] - right.kernel_source.entry_point = "ScaleAddOther" - - with pytest.raises(ValueError) as excinfo: - build_kdp(config, pack, mint_ids(config)) - message = str(excinfo.value) - assert "twin.left" in message and "twin.right" in message - assert "gfx11-generic" in message - def test_an_omitted_field_and_the_kmd_default_are_one_key(self): """One tuple omits an optional field; the other states it at exactly the KMD default. Keyed on the identity function rather than an emitted pair, because diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py index 66ca72d1572a..b2b3a3ec7b6c 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py @@ -1,50 +1,112 @@ # Copyright (c) Advanced Micro Devices, Inc., or its affiliates. # SPDX-License-Identifier: MIT -"""The codegen mirror of the tier algebra, held equal to the C++ loader and to -``hkp_pack.generic_targets`` by the shared golden vectors. Fails (never skips) outside -the monorepo: the vectors and the table are located by repo-relative path.""" +"""The codegen mirror of the tier algebra, over a small in-memory table so the cases +pin the algebra rather than LLVM-owned membership data.""" -import json from pathlib import Path -from codegen import generic_targets - -_SDK_DIR = Path(__file__).resolve().parents[3] / "plugin_sdk" -_VECTORS = _SDK_DIR / "tests" / "data" / "arch_tier_vectors.json" -_TIER_NAMES = { - generic_targets.TIER_EXPLICIT: "explicit", - generic_targets.TIER_GENERIC: "generic", - generic_targets.TIER_UNRESTRICTED: "unrestricted", - None: "none", -} - - -def test_matches_the_shared_golden_vectors(): - table = generic_targets.GenericTargets.load(generic_targets.DEFAULT_TABLE_PATH) - assert ( - generic_targets.DEFAULT_TABLE_PATH - == _SDK_DIR / "data" / "gpu_generic_targets.json" - ) - vectors = json.loads(_VECTORS.read_text()) - failures = [] - for case in vectors["tier"]: - got = _TIER_NAMES[ - generic_targets.list_tier(case["arch"], case["device"], table) - ] - if got != case["expect"]: - failures.append(("tier", case, got)) - for case in vectors["overlap"]: - got = generic_targets.overlaps(case["a"], case["b"], table) - if got != case["expect"]: - failures.append(("overlap", case, got)) - for case in vectors["covers"]: - got = generic_targets.covers(case["outer"], case["inner"], table) - if got != case["expect"]: - failures.append(("covers", case, got)) - for case in vectors["compete"]: - got = generic_targets.compete(case["a"], case["b"], table) - if got != case["expect"]: - failures.append(("compete", case, got)) - assert not failures - assert all(vectors[k] for k in ("tier", "overlap", "covers", "compete")) +import pytest + +from codegen import generic_targets as gt + +_TABLE = gt.GenericTargets( + Path("table.json"), + { + "gfx11-generic": ("gfx1100", "gfx1151"), + "gfx12-generic": ("gfx1200", "gfx1201"), + }, +) +_G11 = "gfx11-generic" +_G12 = "gfx12-generic" +_UNKNOWN = "gfx9-4-generic" + +_UNRESTRICTED = gt.TIER_UNRESTRICTED +_EXPLICIT = gt.TIER_EXPLICIT +_GENERIC = gt.TIER_GENERIC + + +@pytest.mark.parametrize( + ("arch", "device", "expect"), + [ + ([], "gfx1151", _UNRESTRICTED), + ([], "gfx942:sramecc+:xnack-", _UNRESTRICTED), + (["gfx1151"], "gfx1151", _EXPLICIT), + (["gfx942"], "gfx942:sramecc+:xnack-", _EXPLICIT), + ([_G11], "gfx1100", _GENERIC), + ([_G11], "gfx1151:sramecc+", _GENERIC), + # Explicit outranks a generic that also admits the device. + (["gfx1151", _G11], "gfx1151", _EXPLICIT), + (["gfx1151", _G11], "gfx1100", _GENERIC), + (["gfx942", _G11], "gfx1100", _GENERIC), + # Non-matches. + ([_G11], "gfx1154", None), + ([_G12], "gfx1100", None), + (["gfx942"], "gfx950", None), + # An id is a whole-name (or feature-suffix) match, never a prefix match. + (["gfx94"], "gfx942", None), + (["gfx1250"], "gfx1250-strict", None), + (["gfx1250-strict"], "gfx1250-strict", _EXPLICIT), + # A generic-shaped name outside the table matches nothing, itself included. + ([_UNKNOWN], "gfx942", None), + ([_UNKNOWN], _UNKNOWN, None), + ([_UNKNOWN, "gfx942"], "gfx942", _EXPLICIT), + ], +) +def test_list_tier(arch, device, expect): + assert gt.list_tier(arch, device, _TABLE) == expect + + +@pytest.mark.parametrize( + ("outer", "inner", "expect"), + [ + ([], ["gfx942"], True), + (["gfx942"], [], True), + (["gfx942", "gfx950"], ["gfx942"], True), + (["gfx942"], ["gfx942", "gfx950"], False), + ([_G11], ["gfx1151"], True), + ([_G11], ["gfx1100", "gfx1151"], True), + ([_G11], ["gfx1154"], False), + (["gfx1151"], [_G11], False), + (["gfx1100", "gfx1151"], [_G11], True), + ([_G11], [_G11], True), + ([_G11], [_G12], False), + ([_UNKNOWN], [_UNKNOWN], True), + ([_UNKNOWN], ["gfx942"], False), + ], +) +def test_covers(outer, inner, expect): + assert gt.covers(outer, inner, _TABLE) is expect + + +@pytest.mark.parametrize( + ("a", "b", "expect"), + [ + ([], [], True), + ([], ["gfx942"], False), + ([_G11], [], False), + (["gfx942"], ["gfx942"], True), + (["gfx942"], ["gfx950"], False), + # A generic and a member it contains never tie: the member outranks it. + ([_G11], ["gfx1151"], False), + ([_G11], ["gfx1151", "gfx1100"], False), + ([_G11], [_G11], True), + ([_G11], [_G12], False), + # Tied on the generic's other member even though gfx1151 differs in tier. + (["gfx1151", _G11], [_G11], True), + ([_G11], [_G11, "gfx942"], True), + # An unknown generic admits nothing, so it ties with nothing. + ([_UNKNOWN], [_UNKNOWN], False), + ([_UNKNOWN], [], False), + ], +) +def test_compete(a, b, expect): + assert gt.compete(a, b, _TABLE) is expect + assert gt.compete(b, a, _TABLE) is expect + + +def test_the_shipped_table_loads_and_lists_gfx11_generic_members(): + table = gt.default_table() + assert table.has(_G11) + assert "gfx1151" in table.members(_G11) + assert gt.list_tier([_G11], "gfx1151", table) == _GENERIC diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py index 22f8cd3f649c..c71835119c81 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py @@ -343,34 +343,31 @@ def test_equal_tuples_overlapping_on_one_arch_collide(self, gate): assert "loader-tuple collisions" in result.stdout assert "gfx950" in result.stdout - def test_a_wildcard_arch_and_a_concrete_one_do_not_collide(self, gate): - """An absent arch list is the unrestricted tier; the concrete pack outranks it on - its own device, so the catalog holds one candidate per device and never one - tuple twice.""" - self._two_packs(gate, "wildcard", None, ["gfx950"]) - result = gate.run("w", "wildcard", profiled=False) - assert result.returncode == 0, result.stdout + result.stderr - assert "loader-tuple" not in result.stdout - - def test_a_generic_and_an_explicit_member_do_not_collide(self, gate): - self._two_packs(gate, "member", ["gfx11-generic"], ["gfx1151"]) - result = gate.run("m", "member", profiled=False) + @pytest.mark.parametrize( + ("left_arch", "right_arch"), + [ + # The better tier outranks the other on its own device: no tie. + (None, ["gfx950"]), + (["gfx11-generic"], ["gfx1151"]), + ], + ) + def test_a_better_tier_does_not_collide_with_a_worse_one( + self, gate, left_arch, right_arch + ): + self._two_packs(gate, "outranked", left_arch, right_arch) + result = gate.run("o", "outranked", profiled=False) assert result.returncode == 0, result.stdout + result.stderr assert "loader-tuple" not in result.stdout - def test_two_packs_naming_the_same_generic_collide(self, gate): - self._two_packs(gate, "twice", ["gfx11-generic"], ["gfx11-generic"]) - result = gate.run("t", "twice", profiled=False) + @pytest.mark.parametrize( + ("left_arch", "right_arch"), + [(["gfx11-generic"], ["gfx11-generic"]), (None, None)], + ) + def test_two_packs_tied_at_one_tier_collide(self, gate, left_arch, right_arch): + self._two_packs(gate, "tied", left_arch, right_arch) + result = gate.run("t", "tied", profiled=False) assert result.returncode == 1, result.stdout assert "loader-tuple collisions" in result.stdout - assert "generic tier" in result.stdout - assert "gfx1151" in result.stdout - - def test_two_wildcard_packs_collide_at_the_unrestricted_tier(self, gate): - self._two_packs(gate, "wild2", None, None) - result = gate.run("w2", "wild2", profiled=False) - assert result.returncode == 1, result.stdout - assert "unrestricted tier" in result.stdout def test_one_and_one_point_zero_are_one_tuple_on_a_float_field(self, gate): """The catalog holds a value of the field's declared type, so 1 and 1.0 on a @@ -1276,5 +1273,5 @@ def test_a_generic_document_is_read_with_its_generic_archive_key(self, tmp_path) # The control: a document naming the member has no member-keyed archive here. member = self._entry(["gfx1151"]) member.origin_dir = str(tmp_path) - with pytest.raises(gate_module.GateError, match="carries no member"): + with pytest.raises(gate_module.GateError): payloads.read(member, "gfx1151") From c35a52f5c500a9851a649fdf66a42732351dd20e Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 02:09:06 +0000 Subject: [PATCH 30/52] hipdnn: superbuild-only validator mutation tests for the generic target table --- .../hipdnn/plugin_sdk/tests/CMakeLists.txt | 66 +++++++++++++++++++ .../tests/cmake/CheckGpuGenericTargets.cmake | 46 +++++++++++++ projects/hipdnn/test_categories.yaml | 3 + 3 files changed, 115 insertions(+) create mode 100644 projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake diff --git a/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt b/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt index 62435ba5902b..ec1495f45e9c 100644 --- a/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt +++ b/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt @@ -195,3 +195,69 @@ if(HIPDNN_ENABLE_KERNEL_INGESTOR) set_property(GLOBAL APPEND_STRING PROPERTY HIPDNN_INSTALLED_CTEST_STAGING "${_disk_cache_installed_tests}") endif() + +# Validator tests for data/gpu_generic_targets.json. Build tree only: no staging into the +# installed CTestTestfile.cmake, so installed-tree runs never read the source tree. +# Each negative case mutates the real table in memory and must fail with the validator's +# text. PASS_REGULAR_EXPRESSION, never WILL_FAIL, so a crash or a missing input does not +# pass; FAIL_REGULAR_EXPRESSION catches a validator that downgrades the error. CMake wraps +# long messages at spaces, so every space in the expected text matches any whitespace run. +set(_generic_targets_check "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CheckGpuGenericTargets.cmake") + +add_test( + NAME hipdnn_gpu_generic_targets_accepts_real_table + COMMAND ${CMAKE_COMMAND} -DGENERIC_TARGETS_JSON=${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE} -P + ${_generic_targets_check} +) +set_tests_properties( + hipdnn_gpu_generic_targets_accepts_real_table PROPERTIES PASS_REGULAR_EXPRESSION + "gpu_generic_targets: valid" +) +_apply_hipdnn_test_category_labels(hipdnn_gpu_generic_targets_accepts_real_table) + +# Registers ctest hipdnn_gpu_generic_targets_rejects_: applies (see +# CheckGpuGenericTargets.cmake) with and to the real table and expects +# . +function(_hipdnn_add_generic_targets_rejection_test case op path value expected_text) + string(REPLACE " " "[ \n]+" expected_regex "${expected_text}") + set(test_name hipdnn_gpu_generic_targets_rejects_${case}) + add_test( + NAME ${test_name} + COMMAND + ${CMAKE_COMMAND} -DGENERIC_TARGETS_JSON=${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE} + -DMUTATION_OP=${op} -DMUTATION_PATH=${path} "-DMUTATION_VALUE=${value}" + -DMUTATED_JSON=${CMAKE_CURRENT_BINARY_DIR}/generic_targets_cases/${case}.json -P + ${_generic_targets_check} + ) + set_tests_properties( + ${test_name} + PROPERTIES PASS_REGULAR_EXPRESSION "${expected_regex}" FAIL_REGULAR_EXPRESSION + "gpu_generic_targets: valid" + ) + _apply_hipdnn_test_category_labels(${test_name}) +endfunction() + +file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/generic_targets_cases") +_hipdnn_add_generic_targets_rejection_test(not_json RAW "" "{ not json" "not valid JSON") +_hipdnn_add_generic_targets_rejection_test( + wrong_schema_version SET_JSON schemaVersion 2 "'schemaVersion' must be 1, got '2'" +) +_hipdnn_add_generic_targets_rejection_test( + empty_generics SET_JSON generics "{}" "'generics' must be non-empty" +) +_hipdnn_add_generic_targets_rejection_test( + empty_members SET_JSON generics.gfx12-generic "[]" + "generics.gfx12-generic must list at least one member" +) +_hipdnn_add_generic_targets_rejection_test( + duplicate_member SET_JSON generics.gfx12-generic "[\"gfx1200\",\"gfx1200\"]" + "member 'gfx1200' is listed twice in gfx12-generic" +) +_hipdnn_add_generic_targets_rejection_test( + bad_generic_name SET_JSON generics "{\"gfx12generic\":[\"gfx1200\"]}" + "generic name 'gfx12generic' does not match" +) +_hipdnn_add_generic_targets_rejection_test( + bad_member SET_STRING generics.gfx12-generic.0 gfx12zz + "member 'gfx12zz' of gfx12-generic does not match" +) diff --git a/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake b/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake new file mode 100644 index 000000000000..072345613ff7 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake @@ -0,0 +1,46 @@ +# Copyright © Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT + +# Script-mode driver for hipdnn_validate_gpu_generic_targets (run with `cmake -P`): +# cmake -DGENERIC_TARGETS_JSON= [-DMUTATION_OP= ...] -P CheckGpuGenericTargets.cmake +# Without MUTATION_OP the table is validated as is. With it, the table is mutated in +# memory, written to MUTATED_JSON, and that file is validated. MUTATION_PATH is a +# '.'-separated key/index path into the table. MUTATION_OP is one of: +# REMOVE delete the value at MUTATION_PATH +# SET_STRING set MUTATION_PATH to the string MUTATION_VALUE +# SET_JSON set MUTATION_PATH to the JSON text MUTATION_VALUE +# RAW replace the whole document with the text MUTATION_VALUE +# Prints "gpu_generic_targets: valid" only after the table passed every rule, so a ctest +# can require the validator's FATAL_ERROR text for a mutation and forbid that line. A +# crash or a missing input matches neither. + +if(NOT GENERIC_TARGETS_JSON) + message(FATAL_ERROR "CheckGpuGenericTargets: pass -DGENERIC_TARGETS_JSON=") +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/../../cmake/ValidateGpuGenericTargets.cmake") + +set(table_path "${GENERIC_TARGETS_JSON}") +if(MUTATION_OP) + if(NOT MUTATED_JSON) + message(FATAL_ERROR "CheckGpuGenericTargets: MUTATION_OP needs -DMUTATED_JSON=") + endif() + file(READ "${GENERIC_TARGETS_JSON}" json) + string(REPLACE "." ";" path "${MUTATION_PATH}") + if(MUTATION_OP STREQUAL "REMOVE") + string(JSON json REMOVE "${json}" ${path}) + elseif(MUTATION_OP STREQUAL "SET_STRING") + string(JSON json SET "${json}" ${path} "\"${MUTATION_VALUE}\"") + elseif(MUTATION_OP STREQUAL "SET_JSON") + string(JSON json SET "${json}" ${path} "${MUTATION_VALUE}") + elseif(MUTATION_OP STREQUAL "RAW") + set(json "${MUTATION_VALUE}") + else() + message(FATAL_ERROR "CheckGpuGenericTargets: unknown MUTATION_OP '${MUTATION_OP}'") + endif() + file(WRITE "${MUTATED_JSON}" "${json}") + set(table_path "${MUTATED_JSON}") +endif() + +hipdnn_validate_gpu_generic_targets("${table_path}") +message(STATUS "gpu_generic_targets: valid") diff --git a/projects/hipdnn/test_categories.yaml b/projects/hipdnn/test_categories.yaml index 77d76223c5a7..661bc9f13785 100644 --- a/projects/hipdnn/test_categories.yaml +++ b/projects/hipdnn/test_categories.yaml @@ -45,6 +45,9 @@ test_categories: - "hipdnn_frontend_tests" - "hipdnn_test_sdk_tests" - "hipdnn_plugin_sdk_tests" + # Build tree only: these run CMake driver scripts, so they are absent from the + # installed CTestTestfile.cmake. See plugin_sdk/tests/CMakeLists.txt. + - "hipdnn_gpu_generic_targets_.*" integration: description: "Current hipDNN integration-check coverage" From 75a3e3e81c2160315c2ad68bf261fbcad64569f7 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 02:17:49 +0000 Subject: [PATCH 31/52] hkp: generic archive production test, packer test consolidation, literal tier tables --- .../python/hkp_pack/generic_targets.py | 24 +- .../tests/test_desk_check_invariants.py | 51 +- .../tests/test_hkp_generic_targets.py | 106 ++-- .../tests/test_hkp_pack_generic.py | 527 +++++------------- .../shared/conv_fwd/conv_fwd_generic.kdp.json | 54 ++ .../shared/conv_fwd/conv_fwd_generic.ued.json | 16 + .../src/tests/CMakeLists.txt | 1 + .../compilation/TestKpackGenericArchive.cpp | 130 +++++ 8 files changed, 443 insertions(+), 466 deletions(-) create mode 100644 dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/test_descriptors/shared/conv_fwd/conv_fwd_generic.kdp.json create mode 100644 dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/test_descriptors/shared/conv_fwd/conv_fwd_generic.ued.json create mode 100644 dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackGenericArchive.cpp diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py index 2492ac7d055e..89e7a0cee4f8 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py @@ -2,8 +2,7 @@ One JSON document (projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json) maps each generic target (`gfx11-generic`) to the concrete processors it supports. The C++ loader -reads the same document through a generated header; this module is the Python side, and -the two are held equal by the golden vectors in plugin_sdk/tests/data/arch_tier_vectors.json. +reads the same document through a generated header; this module is the Python side. Membership is data, never name shape: a generic-shaped name that the table does not list (an "unknown generic") expands to the empty set and matches no device. Provenance of the @@ -134,10 +133,14 @@ def list_tier(entries, device, table): """Best tier of @entries for @device; an empty list is UNRESTRICTED; None = no match.""" if not entries: return TIER_UNRESTRICTED - tiers = [ - t for t in (entry_tier(e, device, table) for e in entries) if t is not None - ] - return min(tiers) if tiers else None + best = None + for entry in entries: + tier = entry_tier(entry, device, table) + if tier == TIER_EXPLICIT: + return tier + if tier is not None: + best = tier + return best def expand(entries, table): @@ -157,15 +160,6 @@ def expand(entries, table): return frozenset(devices) -def overlaps(a, b, table): - """Can one device satisfy both lists? Empty overlaps everything.""" - expanded_a = expand(a, table) - expanded_b = expand(b, table) - if expanded_a is None or expanded_b is None: - return True - return not expanded_a.isdisjoint(expanded_b) - - def covers(outer, inner, table): """Is every device @inner admits also admitted by @outer? diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py index 0a81957e32e6..3d9e990d2708 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py @@ -212,18 +212,6 @@ def test_an_arch_less_kernel_under_a_disjoint_kdp_arch_does_not_collide(self): == {} ) - def test_a_tie_at_a_worse_tier_than_the_best_is_a_duplicate(self): - """Two arch-less kernels tie at the unrestricted tier on gfx1100 even - though a third, explicit one outranks them there; the loader's pairwise - `archesCompete` refuses them.""" - kernels = [ - {"name": n, "metadata": {"dtype": "FLOAT"}, **({"arch": a} if a else {})} - for n, a in (("a", None), ("b", None), ("c", ["gfx942"])) - ] - assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == { - ("FLOAT",): 2 - } - def test_a_tie_beaten_on_every_device_it_reaches_is_still_a_duplicate(self): """Both gfx12-generic kernels are outranked by an explicit one on each member (gfx1200, gfx1201), so they never tie at the BEST tier anywhere; they still @@ -241,29 +229,26 @@ def test_a_tie_beaten_on_every_device_it_reaches_is_still_a_duplicate(self): ("FLOAT",): 2 } - def test_a_generic_and_an_arch_less_kernel_do_not_collide(self): - """The generic outranks the unrestricted kernel wherever both reach, and the - arch-less one is alone elsewhere: no device sees two at one tier.""" - kernels = self._twins(["gfx11-generic"], None) - assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == {} - - def test_a_generic_and_one_of_its_members_do_not_collide(self): - """The member is EXPLICIT on its own device and outranks the generic.""" - kernels = self._twins(["gfx11-generic"], ["gfx1100"]) - assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == {} - - def test_lists_naming_only_unknown_generics_match_nothing(self): - kernels = self._twins(["gfx99-generic"], ["gfx99-generic"]) + @pytest.mark.parametrize( + "left, right", + [ + (["gfx11-generic"], None), + (["gfx11-generic"], ["gfx1100"]), + (["gfx99-generic"], ["gfx99-generic"]), + ], + ids=["generic_vs_arch_less", "generic_vs_its_member", "unknown_generics"], + ) + def test_lists_that_never_tie_at_one_tier_do_not_collide(self, left, right): + """A generic outranks an arch-less kernel and loses to its own member; an + unknown generic matches no device.""" + kernels = self._twins(left, right) assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == {} - def test_two_arch_less_kernels_collide(self): - kernels = self._twins(None, None) - assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == { - ("FLOAT",): 2 - } - - def test_two_kernels_naming_the_same_generic_collide(self): - kernels = self._twins(["gfx11-generic"], ["gfx11-generic"]) + @pytest.mark.parametrize( + "arch", [None, ["gfx11-generic"]], ids=["arch_less", "same_generic"] + ) + def test_two_kernels_with_the_same_list_collide(self, arch): + kernels = self._twins(arch, arch) assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == { ("FLOAT",): 2 } diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py index 7b9fe92d7744..2e1b60f5a815 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py @@ -1,8 +1,7 @@ """The Python tier algebra over the shared generic target table. -The golden vectors in plugin_sdk/tests/data/arch_tier_vectors.json also drive the C++ -oracle, so a disagreement between the two implementations fails one side. This test -locates the table and the vectors itself rather than through conftest.py. +Literal cases per outcome class; this test locates the table itself rather than through +conftest.py. """ import json @@ -18,7 +17,6 @@ _SDK_DIR = Path(__file__).resolve().parents[4] / "projects" / "hipdnn" / "plugin_sdk" _TABLE = _SDK_DIR / "data" / "gpu_generic_targets.json" -_VECTORS = _SDK_DIR / "tests" / "data" / "arch_tier_vectors.json" _TIER_NAMES = { generic_targets.TIER_EXPLICIT: "explicit", @@ -33,11 +31,6 @@ def table(): return GenericTargets.load(_TABLE) -def test_default_table_path_is_the_in_repo_table(): - assert generic_targets.DEFAULT_TABLE_PATH == _TABLE - assert _TABLE.is_file() - - def test_loads_the_table_members_in_document_order(table): assert table.path == _TABLE assert sorted(table.names()) == ["gfx11-generic", "gfx12-generic"] @@ -99,33 +92,74 @@ def test_rejects_an_unreadable_or_non_json_table(tmp_path): GenericTargets.load(path) -def _vectors(): - return json.loads(_VECTORS.read_text()) - - -def test_matches_the_golden_tier_vectors(table): - vectors = _vectors() - failures = [] - for case in vectors["tier"]: - got = _TIER_NAMES[ - generic_targets.list_tier(case["arch"], case["device"], table) - ] - if got != case["expect"]: - failures.append(("tier", case, got)) - for case in vectors["overlap"]: - got = generic_targets.overlaps(case["a"], case["b"], table) - if got != case["expect"]: - failures.append(("overlap", case, got)) - for case in vectors["covers"]: - got = generic_targets.covers(case["outer"], case["inner"], table) - if got != case["expect"]: - failures.append(("covers", case, got)) - for case in vectors["compete"]: - got = generic_targets.compete(case["a"], case["b"], table) - if got != case["expect"]: - failures.append(("compete", case, got)) - assert not failures - assert all(vectors[k] for k in ("tier", "overlap", "covers", "compete")) +@pytest.mark.parametrize( + "arch, device, expect", + [ + ([], "gfx942:sramecc+:xnack-", "unrestricted"), + (["gfx1151", "gfx11-generic"], "gfx1151:sramecc+:xnack-", "explicit"), + (["gfx1151", "gfx11-generic"], "gfx1100", "generic"), + (["gfx11-generic"], "gfx1151:sramecc+", "generic"), + (["gfx11-generic"], "gfx1154", "none"), + (["gfx12-generic"], "gfx1100", "none"), + (["gfx1250"], "gfx1250-strict", "none"), + (["gfx1250-strict"], "gfx1250-strict", "explicit"), + (["gfx94"], "gfx942", "none"), + (["gfx9-4-generic", "gfx942"], "gfx942", "explicit"), + ], +) +def test_list_tier(table, arch, device, expect): + assert _TIER_NAMES[generic_targets.list_tier(arch, device, table)] == expect + + +def test_entry_tier_of_one_entry(table): + assert generic_targets.entry_tier("gfx1151", "gfx1151:xnack-", table) == ( + generic_targets.TIER_EXPLICIT + ) + assert generic_targets.entry_tier("gfx11-generic", "gfx1151", table) == ( + generic_targets.TIER_GENERIC + ) + assert generic_targets.entry_tier("gfx11-generic", "gfx942", table) is None + + +@pytest.mark.parametrize( + "outer, inner, expect", + [ + ([], ["gfx942"], True), + (["gfx942"], [], True), + (["gfx942", "gfx950"], ["gfx942"], True), + (["gfx942"], ["gfx942", "gfx950"], False), + (["gfx11-generic"], ["gfx1100", "gfx1153"], True), + (["gfx11-generic"], ["gfx1154"], False), + (["gfx1151"], ["gfx11-generic"], False), + (["gfx11-generic"], ["gfx11-generic"], True), + (["gfx11-generic"], ["gfx12-generic"], False), + (["gfx9-4-generic"], ["gfx1151"], False), + ], +) +def test_covers(table, outer, inner, expect): + assert generic_targets.covers(outer, inner, table) is expect + + +@pytest.mark.parametrize( + "a, b, expect", + [ + ([], [], True), + ([], ["gfx942"], False), + (["gfx942"], ["gfx942"], True), + (["gfx942"], ["gfx950"], False), + (["gfx11-generic"], ["gfx1151"], False), + (["gfx11-generic"], [], False), + (["gfx11-generic"], ["gfx12-generic"], False), + (["gfx11-generic"], ["gfx11-generic"], True), + (["gfx1151", "gfx11-generic"], ["gfx11-generic"], True), + (["gfx11-generic"], ["gfx11-generic", "gfx942"], True), + (["gfx1151"], ["gfx11-generic", "gfx1151"], True), + (["gfx1250"], ["gfx1250-strict"], False), + ], +) +def test_compete(table, a, b, expect): + assert generic_targets.compete(a, b, table) is expect + assert generic_targets.compete(b, a, table) is expect def test_unknown_generic_is_not_a_member(table): diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py index a7f7a7b1dbf6..2073edba78f9 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py @@ -4,9 +4,8 @@ once, under the generic's spelling, and that one tree is copied into the folder of every selected member of the generic. The rules that keep that sound (a generic and its member in one list, a UKD's arch under a generic KDP, the reverse direction, an -unknown name, rocKE) are validation errors with stable substrings; the merge into a -member folder is atomic and refuses two shards that would write one path with -different bytes. +unknown name, rocKE) are validation errors; the merge into a member folder is atomic +and refuses two shards that would write one path with different bytes. The quick tier packs `embedded_source` and authored `hsaco` UKDs, which need no compiler. The hipcc tier at the end compiles for real and is not quick. @@ -144,52 +143,11 @@ def _load(root, table=GENERIC_TARGETS): return load_flat_input(root, table, log=lambda *_: None) -def _generic_kdp_root(tmp_path, empty_arch_fixture): - return _root( - tmp_path, - empty_arch_fixture, - {"g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-g")])}, - ) - - # --- layout ------------------------------------------------------------------ @pytest.mark.quick -def test_generic_entry_layout_is_one_copy_per_selected_member_folder( - tmp_path, empty_arch_fixture, rocm_kpack_dir -): - root = _generic_kdp_root(tmp_path, empty_arch_fixture) - results = _pack(root, tmp_path, [MEMBER_A, MEMBER_B], rocm_kpack_dir) - - out = tmp_path / "out" - # One folder per selected member: none for the generic or an unselected member, - # and no staging directory left beside them. - assert sorted(p.name for p in out.iterdir()) == [MEMBER_A, MEMBER_B] - assert not (out / UNSELECTED_MEMBER).exists() - assert not (out / GENERIC).exists() - assert {a: r.out_dir for a, r in results.items()} == { - MEMBER_A: out / MEMBER_A, - MEMBER_B: out / MEMBER_B, - } - - first, second = _files(out / MEMBER_A), _files(out / MEMBER_B) - assert first == second - assert "g.kdp.json" in first - - documents = { - name: json.loads(data) - for name, data in first.items() - if name.endswith((".kdp.json", ".ukd.json")) - } - assert documents - kdp = documents["g.kdp.json"] - assert kdp["arch"] == [GENERIC] - assert [u["arch"] for u in kdp["kernelDescriptors"]] == [[GENERIC]] - - -@pytest.mark.quick -def test_a_member_with_only_generic_content_still_gets_a_folder( +def test_generic_entries_land_in_every_selected_member_folder_only( tmp_path, empty_arch_fixture, rocm_kpack_dir ): root = _root( @@ -197,18 +155,53 @@ def test_a_member_with_only_generic_content_still_gets_a_folder( empty_arch_fixture, { "c": _kdp("kdp-c", ["gfx942"], [_embedded("ukd-c")]), + "explicit": _kdp("kdp-explicit", [MEMBER_B], [_embedded("ukd-e")]), "g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-g")]), + "wild": _kdp("kdp-wild", [], [_embedded("ukd-wild")]), }, ) - results = _pack(root, tmp_path, ["gfx942", MEMBER_A], rocm_kpack_dir) + results = _pack(root, tmp_path, ["gfx942", MEMBER_A, MEMBER_B], rocm_kpack_dir) out = tmp_path / "out" - assert not results[MEMBER_A].skipped - assert (out / MEMBER_A / "g.kdp.json").is_file() - assert not (out / MEMBER_A / "c.kdp.json").exists() - assert (out / "gfx942" / "c.kdp.json").is_file() + # One folder per selected arch: none for the generic or an unselected member, and no + # staging directory left beside them. + assert sorted(p.name for p in out.iterdir()) == [MEMBER_A, MEMBER_B, "gfx942"] + assert not (out / UNSELECTED_MEMBER).exists() + assert not (out / GENERIC).exists() + assert {a: r.out_dir for a, r in results.items()} == { + arch: out / arch for arch in ("gfx942", MEMBER_A, MEMBER_B) + } + + # The generic KDP ships verbatim into each member; a nonmember never gets it. + for member in (MEMBER_A, MEMBER_B): + kdp = _read(out / member / "g.kdp.json") + assert kdp["arch"] == [GENERIC] + assert [u["arch"] for u in kdp["kernelDescriptors"]] == [[GENERIC]] + assert _read(out / member / "wild.kdp.json")["arch"] == [member] + assert (out / MEMBER_A / "g.kdp.json").read_bytes() == ( + out / MEMBER_B / "g.kdp.json" + ).read_bytes() assert not (out / "gfx942" / "g.kdp.json").exists() + # Concrete content stays with its own arch. + assert (out / "gfx942" / "c.kdp.json").is_file() + assert not (out / MEMBER_A / "c.kdp.json").exists() + assert (out / MEMBER_B / "explicit.kdp.json").is_file() + assert not (out / MEMBER_A / "explicit.kdp.json").exists() + + # An empty-arch KDP is not part of the generic pass. + generic_tree = tmp_path / "inter" / ".generic-out" / GENERIC + assert (generic_tree / "g.kdp.json").is_file() + assert not (generic_tree / "wild.kdp.json").exists() + + # Shared engine/dispatch/matcher files are identical in every folder. + shared = [p for p in root.iterdir() if p.name.endswith(_SHARED_SUFFIXES)] + assert shared + for arch in ("gfx942", MEMBER_A, MEMBER_B): + files = _files(out / arch) + for path in shared: + assert files[path.name] == path.read_bytes() + @pytest.mark.quick def test_generic_entry_is_not_materialized_when_no_member_is_selected( @@ -231,81 +224,11 @@ def test_generic_entry_is_not_materialized_when_no_member_is_selected( assert not (tmp_path / "inter" / GENERIC).exists() -@pytest.mark.quick -def test_empty_arch_kdp_is_not_emitted_into_the_generic_copy( - tmp_path, empty_arch_fixture, rocm_kpack_dir -): - root = _root( - tmp_path, - empty_arch_fixture, - { - "wild": _kdp("kdp-wild", [], [_embedded("ukd-wild")]), - "g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-g")]), - }, - ) - _pack(root, tmp_path, [MEMBER_A], rocm_kpack_dir) - - generic_tree = tmp_path / "inter" / ".generic-out" / GENERIC - assert (generic_tree / "g.kdp.json").is_file() - assert not (generic_tree / "wild.kdp.json").exists() - # The member folder holds the wildcard through the concrete pass, narrowed to it. - member = tmp_path / "out" / MEMBER_A - assert _read(member / "wild.kdp.json")["arch"] == [MEMBER_A] - assert _read(member / "g.kdp.json")["arch"] == [GENERIC] - - -@pytest.mark.quick -def test_explicit_and_generic_packs_coexist_in_one_member_folder( - tmp_path, empty_arch_fixture, rocm_kpack_dir -): - root = _root( - tmp_path, - empty_arch_fixture, - { - "explicit": _kdp("kdp-explicit", [MEMBER_B], [_embedded("ukd-e")]), - "generic": _kdp("kdp-generic", [GENERIC], [_embedded("ukd-g")]), - }, - ) - _pack(root, tmp_path, [MEMBER_A, MEMBER_B], rocm_kpack_dir) - - out = tmp_path / "out" - both = _files(out / MEMBER_B) - assert json.loads(both["explicit.kdp.json"])["arch"] == [MEMBER_B] - assert json.loads(both["generic.kdp.json"])["arch"] == [GENERIC] - only_generic = _files(out / MEMBER_A) - assert "explicit.kdp.json" not in only_generic - assert only_generic["generic.kdp.json"] == both["generic.kdp.json"] - - -@pytest.mark.quick -def test_shared_engine_dispatch_and_kmd_files_merge_cleanly( - tmp_path, empty_arch_fixture, rocm_kpack_dir -): - root = _root( - tmp_path, - empty_arch_fixture, - { - "explicit": _kdp("kdp-explicit", [MEMBER_B], [_embedded("ukd-e")]), - "generic": _kdp("kdp-generic", [GENERIC], [_embedded("ukd-g")]), - }, - ) - _pack(root, tmp_path, [MEMBER_B], rocm_kpack_dir) - - member = _files(tmp_path / "out" / MEMBER_B) - shared = [p for p in root.iterdir() if p.name.endswith(_SHARED_SUFFIXES)] - assert shared - for path in shared: - assert member[path.name] == path.read_bytes() - assert sorted(member) == sorted( - ["explicit.kdp.json", "generic.kdp.json", *(p.name for p in shared)] - ) - - # --- merge guard and failure policy ------------------------------------------ @pytest.mark.quick -def test_merge_collision_with_differing_bytes_names_the_id_and_both_paths_and_leaves_no_partial_member_folder( +def test_merge_collision_with_differing_bytes_is_refused_and_leaves_the_member_untouched( tmp_path, ): generic_dir = tmp_path / "inter" / ".generic-out" / GENERIC @@ -320,11 +243,7 @@ def test_merge_collision_with_differing_bytes_names_the_id_and_both_paths_and_le with pytest.raises(HkpPackError) as err: _merge_generic_into_member(generic_dir, member_dir, staging) - message = str(err.value) - assert "would be written by shard" in message - assert "ukd-dup" in message - assert str(member_dir / "dup.ukd.json") in message - assert str(generic_dir / "dup.ukd.json") in message + assert str(member_dir / "dup.ukd.json") in str(err.value) assert _files(member_dir) == before assert not staging.exists() @@ -379,10 +298,9 @@ def failing(flat, inter, out_dir, *args, **kwargs): return real(flat, inter, out_dir, *args, **kwargs) monkeypatch.setattr(pipeline, "pack_arch", failing) - with pytest.raises(HkpPackError) as err: + with pytest.raises(HkpPackError, match="synthetic generic pack failure"): _pack(root, tmp_path, [MEMBER_A, MEMBER_B], rocm_kpack_dir) - assert f"{GENERIC}: synthetic generic pack failure" in str(err.value) out = tmp_path / "out" for member in (MEMBER_A, MEMBER_B): assert (out / member / "c.kdp.json").is_file() @@ -398,13 +316,7 @@ def test_generic_copies_of_every_producer_are_equal_across_separate_builds( gfx1151 from separate roots: the member folders hold identical files, so the copies per-arch shard builds produce collapse to one. Nothing of the build's target list may leak into the generic copy.""" - hsaco = _embedded("ukd-hsaco", [GENERIC]) - hsaco["kernel_source"] = { - "kind": "hsaco", - "file": CO_NAME, - "symbol": "HsacoFixtureAdd", - } - hsaco["provenance"] = copy.deepcopy(_CONTRACT) + hsaco = _hsaco_ukd("ukd-hsaco", [GENERIC]) folders = {} for member in (MEMBER_A, MEMBER_B): base = tmp_path / member @@ -432,13 +344,7 @@ def test_hsaco_under_a_generic_packs_under_the_generic_key_into_every_member_fol tmp_path, empty_arch_fixture, hsaco_fixture_dir, rocm_kpack_dir ): authored = (hsaco_fixture_dir / "gfx942" / CO_NAME).read_bytes() - ukd = _embedded("ukd-hsaco", [GENERIC]) - ukd["kernel_source"] = { - "kind": "hsaco", - "file": CO_NAME, - "symbol": "HsacoFixtureAdd", - } - ukd["provenance"] = copy.deepcopy(_CONTRACT) + ukd = _hsaco_ukd("ukd-hsaco", [GENERIC]) root = _root(tmp_path, empty_arch_fixture, {"g": _kdp("kdp-g", [GENERIC], [ukd])}) shutil.copyfile(hsaco_fixture_dir / "gfx942" / CO_NAME, root / CO_NAME) results = _pack(root, tmp_path, [MEMBER_A, MEMBER_B], rocm_kpack_dir) @@ -458,267 +364,124 @@ def test_hsaco_under_a_generic_packs_under_the_generic_key_into_every_member_fol assert _files(out / MEMBER_A) == _files(out / MEMBER_B) -# --- a generic and its member in one list ------------------------------------- - +# --- arch rules at load ------------------------------------------------------- -@pytest.mark.quick -def test_s5_list_with_a_generic_and_its_member_is_an_error( - tmp_path, empty_arch_fixture -): - root = _root( - tmp_path, - empty_arch_fixture, - {"g": _kdp("kdp-g", [GENERIC, MEMBER_B], [_embedded("ukd-g")])}, - ) - with pytest.raises(HkpPackError) as err: - _load(root) - message = str(err.value) - assert "g.kdp.json" in message - assert f"lists '{GENERIC}' and '{MEMBER_B}'" in message - assert f"{GENERIC} contains {MEMBER_B}" in message - -@pytest.mark.quick -def test_s5_two_generics_sharing_a_member_is_an_error(tmp_path, empty_arch_fixture): - table_path = tmp_path / "overlap.json" - _write( - table_path, - { - "schemaVersion": 1, - "generics": { - "gfx11-generic": ["gfx1100", "gfx1101"], - "gfx11-b-generic": ["gfx1101", "gfx1102"], - }, - }, - ) - root = _root( - tmp_path, - empty_arch_fixture, - {"g": _kdp("kdp-g", ["gfx11-generic", "gfx11-b-generic"], [_embedded("u")])}, - ) - with pytest.raises(HkpPackError) as err: - _load(root, GenericTargets.load(table_path)) - message = str(err.value) - assert message.startswith("g.kdp.json:") - assert "lists 'gfx11-generic' and 'gfx11-b-generic'" in message - assert "share member gfx1101" in message - - -# --- UKD arch under a generic KDP --------------------------------------------- - -_S6_RULE = "must list every generic of the KDP" - - -def _standalone(uid, arch=None): +def _rocke_ukd(uid, arch=None): ukd = _embedded(uid, arch) + ukd["kernel_source"] = { + "kind": "rocke", + "source": "pkg/kernels/attention.py", + "builder": "build_attention", + "spec": {"tile": 64}, + } return ukd -@pytest.mark.quick -@pytest.mark.parametrize( - "ukd_arch, accepted", - [([GENERIC], True), ([MEMBER_B], False)], - ids=["exact_generic", "narrower"], -) -def test_s6_standalone_ukd_under_a_generic_kdp( - tmp_path, empty_arch_fixture, ukd_arch, accepted -): - root = _root( - tmp_path, - empty_arch_fixture, - {"g": _kdp("kdp-g", [GENERIC], ["ukd-s"])}, - {"s": _standalone("ukd-s", ukd_arch)}, - ) - if accepted: - _load(root) - return - with pytest.raises(HkpPackError) as err: - _load(root) - message = str(err.value) - assert _S6_RULE in message - assert "ukd-s" in message and "s.ukd.json" in message and "g.kdp.json" in message - - -_S7_RULE = "has an empty 'arch' (unrestricted) but the KDP lists" - - -@pytest.mark.quick -@pytest.mark.parametrize( - "kdp_arch", [["gfx942"], [GENERIC]], ids=["concrete", "generic"] -) -def test_s7_standalone_ukd_without_arch_under_a_kdp_with_an_arch_is_an_error( - tmp_path, empty_arch_fixture, kdp_arch -): - root = _root( - tmp_path, - empty_arch_fixture, - {"g": _kdp("kdp-g", kdp_arch, ["ukd-s"])}, - {"s": _standalone("ukd-s")}, - ) - with pytest.raises(HkpPackError) as err: - _load(root) - message = str(err.value) - assert message.startswith("g.kdp.json:") - assert _S7_RULE in message - assert "'ukd-s'" in message and "s.ukd.json" in message - - -@pytest.mark.quick -def test_s7_standalone_ukd_without_arch_under_an_arch_less_kdp_is_accepted( - tmp_path, empty_arch_fixture -): - root = _root( - tmp_path, - empty_arch_fixture, - {"g": _kdp("kdp-g", [], ["ukd-s"])}, - {"s": _standalone("ukd-s")}, - ) - _load(root) - - -@pytest.mark.quick -def test_s7_inline_ukd_without_arch_under_a_concrete_kdp_inherits_and_is_accepted( - tmp_path, empty_arch_fixture -): - root = _root( - tmp_path, - empty_arch_fixture, - {"g": _kdp("kdp-g", ["gfx942"], [_embedded("ukd-i")])}, - ) - _load(root) - - -@pytest.mark.quick -def test_s6_inline_ukd_with_a_narrower_arch_under_a_generic_kdp_is_an_error( - tmp_path, empty_arch_fixture -): - root = _root( - tmp_path, - empty_arch_fixture, - {"g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-i", [MEMBER_B])])}, - ) - with pytest.raises(HkpPackError, match=_S6_RULE) as err: - _load(root) - assert str(err.value).startswith("g.kdp.json:") - assert "inline UKD 'ukd-i'" in str(err.value) - - -@pytest.mark.quick -@pytest.mark.parametrize( - "ukd_arch, rule", - [ - (["gfx99-generic"], "generic target name absent"), - ([GENERIC, MEMBER_B], f"{GENERIC} contains {MEMBER_B}"), - ], - ids=["unknown_generic", "generic_beside_member"], -) -def test_per_list_rules_apply_to_an_inline_ukd_arch( - tmp_path, empty_arch_fixture, ukd_arch, rule -): - """An inline UKD's own list is held to the per-list rules, which name the actual - defect rather than a later KDP-level rule.""" - root = _root( - tmp_path, - empty_arch_fixture, - {"g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-i", ukd_arch)])}, - ) - with pytest.raises(HkpPackError, match=rule): - _load(root) +def _hsaco_ukd(uid, arch): + ukd = _embedded(uid, arch) + ukd["kernel_source"] = { + "kind": "hsaco", + "file": CO_NAME, + "symbol": "HsacoFixtureAdd", + } + ukd["provenance"] = copy.deepcopy(_CONTRACT) + return ukd -@pytest.mark.quick -def test_inline_ukd_without_its_own_arch_under_a_generic_kdp_is_accepted( - tmp_path, empty_arch_fixture -): - _load(_generic_kdp_root(tmp_path, empty_arch_fixture)) +_OVERLAPPING_TABLE = { + "schemaVersion": 1, + "generics": { + "gfx11-generic": ["gfx1100", "gfx1101"], + "gfx11-b-generic": ["gfx1101", "gfx1102"], + }, +} +# (kdp arch, UKD form, UKD arch, table override). UKD form: "inline" puts the UKD in the +# KDP, "standalone" is a separate .ukd.json the KDP names, "rocke" is an inline rocKE UKD. +_REJECTED = { + "generic_beside_its_member": ([GENERIC, MEMBER_B], "inline", None, None), + "two_generics_sharing_a_member": ( + ["gfx11-generic", "gfx11-b-generic"], + "inline", + None, + _OVERLAPPING_TABLE, + ), + "unknown_generic": (["gfx99-generic"], "inline", None, None), + "standalone_narrower_than_generic_kdp": ([GENERIC], "standalone", [MEMBER_B], None), + "inline_narrower_than_generic_kdp": ([GENERIC], "inline", [MEMBER_B], None), + "inline_unknown_generic": ([GENERIC], "inline", ["gfx99-generic"], None), + "inline_generic_beside_member": ([GENERIC], "inline", [GENERIC, MEMBER_B], None), + "mixed_list_ukd_missing_the_generic": ( + ["gfx942", GENERIC], + "inline", + ["gfx942"], + None, + ), + "standalone_without_arch_under_concrete_kdp": ( + ["gfx942"], + "standalone", + None, + None, + ), + "standalone_without_arch_under_generic_kdp": ([GENERIC], "standalone", None, None), + "generic_ukd_under_empty_arch_kdp": ([], "inline", [GENERIC], None), + "generic_ukd_under_kdp_not_listing_it": ([MEMBER_B], "inline", [GENERIC], None), + "rocke_inheriting_a_generic": ([GENERIC], "rocke", None, None), + "rocke_with_own_generic": ([GENERIC], "rocke", [GENERIC], None), +} -@pytest.mark.quick -def test_s6_mixed_list_ukd_listing_every_entry_is_accepted( - tmp_path, empty_arch_fixture -): - both = ["gfx942", GENERIC] - root = _root( - tmp_path, - empty_arch_fixture, - {"m": _kdp("kdp-m", both, [_embedded("ukd-m", both)])}, - ) - _load(root) +_ACCEPTED = { + "standalone_listing_the_generic": ([GENERIC], "standalone", [GENERIC]), + "standalone_without_arch_under_arch_less_kdp": ([], "standalone", None), + "inline_inheriting_a_concrete_kdp": (["gfx942"], "inline", None), + "inline_inheriting_a_generic_kdp": ([GENERIC], "inline", None), + "mixed_list_ukd_listing_every_entry": ( + ["gfx942", GENERIC], + "inline", + ["gfx942", GENERIC], + ), +} -@pytest.mark.quick -def test_s6_mixed_list_ukd_missing_the_generic_is_an_error( - tmp_path, empty_arch_fixture -): - root = _root( - tmp_path, - empty_arch_fixture, - {"m": _kdp("kdp-m", ["gfx942", GENERIC], [_embedded("ukd-m", ["gfx942"])])}, +def _arch_rule_root(tmp_path, empty_arch_fixture, kdp_arch, form, ukd_arch): + if form == "standalone": + return _root( + tmp_path, + empty_arch_fixture, + {"g": _kdp("kdp-g", kdp_arch, ["ukd-s"])}, + {"s": _embedded("ukd-s", ukd_arch)}, + ) + ukd = ( + _rocke_ukd("ukd-i", ukd_arch) + if form == "rocke" + else _embedded("ukd-i", ukd_arch) ) - with pytest.raises(HkpPackError, match=_S6_RULE) as err: - _load(root) - assert str(err.value).startswith("m.kdp.json:") - assert "inline UKD 'ukd-m'" in str(err.value) - - -# --- reverse direction, unknown names, rocKE --------------------------------- + return _root(tmp_path, empty_arch_fixture, {"g": _kdp("kdp-g", kdp_arch, [ukd])}) @pytest.mark.quick -@pytest.mark.parametrize("kdp_arch", [[], [MEMBER_B]], ids=["empty", "explicit_member"]) -def test_a_generic_ukd_under_a_kdp_that_does_not_list_it_is_an_error( - tmp_path, empty_arch_fixture, kdp_arch +@pytest.mark.parametrize("case", sorted(_REJECTED)) +def test_an_arch_rule_violation_is_an_error_naming_the_kdp( + tmp_path, empty_arch_fixture, case ): - root = _root( - tmp_path, - empty_arch_fixture, - {"k": _kdp("kdp-k", kdp_arch, [_embedded("ukd-k", [GENERIC])])}, - ) - with pytest.raises(HkpPackError) as err: - _load(root) - message = str(err.value) - assert f"declares generic target '{GENERIC}'" in message - assert "ship in no shard" in message - assert message.startswith("k.kdp.json:") - assert "UKD 'ukd-k'" in message - - -@pytest.mark.quick -def test_unknown_generic_name_is_an_error_naming_the_file(tmp_path, empty_arch_fixture): - root = _root( - tmp_path, - empty_arch_fixture, - {"u": _kdp("kdp-u", ["gfx99-generic"], [_embedded("ukd-u")])}, - ) + kdp_arch, form, ukd_arch, table_doc = _REJECTED[case] + root = _arch_rule_root(tmp_path, empty_arch_fixture, kdp_arch, form, ukd_arch) + table = GENERIC_TARGETS + if table_doc is not None: + table_path = tmp_path / "table.json" + _write(table_path, table_doc) + table = GenericTargets.load(table_path) with pytest.raises(HkpPackError) as err: - _load(root) - message = str(err.value) - assert message.startswith("u.kdp.json:") - assert "generic target name absent" in message - assert "'gfx99-generic'" in message and str(GENERIC_TARGETS.path) in message + _load(root, table) + assert "g.kdp.json" in str(err.value) @pytest.mark.quick -@pytest.mark.parametrize( - "ukd_arch", [None, [GENERIC]], ids=["no_own_arch", "own_generic"] -) -def test_rocke_under_a_generic_kdp_is_an_error(tmp_path, empty_arch_fixture, ukd_arch): - """A rocKE UKD is refused whether it carries the generic itself or merely - inherits it from the KDP.""" - ukd = _embedded("ukd-r", ukd_arch) - ukd["kernel_source"] = { - "kind": "rocke", - "source": "pkg/kernels/attention.py", - "builder": "build_attention", - "spec": {"tile": 64}, - } - root = _root(tmp_path, empty_arch_fixture, {"r": _kdp("kdp-r", [GENERIC], [ukd])}) - with pytest.raises(HkpPackError) as err: - _load(root) - message = str(err.value) - assert message.startswith("r.kdp.json:") - assert "does not support generic targets yet" in message - assert f"'{GENERIC}'" in message +@pytest.mark.parametrize("case", sorted(_ACCEPTED)) +def test_a_consistent_arch_declaration_loads(tmp_path, empty_arch_fixture, case): + kdp_arch, form, ukd_arch = _ACCEPTED[case] + _load(_arch_rule_root(tmp_path, empty_arch_fixture, kdp_arch, form, ukd_arch)) # --- hipcc tier (not quick) --------------------------------------------------- diff --git a/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/test_descriptors/shared/conv_fwd/conv_fwd_generic.kdp.json b/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/test_descriptors/shared/conv_fwd/conv_fwd_generic.kdp.json new file mode 100644 index 000000000000..5d9962655083 --- /dev/null +++ b/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/test_descriptors/shared/conv_fwd/conv_fwd_generic.kdp.json @@ -0,0 +1,54 @@ +{ + "version": "1.0", + "id": "11111111-aaaa-4bbb-8ccc-000000000002", + "name": "hipkernel:conv_fwd_generic", + "arch": [ + "gfx11-generic" + ], + "matchers": [ + "91d0e798-8522-4a50-9db7-5e0d01b1fdec" + ], + "engine": "11111111-aaaa-4bbb-8ccc-000000000001", + "dispatch": "e2564353-856f-43dc-b82c-1400a4c1929d", + "kernelDescriptors": [ + { + "version": "1.0", + "id": "11111111-aaaa-4bbb-8ccc-000000000003", + "name": "conv_fwd_generic.f32_block64", + "kernel_source": { + "kind": "hip", + "source": "kernels/ConvFwd.cpp", + "entry": "ConvFwd", + "build": { + "defines": { + "HIP_PLUGIN_CONV_TYPE": "float", + "HIP_PLUGIN_CONV_BLOCK_SIZE": 64 + } + } + }, + "metadata": { + "block_size": 64, + "dtype": "FLOAT" + }, + "priority": 0, + "provenance": { + "specialization_contract": { + "schema_version": 1, + "consumers": [ + { + "engine_id": "11111111-aaaa-4bbb-8ccc-000000000001", + "kmd_id": "a7745b41-dcd5-4b4f-89ca-a98f608dba38", + "metadata_fields": [], + "matcher_only_fields": [ + "block_size", + "dtype" + ], + "bindings": {}, + "vocabulary": {} + } + ] + } + } + } + ] +} diff --git a/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/test_descriptors/shared/conv_fwd/conv_fwd_generic.ued.json b/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/test_descriptors/shared/conv_fwd/conv_fwd_generic.ued.json new file mode 100644 index 000000000000..6f555216d6a9 --- /dev/null +++ b/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/test_descriptors/shared/conv_fwd/conv_fwd_generic.ued.json @@ -0,0 +1,16 @@ +{ + "version": "1.0", + "id": "11111111-aaaa-4bbb-8ccc-000000000001", + "name": "hipkernel:ConvFwdGeneric", + "graph_match": { + "native": "hipkernel.conv_fwd.graph_match" + }, + "heuristic": "e8066dca-b56e-4dfa-8755-a1e1bcadf09f", + "metadata": "a7745b41-dcd5-4b4f-89ca-a98f608dba38", + "knobs": [ + "block_size" + ], + "behavior_notes": [ + "runtime_compilation" + ] +} diff --git a/dnn-providers/hip-kernel-provider/src/tests/CMakeLists.txt b/dnn-providers/hip-kernel-provider/src/tests/CMakeLists.txt index 2debc47b2625..15a5ccae5d42 100644 --- a/dnn-providers/hip-kernel-provider/src/tests/CMakeLists.txt +++ b/dnn-providers/hip-kernel-provider/src/tests/CMakeLists.txt @@ -160,6 +160,7 @@ if(HIPDNN_ENABLE_KERNEL_INGESTOR) # because they need a real container rather than a matching device. target_sources(hip_kernel_provider_tests PRIVATE compilation/TestKpackArchive.cpp + compilation/TestKpackGenericArchive.cpp compilation/TestKpackKernelLoader.cpp compilation/TestKpackModuleCache.cpp engines/kernel_ingestor_engine/packs/TestPointwiseKpackDispatch.cpp diff --git a/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackGenericArchive.cpp b/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackGenericArchive.cpp new file mode 100644 index 000000000000..f26b72d3737c --- /dev/null +++ b/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackGenericArchive.cpp @@ -0,0 +1,130 @@ +// Copyright © Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT + +#ifdef HIPDNN_ENABLE_KERNEL_INGESTOR + +#include +#include +#include +#include + +#include + +#include + +#include "PackedKernelSource.hpp" +#include "TestDescriptorRoot.hpp" +#include "compilation/KpackArchive.hpp" +#include "compilation/KpackModuleCache.hpp" + +namespace hip_kernel_provider::compilation +{ +namespace +{ + +using hip_kernel_provider::testing::PackedKernelSource; +using hip_kernel_provider::testing::readPackedKernelSource; +using hip_kernel_provider::testing::unitKpackRoot; + +/// The generic named by test_descriptors/shared/conv_fwd/conv_fwd_generic.kdp.json. +constexpr const char* GENERIC = "gfx11-generic"; +constexpr const char* GENERIC_KDP = "conv_fwd_generic.kdp.json"; + +std::vector archivesOf(const std::filesystem::path& kpackFile) +{ + KpackArchive archive; + KpackError error; + std::vector arches; + EXPECT_TRUE(archive.open(kpackFile, error)) << kpackFile << " (" << error.codeName << ")"; + EXPECT_TRUE(archive.architectures(arches, error)) << kpackFile << " (" << error.codeName << ")"; + return arches; +} + +/// Packed arch shards present in this tree: every directory of the packed set holding a +/// kpack/ child. Enumerated from the filesystem so the test needs no device. +std::vector packedShards() +{ + std::vector shards; + std::error_code ec; + for(const auto& entry : std::filesystem::directory_iterator(unitKpackRoot(), ec)) + { + if(entry.is_directory(ec) && std::filesystem::is_directory(entry.path() / "kpack", ec)) + { + shards.push_back(entry.path()); + } + } + std::sort(shards.begin(), shards.end()); + return shards; +} + +} // namespace + +TEST(TestPackedGenericArchive, ShardsCarryTheGenericArchiveExactlyWhenTheirArchIsAMember) +{ + const auto shards = packedShards(); + ASSERT_FALSE(shards.empty()) << "no packed arch shard under " << unitKpackRoot(); + + for(const auto& shard : shards) + { + const std::string arch = shard.filename().string(); + SCOPED_TRACE(arch); + + std::vector kpackFiles; + for(const auto& entry : std::filesystem::directory_iterator(shard / "kpack")) + { + kpackFiles.push_back(entry.path()); + } + ASSERT_FALSE(kpackFiles.empty()); + + const bool member = hipdnn_plugin_sdk::genericTargetContains(GENERIC, arch); + + if(!member) + { + for(const auto& file : kpackFiles) + { + const auto arches = archivesOf(file); + EXPECT_EQ(std::count(arches.begin(), arches.end(), GENERIC), 0) << file; + } + EXPECT_FALSE(std::filesystem::exists(shard / "conv_fwd" / GENERIC_KDP)); + continue; + } + + PackedKernelSource packed; + ASSERT_NO_FATAL_FAILURE(readPackedKernelSource(shard, GENERIC_KDP, packed)); + + // The generic descriptor points at an archive keyed by the generic alone. + const std::vector keys = archivesOf(packed.archive); + ASSERT_EQ(keys, std::vector{GENERIC}); + + // KpackModuleCache serves this shard's device from that archive's generic key. + const std::string* selected = KpackModuleCache::selectArch(keys, arch); + ASSERT_NE(selected, nullptr); + EXPECT_EQ(*selected, GENERIC); + + // The device's own archive keeps winning when both are visible. + std::vector both; + for(const auto& file : kpackFiles) + { + for(const auto& key : archivesOf(file)) + { + both.push_back(key); + } + } + ASSERT_NE(std::count(both.begin(), both.end(), arch), 0); + const std::string* preferred = KpackModuleCache::selectArch(both, arch); + ASSERT_NE(preferred, nullptr); + EXPECT_EQ(*preferred, arch); + + // The generic key resolves the descriptor's own toc_key to a code object. + KpackArchive archive; + KpackError error; + ASSERT_TRUE(archive.open(packed.archive, error)) << error.codeName; + KpackCodeObject object; + ASSERT_TRUE(archive.codeObject(packed.tocKey, *selected, object, error)) << error.codeName; + EXPECT_FALSE(object.empty()); + } +} + +} // namespace hip_kernel_provider::compilation + +#endif // HIPDNN_ENABLE_KERNEL_INGESTOR From b2ab8eaf4c3884cc851caa3e94a43cfa1f166a2f Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 02:18:21 +0000 Subject: [PATCH 32/52] hipdnn: build arch violation messages without string-concatenation chains --- .../ingestor/DescriptorLoader.hpp | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp index b23901adf95b..1ce687d820d6 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp @@ -788,8 +788,12 @@ inline std::vector requireArchList(const nlohmann::json& object, { if(isGenericShapedArchName(value) && findGenericTarget(value) == nullptr) { - fail("key 'arch' in " + where + " names '" + value - + "', a generic target absent from the generic target table"); + std::string message = "key 'arch' in "; + message += where; + message += " names '"; + message += value; + message += "', a generic target absent from the generic target table"; + fail(message); } } for(size_t i = 0; i < values.size(); ++i) @@ -798,8 +802,15 @@ inline std::vector requireArchList(const nlohmann::json& object, { if(const auto shared = sharedGenericMember(values[i], values[j]); !shared.empty()) { - fail("key 'arch' in " + where + " lists '" + values[i] + "' and '" + values[j] - + "', which both cover " + shared); + std::string message = "key 'arch' in "; + message += where; + message += " lists '"; + message += values[i]; + message += "' and '"; + message += values[j]; + message += "', which both cover "; + message += shared; + fail(message); } } } @@ -1174,7 +1185,13 @@ inline KernelDescriptorPack parseKernelDescriptorPack(const nlohmann::json& root if(const auto violation = genericArchViolation(pack.arch, kernel->arch); !violation.empty()) { - fail("kernel '" + kernel->name + "' in " + where + " " + violation); + std::string message = "kernel '"; + message += kernel->name; + message += "' in "; + message += where; + message += " "; + message += violation; + fail(message); } pack.kernels.push_back(std::move(*kernel)); } From 4b7c4138abf372fbd71046472bed840deec7c973 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 02:23:43 +0000 Subject: [PATCH 33/52] docs: one home for generic target and hsaco arch rules --- .../descriptor-packaging/README.md | 82 ++++++++----------- .../examples/descriptors/README.md | 9 +- .../rfcs/0017_UniversalKernelDescriptor.md | 15 ++-- .../hipdnn/tools/IngestorGenerator/README.md | 43 +++------- .../ai/skills/hipdnn-ingestor-engine/SKILL.md | 4 +- .../skills/hipdnn-ingestor-engine/extend.md | 2 +- 6 files changed, 57 insertions(+), 98 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 8287cb267f77..2bc340a438a1 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -6,48 +6,38 @@ minimal authored source root. ## Generic GPU targets -`arch` may name a LLVM generic target (`gfx11-generic`) listed in the shared table -`projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json`; the packer reads it through -the required `--generic-targets-json` (CMake passes -`HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON`). `GPU_TARGETS` stays concrete. After the -concrete passes, every table generic that a KDP lists and that contains a selected arch -is compiled and packed once under its own spelling (`kpack/hip_kernel_provider_.kpack`, -every emitted document `arch: []`), and that one tree is copied into the folder of each -selected member whose concrete pass succeeded. No folder is named for the generic, and an -empty-`arch` KDP is never emitted into a generic copy. The copy is a pure function of the -source tree, the generic's spelling, hipcc and the table, so the copies are byte-identical -across member folders and across separate per-arch builds. - -Known limitation: every installed member package carries its own copy of the generic content, so disk use scales with the number of installed members. Identical copies collapse to one catalog entry at load; copies built by different compilers differ in `provenance` and are both dropped. - -Known limitation: the configure-time arch probe starts one Python interpreter per KDP of the root (about 135 ms each), so a root of hundreds of KDPs adds tens of seconds to each reconfigure. - -Known limitation: a generic copy is merged into its member folder by staging then renaming; a filesystem failure part-way through the swap (a failed `rmtree` or rename) can lose that member's content, and the build fails. - -Authoring rules (packer errors, stable substrings in parentheses): - -- A name ending `-generic` that the table does not list is an error (`generic target name - absent`). -- One `arch` list may not hold a generic with a member it contains, nor two generics that - share a member (`lists '`). Author two KDPs instead: an explicit override plus a generic - fallback are separate packs. -- Under a KDP whose list holds a generic, every UKD with its own `arch` must list every - generic of the KDP and only entries the KDP lists (`must list every generic of the KDP`). - An inline UKD with no `arch` inherits the pack. -- A standalone UKD (referenced by id) with an empty or absent `arch` is unrestricted, and is - valid only under a KDP whose `arch` is empty too; under a KDP listing any arch, concrete or - generic, it is an error (`has an empty 'arch' (unrestricted) but the KDP lists`). -- A UKD may not name a generic its KDP does not list, including under an empty-`arch` KDP - (`ship in no shard`). -- rocKE does not support generics yet (`does not support generic targets yet`). -- Two shards whose member sets intersect may not write one path with different bytes - (`would be written by shard`); identical shared descriptors merge. - -Duplicate matcher tuples are judged per arch tier (`hkp_desk_check`): an explicit arch -beats a generic beats an arch-less kernel on a device, so an arch-less kernel and an -explicit one do not collide, while two arch-less kernels, or two naming the same -generic, do. `tools/hkp_arch_probe.py` answers the CMake configure's "does this KDP reach -any selected arch" with the same rule. +`arch` accepts the LLVM generic targets in `projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json` +(for example `gfx11-generic`), read through the required `--generic-targets-json`. Keep +`GPU_TARGETS` concrete. The packer builds each selected generic once and copies its descriptors +and archive (`arch: []`) into each selected member's folder; no generic-named folder is +installed. Disk use grows with the number of members. + +Copies depend only on the source tree, the generic's spelling, hipcc and the table, so they are +identical across member folders and separate per-arch builds, and the loader collapses them to +one entry. Copies from different compilers differ in `provenance`, and the loader drops both. + +A device ranks an `arch` list explicit, then generic, then empty; duplicate tuples conflict within one tier. + +Authoring rules. Each violation fails the packer, raises a generator ConfigError, and makes the +loader log an ERROR and drop the pack: + +- Every `-generic` name is in the table. +- One `arch` list never holds a generic and one of its members, or two generics sharing a member. +- A UKD names no generic its KDP does not list. +- Under a KDP listing generics, a UKD with its own `arch` lists every KDP generic and only + entries the KDP lists; an inline UKD without `arch` inherits the pack. + +Packer only: a standalone UKD with empty `arch` needs an empty-`arch` KDP, and shards whose member +sets intersect may not write one path with different bytes. `kind: rocke` takes no generic. + +### hsaco kernels and `arch` + +An `hsaco` kernel's `arch` resolves in order: the kernel's own `arch`, else its pack's. The +result must be non-empty; the generator rejects the kernel otherwise, because an unrestricted +object would ship the same bytes to every shard. The generator writes the resolved list onto the +descriptor, and the packer requires a non-empty `arch` there; it does not inherit, so an inline +hsaco UKD without `arch` is refused even under an arch-scoped KDP. A generic-target object +lists each member or its generic. ## Source roots and what the walk accepts @@ -96,11 +86,9 @@ bytes are packed as-is into the arch's kpack, the kernel signature is read from object's AMDGPU metadata as for a compiled object, and the UKD ships as `kind: kpack`. The toc key derives from the file's resolved root-relative path, so one file serving several symbols is one archive entry, and one key claimed by two different files is a -hard error. The packer does not check the object's format or target processor. An -`hsaco` UKD must list the arch(es) its object runs on in `arch` (a generic spelling such -as `gfx11-generic` is allowed and packs the authored bytes under that key, with the -generic rules above and no ELF check); an absent or empty `arch` is rejected. The author's own load test on the target arch -is the only check; no in-tree load test covers `hsaco`. The shipped provenance records +hard error. The packer does not check the object's format or target processor; +`arch` follows [hsaco kernels and `arch`](#hsaco-kernels-and-arch). The author's own load +test on the target arch is the only check; no in-tree load test covers `hsaco`. The shipped provenance records `origin_kind: "hsaco"`, the root-relative `file`, its `sha256` and the `symbol`, and makes no toolchain claim. As for `hip`, the specialization contract must declare `metadata_fields: []`: no compiler ran whose specialization a binding could observe. diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/examples/descriptors/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/examples/descriptors/README.md index a4ff48b13d0c..8f372485ee19 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/examples/descriptors/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/examples/descriptors/README.md @@ -79,14 +79,11 @@ compiled artifact. Authoring it would let the descriptor disagree with the kerne authored.** `file` resolves relative to the descriptor that names it, must stay inside the source root, and has no root-relative fallback, like a hip `source`. The object is packed as-is, with no compile. This tree contains no hsaco example. -Each hsaco UKD must list in `arch` the arch(es) its object runs on (it may -be a generic spelling such as `gfx11-generic`, packed under that key); an absent -or empty `arch` is rejected. +Each hsaco UKD needs a non-empty `arch`: [rules](../../README.md#hsaco-kernels-and-arch). **`arch` filters which shard a descriptor ships in.** It does not select a -builder: naming `gfx942` does not make a gfx950 builder produce gfx942 code. It -may name a generic target (`gfx11-generic`): that pack is built once and copied -into the folder of every selected member of the generic. +builder: naming `gfx942` does not make a gfx950 builder produce gfx942 code. For generic +targets, see [Generic GPU targets](../../README.md#generic-gpu-targets). **`library` is relative to the descriptor that declared it**, and the archive is one per arch at the arch root — so a descriptor in a child folder climbs back out diff --git a/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md b/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md index 941ddd2e0204..c76d6da479a3 100644 --- a/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md +++ b/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md @@ -1422,16 +1422,11 @@ provider's descriptor cache, reused by every later graph. **4. Resolve the kernel packs that name this engine, and apply the arch gate.** Each pack (**KDP**) contributes a matcher set, one dispatch-descriptor (**UDD**) id, and a kernel vector with each -kernel's metadata values. A pack whose `arch` list excludes this device is dropped here. An `arch` -list ranks per device in tiers: an explicit entry (the device's own base id) beats an LLVM generic -target containing it (e.g. `gfx11-generic`, whose members come from the shared generic target -table), which beats an empty list. Two kernels of one engine collide on a tuple only where they -select a device at the same tier, so a generic fallback and an explicit override of one tuple -coexist. The tiering and the shadowing of a less specific kernel by a more specific one run after -the matcher set admits the pack, not at the arch gate, so a generic pack is dropped here only when -no tier of its list reaches the device; shadowing costs one extra pass over the pack's kernels when -a device's catalog is first built. The dispatch descriptor is named but **not** loaded; nothing -dispatches yet. +kernel's metadata values. A pack whose `arch` list excludes this device is dropped here. After its +matchers admit it, explicit kernels outrank kernels of an LLVM generic target containing the +device (e.g. `gfx11-generic`), which outrank unrestricted kernels; kernels with one metadata +tuple coexist at different tiers and the highest applicable tier wins. The +dispatch descriptor is named but **not** loaded; nothing dispatches yet. *Stored:* the parsed packs, matchers, and kernel metadata, in the same descriptor cache. **5. Run the engine's `graph_match`, lazily, on the first pack that cleared the gate.** It either diff --git a/projects/hipdnn/tools/IngestorGenerator/README.md b/projects/hipdnn/tools/IngestorGenerator/README.md index 47b723d639af..f821f7335b90 100644 --- a/projects/hipdnn/tools/IngestorGenerator/README.md +++ b/projects/hipdnn/tools/IngestorGenerator/README.md @@ -496,37 +496,19 @@ Run, in this order, **before any UUID is minted**: plan-build time against a real device. 3. Every kernel's `metadata` type-checks against the KMD, with no mandatory field (one with no `default_value`) omitted; the real loader drops the whole pack instead. -4. Every kernel's `arch` is a subset of its pack's `arch`, compared as expanded device - sets (a generic stands for its table members), and the generic-target rules below hold. +4. Every kernel's `arch` is a subset of its pack's `arch`. 5. Every `arch` entry is a plausible `gfx`-prefixed base id (lowercase, no feature suffix): an error if malformed, a **warning** if well-formed but unrecognized (e.g. `gfx94` for `gfx942`), since either looks like an ordinary INFO decline at match time and this tool - keeps no exhaustive arch list. A generic named in the table is recognized. - -### Generic targets and tiered `arch` matching - -An `arch` entry may be an LLVM generic target (`gfx11-generic`). Membership is data in -`plugin_sdk/data/gpu_generic_targets.json`; `codegen/generic_targets.py` reads that file -(no flag) and mirrors the loader's and the packer's tier rules, held equal by the golden -vectors in `plugin_sdk/tests/data/arch_tier_vectors.json` (`tests/test_generic_targets.py`). -A device is matched per `arch` list at one tier: an explicit entry (the device's own base -id) beats a generic containing it, which beats an empty list. Consequences for a config: - -- De-duplication (`build_kdp`) refuses a tuple shared by two kernels only where they select - a device at the **same** tier. A generic kernel and an explicit member kernel with one - tuple coexist (the explicit one wins on its own device), as do an empty-`arch` kernel and - a concrete one; two kernels under the same generic, or two empty lists, still collide. -- A list may not hold a generic with one of its members, nor two generics sharing a member; - an unknown generic-shaped name is an error. The packer enforces the same rules. -- In a pack whose `arch` lists a generic, a kernel with its own non-empty `arch` must list - every generic of the pack and only entries the pack lists; a kernel with no `arch` - inherits the pack. A kernel may not name a generic its pack does not list. An `hsaco` - kernel is stamped with its own `arch`, else the whole pack list, which satisfies this. -- The generator emits every kernel inline in its pack and never a standalone UKD; a kernel without its own `arch` inherits the pack's (an `hsaco` kernel is stamped with the whole pack list). A `kind: rocke` kernel under a generic is a ConfigError (`does not support generic targets yet`). a hand-authored standalone UKD (referenced by id) with no `arch` is unrestricted, and the packer accepts it only under a KDP whose `arch` is empty too (`has an empty 'arch' (unrestricted) but the KDP lists`). -- The emitted inventory (and so the generated census) gives every member of a generic row - the generic's descriptors, and the census checks coverage with the SDK's `archSupports`. - An `hsaco` object packed under a generic must really be generic-compatible; neither the - generator nor the packer checks this (a mismatch fails at module load). + keeps no exhaustive arch list. + +### Generic targets + +Configs accept generic `arch` targets under the +[packaging rules](../../../../dnn-providers/hip-kernel-provider/descriptor-packaging/README.md#generic-gpu-targets). +`build_kdp` refuses a tuple shared by two kernels only at the same tier, so an explicit kernel +may override a generic kernel with the same tuple. The generated census gives every member of +a generic row the generic's descriptors. ## Source adapters (`codegen/sources/`) @@ -543,9 +525,8 @@ producing compiler. `hsaco` authoring also uses the packaged path: `kernel_sourc descriptor that names it and `symbol` its kernel. `file` must stay inside the source root, with no root-relative fallback. `hkp_pack` packs that object as-is, without compiling, so like `hip` the specialization declares `metadata_fields: []`. The packer does not check the -object's format or target processor: every hsaco kernel must carry a non-empty per-kernel `arch` listing the -arch(es) its object runs on (a generic-target object lists the generic, or each member -under an explicit pack), and the loader rejects one without. +object's format or target processor; `arch` follows +[hsaco kernels and `arch`](../../../../dnn-providers/hip-kernel-provider/descriptor-packaging/README.md#hsaco-kernels-and-arch). `hsaco_file` is rejected explicitly, naming `supportsSourceKind()` as the missing prerequisite on `IKernelDispatchHandler`. diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md index 7905c60af0f5..5e77590c0293 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md @@ -41,9 +41,7 @@ AMDGPU code object resolved relative to the descriptor that names it (inside the root, no root-relative fallback) and `symbol` is authored. The packer copies the bytes as-is into the arch's archive with no compile step, reads the signature from the object's metadata, and declares `metadata_fields: []`. It does not check the object's -format or target processor, so every `hsaco` kernel must state a non-empty `arch` listing the arch(es) its -object runs on (a generic-target object lists each one); the generator rejects a kernel -without one, since an unrestricted one would ship the same bytes to every shard. `hsaco_file` (direct load) remains unsupported. +format or target processor; `arch` follows [hsaco kernels and `arch`](../../../../../../dnn-providers/hip-kernel-provider/descriptor-packaging/README.md#hsaco-kernels-and-arch). `hsaco_file` (direct load) remains unsupported. Both dialects are packed and staged per architecture, and **neither registers a descriptor in CMake**: the authored subpath is the whole mechanism. Only diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md index 66ce67b7eea1..7113f7bc2b71 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md @@ -68,7 +68,7 @@ The one registration a generated engine needs is the **kernel source**, and only | Group | Kinds | What happens | |---|---|---| | Compilation inputs | `hip`, `rocke` | Authored, lowered to a code object at pack time. No CMake registration. | -| Prebuilt input | `hsaco` | Authored as `{file, symbol}`, packed byte-for-byte: no compile, no CMake registration. `arch` is mandatory: list every arch the object runs on (a generic-target object lists all of them), on the kernel or inherited from its pack. | +| Prebuilt input | `hsaco` | Authored as `{file, symbol}`, packed byte-for-byte: no compile, no CMake registration. `arch` is mandatory; see [hsaco kernels and `arch`](../../../../../../dnn-providers/hip-kernel-provider/descriptor-packaging/README.md#hsaco-kernels-and-arch). | | Passthrough | `embedded_source` | Shipped as authored, no producer runs, no archive entry. **Needs the registration below.** | | Runtime descriptor form | `kpack` | Written *by* the packer and shipped; the runtime sees only this. The packer's intermediate also uses `kind: hsaco` for compiled variants. | From a19f9849f94c09b4f8339795c9f061a3276bc7ad Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 02:37:36 +0000 Subject: [PATCH 34/52] hipdnn: satisfy clang-tidy in generic arch tests --- .../hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp | 3 ++- .../tests/ingestor/TestKernelIngestorStateManager.cpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp index a861b78fbbda..75d991094cea 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp @@ -1987,7 +1987,8 @@ const std::vector BOTH{"test:candidate", "test:intact"}; nlohmann::json arch(std::initializer_list entries) { - return nlohmann::json(std::vector(entries)); + const nlohmann::json list = std::vector(entries); + return list; } /// Authors the candidate's first kernel with @p kernelArch under a pack listing diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp index da47c0281d5c..fb7be5d11c56 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp @@ -333,7 +333,7 @@ TEST(TestKernelIngestorStateManager, KeepsOnlyTheBestArchTierOfASharedTuplePerDe member.id = testId(0x96); member.kernels = {makeKernel(testId(0x97), "kernel_gfx1151", 64, "FLOAT")}; - std::vector> orders{ + const std::vector> orders{ {anywhere, generic, member}, {member, generic, anywhere}, {generic, member, anywhere}}; uint8_t graphIndex = 0x25; for(const auto& packs : orders) From 4ea6d0d13342d7abbbe37c42c8348f308ef1a341 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 03:31:57 +0000 Subject: [PATCH 35/52] hkp: make the arch probe path visible to sibling directory scopes; record compiler in pinnedBy --- .../descriptor-packaging/cmake/HkpPackaging.cmake | 4 +++- projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake index f9a82d295552..5b8c1dbb9903 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake @@ -12,7 +12,9 @@ include_guard(GLOBAL) set(HKP_PKG_DIR "${CMAKE_CURRENT_LIST_DIR}/..") set(HKP_PYTHON_ROOT "${HKP_PKG_DIR}/python") set(HKP_TOOL "${HKP_PKG_DIR}/tools/hkp_pack.py") -set(HKP_ARCH_PROBE_TOOL "${HKP_PKG_DIR}/tools/hkp_arch_probe.py") +# Cached so the probe path is visible to callers in sibling directory scopes. +set(HKP_ARCH_PROBE_TOOL "${HKP_PKG_DIR}/tools/hkp_arch_probe.py" CACHE INTERNAL + "Packer arch probe used by configure-time arch gating") set(HKP_WHEEL_DIGEST_TOOL "${HKP_PKG_DIR}/tools/hkp_wheel_digest.py") set(HKP_FIXTURES "${HKP_PKG_DIR}/tests/fixtures") diff --git a/projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json b/projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json index 9ed9a0621d2e..0fd253f3a7d4 100644 --- a/projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json +++ b/projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json @@ -4,7 +4,7 @@ "source": { "document": "llvm/docs/AMDGPUUsage.rst, 'AMDGPU Generic Processors' table", "revision": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", - "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with)" + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with); packer exercised with TheRock e8958f6709fa (run 37549184513) compiler: AMD clang version 24.0.0git (https://github.com/ROCm/llvm-project.git 7667f52dd687899256a3285ab0bce3c53bc73394)" }, "generics": { "gfx11-generic": ["gfx1100", "gfx1101", "gfx1102", "gfx1103", "gfx1150", "gfx1151", "gfx1152", "gfx1153"], From 49ce9d4d1adce0c810a7f6320ea9e7315e55e26f Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 10:14:22 -0400 Subject: [PATCH 36/52] generic targets: condense source comments, state hsaco intent once --- .../hip-kernel-provider/CMakeLists.txt | 3 +- .../cmake/HkpPackaging.cmake | 16 +---- .../python/hkp_pack/__init__.py | 6 +- .../python/hkp_pack/descriptor_context.py | 13 +--- .../python/hkp_pack/descriptors.py | 35 +++-------- .../python/hkp_pack/desk_check.py | 17 ++---- .../python/hkp_pack/generic_targets.py | 47 +++----------- .../python/hkp_pack/pipeline.py | 32 ++++------ .../descriptor-packaging/tests/conftest.py | 2 - .../tests/test_desk_check_invariants.py | 11 +--- .../tests/test_hkp_arch_probe.py | 1 - .../tests/test_hkp_generic_targets.py | 6 +- .../tests/test_hkp_pack_generic.py | 61 ++----------------- .../tests/test_hkp_pack_hip.py | 6 +- .../tests/test_hkp_pack_layout.py | 2 - .../tests/test_hkp_pack_parallel.py | 21 +------ .../tools/hkp_arch_probe.py | 15 ++--- .../descriptor-packaging/tools/hkp_pack.py | 5 +- .../src/compilation/KpackModuleCache.hpp | 7 +-- .../compilation/TestKpackGenericArchive.cpp | 8 +-- .../compilation/TestKpackModuleCache.cpp | 6 +- projects/hipdnn/plugin_sdk/CMakeLists.txt | 12 +--- .../cmake/GpuGenericTargetsTable.hpp.in | 3 +- .../cmake/ValidateGpuGenericTargets.cmake | 12 +--- .../cmake/hipdnn_plugin_sdkConfig.cmake.in | 2 - .../hipdnn_plugin_sdk/GpuGenericTargets.hpp | 26 +++----- .../ingestor/DescriptorLoader.hpp | 9 +-- .../ingestor/DeviceProperties.hpp | 27 +++----- .../ingestor/KernelIngestorStateManager.hpp | 21 ++----- .../hipdnn/plugin_sdk/tests/CMakeLists.txt | 13 ++-- .../tests/cmake/CheckGpuGenericTargets.cmake | 16 ++--- .../tests/ingestor/TestArchGate.cpp | 4 +- .../tests/ingestor/TestDescriptorLoader.cpp | 21 ++----- .../tests/ingestor/TestGenericTargets.cpp | 5 -- .../TestKernelIngestorStateManager.cpp | 20 ++---- projects/hipdnn/test_categories.yaml | 3 +- .../codegen/config_loader.py | 27 ++------ .../IngestorGenerator/codegen/generator.py | 27 +++----- .../codegen/generic_targets.py | 39 +++--------- .../tools/IngestorGenerator/codegen/models.py | 3 +- .../tests/test_config_loader.py | 12 +--- .../IngestorGenerator/tests/test_generator.py | 16 +---- .../tests/test_generic_targets.py | 10 +-- .../IngestorGenerator/tests/test_models.py | 1 - .../tests/test_native_stub.py | 3 +- .../tests/test_verify_variant_sets.py | 5 +- .../tools/verify_variant_sets.py | 13 ++-- 47 files changed, 145 insertions(+), 525 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/CMakeLists.txt b/dnn-providers/hip-kernel-provider/CMakeLists.txt index 273208b7a58c..458f2dec7e35 100644 --- a/dnn-providers/hip-kernel-provider/CMakeLists.txt +++ b/dnn-providers/hip-kernel-provider/CMakeLists.txt @@ -249,8 +249,7 @@ if(NOT TARGET hipdnn_flatbuffers_sdk) find_package(hipdnn_flatbuffers_sdk CONFIG REQUIRED) endif() if(NOT TARGET hipdnn_plugin_sdk) - # A superbuild of this binary dir cached the in-tree table path; an older installed - # SDK whose Config does not set it would otherwise be answered by that stale entry. + # Drop a stale cached table path so an older SDK Config cannot be answered by it. unset(HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON CACHE) find_package(hipdnn_plugin_sdk CONFIG REQUIRED) endif() diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake index 5b8c1dbb9903..f0826dd0ca79 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake @@ -12,7 +12,7 @@ include_guard(GLOBAL) set(HKP_PKG_DIR "${CMAKE_CURRENT_LIST_DIR}/..") set(HKP_PYTHON_ROOT "${HKP_PKG_DIR}/python") set(HKP_TOOL "${HKP_PKG_DIR}/tools/hkp_pack.py") -# Cached so the probe path is visible to callers in sibling directory scopes. +# Cached for callers in sibling directory scopes. set(HKP_ARCH_PROBE_TOOL "${HKP_PKG_DIR}/tools/hkp_arch_probe.py" CACHE INTERNAL "Packer arch probe used by configure-time arch gating") set(HKP_WHEEL_DIGEST_TOOL "${HKP_PKG_DIR}/tools/hkp_wheel_digest.py") @@ -1027,10 +1027,7 @@ endfunction() # --------------------------------------------------------------------------- # _hkp_root_covers_any_arch( ) -# TRUE when at least one non-hidden *.kdp.json under would survive: its arch -# must admit at least one arch in (_hkp_kdp_arch_matches: an absent or empty `arch` -# is a wildcard; an entry admits a target it names or a table generic containing it). -# Consulted for the default root alone (hkp_add_packaging below). +# TRUE when a non-hidden *.kdp.json under admits an arch in . # # Only FALSE is authoritative. kdp_survives() tests the KDP-level arch rule first, so a # root no arch reaches is provably empty; TRUE claims nothing beyond "not provably empty", @@ -1074,12 +1071,7 @@ endfunction() # _hkp_kdp_arch_matches( ) # TRUE when the KDP at ships for any architecture in . # -# Answered by tools/hkp_arch_probe.py, which runs the packer's own tier rule -# (hkp_pack.generic_targets.admits_target): a KDP naming no architecture, or an empty -# list, wildcards; an entry admits a target when it names it or is a generic target -# of the shared table that contains it. The generic table is the SDK's -# HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON, so the verdict and the packer read the -# same data. +# Answered by tools/hkp_arch_probe.py (the packer's own admits_target rule). # # Every ambiguous case also resolves TRUE, deliberately: only FALSE is authoritative. # Resolving ambiguity the other way would let a malformed declaration read as a clean @@ -1334,8 +1326,6 @@ function(hkp_add_packaging) "(gpu_generic_targets.json); an older installed hipdnn_plugin_sdk lacks it. " "Build against a hipdnn_plugin_sdk that provides the table.") endif() - # The dormancy verdicts below turn on the table's contents, so an edit to it (a - # reinstalled SDK) must re-run configure, as an edit to a KDP does. set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}") find_package(Python3 COMPONENTS Interpreter REQUIRED) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/__init__.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/__init__.py index d18fdf347720..f92abf61cee8 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/__init__.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/__init__.py @@ -15,10 +15,8 @@ its descriptor-relative file is packed as-is, with no compile, into the same archive, and its signature is read from the object's AMDGPU metadata, as for a compiled object. The packer does not check the object's format or target -processor. Each such UKD must list the arch(es) its object runs on, which may be a -generic target spelling of the generic target table (the object is then packed under -that spelling); a wildcard is rejected. The author's own load test -on the target arch is the only check; no in-tree load test covers hsaco. +processor; hsaco kernels declare `arch` and are tested on device +(descriptor-packaging/README.md, "hsaco kernels and arch"). """ from .errors import HkpPackError diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py index 33c4112395aa..16ea6056ed40 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py @@ -209,12 +209,7 @@ def declarations(bundles: list[Bundle], schemas: dict) -> dict: def archive_arch(entry_arch: list, target: str, generic_targets) -> str: - """The spelling to read a kpack archive with for @target. - - @target itself when @entry_arch lists it; otherwise the first generic in - @entry_arch that contains it (the archive of a generic copy is keyed by the - generic's name); otherwise @target. - """ + """The arch spelling of the kpack archive for @target (a generic copy is keyed by its generic).""" if target in entry_arch: return target for entry in entry_arch: @@ -228,9 +223,6 @@ def consumer_records( ) -> dict: """UKD id -> canonical records over EVERY consumer inside the caller root. - An entry is a consumer of @arch when its arch is empty, lists @arch, or - names a generic of the table that contains @arch. - Shared standalone UKDs carry all KDP/engine/KMD bindings, not just the selected bundle's. Agreement stays the sole record-construction authority. """ @@ -239,8 +231,7 @@ def consumer_records( header = {k: v for k, v in bundle.kdp_doc.items() if k != "kernelDescriptors"} header["arch"] = [arch] for entry in bundle.entries: - # The literal-arch test is not subsumed by `admits_target`: a generic - # copy's @arch is the generic's own spelling, which names no device. + # Literal test kept: a generic copy's @arch is the generic's own name, no device. if ( entry.arch and arch not in entry.arch diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py index 18f556e46f90..e5066cd3f390 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py @@ -66,8 +66,6 @@ def id(self): @dataclass class FlatInput: descriptors: list = field(default_factory=list) - # The generic target table the root was validated against; every arch rule - # downstream (kdp_survives, the pipeline's selection) reads it from here. generic_targets: object = None def by_type(self, dtype): @@ -132,12 +130,7 @@ def arch_matches(kdp_doc, arch): def kdp_arch_matches(kdp_doc, arch, generic_targets): - """Whether a KDP is emitted into the pass for @arch. - - A generic pass (@arch is a table generic) takes literal membership only: an - empty-arch KDP is a concrete-shard wildcard and is NOT emitted into the - generic copy. Any other arch follows arch_matches. - """ + """Whether a KDP is emitted into the pass for @arch (a generic pass: literal membership only).""" if generic_targets.has(arch): return arch in (kdp_doc.get("arch") or []) return arch_matches(kdp_doc, arch) @@ -149,8 +142,7 @@ def _arch_subset_ok(ukd_arch, kdp_arch, generic_targets): An empty list on either side is a wildcard: a wildcard KDP admits any UKD, and a wildcard UKD is admissible under any KDP. Two explicit lists require the UKD's expanded device set to lie within the KDP's (a generic stands for - its members). The stricter literal rules for generic KDPs live in - validate_generic_arch. + its members). """ if not ukd_arch or not kdp_arch: return True @@ -532,9 +524,7 @@ def load_flat_input(root, generic_targets, log=print): is not one of ours: warn and skip it rather than aborting the pack, so an incidental file in the source folder is tolerated. A hidden path -- any dot-prefixed segment, or a dot-prefixed filename -- is warned and skipped - the same way, so nothing the walk passes over is invisible. `generic_targets` - is the loaded GenericTargets table the arch rules are checked against and - that the returned FlatInput carries. Raises + the same way, so nothing the walk passes over is invisible. Raises HkpPackError on any malformed / missing-field / unknown-type / dangling-reference descriptor that IS type-tagged. @@ -615,8 +605,7 @@ def _reject_unknown_generics(archs, file_name, table): def _reject_mixed_generic_list(archs, file_name, table): - """One list may not hold a generic together with a member it contains, nor - two generics that share a member.""" + """A list may not hold a generic with a member it contains, nor two generics sharing a member.""" advice = ( "list one, or author two KDPs (an explicit override plus a generic " "fallback are separate packs)" @@ -640,17 +629,7 @@ def _reject_mixed_generic_list(archs, file_name, table): def validate_generic_arch(flat): - """Generic-target rules over every KDP/UKD `arch` in the root. - - Per list: names that look generic but are not in the table are errors, and a - list may not mix a generic with a member it contains or two generics sharing - a member. Per KDP holding a generic: a UKD with its own arch must list - every generic of the KDP and only entries the KDP lists. Per KDP of any - shape: a standalone UKD with an empty `arch` (unrestricted) is valid only under - a KDP whose `arch` is empty too (an inline UKD with none inherits the KDP's); a UKD may not - name a generic the KDP does not list. rocKE does not support generics yet. - The loader accepts the lenient forms; this is the stricter packer-side layer. - """ + """Packer-side generic-target rules over every KDP/UKD `arch`; see descriptor-packaging/README.md.""" table = flat.generic_targets ukd_by_id = flat.ukd_by_id() for desc in flat.descriptors: @@ -673,7 +652,7 @@ def validate_generic_arch(flat): if isinstance(entry, str): sdesc = ukd_by_id.get(entry) if sdesc is None: - continue # reported by _validate_references + continue ukd, standalone = sdesc.doc, sdesc else: ukd, standalone = entry, None @@ -708,7 +687,7 @@ def validate_generic_arch(flat): else: who = f"inline UKD '{ukd.get('name', ukd.get('id', '?'))}'" if standalone is None and not ukd_arch: - continue # an inline UKD with no arch inherits the pack + continue # inline UKD with no arch inherits the KDP's lists_all = all(g in ukd_arch for g in kdp_generics) only_listed = all(a in kdp_arch for a in ukd_arch) if not lists_all or not only_listed: diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py index a9fa7af621f7..b4a920b5e9f7 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py @@ -47,8 +47,7 @@ # identity can say so explicitly instead of silently shortening. _ABSENT = "" -#: A device no table generic contains, standing in for "any device" when a list is -#: empty (unrestricted). +#: Stands in for "any device" when a list is empty (unrestricted). _SYNTHETIC_DEVICE = "" @@ -173,7 +172,7 @@ def metadata_identity_fields(kernels: list[dict]) -> tuple[str, ...]: def load_kdp_arch(kdp_path: Path) -> list[str]: - """The arch list a `.kdp.json` declares; what a kernel with no list inherits.""" + """The arch list a `.kdp.json` declares (inherited by kernels with none).""" return list( json.loads(Path(kdp_path).read_text(encoding="utf-8")).get("arch") or [] ) @@ -388,15 +387,8 @@ def metadata_spec_drift(kernels: list[dict], fields=None) -> list[tuple[str, str def _reachable_together(group: list[dict], generic_targets, kdp_arch=()) -> int: - """The largest number of kernels in `group` that tie at one arch tier on one - device -- the loader's rule (`archesCompete`): any two kernels sharing a tier on - some device collide, whether or not that tier is the best one there. - - A kernel's arch is its own list, else the KDP's (`kdp_arch`), as - `KernelIngestorStateManager` takes it. Candidate devices are every explicit id - and every member of a table generic in those lists, plus one synthetic device - when any list is empty (an empty list is unrestricted, so it ties there at the - unrestricted tier). A list naming only unknown generics matches nothing. + """The largest number of kernels in `group` tying at one arch tier on one device + (the loader's `archesCompete` rule). A kernel's arch is its own list, else `kdp_arch`. """ lists = [list(k.get("arch") or kdp_arch) for k in group] if not any(lists): @@ -426,7 +418,6 @@ def duplicate_matcher_tuples( """Invariant 2: no two kernels may share a matcher tuple on the same arch -- one is unreachable. Returns {tuple: count} for every tuple two kernels reach one device at the same arch tier with, the scope the runtime refuses in. - `kdp_arch` is the arch a kernel without its own list inherits. The compared set is the union of `fields` present in any kernel's metadata, never ``kernels[0]``'s, which would make the identity list-order dependent. A diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py index 89e7a0cee4f8..2aeec54e187e 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py @@ -1,17 +1,4 @@ -"""LLVM generic GPU targets as data: the shared table and the tier algebra over it. - -One JSON document (projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json) maps each -generic target (`gfx11-generic`) to the concrete processors it supports. The C++ loader -reads the same document through a generated header; this module is the Python side. - -Membership is data, never name shape: a generic-shaped name that the table does not list -(an "unknown generic") expands to the empty set and matches no device. Provenance of the -table is validated once, by the CMake validator, not here. - -An `arch` list ranks per device: EXPLICIT (the device's own base id) beats GENERIC (a table -generic containing the device) beats UNRESTRICTED (an empty list, any device). Lists are -sequences of strings. -""" +"""Generic GPU target table and arch-tier algebra; see descriptor-packaging/README.md.""" import json from pathlib import Path @@ -36,7 +23,7 @@ class GenericTargets: - """The generic target table: generic name -> member processor names, document order.""" + """Generic name -> member processor names.""" def __init__(self, path, generics): self._path = Path(path) @@ -94,7 +81,7 @@ def has(self, name): return name in self._generics def members(self, name): - """Members of generic @name in document order; KeyError when it is not listed.""" + """Members of generic @name; KeyError when unlisted.""" return self._generics[name] def names(self): @@ -102,7 +89,7 @@ def names(self): def is_generic_shaped(name): - """Shape only (`...-generic`); use GenericTargets.has for membership.""" + """Name shape only; GenericTargets.has tests membership.""" return name.endswith(_GENERIC_SUFFIX) @@ -116,10 +103,7 @@ def _prefix_match(device, entry): def entry_tier(entry, device, table): - """Tier of one list entry for @device (features allowed), or None when it admits it. - - A generic-shaped entry is never EXPLICIT, so an unknown generic matches no device. - """ + """Tier of one entry for @device, or None.""" if is_generic_shaped(entry): if table.has(entry) and _strip_features(device) in table.members(entry): return TIER_GENERIC @@ -130,7 +114,7 @@ def entry_tier(entry, device, table): def list_tier(entries, device, table): - """Best tier of @entries for @device; an empty list is UNRESTRICTED; None = no match.""" + """Best tier of @entries for @device; None = no match.""" if not entries: return TIER_UNRESTRICTED best = None @@ -144,10 +128,7 @@ def list_tier(entries, device, table): def expand(entries, table): - """The set of device ids @entries admits; None (unrestricted) for an empty list. - - A generic stands for its members; an unknown generic contributes nothing. - """ + """Device ids @entries admits; None for an empty list.""" if not entries: return None devices = set() @@ -161,11 +142,7 @@ def expand(entries, table): def covers(outer, inner, table): - """Is every device @inner admits also admitted by @outer? - - Empty @outer covers anything and empty @inner is covered by anything; an unknown - generic in @inner expands to nothing and is covered vacuously. - """ + """Does @outer admit every device @inner admits?""" expanded_outer = expand(outer, table) expanded_inner = expand(inner, table) if expanded_outer is None or expanded_inner is None: @@ -174,11 +151,7 @@ def covers(outer, inner, table): def compete(a, b, table): - """Do the lists tie on some candidate device (both match it at the same tier)? - - Candidates are every explicit id in either list and every member of every table - generic in either list. Two empty lists compete. - """ + """Do the lists tie at one tier on some device?""" if not a and not b: return True candidates = set() @@ -196,5 +169,5 @@ def compete(a, b, table): def admits_target(entries, target, table): - """Does @entries admit @target: empty, listing it literally, or a generic containing it.""" + """Does @entries admit @target?""" return list_tier(entries, target, table) is not None diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py index c16393b90253..763035015b7b 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py @@ -127,7 +127,7 @@ class ArchResult: out_dir: Path kpack_path: Path skipped: bool = False - # Archives of the generic passes copied into this member's folder. + # Generic-pass archives copied into this member's folder. generic_kpack_paths: tuple = () @@ -317,6 +317,9 @@ def _compile_ukd_variant( "consumers": consumers, } elif kind == "hsaco": + # The packer never reads the object's target processor; hsaco kernels declare + # `arch` and are tested on device (descriptor-packaging/README.md, + # "hsaco kernels and arch"). file = ks["file"] symbol = ks["symbol"] path = resolve_hsaco_file(source_root, rel_dir, file, where) @@ -420,8 +423,7 @@ def _selected_entries(doc, arch, ukd_by_id, generic_targets): because the walk needs its `path.name` for the error context and for the shipped filename as well as its `rel_dir` for the variant key. - All three arch filters live here and nowhere else (the KDP-level one is - `kdp_arch_matches`, the two UKD-level ones `arch_matches`), so the prewarm and the + All three arch filters live here and nowhere else, so the prewarm and the serial walk cannot select different variant sets. `ukd_by_id` arrives as a parameter rather than being reached for through `flat`, which leaves the generator no way to enumerate a standalone UKD no KDP references: an orphan @@ -1462,15 +1464,10 @@ def _all_ukds(): def _merge_generic_into_member(generic_dir, member_dir, staging_dir): - """Merge one generic pass's tree into a member shard folder, atomically. - - Builds `staging_dir` as a copy of `member_dir` (when it exists) plus every - file of `generic_dir`, then swaps it over `member_dir`. A destination that - already exists is skipped when its bytes equal the source's (the shared - engine/dispatch/matcher/KMD descriptors both passes emit verbatim) and - refused when they differ: two shards whose member sets intersect would then - write one path with different content. Any failure removes the staging dir - and leaves `member_dir` exactly as it was. + """Merge a generic pass's tree into a member folder atomically via `staging_dir`. + + An existing destination file is skipped when byte-identical and refused when + it differs. Any failure leaves `member_dir` untouched. """ generic_dir, member_dir, staging_dir = ( Path(generic_dir), @@ -1546,16 +1543,11 @@ def run_pipeline( of a pass-through kind runs no producer and is emitted as authored, so a root that holds only pass-through UKDs writes descriptors and no archive. An arch with no surviving KDP is skipped cleanly (no folder, no kpack) and - logged with 'no kernels for , skipping' (a selected member whose KDPs are - all generic still gets a folder, from the generic copy); every arch skipping + logged with 'no kernels for , skipping'; every arch skipping is a failure, not a pack. Empty arch list installs nothing (exit 0). - `generic_targets_json` names the generic target table. After the concrete - passes, every table generic that some KDP lists and that contains a selected - arch is compiled and packed ONCE (a pure function of the source tree, the - generic's spelling, hipcc and the table), and that one tree is merged into - the folder of every selected member whose concrete pass did not fail. A - generic never gets a folder of its own under `out_root`. + `generic_targets_json` is the generic target table; generic passes are + merged into member folders (descriptor-packaging/README.md). """ out_root = Path(out_root) results = {} diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/conftest.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/conftest.py index ff8b912c14b2..a7b852168f0b 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/conftest.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/conftest.py @@ -12,8 +12,6 @@ if str(_PKG_ROOT) not in sys.path: sys.path.insert(0, str(_PKG_ROOT)) -# The in-repo generic GPU target table every pack in these tests is validated against. -# Exported so a test names it once; a test needing a different table writes its own. from hkp_pack.generic_targets import DEFAULT_TABLE_PATH, GenericTargets # noqa: E402 GENERIC_TARGETS_JSON = DEFAULT_TABLE_PATH diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py index 3d9e990d2708..cdde43057e94 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py @@ -197,9 +197,7 @@ def test_one_tuple_on_a_shared_arch_is_still_a_duplicate(self): def test_an_arch_less_kernel_inherits_the_kdp_arch_and_collides_with_an_explicit_one( self, ): - """The loader gives a kernel with no arch of its own the KDP's list, so - under a KDP listing gfx942 the arch-less kernel and the explicit gfx942 one - tie at the same tier on gfx942.""" + """An arch-less kernel inherits the KDP list, so it ties an explicit gfx942 one.""" kernels = self._twins(None, ["gfx942"]) assert duplicate_matcher_tuples( kernels, ("dtype",), GENERIC_TARGETS, ["gfx942"] @@ -213,9 +211,7 @@ def test_an_arch_less_kernel_under_a_disjoint_kdp_arch_does_not_collide(self): ) def test_a_tie_beaten_on_every_device_it_reaches_is_still_a_duplicate(self): - """Both gfx12-generic kernels are outranked by an explicit one on each member - (gfx1200, gfx1201), so they never tie at the BEST tier anywhere; they still - tie at the generic tier on both, which the loader refuses.""" + """Generic kernels outranked on every member still tie at the generic tier.""" kernels = [ {"name": n, "metadata": {"dtype": "FLOAT"}, "arch": a} for n, a in ( @@ -239,8 +235,7 @@ def test_a_tie_beaten_on_every_device_it_reaches_is_still_a_duplicate(self): ids=["generic_vs_arch_less", "generic_vs_its_member", "unknown_generics"], ) def test_lists_that_never_tie_at_one_tier_do_not_collide(self, left, right): - """A generic outranks an arch-less kernel and loses to its own member; an - unknown generic matches no device.""" + """A generic outranks arch-less and loses to its own member; unknown matches nothing.""" kernels = self._twins(left, right) assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == {} diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py index 1163a9c85337..6f5a7f1d0e4d 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py @@ -45,7 +45,6 @@ def test_probe_false_when_no_entry_reaches_any_selected_target(tmp_path): def test_probe_true_for_a_generic_absent_from_the_table(tmp_path): - """The packer reports it as an error; a FALSE would hide it behind a dormant root.""" assert _probe(tmp_path, ["gfx99-generic"], "gfx942") == "TRUE" assert _probe(tmp_path, ["gfx99-generic", "gfx950"], "gfx942") == "TRUE" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py index 2e1b60f5a815..663685f56d74 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py @@ -1,8 +1,4 @@ -"""The Python tier algebra over the shared generic target table. - -Literal cases per outcome class; this test locates the table itself rather than through -conftest.py. -""" +"""The Python tier algebra over the shared generic target table.""" import json from pathlib import Path diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py index 2073edba78f9..ab691316bf02 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py @@ -1,15 +1,4 @@ -"""Generic GPU targets at pack time. - -A KDP whose `arch` names a table generic (`gfx11-generic`) is compiled and packed -once, under the generic's spelling, and that one tree is copied into the folder of -every selected member of the generic. The rules that keep that sound (a generic and -its member in one list, a UKD's arch under a generic KDP, the reverse direction, an -unknown name, rocKE) are validation errors; the merge into a member folder is atomic -and refuses two shards that would write one path with different bytes. - -The quick tier packs `embedded_source` and authored `hsaco` UKDs, which need no -compiler. The hipcc tier at the end compiles for real and is not quick. -""" +"""Generic GPU targets at pack time; see descriptor-packaging/README.md.""" import copy import json @@ -68,7 +57,6 @@ def _embedded(uid, arch=None): return ukd -# What a producing UKD must carry: the consumer declaration its engine's KMD checks. _CONTRACT = { "specialization_contract": { "schema_version": 1, @@ -100,11 +88,7 @@ def _kdp(kid, arch, ukds): def _root(tmp_path, empty_arch_fixture, kdps, standalone=()): - """A source root: the fixture's shared descriptors plus the given KDPs. - - `kdps` maps a file stem to a KDP document, `standalone` maps a file stem to a - standalone UKD document. - """ + """A source root: the fixture's shared descriptors plus KDP and standalone-UKD docs by stem.""" root = tmp_path / "src" root.mkdir() for path in empty_arch_fixture.iterdir(): @@ -131,7 +115,6 @@ def _pack(root, tmp_path, arches, rocm_kpack_dir, table=GENERIC_TARGETS_JSON): def _files(folder): - """Relative path -> bytes of every file under `folder`.""" return { p.relative_to(folder).as_posix(): p.read_bytes() for p in sorted(folder.rglob("*")) @@ -143,9 +126,6 @@ def _load(root, table=GENERIC_TARGETS): return load_flat_input(root, table, log=lambda *_: None) -# --- layout ------------------------------------------------------------------ - - @pytest.mark.quick def test_generic_entries_land_in_every_selected_member_folder_only( tmp_path, empty_arch_fixture, rocm_kpack_dir @@ -163,8 +143,6 @@ def test_generic_entries_land_in_every_selected_member_folder_only( results = _pack(root, tmp_path, ["gfx942", MEMBER_A, MEMBER_B], rocm_kpack_dir) out = tmp_path / "out" - # One folder per selected arch: none for the generic or an unselected member, and no - # staging directory left beside them. assert sorted(p.name for p in out.iterdir()) == [MEMBER_A, MEMBER_B, "gfx942"] assert not (out / UNSELECTED_MEMBER).exists() assert not (out / GENERIC).exists() @@ -172,7 +150,6 @@ def test_generic_entries_land_in_every_selected_member_folder_only( arch: out / arch for arch in ("gfx942", MEMBER_A, MEMBER_B) } - # The generic KDP ships verbatim into each member; a nonmember never gets it. for member in (MEMBER_A, MEMBER_B): kdp = _read(out / member / "g.kdp.json") assert kdp["arch"] == [GENERIC] @@ -183,18 +160,15 @@ def test_generic_entries_land_in_every_selected_member_folder_only( ).read_bytes() assert not (out / "gfx942" / "g.kdp.json").exists() - # Concrete content stays with its own arch. assert (out / "gfx942" / "c.kdp.json").is_file() assert not (out / MEMBER_A / "c.kdp.json").exists() assert (out / MEMBER_B / "explicit.kdp.json").is_file() assert not (out / MEMBER_A / "explicit.kdp.json").exists() - # An empty-arch KDP is not part of the generic pass. generic_tree = tmp_path / "inter" / ".generic-out" / GENERIC assert (generic_tree / "g.kdp.json").is_file() assert not (generic_tree / "wild.kdp.json").exists() - # Shared engine/dispatch/matcher files are identical in every folder. shared = [p for p in root.iterdir() if p.name.endswith(_SHARED_SUFFIXES)] assert shared for arch in ("gfx942", MEMBER_A, MEMBER_B): @@ -224,9 +198,6 @@ def test_generic_entry_is_not_materialized_when_no_member_is_selected( assert not (tmp_path / "inter" / GENERIC).exists() -# --- merge guard and failure policy ------------------------------------------ - - @pytest.mark.quick def test_merge_collision_with_differing_bytes_is_refused_and_leaves_the_member_untouched( tmp_path, @@ -312,10 +283,7 @@ def failing(flat, inter, out_dir, *args, **kwargs): def test_generic_copies_of_every_producer_are_equal_across_separate_builds( tmp_path, empty_arch_fixture, hsaco_fixture_dir, rocm_kpack_dir ): - """An embedded and an hsaco generic entry, packed once for gfx1100 and once for - gfx1151 from separate roots: the member folders hold identical files, so the - copies per-arch shard builds produce collapse to one. Nothing of the build's - target list may leak into the generic copy.""" + """Generic copies from separate per-arch builds are identical; the build's targets never leak in.""" hsaco = _hsaco_ukd("ukd-hsaco", [GENERIC]) folders = {} for member in (MEMBER_A, MEMBER_B): @@ -336,9 +304,6 @@ def test_generic_copies_of_every_producer_are_equal_across_separate_builds( assert folders[MEMBER_A] == folders[MEMBER_B] -# --- authored hsaco under a generic ------------------------------------------ - - @pytest.mark.quick def test_hsaco_under_a_generic_packs_under_the_generic_key_into_every_member_folder( tmp_path, empty_arch_fixture, hsaco_fixture_dir, rocm_kpack_dir @@ -364,9 +329,6 @@ def test_hsaco_under_a_generic_packs_under_the_generic_key_into_every_member_fol assert _files(out / MEMBER_A) == _files(out / MEMBER_B) -# --- arch rules at load ------------------------------------------------------- - - def _rocke_ukd(uid, arch=None): ukd = _embedded(uid, arch) ukd["kernel_source"] = { @@ -397,8 +359,7 @@ def _hsaco_ukd(uid, arch): }, } -# (kdp arch, UKD form, UKD arch, table override). UKD form: "inline" puts the UKD in the -# KDP, "standalone" is a separate .ukd.json the KDP names, "rocke" is an inline rocKE UKD. +# (kdp arch, UKD form: inline|standalone|rocke, UKD arch, table override) _REJECTED = { "generic_beside_its_member": ([GENERIC, MEMBER_B], "inline", None, None), "two_generics_sharing_a_member": ( @@ -484,11 +445,7 @@ def test_a_consistent_arch_declaration_loads(tmp_path, empty_arch_fixture, case) _load(_arch_rule_root(tmp_path, empty_arch_fixture, kdp_arch, form, ukd_arch)) -# --- hipcc tier (not quick) --------------------------------------------------- - - def _hip_generic_root(dest, empty_arch_fixture): - """The empty_arch hip fixture with its KDP rewritten to the generic.""" shutil.copytree(empty_arch_fixture, dest) kdp_path = dest / "solo.kdp.json" doc = _read(kdp_path) @@ -533,9 +490,7 @@ def test_generic_hip_archive_is_keyed_by_the_generic_spelling( def test_compiled_agreement_holds_on_a_generic_copy( tmp_path, empty_arch_fixture, hipcc, rocm_kpack_dir ): - """A shipped generic copy carries the generic as its literal arch, which names no - device: `consumer_records` must still count its own entry as a consumer, or the - record lookup for the copy's kernel raises.""" + """A generic copy's literal arch names no device; `consumer_records` must still count it.""" from hkp_pack.desk_check import compiled_agreement root = _hip_generic_root(tmp_path / "src", empty_arch_fixture) @@ -551,11 +506,7 @@ def test_compiled_agreement_holds_on_a_generic_copy( def test_generic_pack_is_byte_identical_across_separate_per_arch_builds( tmp_path, empty_arch_fixture, hipcc, rocm_kpack_dir, monkeypatch ): - """Three separate builds of one hip root -- GPU_TARGETS gfx1100 only, gfx1151 - only, and both -- each with its own out root, inter root, cwd and absolute copy - of the source. Every file of the generic entry is identical across all of them - and across the member folders of the two-target build: the copies a real - per-arch shard build produces must collapse to one.""" + """Separate per-arch builds (own roots, cwd, source copy) yield identical generic files.""" builds = {} for label, arches in ( ("only_a", [MEMBER_A]), diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py index ef40d8482c88..dedd681ceb0a 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py @@ -772,8 +772,7 @@ def test_standalone_ukd_shared_by_two_kdps_stored_once( # is stored once (deduped by variant_key), not once per referencing KDP. src = _copy_fixture(tmp_path, main_fixture) # copy.kdp.json is gfx942-only; add the standalone ref to it too, so both it - # and pointwise.kdp.json reference the same standalone id on gfx942. The - # standalone declares [gfx942, gfx950], so copy.kdp.json lists both. + # and pointwise.kdp.json reference the same standalone id on gfx942. p = src / "copy.kdp.json" doc = _read(p) doc["arch"] = ["gfx942", "gfx950"] @@ -828,9 +827,6 @@ def test_standalone_ukd_referenced_by_a_second_engine_without_declaring_it_fails def test_standalone_ukd_referenced_by_wildcard_kdp( tmp_path, main_fixture, hipcc, rocm_kpack_dir ): - # An arch-less standalone UKD (unrestricted) referenced by a wildcard KDP - # (arch []) ships in every shard. The fixture's own standalone is authored for - # gfx942, so this one is a sibling without an arch. src = _copy_fixture(tmp_path, main_fixture) wild_doc = _read(src / _STANDALONE_UKD_FILE) wild_doc["id"] = "ukd-pointwise-add-f32-b128-wild" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py index 9283ddb31161..fc4fec07377a 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py @@ -1523,8 +1523,6 @@ def _make_embedded(folder, arch=None): "priority": 0, } if arch: - # A standalone UKD without an arch is unrestricted, valid only under a KDP - # with none; under an arch'd KDP it declares that arch itself. standalone["arch"] = list(arch) (folder / _STANDALONE_FILE).write_text( json.dumps(standalone, indent=2) + "\n", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py index a514d142b813..116fd11aa9af 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py @@ -519,11 +519,7 @@ def test_prewarm_jobs_are_deduped_on_variant_key(corpus): def _call_sites(callee): - """`callee` call counts in pipeline.py, keyed by enclosing function. - - Parsed rather than counted as strings: an explanatory comment naming the - callee is not a call. - """ + """`callee` call counts in pipeline.py, keyed by enclosing function.""" tree = ast.parse(inspect.getsource(pipeline)) counts = {} for node in tree.body: @@ -548,24 +544,13 @@ def _call_sites(callee): @pytest.mark.quick def test_arch_matches_call_sites_are_pinned(): - """The two UKD-level selection filters live in the generator and nowhere else. - - `arch_matches` is the empty-inherits filter on a UKD. `compile_intermediate`'s - KDP disposition call is `kdp_arch_matches`. - """ + """The UKD-level selection filters live in the generator and nowhere else.""" assert _call_sites("arch_matches") == {"_selected_entries": 2} @pytest.mark.quick def test_kdp_arch_matches_call_sites_are_pinned(): - """The KDP-level filter is in the generator; `compile_intermediate` keeps one call. - - That call is not a filter: it decides KDP disposition -- copy the authored KDP - through verbatim -- before the deepcopy the generator would consume. A call in - any other pipeline function is a further selection site, which is the - divergence a single shared generator exists to make impossible. The - `kdp_survives` call lives in descriptors.py, not pipeline.py. - """ + """The KDP-level filter lives in the generator; `compile_intermediate` only decides disposition.""" assert _call_sites("kdp_arch_matches") == { "_selected_entries": 1, "compile_intermediate": 1, diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py index 94293da82525..4584c4c783ae 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py @@ -3,14 +3,8 @@ python3 tools/hkp_arch_probe.py --generic-targets-json \\ --arches "gfx1100;gfx1151" --kdp -The CMake configure step asks this to decide whether a root is dormant, so it -must give the packer's own answer: it applies `generic_targets.admits_target` -(an empty `arch` is a wildcard; an entry admits a target it names or a table -generic containing it) to each selected arch. - -Only FALSE is authoritative. A KDP that does not parse, or whose `arch` is not -an array of strings, or that names a generic absent from the table, prints TRUE so the packer reports what is wrong with it. -Exits 0 whenever it answered; non-zero only when the table cannot be read. +Only FALSE is authoritative: a malformed KDP or unknown generic prints TRUE so the +packer reports it. Non-zero exit only when the table cannot be read. """ import argparse @@ -18,7 +12,7 @@ import sys from pathlib import Path -# Same shadowing hazard hkp_pack.py's own tool guards against. +# Same shadowing guard as hkp_pack.py. _PKG_ROOT = str(Path(__file__).resolve().parent.parent / "python") while _PKG_ROOT in sys.path: sys.path.remove(_PKG_ROOT) @@ -46,8 +40,7 @@ def _covers_any(kdp_path, arches, table): return True if not entries: return True - # An unknown generic admits nothing, which would read as a clean absence; the packer - # names it as an error, so it must be reached. + # An unknown generic would read as absence; the packer must reach it to report it. if any(is_generic_shaped(e) and not table.has(e) for e in entries): return True return any(admits_target(entries, arch, table) for arch in arches) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_pack.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_pack.py index 254ab33db69c..5782e2cd0d29 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_pack.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_pack.py @@ -115,10 +115,7 @@ def _parse_args(argv): p.add_argument( "--generic-targets-json", required=True, - help="The generic GPU target table (gpu_generic_targets.json) shared with " - "the loader. GPU_TARGETS stays concrete: every table generic that a KDP " - "lists and that contains a selected arch is packed once and copied into " - "each selected member's folder.", + help="The generic GPU target table (gpu_generic_targets.json).", ) p.add_argument( "--kpack-python-dir", diff --git a/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp b/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp index 78ae473a68c3..c8e0dd63d16f 100644 --- a/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp +++ b/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp @@ -105,11 +105,8 @@ class KpackModuleCache : public ModuleCache& archiveArches, std::string_view deviceArch) { diff --git a/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackGenericArchive.cpp b/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackGenericArchive.cpp index f26b72d3737c..593b45d6350b 100644 --- a/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackGenericArchive.cpp +++ b/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackGenericArchive.cpp @@ -26,7 +26,6 @@ using hip_kernel_provider::testing::PackedKernelSource; using hip_kernel_provider::testing::readPackedKernelSource; using hip_kernel_provider::testing::unitKpackRoot; -/// The generic named by test_descriptors/shared/conv_fwd/conv_fwd_generic.kdp.json. constexpr const char* GENERIC = "gfx11-generic"; constexpr const char* GENERIC_KDP = "conv_fwd_generic.kdp.json"; @@ -40,8 +39,7 @@ std::vector archivesOf(const std::filesystem::path& kpackFile) return arches; } -/// Packed arch shards present in this tree: every directory of the packed set holding a -/// kpack/ child. Enumerated from the filesystem so the test needs no device. +/// Packed arch shards in this tree, enumerated from the filesystem (no device needed). std::vector packedShards() { std::vector shards; @@ -92,16 +90,13 @@ TEST(TestPackedGenericArchive, ShardsCarryTheGenericArchiveExactlyWhenTheirArchI PackedKernelSource packed; ASSERT_NO_FATAL_FAILURE(readPackedKernelSource(shard, GENERIC_KDP, packed)); - // The generic descriptor points at an archive keyed by the generic alone. const std::vector keys = archivesOf(packed.archive); ASSERT_EQ(keys, std::vector{GENERIC}); - // KpackModuleCache serves this shard's device from that archive's generic key. const std::string* selected = KpackModuleCache::selectArch(keys, arch); ASSERT_NE(selected, nullptr); EXPECT_EQ(*selected, GENERIC); - // The device's own archive keeps winning when both are visible. std::vector both; for(const auto& file : kpackFiles) { @@ -115,7 +110,6 @@ TEST(TestPackedGenericArchive, ShardsCarryTheGenericArchiveExactlyWhenTheirArchI ASSERT_NE(preferred, nullptr); EXPECT_EQ(*preferred, arch); - // The generic key resolves the descriptor's own toc_key to a code object. KpackArchive archive; KpackError error; ASSERT_TRUE(archive.open(packed.archive, error)) << error.codeName; diff --git a/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp b/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp index 7f4f8827f626..70502a9693a1 100644 --- a/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp +++ b/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp @@ -159,7 +159,7 @@ TEST(TestKpackModuleCacheLoad, ReportsAnArchTheArchiveDoesNotHold) TEST(TestKpackModuleCacheSelectArch, PrefersTheExactDeviceArchOverAGenericKey) { - // The generic key sorts first, so a first-match-wins over mixed keys would pick it. + // The generic key sorts first; first-match-wins would pick it. const std::vector keys{"gfx11-generic", "gfx1151"}; const auto* selected = KpackModuleCache::selectArch(keys, "gfx1151:sramecc+:xnack-"); @@ -180,9 +180,7 @@ TEST(TestKpackModuleCacheSelectArch, FallsBackToAGenericKeyContainingTheDevice) TEST(TestKpackModuleCacheSelectArch, DeclinesAKeyThatDoesNotNameTheDevice) { - // gfx1250 is not a gfx12-generic member however close its name is to the family, and - // shape never implies membership: a table-absent generic admits no device, not even the - // one it spells. + // Name shape never implies membership. EXPECT_EQ(KpackModuleCache::selectArch({"gfx12-generic"}, "gfx1250"), nullptr); EXPECT_EQ(KpackModuleCache::selectArch({"gfx11-generic", "gfx12-generic"}, "gfx942"), nullptr); EXPECT_EQ(KpackModuleCache::selectArch({"gfx99-generic"}, "gfx99-generic"), nullptr); diff --git a/projects/hipdnn/plugin_sdk/CMakeLists.txt b/projects/hipdnn/plugin_sdk/CMakeLists.txt index 1a0d27a7ce4a..46146d57f664 100644 --- a/projects/hipdnn/plugin_sdk/CMakeLists.txt +++ b/projects/hipdnn/plugin_sdk/CMakeLists.txt @@ -21,9 +21,7 @@ hipdnn_generate_version_header(hipdnn_plugin_sdk) target_link_libraries(hipdnn_plugin_sdk INTERFACE hipdnn_flatbuffers_sdk hipdnn_data_sdk) -# Generic GPU target table: one JSON document is the source of truth. It is validated here, -# turned into a constexpr C++ table at configure time, installed beside the Config for -# the packer, and read directly by Python tools. Never keep a second copy. +# Generic GPU target table: data/gpu_generic_targets.json is the single source of truth. include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/ValidateGpuGenericTargets.cmake") set(HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/data/gpu_generic_targets.json") hipdnn_validate_gpu_generic_targets("${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}") @@ -37,7 +35,6 @@ set(HIPDNN_GENERIC_TARGET_MEMBER_ARRAYS "") set(HIPDNN_GENERIC_TARGET_ROWS "") set(HIPDNN_GENERIC_TARGET_ROW_COUNT ${_generic_count}) math(EXPR _last_generic "${_generic_count} - 1") -# cmake-lint reports E1120 on foreach(... RANGE ...) arguments, which are valid CMake. # cmake-lint: disable=E1120 foreach(_g RANGE 0 ${_last_generic}) string(JSON _generic MEMBER "${_generic_targets_json}" generics ${_g}) @@ -60,14 +57,12 @@ configure_file( "${CMAKE_CURRENT_BINARY_DIR}/include/hipdnn_plugin_sdk/GpuGenericTargetsTable.hpp" @ONLY ) -# Build-tree copy next to the build-tree Config; the installed copy sits beside the -# installed Config, so one CMAKE_CURRENT_LIST_DIR expression serves both. +# Beside the build-tree Config, as the installed copy sits beside the installed Config. set(_generic_targets_build_copy "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/cmake/hipdnn_plugin_sdk/gpu_generic_targets.json" ) configure_file("${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}" "${_generic_targets_build_copy}" COPYONLY) -# CACHE INTERNAL so sibling subdirectories (the hip-kernel-provider) see it; directory -# scope would not reach them. +# CACHE INTERNAL so sibling subdirectories see it. set(HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON "${_generic_targets_build_copy}" CACHE INTERNAL "Path to the generic GPU target table JSON shipped with the plugin SDK" ) @@ -142,7 +137,6 @@ install( COMPONENT Development ) -# Install the generic target table and its generated C++ form install( FILES "${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}" DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hipdnn_plugin_sdk diff --git a/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in b/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in index cb94152acc16..bf8e8524decc 100644 --- a/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in +++ b/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in @@ -11,7 +11,7 @@ namespace hipdnn_plugin_sdk::generated { -/// One LLVM generic processor and the processors it supports, in document order. +/// One LLVM generic processor and its member processors. struct GenericTargetRow { std::string_view name; @@ -20,7 +20,6 @@ struct GenericTargetRow }; // clang-format off -// Constexpr tables of string_view; arrays keep the data in static storage with no allocation. // NOLINTBEGIN(modernize-avoid-c-arrays) @HIPDNN_GENERIC_TARGET_MEMBER_ARRAYS@ inline constexpr GenericTargetRow GENERIC_TARGET_ROWS[] = { diff --git a/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake b/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake index 254390c714b7..ef9fe4d50406 100644 --- a/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake +++ b/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake @@ -1,14 +1,10 @@ # Copyright © Advanced Micro Devices, Inc., or its affiliates. # SPDX-License-Identifier: MIT -# Validates data/gpu_generic_targets.json: the schema version and the shape of the -# generic-target -> member-processor table. Every violation is a FATAL_ERROR that names -# the offending key or member, so a failing configure points at the line to fix. -# -# cmake-lint does not model foreach(... RANGE ...) and reports E1120 on its arguments. +# Validates data/gpu_generic_targets.json; each violation is a FATAL_ERROR naming the key. # cmake-lint: disable=E1120 -# Checks the 'generics' map: names, member shape, duplicates. +# Checks the generics map: names, member shape, duplicates. function(_hipdnn_gpu_generic_targets_validate_generics json json_path) string(JSON generics_type ERROR_VARIABLE err TYPE "${json}" generics) if(err OR NOT generics_type STREQUAL "OBJECT") @@ -54,9 +50,7 @@ function(_hipdnn_gpu_generic_targets_validate_generics json json_path) endforeach() endfunction() -# Validates the generic target table at : the schema version and every generic's -# name and member list. FATAL_ERROR naming the offending key or member on the first -# violation. +# Validates the schema version and every generic; FATAL_ERROR on the first violation. function(hipdnn_validate_gpu_generic_targets json_path) if(NOT EXISTS "${json_path}") message(FATAL_ERROR "gpu_generic_targets: cannot read '${json_path}'") diff --git a/projects/hipdnn/plugin_sdk/cmake/hipdnn_plugin_sdkConfig.cmake.in b/projects/hipdnn/plugin_sdk/cmake/hipdnn_plugin_sdkConfig.cmake.in index f9608e203f35..88ab2a62399f 100644 --- a/projects/hipdnn/plugin_sdk/cmake/hipdnn_plugin_sdkConfig.cmake.in +++ b/projects/hipdnn/plugin_sdk/cmake/hipdnn_plugin_sdkConfig.cmake.in @@ -15,8 +15,6 @@ if(HIPDNN_PLUGIN_SDK_ENABLE_KERNEL_INGESTOR) find_dependency(nlohmann_json REQUIRED) endif() -# The generic GPU target table sits beside this Config in both the build tree and the -# install tree. set(HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON "${CMAKE_CURRENT_LIST_DIR}/gpu_generic_targets.json") include("${CMAKE_CURRENT_LIST_DIR}/hipdnn_plugin_sdkTargets.cmake") diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp index 3fb285dfa29c..e64f045e8318 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp @@ -3,15 +3,8 @@ #pragma once -/** - * @file GpuGenericTargets.hpp - * @brief LLVM generic GPU targets (`gfx11-generic`, ...) as data: which concrete - * processors each one supports, and how an `arch` entry ranks for a device. - * - * Membership comes from the table generated out of data/gpu_generic_targets.json; a - * name's shape never implies membership, so a generic-shaped name absent from the table - * matches no device. Nothing here allocates. - */ +/// LLVM generic GPU targets (`gfx11-generic`, ...) and how an `arch` entry ranks for a device. +/// Membership comes only from the generated table, never from a name's shape. #include #include @@ -23,8 +16,7 @@ namespace hipdnn_plugin_sdk { -/// Does @p name have the shape of a generic target (`...-generic`)? Shape only; use -/// findGenericTarget for membership in the table. +/// Shape only (`...-generic`); use findGenericTarget for membership. inline bool isGenericShapedArchName(std::string_view name) { constexpr std::string_view SUFFIX = "-generic"; @@ -32,7 +24,7 @@ inline bool isGenericShapedArchName(std::string_view name) && name.compare(name.size() - SUFFIX.size(), std::string_view::npos, SUFFIX) == 0; } -/// The table row for the generic named exactly @p name, or nullptr when absent. +/// The table row for @p name, or nullptr. inline const generated::GenericTargetRow* findGenericTarget(std::string_view name) { for(const auto& row : generated::GENERIC_TARGET_ROWS) @@ -45,8 +37,7 @@ inline const generated::GenericTargetRow* findGenericTarget(std::string_view nam return nullptr; } -/// Is @p baseDeviceId (features already stripped) a member of the table generic -/// @p generic? False when @p generic is not in the table. +/// Is @p baseDeviceId (features stripped) a member of @p generic? False if @p generic is unknown. inline bool genericTargetContains(std::string_view generic, std::string_view baseDeviceId) { const auto* row = findGenericTarget(generic); @@ -72,11 +63,8 @@ enum class ArchTier : int UNRESTRICTED = 2, ///< An empty list: any device. }; -/// Rank of one `arch` list entry for the device @p rawDeviceArch as the device reports it -/// (features included): EXPLICIT when the entry is the device's own base id (PREFIX -/// match, so `gfx942` is EXPLICIT for `gfx942:sramecc+:xnack-` and never matches -/// `gfx950`), GENERIC when a table generic containing the device's base id, else nullopt. -/// A generic-shaped entry is never EXPLICIT, so an unknown generic matches no device. +/// Rank of @p entry for @p rawDeviceArch (features included); nullopt when it does not +/// admit the device. A generic-shaped entry is never EXPLICIT. inline std::optional entryTier(std::string_view entry, std::string_view rawDeviceArch) { if(isGenericShapedArchName(entry)) diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp index 1ce687d820d6..2f2058825305 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp @@ -684,8 +684,7 @@ inline std::string describeArch(const std::vector& arch) return text + "]"; } -/// A device both table entries @p x and @p y cover when either is a table generic and the -/// other is a member it contains or a generic sharing a member; empty when none. +/// A device both table entries @p x and @p y cover, or empty. inline std::string sharedGenericMember(const std::string& x, const std::string& y) { const auto* generic = findGenericTarget(x); @@ -708,10 +707,8 @@ inline std::string sharedGenericMember(const std::string& x, const std::string& return {}; } -/// Why @p kernelArch breaks the generic rules against its pack's @p packArch, or empty. -/// A kernel may name a generic only if the pack lists it. When the pack lists generics, a -/// kernel with an arch of its own must list every one of them and only entries the pack -/// lists. A kernel with no arch inherits the pack's and is never in violation. +/// Why @p kernelArch breaks the generic rules against its pack's @p packArch, or empty: a +/// kernel with its own arch must list every pack generic and only entries the pack lists. inline std::string genericArchViolation(const std::vector& packArch, const std::vector& kernelArch) { diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp index 3a2facd62085..aa0aa8a8917a 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp @@ -30,9 +30,7 @@ struct DeviceProperties int multiProcessorCount = 0; ///< Compute units; 0 if unresolved. }; -/// How @p arch (a KDP's supported-target list) ranks for @p deviceArch: the best tier of -/// any entry (see entryTier), or nullopt when no entry admits the device. An empty list is -/// UNRESTRICTED. +/// Best entryTier of any entry of @p arch for @p deviceArch; empty list is UNRESTRICTED. inline std::optional archTier(const std::vector& arch, std::string_view deviceArch) { @@ -52,8 +50,6 @@ inline std::optional archTier(const std::vector& arch, return best; } -/// Does @p arch (a KDP's supported-target list; empty admits everything) admit -/// @p deviceArch, by any tier? inline bool archSupports(const std::vector& arch, std::string_view deviceArch) { return archTier(arch, deviceArch).has_value(); @@ -69,8 +65,7 @@ inline bool listAdmits(const std::vector& list, std::string_view de }); } -/// Calls @p visit with every device id in @p entry's expansion (a generic's members, an -/// explicit entry itself, nothing for an unknown generic) until it returns true. +/// Calls @p visit with each device id @p entry expands to until it returns true. template inline bool anyExpandedMember(std::string_view entry, Visit&& visit) { @@ -93,7 +88,6 @@ inline bool anyExpandedMember(std::string_view entry, Visit&& visit) return false; } -/// Does some device in the expansion of @p from get admitted by @p into? inline bool anyExpandedMemberAdmittedBy(const std::vector& from, const std::vector& into) { @@ -105,9 +99,8 @@ inline bool anyExpandedMemberAdmittedBy(const std::vector& from, } // namespace detail -/// Can one device satisfy both @p a and @p b? Compared over expanded member sets: a -/// generic stands for its table members. Empty means "every arch", so it overlaps -/// everything; an unknown generic expands to nothing and overlaps nothing. +/// Can one device satisfy both @p a and @p b, over generics expanded to members? Empty +/// means every arch; an unknown generic expands to nothing. inline bool archOverlaps(const std::vector& a, const std::vector& b) { if(a.empty() || b.empty()) @@ -118,10 +111,7 @@ inline bool archOverlaps(const std::vector& a, const std::vector& outer, const std::vector& inner) { if(outer.empty()) @@ -135,11 +125,8 @@ inline bool archCovers(const std::vector& outer, const std::vector< }); } -/// Do @p a and @p b tie? True when some candidate device is matched by both at the same -/// tier, where the candidates are every explicit id in either list and every member of -/// every table generic in either list. Two empty lists compete (both unrestricted on -/// every device). Lists whose best tiers always differ per device do not: the better -/// tier shadows the other, so they may coexist. +/// Do @p a and @p b match some device at the same tier? A better tier shadows a worse one, +/// so lists whose tiers always differ may coexist. Two empty lists compete. inline bool archesCompete(const std::vector& a, const std::vector& b) { if(a.empty() && b.empty()) diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp index 05e21988f5ca..01b0b994a6b9 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp @@ -387,12 +387,9 @@ class KernelIngestorStateManager /// than completing each kernel's metadata again on every graph. void validateAndIndexPacks() { - // Two kernels may share a tuple when no single device can see both at the same arch - // tier -- the per-arch shard layout, and an explicit or generic kernel beside a - // less specific one, which buildCatalog shadows per device. Uniqueness is therefore - // per competing-arch group, not per engine: the tuple is the catalog key, and a - // catalog is built for one device. Keyed by the tuple (an ordered map, so it already - // orders) rather than scanned, which would be quadratic. + // Two kernels may share a tuple when no single device sees both at the same arch + // tier (per-arch shards, or a more specific kernel that buildCatalog shadows + // per device). Uniqueness is per competing-arch group, keyed by the tuple. std::map>> archesClaimingTuple; _definitions.reserve(_packs.size()); @@ -681,11 +678,8 @@ class KernelIngestorStateManager return catalog; } - /// Among admitted entries sharing a metadata tuple, keeps only those whose arch list - /// ranks best for the device (EXPLICIT over GENERIC over UNRESTRICTED), preserving - /// catalog order. Runs after matcher admission, so a kernel a matcher declined never - /// hides a lower-tier one. Uniqueness at construction guarantees at most one entry per - /// tuple per tier on any device. + /// Per metadata tuple, keeps only the entries with the best arch tier for the device. + /// Runs after matcher admission, so a declined kernel never hides a lower-tier one. static void shadowByArchTier(Catalog& catalog, const MatchContext& context) { auto& entries = catalog.entries; @@ -704,7 +698,6 @@ class KernelIngestorStateManager constexpr int SHADOWED = -1; std::vector tiers; tiers.reserve(entries.size()); - // Index of the best-tier entry per tuple; the keys point into entries. std::map winners; for(size_t i = 0; i < entries.size(); ++i) { @@ -718,7 +711,6 @@ class KernelIngestorStateManager } } - // Name every loser's winner before any entry moves. bool anyShadowed = false; for(size_t i = 0; i < entries.size(); ++i) { @@ -739,8 +731,7 @@ class KernelIngestorStateManager return; } - // remove_if tests each element in place before any move reaches it, so its - // position indexes tiers. + // remove_if tests each element in place before moving, so its position indexes tiers. entries.erase(std::remove_if(entries.begin(), entries.end(), [&](const KernelDefinition& entry) { diff --git a/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt b/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt index ec1495f45e9c..4d30fd5fd4af 100644 --- a/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt +++ b/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt @@ -196,12 +196,9 @@ if(HIPDNN_ENABLE_KERNEL_INGESTOR) HIPDNN_INSTALLED_CTEST_STAGING "${_disk_cache_installed_tests}") endif() -# Validator tests for data/gpu_generic_targets.json. Build tree only: no staging into the -# installed CTestTestfile.cmake, so installed-tree runs never read the source tree. -# Each negative case mutates the real table in memory and must fail with the validator's -# text. PASS_REGULAR_EXPRESSION, never WILL_FAIL, so a crash or a missing input does not -# pass; FAIL_REGULAR_EXPRESSION catches a validator that downgrades the error. CMake wraps -# long messages at spaces, so every space in the expected text matches any whitespace run. +# Validator tests for data/gpu_generic_targets.json; build tree only (not staged into the +# installed CTestTestfile.cmake). PASS_REGULAR_EXPRESSION rather than WILL_FAIL so a crash +# does not pass. CMake wraps messages at spaces, so spaces match any whitespace run. set(_generic_targets_check "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CheckGpuGenericTargets.cmake") add_test( @@ -215,9 +212,7 @@ set_tests_properties( ) _apply_hipdnn_test_category_labels(hipdnn_gpu_generic_targets_accepts_real_table) -# Registers ctest hipdnn_gpu_generic_targets_rejects_: applies (see -# CheckGpuGenericTargets.cmake) with and to the real table and expects -# . +# Registers a build-tree test: mutate the real table, expect the validator to reject it with expected_text. function(_hipdnn_add_generic_targets_rejection_test case op path value expected_text) string(REPLACE " " "[ \n]+" expected_regex "${expected_text}") set(test_name hipdnn_gpu_generic_targets_rejects_${case}) diff --git a/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake b/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake index 072345613ff7..5a28751f8db6 100644 --- a/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake +++ b/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake @@ -1,18 +1,10 @@ # Copyright © Advanced Micro Devices, Inc., or its affiliates. # SPDX-License-Identifier: MIT -# Script-mode driver for hipdnn_validate_gpu_generic_targets (run with `cmake -P`): -# cmake -DGENERIC_TARGETS_JSON= [-DMUTATION_OP= ...] -P CheckGpuGenericTargets.cmake -# Without MUTATION_OP the table is validated as is. With it, the table is mutated in -# memory, written to MUTATED_JSON, and that file is validated. MUTATION_PATH is a -# '.'-separated key/index path into the table. MUTATION_OP is one of: -# REMOVE delete the value at MUTATION_PATH -# SET_STRING set MUTATION_PATH to the string MUTATION_VALUE -# SET_JSON set MUTATION_PATH to the JSON text MUTATION_VALUE -# RAW replace the whole document with the text MUTATION_VALUE -# Prints "gpu_generic_targets: valid" only after the table passed every rule, so a ctest -# can require the validator's FATAL_ERROR text for a mutation and forbid that line. A -# crash or a missing input matches neither. +# Runs hipdnn_validate_gpu_generic_targets via `cmake -P`, optionally on a mutated copy: +# -DGENERIC_TARGETS_JSON= [-DMUTATION_OP= -DMUTATION_PATH= -DMUTATION_VALUE=] +# MUTATION_OP: REMOVE, SET_STRING, SET_JSON, or RAW (replace the whole document). +# Prints "gpu_generic_targets: valid" only if every rule passed. if(NOT GENERIC_TARGETS_JSON) message(FATAL_ERROR "CheckGpuGenericTargets: pass -DGENERIC_TARGETS_JSON=") diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp index 26b9c74a9773..f13be243df0f 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp @@ -71,10 +71,8 @@ TEST(TestIngestorArchGate, AdmitsOnlyTheMembersOfAGeneric) { EXPECT_TRUE(archSupports({"gfx11-generic"}, "gfx1151")); EXPECT_TRUE(archSupports({"gfx942", "gfx12-generic"}, "gfx1201")); - // Features on the device never change membership. EXPECT_TRUE(archSupports({"gfx11-generic"}, "gfx1151:sramecc+:xnack-")); - // gfx1154 and gfx1250 are real-looking siblings that no table row lists; membership is - // data, so a name that merely shares the family stem is refused. + // Siblings no table row lists are refused. EXPECT_FALSE(archSupports({"gfx11-generic"}, "gfx1154")); EXPECT_FALSE(archSupports({"gfx12-generic"}, "gfx1250:sramecc+")); EXPECT_FALSE(archSupports({"gfx9-4-generic"}, "gfx942")); diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp index 75d991094cea..81163890b540 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp @@ -1991,8 +1991,6 @@ nlohmann::json arch(std::initializer_list entries) return list; } -/// Authors the candidate's first kernel with @p kernelArch under a pack listing -/// @p packArch, spelled inline or as a standalone `.ukd.json`. Documents candidateWithKernelArch(const nlohmann::json& packArch, const nlohmann::json& kernelArch, bool standalone) @@ -2011,8 +2009,7 @@ Documents candidateWithKernelArch(const nlohmann::json& packArch, return documents; } -/// The names of the engines that load when @p candidate sits beside an intact engine. -/// Sorted, so a case asserts which pack survives, not the order the catalog lists it in. +/// Sorted names of the engines that load when @p candidate sits beside an intact engine. std::vector engineNames(const Documents& candidate) { const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("generic_arch_rule")); @@ -2030,9 +2027,6 @@ std::vector engineNames(const Documents& candidate) } // namespace -/// An authored generic spells a bare base id with a '-' in it, which the arch validator -/// must admit; it sits in a list beside an unrelated explicit id, and a kernel spelled -/// with the same list keeps it. TEST(TestDescriptorLoader, AcceptsAGenericTargetBesideAnUnrelatedExplicitId) { const ScopedSymbols symbols; @@ -2053,8 +2047,7 @@ TEST(TestDescriptorLoader, AcceptsAGenericTargetBesideAnUnrelatedExplicitId) (std::vector{"gfx942", "gfx11-generic"})); } -/// R1: a generic-shaped name the table does not list matches no device, so the file that -/// names it is dropped, whether the name is the pack's or a kernel's. +/// A generic-shaped name the table does not list matches no device, so its file is dropped. TEST(TestDescriptorLoader, DropsAPackNamingAGenericAbsentFromTheTable) { EXPECT_EQ(engineNames(candidateWithKernelArch(arch({"gfx99-generic"}), arch({}), false)), @@ -2065,8 +2058,6 @@ TEST(TestDescriptorLoader, DropsAPackNamingAGenericAbsentFromTheTable) INTACT_ONLY); } -/// R2: one list may not hold a generic together with a member it contains. Generics with -/// no member in common coexist. TEST(TestDescriptorLoader, DropsAPackListingAGenericAndOneOfItsMembers) { EXPECT_EQ( @@ -2080,8 +2071,7 @@ TEST(TestDescriptorLoader, DropsAPackListingAGenericAndOneOfItsMembers) BOTH); } -/// R3: a kernel names a generic only when its pack lists that generic itself, even when -/// the pack's explicit entries already cover every member. +/// A kernel names a generic only when its pack lists that generic itself. TEST(TestDescriptorLoader, DropsAPackWhoseKernelNamesAGenericThePackDoesNotList) { nlohmann::json members = nlohmann::json::array(); @@ -2104,8 +2094,6 @@ TEST(TestDescriptorLoader, DropsAPackWhoseKernelNamesAGenericThePackDoesNotList) } } -/// R4: under a pack listing a generic, a kernel with an arch of its own lists every -/// generic of the pack and only entries the pack lists. A kernel with no arch inherits. TEST(TestDescriptorLoader, DropsAPackWhoseKernelOmitsOrExceedsTheGenericPackArch) { for(const bool standalone : {false, true}) @@ -2126,8 +2114,7 @@ TEST(TestDescriptorLoader, DropsAPackWhoseKernelOmitsOrExceedsTheGenericPackArch EXPECT_EQ(engineNames(candidateWithKernelArch(arch({"gfx11-generic"}), arch({}), false)), BOTH); } -/// Unchanged loader rule: a standalone kernel id defined for the generic and for one of its -/// members is two covered definitions under a generic pack, and nothing ranks them. +/// A standalone kernel id defined for a generic and for one of its members is ambiguous. TEST(TestDescriptorLoader, StillRejectsAStandaloneKernelDefinedForAGenericAndForOneOfItsMembers) { const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("generic_and_member")); diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp index 35513b93f05d..8ba6dd02bec9 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp @@ -13,11 +13,6 @@ #include #include -/** - * @file TestGenericTargets.cpp - * @brief The generic GPU target table and the tier algebra over it, as C++ evaluates - * them, on the generated constexpr table. - */ namespace { diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp index fb7be5d11c56..fe9b1b54c1ec 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp @@ -314,11 +314,8 @@ TEST(TestKernelIngestorStateManager, OffersAnUnstampedKernelEverywhereItsPackRea } } -/// Packs share a tuple whenever no device ranks them at the same arch tier. One tuple -/// offered at all three tiers keeps, per device, only the best: EXPLICIT over GENERIC over -/// UNRESTRICTED. Every pack order is exercised so a keep-the-first-inserted shortcut fails, -/// and each device is asked twice, interleaved, so the second answer comes from the -/// per-(graph, device) catalog cache and must not be another device's. +/// One tuple offered at all three tiers keeps only the best per device. Every pack order +/// and an interleaved second query per device (served from the catalog cache) are exercised. TEST(TestKernelIngestorStateManager, KeepsOnlyTheBestArchTierOfASharedTuplePerDevice) { const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); @@ -364,8 +361,7 @@ TEST(TestKernelIngestorStateManager, KeepsOnlyTheBestArchTierOfASharedTuplePerDe } } -/// Same generic, same tuple: every member device would rank both at the GENERIC tier, so -/// nothing could break the tie. +/// Same generic, same tuple: both rank GENERIC on every member, so the tie is rejected. TEST(TestKernelIngestorStateManager, RejectsATupleSharedByTwoPacksAtTheSameGenericTier) { const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); @@ -386,8 +382,6 @@ TEST(TestKernelIngestorStateManager, RejectsATupleSharedByTwoPacksAtTheSameGener std::invalid_argument); } -/// Kernel matchers that decline the explicit kernel by name, so the better-tier kernel is -/// never admitted. inline bool declineTheExplicitKernel(const MatchContext& context, const BoundTokens& bound, const KernelDefinition& kernel) @@ -395,8 +389,7 @@ inline bool declineTheExplicitKernel(const MatchContext& context, return kernel.name != "kernel_gfx942" && acceptFloatKernels(context, bound, kernel); } -/// Shadowing compares only entries a matcher admitted. A better-tier kernel the -/// matcher declined must not hide the lower-tier one that remains. +/// A better-tier kernel a matcher declined must not hide the lower-tier one. TEST(TestKernelIngestorStateManager, ADeclinedBetterTierKernelDoesNotHideALowerTierOne) { const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", declineTheExplicitKernel); @@ -428,9 +421,7 @@ TEST(TestKernelIngestorStateManager, ADeclinedBetterTierKernelDoesNotHideALowerT } } -/// The winner cache is read back through the catalog, so a record naming a shadowed kernel -/// cannot resurrect it: the record does not cover the surviving explicit kernel, so it is -/// declined and the heuristic orders the catalog. +/// A winner-cache record naming a shadowed kernel cannot resurrect it; it is declined. TEST(TestKernelIngestorStateManager, AWinnerRecordNamingAShadowedKernelFallsBackToHeuristicOrdering) { const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); @@ -459,7 +450,6 @@ TEST(TestKernelIngestorStateManager, AWinnerRecordNamingAShadowedKernelFallsBack ASSERT_EQ(baseline.entries.size(), 2U); ASSERT_FALSE(baseline.orderedFromRecord); - // The shadowed kernel is the record's fastest; the surviving explicit kernel is absent. const WinnerRecord record{ RankedEntry{testId(0x93), anywhere.id, DISPATCH_ID, 0.1}, RankedEntry{testId(0x96), anywhere.id, DISPATCH_ID, 0.2}, diff --git a/projects/hipdnn/test_categories.yaml b/projects/hipdnn/test_categories.yaml index 661bc9f13785..13c7c2b6cf27 100644 --- a/projects/hipdnn/test_categories.yaml +++ b/projects/hipdnn/test_categories.yaml @@ -45,8 +45,7 @@ test_categories: - "hipdnn_frontend_tests" - "hipdnn_test_sdk_tests" - "hipdnn_plugin_sdk_tests" - # Build tree only: these run CMake driver scripts, so they are absent from the - # installed CTestTestfile.cmake. See plugin_sdk/tests/CMakeLists.txt. + # Build tree only; see plugin_sdk/tests/CMakeLists.txt. - "hipdnn_gpu_generic_targets_.*" integration: diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py b/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py index 7ebad77c651a..db0fb066a996 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py @@ -1366,7 +1366,7 @@ def _check_kernel_priority(config: IngestorConfig) -> None: def _generic_table() -> "gtmod.GenericTargets": - """The in-repo generic target table, with a read failure surfaced as a ConfigError.""" + """The generic target table; a read failure is a ConfigError.""" try: return gtmod.default_table() except ValueError as exc: @@ -1374,9 +1374,7 @@ def _generic_table() -> "gtmod.GenericTargets": def _check_arch_list_generics(archs: list, where: str, table) -> None: - """Per-list generic rules: an unknown generic-shaped name is an error, and one - list may not hold a generic with a member it contains or two generics sharing a - member, as the packer does.""" + """Per-list rules: no unknown generics, no generic beside its member or a generic sharing one.""" for entry in archs: if gtmod.is_generic_shaped(entry) and not table.has(entry): raise ConfigError( @@ -1407,19 +1405,7 @@ def _check_arch_list_generics(archs: list, where: str, table) -> None: def _check_generic_arch(config: IngestorConfig) -> None: - """Pre-mint check #4a: the generic-target rules, the packer's own (``hkp_pack`` - ``validate_generic_arch``) so a config the generator accepts is one the packer - accepts. - - Per list: unknown generic names and a generic listed beside its member (or beside - a generic sharing a member) are errors. Per pack of any shape: a kernel may not - name a generic the pack does not list, an empty pack list included, since it - would ship in no shard. In a pack that lists a generic, a kernel with its own - non-empty ``arch`` must list every generic of the KDP and only entries the pack - lists; a kernel with no own ``arch`` inherits the pack and is unaffected. A hsaco - kernel is checked on the list it is stamped with (its own, else the pack's). A - rocKE kernel may not sit under a generic, whether the pack's or its own. - """ + """Pre-mint check #4a: the packer's generic-target rules (see descriptor-packaging/README.md).""" table = _generic_table() for pack in config.packs: where = f"pack '{pack.name}'" @@ -1467,9 +1453,7 @@ def _check_generic_arch(config: IngestorConfig) -> None: def _check_kernel_arch_subset_of_pack(config: IngestorConfig) -> None: """Pre-mint check #4: a kernel's arch must be a subset of its pack's. - Mirrors ``DescriptorLoader.hpp``'s ``archCovers(pack.arch, kernel.arch)`` over - the expanded device sets (a generic stands for its table members). An empty - pack.arch covers everything; an empty kernel.arch inherits it. + Mirrors ``DescriptorLoader.hpp``'s ``archCovers`` over expanded device sets. """ table = _generic_table() for pack in config.packs: @@ -1496,8 +1480,7 @@ def _check_arch_shape(config: IngestorConfig) -> list[str]: A shape violation (``GFX942``, ``" gfx942"``, a feature suffix) is a ``ConfigError``, mirroring ``isPlausibleArchBaseId``; a well-formed but - unrecognized id (``gfx94``) is a warning; a generic named in the table is - recognized. Returns the warnings emitted, + unrecognized id (``gfx94``) is a warning. Returns the warnings emitted, also raised via ``warnings.warn``. """ messages: list[str] = [] diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py b/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py index 14c6129b1ac8..610c30ac937b 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py @@ -337,8 +337,7 @@ def _dedup_key(metadata: dict, config: IngestorConfig) -> str: """Identity of a descriptor as the matcher sees it: the completed tuple, not the emitted document. - Architecture is not part of this key; `build_kdp` decides competition against - the arch coverage recorded beside each key. + Architecture is not part of this key; `build_kdp` compares arch separately. """ return json.dumps(_completed_metadata(metadata, config), sort_keys=True) @@ -358,11 +357,7 @@ def _candidate_identity(kernel: KernelSpec) -> str: def _arch_competes(left: list, right: list) -> bool: - """Whether two arch coverages tie on some device: both select it at the same - tier (explicit, generic, or unrestricted), so the matcher would see one tuple - twice there. A device where one outranks the other is not a collision: an - explicit entry beats a generic containing it, which beats an empty list. - Mirrors ``generic_targets.compete`` of the packer.""" + """Whether two arch lists tie on some device at the same tier.""" return gtmod.compete(left, right, gtmod.default_table()) @@ -454,11 +449,8 @@ def build_kdp( if config.is_multi_pack: matchers.insert(0, ids[("operation_umd", pack_index)]) # Generation expressions targeting one engine may overlap, so a shared - # tuple has four outcomes: coverage that never ties on a device (disjoint, - # or an explicit entry against a generic or empty list it outranks) is not - # a duplicate; competing coverage with the same candidate and equal arch - # de-duplicates here; a different candidate, or unequal coverage, is - # refused. + # tuple is a duplicate only when arch coverage competes: the same candidate + # with equal arch de-duplicates, anything else is refused. kernel_descriptors = [] if seen_metadata is None: seen_metadata = {} @@ -536,9 +528,7 @@ def build_kdp( "priority": kernel.priority, } # hkp_pack validates the kernel's own arch for hsaco and rejects a - # wildcard, so the inherited pack arch is stated on the descriptor. The - # whole pack list is the one stamp that satisfies the generic-pack rule - # (list every generic of the pack, only entries the pack lists). + # wildcard, so the inherited pack arch is stated on the descriptor. if kernel.kernel_source.kind == KERNEL_SOURCE_KIND_HSACO: entry["arch"] = arch elif kernel.arch: @@ -623,11 +613,8 @@ def emitted_inventory(config: IngestorConfig, kdp_documents: list) -> dict: lowered to ``kpack`` first. A descriptor with no ``arch`` is filed under its pack's; a pack with none under `ARCH_WILDCARD`. - A table generic's row is shared by every member of the generic: each member's - row is created if absent and holds its own descriptors, those of every - generic row containing it, and the wildcard row's. A wildcard entry ships on - every device too, so every non-wildcard row also takes the wildcard row. - These unions are one way only. + Each generic's descriptors also fill the rows of its members; the wildcard + row fills every row. """ arches: dict[str, dict] = {} diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py b/projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py index a59e2c1858c0..d296b2e021de 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py @@ -1,19 +1,9 @@ # Copyright (c) Advanced Micro Devices, Inc., or its affiliates. # SPDX-License-Identifier: MIT -"""LLVM generic GPU targets as data, and the tier algebra over them, for ``codegen/``. - -An in-repo mirror of ``hkp_pack.generic_targets`` and of the C++ loader's -``archTier``: the generator may not import the kernel provider's packaging tree -(descriptor generation must not need the kernel toolchain), so each implementation -tests itself with literal cases (``tests/test_generic_targets.py``). -Packer-side verifier tools import ``hkp_pack.generic_targets`` instead; this module is -used only inside ``codegen/``. - -Membership is data, never name shape: a generic-shaped name the table does not list -expands to the empty set and matches no device. An ``arch`` list ranks per device: -EXPLICIT (the device's own base id) beats GENERIC (a table generic containing the -device) beats UNRESTRICTED (an empty list). +"""Generic GPU target table and arch-list tier algebra; see descriptor-packaging/README.md. + +Mirrors ``hkp_pack.generic_targets`` and the C++ ``archTier`` (not importable from here). """ import json @@ -24,7 +14,6 @@ TIER_GENERIC = 1 TIER_UNRESTRICTED = 2 -# .../projects/hipdnn/tools/IngestorGenerator/codegen/ DEFAULT_TABLE_PATH = ( Path(__file__).resolve().parents[3] / "plugin_sdk" @@ -36,15 +25,13 @@ class GenericTargets: - """The generic target table: generic name -> member processor names, document order.""" - def __init__(self, path: Path, generics: dict): self._path = Path(path) self._generics = generics @classmethod def load(cls, path) -> "GenericTargets": - """Read the table at @path; ``ValueError`` names the path on any defect.""" + """Read the table at @path; ``ValueError`` on any defect.""" path = Path(path) try: doc = json.loads(path.read_text(encoding="utf-8")) @@ -80,7 +67,6 @@ def has(self, name: str) -> bool: return name in self._generics def members(self, name: str) -> tuple: - """Members of generic @name in document order; KeyError when it is not listed.""" return self._generics[name] def names(self) -> tuple: @@ -89,12 +75,10 @@ def names(self) -> tuple: @cache def default_table() -> GenericTargets: - """The table at ``DEFAULT_TABLE_PATH``, read once per process.""" return GenericTargets.load(DEFAULT_TABLE_PATH) def is_generic_shaped(name: str) -> bool: - """Shape only (``...-generic``); use ``GenericTargets.has`` for membership.""" return name.endswith(_GENERIC_SUFFIX) @@ -103,10 +87,7 @@ def _strip_features(arch: str) -> str: def entry_tier(entry: str, device: str, table: GenericTargets): - """Tier of one list entry for @device (features allowed), or None when it admits it. - - A generic-shaped entry is never EXPLICIT, so an unknown generic matches no device. - """ + """Tier of one entry for @device, or None when it does not admit it.""" if is_generic_shaped(entry): if table.has(entry) and _strip_features(device) in table.members(entry): return TIER_GENERIC @@ -117,7 +98,7 @@ def entry_tier(entry: str, device: str, table: GenericTargets): def list_tier(entries, device: str, table: GenericTargets): - """Best tier of @entries for @device; empty is UNRESTRICTED; None = no match.""" + """Best tier of @entries for @device; None = no match.""" if not entries: return TIER_UNRESTRICTED tiers = [ @@ -127,7 +108,7 @@ def list_tier(entries, device: str, table: GenericTargets): def expand(entries, table: GenericTargets): - """The set of device ids @entries admits; None (unrestricted) for an empty list.""" + """Device ids @entries admits; None when unrestricted.""" if not entries: return None devices: set = set() @@ -149,11 +130,7 @@ def covers(outer, inner, table: GenericTargets) -> bool: def compete(a, b, table: GenericTargets) -> bool: - """Do the lists tie on some candidate device (both match it at the same tier)? - - Candidates are every explicit id in either list and every member of every table - generic in either list. Two empty lists compete. - """ + """Do the lists match some device at the same tier?""" if not a and not b: return True candidates: set = set() diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/models.py b/projects/hipdnn/tools/IngestorGenerator/codegen/models.py index 591ed177832b..1c9016a287c9 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/models.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/models.py @@ -389,8 +389,7 @@ def kdp_stem(self, pack: PackSpec) -> str: def device_fixture_arch(self) -> str: """Architecture named by the emitted matcher-test device fixture: the first pack's first arch, or `DEFAULT_FIXTURE_ARCH` when the config - restricts none. A generic names no device, so it stands for its first - table member.""" + restricts none; a generic stands for its first member.""" first_pack_arch = self.packs[0].arch if self.packs else [] if not first_pack_arch: return DEFAULT_FIXTURE_ARCH diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py index f86c5f738eea..713726780a03 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py @@ -193,9 +193,7 @@ def test_empty_pack_arch_covers_everything(self): class TestGenericArchRules: - """The packer's generic-target rules R1-R4 and R6, applied before a UUID is - minted. ``gfx11-generic`` holds gfx1100..gfx1153; ``gfx12-generic`` holds - gfx1200/gfx1201. Each case is judged by accept or reject only.""" + """Generic-target rules; ``gfx11-generic`` holds gfx1100..gfx1153, ``gfx12-generic`` gfx1200/gfx1201.""" @staticmethod def _config(pack_arch, kernel_arch, kind=None): @@ -219,20 +217,14 @@ def test_accepted_shapes(self, pack_arch, kernel_arch): @pytest.mark.parametrize( ("pack_arch", "kernel_arch"), [ - # R1: a generic-shaped name the table does not list. (["gfx9-4-generic"], []), ([], ["gfx9-4-generic"]), - # R2: a generic beside a member it contains, in either order and on - # either list (the real table's generics are disjoint). (["gfx11-generic", "gfx1151"], []), (["gfx1151", "gfx11-generic"], []), (["gfx942", "gfx11-generic"], ["gfx1151", "gfx11-generic"]), - # R3: a kernel naming a generic its pack does not list. ([], ["gfx11-generic"]), (["gfx942"], ["gfx11-generic"]), (["gfx942", "gfx12-generic"], ["gfx11-generic"]), - # R4: a kernel with its own arch under a generic pack must list every - # generic of the pack and only entries the pack lists. (["gfx11-generic"], ["gfx1151"]), (["gfx942", "gfx11-generic"], ["gfx942"]), (["gfx11-generic", "gfx12-generic"], ["gfx11-generic"]), @@ -266,8 +258,6 @@ def test_known_generic_produces_no_unrecognised_arch_warning(self): def test_an_expanded_subset_is_accepted_by_the_coverage_check(self): from codegen.config_loader import _check_kernel_arch_subset_of_pack - # Loader-equivalent coverage: gfx1151 is inside gfx11-generic's members, but - # gfx1250 is not. _check_kernel_arch_subset_of_pack(self._config(["gfx11-generic"], ["gfx1151"])) with pytest.raises(ConfigError): _check_kernel_arch_subset_of_pack( diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py index 57fba3dccf1f..f47dfb5cced0 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py @@ -728,19 +728,14 @@ def test_equal_tuples_on_disjoint_arches_both_survive(self): @pytest.mark.parametrize( ("pack_arch", "left_arch", "right_arch", "other_binary"), [ - # Same tuple, same device, different binary. (["gfx942"], [], [], True), - # Same tuple, same binary, unequal overlapping coverage. (["gfx942", "gfx950"], ["gfx942"], ["gfx942", "gfx950"], False), - # Both select gfx1100 at the generic tier. (["gfx11-generic"], [], [], True), ], ) def test_equal_tuples_tied_on_a_device_are_refused_naming_both( self, pack_arch, left_arch, right_arch, other_binary ): - """One tuple twice on a device drops the engine at load, so the generator - refuses and names both kernels.""" config, pack, left, right = self._twin_config() pack.arch = pack_arch left.arch = left_arch @@ -754,9 +749,7 @@ def test_equal_tuples_tied_on_a_device_are_refused_naming_both( assert "twin.left" in message and "twin.right" in message def test_a_wildcard_arch_and_a_concrete_one_with_one_tuple_both_survive(self): - """An empty ``arch`` is the unrestricted tier and a concrete entry outranks it - on its own device, so the two never tie and the matcher never sees one tuple - twice.""" + """A concrete arch outranks an empty one on its own device, so they never tie.""" config, pack, left, right = self._twin_config() pack.arch = [] left.arch = [] # every device @@ -1039,9 +1032,7 @@ def test_two_distinct_arches_each_count_the_descriptor_once(self, tmp_path): assert inventory["total_descriptor_count"] == 1 def test_inventory_gives_every_member_the_generic_rows(self): - """A generic pack ships in every member's folder, so each member's row holds the - generic's descriptors, its own, and the wildcard's; a member of no generic row - gets only its own and the wildcard's.""" + """Each member's row holds the generic's descriptors, its own, and the wildcard's.""" generic = make_pack( name="generic", arch=["gfx11-generic"], @@ -1762,9 +1753,6 @@ class TestPackagedHsacoKdp: def test_hsaco_kernel_inheriting_a_generic_pack_arch_conforms_to_the_pack_rule( self, ): - """hkp_pack requires an own arch on an hsaco kernel, so the generator stamps the - whole pack list; that stamp lists every generic of the pack and only entries the - pack lists, which is the pack rule the config loader enforces.""" from codegen.config_loader import _check_generic_arch kernel = make_kernel( diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py index b2b3a3ec7b6c..561853ad4f3f 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py @@ -1,8 +1,7 @@ # Copyright (c) Advanced Micro Devices, Inc., or its affiliates. # SPDX-License-Identifier: MIT -"""The codegen mirror of the tier algebra, over a small in-memory table so the cases -pin the algebra rather than LLVM-owned membership data.""" +"""Tier algebra over a small in-memory table.""" from pathlib import Path @@ -35,19 +34,15 @@ (["gfx942"], "gfx942:sramecc+:xnack-", _EXPLICIT), ([_G11], "gfx1100", _GENERIC), ([_G11], "gfx1151:sramecc+", _GENERIC), - # Explicit outranks a generic that also admits the device. (["gfx1151", _G11], "gfx1151", _EXPLICIT), (["gfx1151", _G11], "gfx1100", _GENERIC), (["gfx942", _G11], "gfx1100", _GENERIC), - # Non-matches. ([_G11], "gfx1154", None), ([_G12], "gfx1100", None), (["gfx942"], "gfx950", None), - # An id is a whole-name (or feature-suffix) match, never a prefix match. (["gfx94"], "gfx942", None), (["gfx1250"], "gfx1250-strict", None), (["gfx1250-strict"], "gfx1250-strict", _EXPLICIT), - # A generic-shaped name outside the table matches nothing, itself included. ([_UNKNOWN], "gfx942", None), ([_UNKNOWN], _UNKNOWN, None), ([_UNKNOWN, "gfx942"], "gfx942", _EXPLICIT), @@ -87,15 +82,12 @@ def test_covers(outer, inner, expect): ([_G11], [], False), (["gfx942"], ["gfx942"], True), (["gfx942"], ["gfx950"], False), - # A generic and a member it contains never tie: the member outranks it. ([_G11], ["gfx1151"], False), ([_G11], ["gfx1151", "gfx1100"], False), ([_G11], [_G11], True), ([_G11], [_G12], False), - # Tied on the generic's other member even though gfx1151 differs in tier. (["gfx1151", _G11], [_G11], True), ([_G11], [_G11, "gfx942"], True), - # An unknown generic admits nothing, so it ties with nothing. ([_UNKNOWN], [_UNKNOWN], False), ([_UNKNOWN], [], False), ], diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_models.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_models.py index 51e913d598ea..8f4bebe2d772 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_models.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_models.py @@ -120,7 +120,6 @@ def test_device_fixture_arch_is_the_first_packs_first_arch(self): assert config.device_fixture_arch == "gfx1201" def test_device_fixture_arch_of_a_generic_pack_is_its_first_member(self): - """A generic names no device; a fixture claiming one matches nothing.""" config = make_minimal_config(packs=[make_pack(arch=["gfx11-generic"])]) assert config.device_fixture_arch == "gfx1100" diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_native_stub.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_native_stub.py index 40b096085fec..1a092e3585e7 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_native_stub.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_native_stub.py @@ -79,8 +79,7 @@ def compile_env(): out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(text) - # GpuGenericTargetsTable.hpp: rendered as plugin_sdk/CMakeLists.txt does, from the - # one JSON table. + # GpuGenericTargetsTable.hpp is rendered from the JSON table, as plugin_sdk/CMakeLists.txt does. table_in = (plugin_sdk / ".." / "cmake" / "GpuGenericTargetsTable.hpp.in").resolve() table_json = (plugin_sdk / ".." / "data" / "gpu_generic_targets.json").resolve() if not (table_in.is_file() and table_json.is_file()): diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py index c71835119c81..df4c3817fda1 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py @@ -346,7 +346,6 @@ def test_equal_tuples_overlapping_on_one_arch_collide(self, gate): @pytest.mark.parametrize( ("left_arch", "right_arch"), [ - # The better tier outranks the other on its own device: no tie. (None, ["gfx950"]), (["gfx11-generic"], ["gfx1151"]), ], @@ -1228,8 +1227,7 @@ def test_unclaimed_hip_must_still_be_packed(self, tmp_path, real_archive): @pytest.mark.needs_rocm_kpack class TestAGenericDocumentIsReadWithItsGenericArchiveKey: - """The archive of a generic copy is keyed by the generic's name, so a member being - checked reads it under that key.""" + """A generic copy's archive is keyed by the generic's name.""" @staticmethod def _entry(arch): @@ -1270,7 +1268,6 @@ def test_a_generic_document_is_read_with_its_generic_archive_key(self, tmp_path) generic.origin_dir = str(tmp_path) assert payloads.read(generic, "gfx1151") == _PAYLOAD - # The control: a document naming the member has no member-keyed archive here. member = self._entry(["gfx1151"]) member.origin_dir = str(tmp_path) with pytest.raises(gate_module.GateError): diff --git a/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py b/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py index b65ebb629f2e..577ff57525b4 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py +++ b/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py @@ -5,8 +5,7 @@ 1. BINARY NESTING -- the larger set can still choose every binary the smaller one could. 2. LOADER-TUPLE UNIQUENESS -- with absent keys filled from the KMD - `default_value`, each tuple is unique per device and tier: two entries - collide only where both select a device at the same tier (explicit, + `default_value`, each tuple is unique per device and tier (explicit, generic, unrestricted). A duplicate rejects the whole engine at load. 3. NO SENTINEL -- `-1` never reaches a shipped descriptor. 4. METADATA MATCHES ITS BINARY -- the matcher selects on metadata, the spec @@ -218,8 +217,7 @@ def _specialization_twins(order: list, by_label: dict, knobs: set) -> list: def _tie(left: list, right: list, table) -> tuple[str, str]: - """Where two competing arch lists tie: the devices and the tier name. Two empty - lists tie everywhere, at the unrestricted tier.""" + """Devices and tier name where two competing arch lists tie.""" if not left and not right: return "every arch", _TIER_NAMES[gtmod.TIER_UNRESTRICTED] candidates = set() @@ -303,8 +301,7 @@ def read(self, entry: descriptor_context.Entry, arch: str) -> bytes: f"{entry.ukd.get('name')}: kernel_source.library resolves to " f"{archive_path}, which does not exist." ) - # A generic document's archive is keyed by the generic's name, not by the - # member being checked. + # A generic document's archive is keyed by the generic's name. if self._table is None: self._table = gtmod.GenericTargets.load(gtmod.DEFAULT_TABLE_PATH) key = descriptor_context.archive_arch(entry.arch, arch, self._table) @@ -652,9 +649,7 @@ def main(argv=None) -> int: parser.add_argument( "--generic-targets-json", default=str(gtmod.DEFAULT_TABLE_PATH), - help="The generic GPU target table (gpu_generic_targets.json). Tuple " - "collisions are judged per arch tier and a generic stands for its table " - "members. Defaults to the table in this checkout.", + help="The generic GPU target table (gpu_generic_targets.json).", ) args = parser.parse_args(argv) From f8415a119b54029066effe2b895dd8dccf6153b5 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 10:39:01 -0400 Subject: [PATCH 37/52] generic targets: drop redundant and wording-pinning tests, fix README R6 attribution --- .../descriptor-packaging/README.md | 4 +- .../cmake/HkpPackaging.cmake | 8 +- .../python/hkp_pack/generic_targets.py | 42 +---- .../python/hkp_pack/pipeline.py | 12 +- .../descriptor-packaging/tests/conftest.py | 8 + .../tests/test_desk_check_invariants.py | 21 +-- .../tests/test_hkp_arch_probe.py | 23 ++- .../tests/test_hkp_generic_targets.py | 92 +--------- .../tests/test_hkp_pack_generic.py | 165 ++++++------------ .../tests/test_hkp_pack_hip.py | 12 +- .../tests/test_hkp_pack_hsaco.py | 5 +- .../tests/test_hkp_pack_layout.py | 30 +--- .../tests/test_hkp_pack_parallel.py | 41 ----- .../tests/test_hkp_pack_rocke.py | 7 +- .../tools/hkp_arch_probe.py | 24 +-- .../compilation/TestKpackModuleCache.cpp | 9 - .../tests/cmake/CheckGpuGenericTargets.cmake | 6 +- .../tests/ingestor/TestArchGate.cpp | 11 -- .../tests/ingestor/TestDescriptorLoader.cpp | 49 ------ .../tests/ingestor/TestGenericTargets.cpp | 33 ---- .../tests/test_config_loader.py | 2 - .../IngestorGenerator/tests/test_generator.py | 22 +-- .../tests/test_generic_targets.py | 12 -- .../tests/test_verify_variant_sets.py | 36 +--- .../tools/verify_variant_sets.py | 41 ++--- 25 files changed, 146 insertions(+), 569 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 2bc340a438a1..759d555b9f69 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -27,8 +27,8 @@ loader log an ERROR and drop the pack: - Under a KDP listing generics, a UKD with its own `arch` lists every KDP generic and only entries the KDP lists; an inline UKD without `arch` inherits the pack. -Packer only: a standalone UKD with empty `arch` needs an empty-`arch` KDP, and shards whose member -sets intersect may not write one path with different bytes. `kind: rocke` takes no generic. +Packer only: a standalone UKD with empty `arch` needs an empty-`arch` KDP; shards with intersecting member sets may not write one path with different bytes. +Packer and generator only: `kind: rocke` takes no generic. ### hsaco kernels and `arch` diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake index f0826dd0ca79..f80c3ef44845 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake @@ -1320,11 +1320,9 @@ function(hkp_add_packaging) if(NOT HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON OR NOT EXISTS "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}") message(FATAL_ERROR - "hkp: HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON is empty or names a missing " - "file ('${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}'). The packer reads " - "the generic GPU target table that hipdnn_plugin_sdk ships " - "(gpu_generic_targets.json); an older installed hipdnn_plugin_sdk lacks it. " - "Build against a hipdnn_plugin_sdk that provides the table.") + "hkp: HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON names no file " + "('${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}'); build against a " + "hipdnn_plugin_sdk that ships gpu_generic_targets.json.") endif() set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}") diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py index 2aeec54e187e..720e997a3df9 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py @@ -33,44 +33,10 @@ def __init__(self, path, generics): def load(cls, path): path = Path(path) try: - text = path.read_text(encoding="utf-8") - except OSError as exc: - raise HkpPackError(f"{path}: cannot read generic target table: {exc}") - try: - doc = json.loads(text) - except ValueError as exc: - raise HkpPackError(f"{path}: generic target table is not valid JSON: {exc}") - if not isinstance(doc, dict): - raise HkpPackError(f"{path}: generic target table must be a JSON object") - version = doc.get("schemaVersion") - if type(version) is not int or version != 1: - raise HkpPackError(f"{path}: 'schemaVersion' must be 1, got {version!r}") - raw = doc.get("generics") - if not isinstance(raw, dict): - raise HkpPackError(f"{path}: 'generics' must be an object") - generics = {} - for name, members in raw.items(): - if not is_generic_shaped(name): - raise HkpPackError( - f"{path}: generics.{name}: a generic target name must end in " - f"'-generic'; a concrete processor name here would turn its own " - f"pass into a generic one" - ) - if not isinstance(members, list) or not members: - raise HkpPackError( - f"{path}: generics.{name} must be a non-empty array of names" - ) - for member in members: - if not isinstance(member, str) or not member: - raise HkpPackError( - f"{path}: generics.{name} holds a member that is not a " - f"non-empty string: {member!r}" - ) - if len(set(members)) != len(members): - raise HkpPackError( - f"{path}: generics.{name} lists a member more than once" - ) - generics[name] = tuple(members) + doc = json.loads(path.read_text(encoding="utf-8")) + generics = {n: tuple(m) for n, m in doc["generics"].items()} + except (OSError, ValueError, KeyError, TypeError, AttributeError) as exc: + raise HkpPackError(f"{path}: unusable generic target table: {exc!r}") return cls(path, generics) @property diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py index 763035015b7b..ca5f85251e85 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py @@ -22,7 +22,6 @@ kdp_survives, load_flat_input, reachable_generic_ids, - type_from_filename, ) from .errors import HkpPackError from .generic_targets import GenericTargets @@ -1487,17 +1486,8 @@ def _merge_generic_into_member(generic_dir, member_dir, staging_dir): if dest.exists(): if dest.read_bytes() == src.read_bytes(): continue - described = "" - if src.suffix == ".json": - try: - doc = json.loads(src.read_text(encoding="utf-8")) - token = type_from_filename(src) - if isinstance(doc, dict) and doc.get("id") is not None: - described = f" ({str(token).upper()} id '{doc['id']}')" - except ValueError: - pass raise HkpPackError( - f"{rel.as_posix()}{described} would be written by shard " + f"{rel.as_posix()} would be written by shard " f"'{member_dir.name}' ({member_dir / rel}) and shard " f"'{generic_dir.name}' ({src}) with different bytes; the " "shards' member sets intersect" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/conftest.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/conftest.py index a7b852168f0b..c9bce72c895b 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/conftest.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/conftest.py @@ -17,6 +17,14 @@ GENERIC_TARGETS_JSON = DEFAULT_TABLE_PATH GENERIC_TARGETS = GenericTargets.load(GENERIC_TARGETS_JSON) + +def run_pipeline(**kwargs): + """`pipeline.run_pipeline` with the shipped generic target table.""" + from hkp_pack import pipeline + + return pipeline.run_pipeline(generic_targets_json=GENERIC_TARGETS_JSON, **kwargs) + + # rocm_kpack location: CMake passes HIPKERNELPROVIDER_ROCM_KPACK_DIR; otherwise # rely on an installed rocm_kpack already importable. No skip on absence — the # compiler and kpack are load-bearing; a missing dependency is a hard failure. diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py index cdde43057e94..66cc84ac07cd 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py @@ -20,7 +20,7 @@ import pytest -from conftest import GENERIC_TARGETS, GENERIC_TARGETS_JSON +from conftest import GENERIC_TARGETS, GENERIC_TARGETS_JSON, run_pipeline from hkp_pack.desk_check import ( DEFAULT_MATCHER_FIELDS, MODES, @@ -36,7 +36,6 @@ toc_key_uniqueness, ) from hkp_pack.errors import HkpPackError -from hkp_pack.pipeline import run_pipeline ARCH = "gfx950" # The KMD fields the desk-check compares -- `DEFAULT_MATCHER_FIELDS`, narrowed @@ -65,7 +64,6 @@ def packed_desk_check(tmp_path_factory, desk_check_fixture, hipcc, rocm_kpack_di attention_dense variants: head_size 64 and 128, both batch=1).""" tmp_path = tmp_path_factory.mktemp("desk_check_pack") run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=desk_check_fixture, arches=[ARCH], out_root=tmp_path / "out", @@ -86,7 +84,6 @@ def _pack_mutated(tmp_path, desk_check_fixture, hipcc, rocm_kpack_dir, mutate): mutate(doc) kdp_path.write_text(json.dumps(doc), encoding="utf-8") run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=src, arches=[ARCH], out_root=tmp_path / "out", @@ -203,13 +200,6 @@ def test_an_arch_less_kernel_inherits_the_kdp_arch_and_collides_with_an_explicit kernels, ("dtype",), GENERIC_TARGETS, ["gfx942"] ) == {("FLOAT",): 2} - def test_an_arch_less_kernel_under_a_disjoint_kdp_arch_does_not_collide(self): - kernels = self._twins(None, ["gfx942"]) - assert ( - duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS, ["gfx950"]) - == {} - ) - def test_a_tie_beaten_on_every_device_it_reaches_is_still_a_duplicate(self): """Generic kernels outranked on every member still tie at the generic tier.""" kernels = [ @@ -239,15 +229,6 @@ def test_lists_that_never_tie_at_one_tier_do_not_collide(self, left, right): kernels = self._twins(left, right) assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == {} - @pytest.mark.parametrize( - "arch", [None, ["gfx11-generic"]], ids=["arch_less", "same_generic"] - ) - def test_two_kernels_with_the_same_list_collide(self, arch): - kernels = self._twins(arch, arch) - assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == { - ("FLOAT",): 2 - } - def test_real_pack_of_two_identical_matcher_tuples_is_detected( self, tmp_path, desk_check_fixture, hipcc, rocm_kpack_dir ): diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py index 6f5a7f1d0e4d..c6f879dadbd8 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py @@ -46,17 +46,26 @@ def test_probe_false_when_no_entry_reaches_any_selected_target(tmp_path): def test_probe_true_for_a_generic_absent_from_the_table(tmp_path): assert _probe(tmp_path, ["gfx99-generic"], "gfx942") == "TRUE" - assert _probe(tmp_path, ["gfx99-generic", "gfx950"], "gfx942") == "TRUE" - - -def test_probe_true_for_empty_arch(tmp_path): - assert _probe(tmp_path, [], "gfx942") == "TRUE" @pytest.mark.parametrize( "raw", - ['{"arch": "gfx942"}', "{not json", '{"arch": [1]}', "[]", '{"name": "x"}'], - ids=["string", "unparseable", "non_string_entry", "not_an_object", "absent"], + [ + '{"arch": "gfx942"}', + "{not json", + '{"arch": [1]}', + "[]", + '{"name": "x"}', + '{"arch": []}', + ], + ids=[ + "string", + "unparseable", + "non_string_entry", + "not_an_object", + "absent", + "empty", + ], ) def test_probe_true_for_an_unparseable_or_non_array_arch(tmp_path, raw): assert _probe(tmp_path, None, "gfx1100", raw=raw) == "TRUE" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py index 663685f56d74..ef35967c2766 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py @@ -27,64 +27,14 @@ def table(): return GenericTargets.load(_TABLE) -def test_loads_the_table_members_in_document_order(table): - assert table.path == _TABLE - assert sorted(table.names()) == ["gfx11-generic", "gfx12-generic"] - assert table.members("gfx11-generic") == ( - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - "gfx1150", - "gfx1151", - "gfx1152", - "gfx1153", - ) - assert table.members("gfx12-generic") == ("gfx1200", "gfx1201") - assert table.has("gfx11-generic") - - @pytest.mark.parametrize( - "doc, message", - [ - ({"schemaVersion": 2, "generics": {}}, "schemaVersion"), - ({"schemaVersion": True, "generics": {}}, "schemaVersion"), - ({"generics": {}}, "schemaVersion"), - ({"schemaVersion": 1}, "'generics' must be an object"), - ({"schemaVersion": 1, "generics": []}, "'generics' must be an object"), - ({"schemaVersion": 1, "generics": {"gfx11-generic": []}}, "non-empty array"), - ( - {"schemaVersion": 1, "generics": {"gfx11-generic": "gfx1100"}}, - "non-empty array", - ), - ( - {"schemaVersion": 1, "generics": {"gfx11-generic": [1100]}}, - "non-empty string", - ), - ( - {"schemaVersion": 1, "generics": {"gfx11-generic": ["gfx1100", "gfx1100"]}}, - "more than once", - ), - ( - {"schemaVersion": 1, "generics": {"gfx1100": ["gfx1100"]}}, - "must end in '-generic'", - ), - ([], "must be a JSON object"), - ], + "doc", + [{"schemaVersion": 1}, {"generics": []}, {"generics": {"gfx11-generic": 5}}, []], ) -def test_rejects_a_malformed_table(tmp_path, doc, message): +def test_rejects_a_malformed_table(tmp_path, doc): path = tmp_path / "table.json" path.write_text(json.dumps(doc)) - with pytest.raises(HkpPackError, match=message): - GenericTargets.load(path) - - -def test_rejects_an_unreadable_or_non_json_table(tmp_path): - with pytest.raises(HkpPackError, match="cannot read"): - GenericTargets.load(tmp_path / "absent.json") - path = tmp_path / "table.json" - path.write_text("{not json") - with pytest.raises(HkpPackError, match="not valid JSON"): + with pytest.raises(HkpPackError): GenericTargets.load(path) @@ -107,16 +57,6 @@ def test_list_tier(table, arch, device, expect): assert _TIER_NAMES[generic_targets.list_tier(arch, device, table)] == expect -def test_entry_tier_of_one_entry(table): - assert generic_targets.entry_tier("gfx1151", "gfx1151:xnack-", table) == ( - generic_targets.TIER_EXPLICIT - ) - assert generic_targets.entry_tier("gfx11-generic", "gfx1151", table) == ( - generic_targets.TIER_GENERIC - ) - assert generic_targets.entry_tier("gfx11-generic", "gfx942", table) is None - - @pytest.mark.parametrize( "outer, inner, expect", [ @@ -156,27 +96,3 @@ def test_covers(table, outer, inner, expect): def test_compete(table, a, b, expect): assert generic_targets.compete(a, b, table) is expect assert generic_targets.compete(b, a, table) is expect - - -def test_unknown_generic_is_not_a_member(table): - assert not table.has("gfx9-4-generic") - assert generic_targets.is_generic_shaped("gfx9-4-generic") - assert generic_targets.entry_tier("gfx9-4-generic", "gfx942", table) is None - assert generic_targets.entry_tier("gfx9-4-generic", "gfx9-4-generic", table) is None - assert generic_targets.expand(["gfx9-4-generic"], table) == frozenset() - assert not generic_targets.admits_target(["gfx9-4-generic"], "gfx942", table) - - -def test_expand_of_an_empty_list_is_unrestricted(table): - assert generic_targets.expand([], table) is None - assert generic_targets.expand(["gfx11-generic", "gfx942"], table) == frozenset( - [*table.members("gfx11-generic"), "gfx942"] - ) - - -def test_admits_target_is_empty_literal_or_containing_generic(table): - assert generic_targets.admits_target([], "gfx1151", table) - assert generic_targets.admits_target(["gfx1151"], "gfx1151", table) - assert generic_targets.admits_target(["gfx11-generic"], "gfx1151", table) - assert not generic_targets.admits_target(["gfx11-generic"], "gfx1154", table) - assert not generic_targets.admits_target(["gfx942"], "gfx1151", table) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py index ab691316bf02..749e3aa224e3 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py @@ -101,12 +101,19 @@ def _root(tmp_path, empty_arch_fixture, kdps, standalone=()): return root -def _pack(root, tmp_path, arches, rocm_kpack_dir, table=GENERIC_TARGETS_JSON): +def _pack( + root, + tmp_path, + arches, + rocm_kpack_dir, + table=GENERIC_TARGETS_JSON, + hipcc="hipcc-not-invoked", +): return run_pipeline( source_root=root, arches=list(arches), out_root=tmp_path / "out", - hipcc="hipcc-not-invoked", + hipcc=hipcc, generic_targets_json=table, rocm_kpack_dir=rocm_kpack_dir, inter_root=tmp_path / "inter", @@ -140,15 +147,12 @@ def test_generic_entries_land_in_every_selected_member_folder_only( "wild": _kdp("kdp-wild", [], [_embedded("ukd-wild")]), }, ) - results = _pack(root, tmp_path, ["gfx942", MEMBER_A, MEMBER_B], rocm_kpack_dir) + _pack(root, tmp_path, ["gfx942", MEMBER_A, MEMBER_B], rocm_kpack_dir) out = tmp_path / "out" assert sorted(p.name for p in out.iterdir()) == [MEMBER_A, MEMBER_B, "gfx942"] assert not (out / UNSELECTED_MEMBER).exists() assert not (out / GENERIC).exists() - assert {a: r.out_dir for a, r in results.items()} == { - arch: out / arch for arch in ("gfx942", MEMBER_A, MEMBER_B) - } for member in (MEMBER_A, MEMBER_B): kdp = _read(out / member / "g.kdp.json") @@ -165,10 +169,6 @@ def test_generic_entries_land_in_every_selected_member_folder_only( assert (out / MEMBER_B / "explicit.kdp.json").is_file() assert not (out / MEMBER_A / "explicit.kdp.json").exists() - generic_tree = tmp_path / "inter" / ".generic-out" / GENERIC - assert (generic_tree / "g.kdp.json").is_file() - assert not (generic_tree / "wild.kdp.json").exists() - shared = [p for p in root.iterdir() if p.name.endswith(_SHARED_SUFFIXES)] assert shared for arch in ("gfx942", MEMBER_A, MEMBER_B): @@ -194,8 +194,6 @@ def test_generic_entry_is_not_materialized_when_no_member_is_selected( out = tmp_path / "out" assert sorted(p.name for p in out.iterdir()) == ["gfx942"] assert not (out / "gfx942" / "g.kdp.json").exists() - assert not (tmp_path / "inter" / ".generic-out").exists() - assert not (tmp_path / "inter" / GENERIC).exists() @pytest.mark.quick @@ -211,48 +209,31 @@ def test_merge_collision_with_differing_bytes_is_refused_and_leaves_the_member_u _write(member_dir / "concrete_only.kdp.json", {"id": "kdp-c"}) before = _files(member_dir) - with pytest.raises(HkpPackError) as err: + with pytest.raises(HkpPackError): _merge_generic_into_member(generic_dir, member_dir, staging) - assert str(member_dir / "dup.ukd.json") in str(err.value) assert _files(member_dir) == before assert not staging.exists() @pytest.mark.quick -def test_a_member_whose_concrete_pass_failed_gets_no_generic_copy( - tmp_path, empty_arch_fixture, rocm_kpack_dir, monkeypatch -): - root = _root( - tmp_path, - empty_arch_fixture, - { - "c": _kdp("kdp-c", [MEMBER_A, MEMBER_B], [_embedded("ukd-c")]), - "g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-g")]), - }, - ) - real = pipeline.compile_intermediate - - def failing(flat, source_root, arch, *args, **kwargs): - if arch == MEMBER_A: - raise HkpPackError("synthetic compile failure") - return real(flat, source_root, arch, *args, **kwargs) - - monkeypatch.setattr(pipeline, "compile_intermediate", failing) - with pytest.raises(HkpPackError, match="synthetic compile failure"): - _pack(root, tmp_path, [MEMBER_A, MEMBER_B], rocm_kpack_dir) - - out = tmp_path / "out" - assert not (out / MEMBER_A).exists() - assert (out / MEMBER_B / "g.kdp.json").is_file() - assert (out / MEMBER_B / "c.kdp.json").is_file() - assert not [p for p in out.iterdir() if p.name.startswith(".")] - - -@pytest.mark.quick -def test_a_failed_generic_pass_leaves_no_member_copy( - tmp_path, empty_arch_fixture, rocm_kpack_dir, monkeypatch +@pytest.mark.parametrize( + "step, dropped, with_generic", + [ + ("compile_intermediate", (MEMBER_A,), (MEMBER_B,)), + ("pack_arch", (), ()), + ], +) +def test_a_failed_pass_leaves_no_partial_member_or_generic_copy( + tmp_path, + empty_arch_fixture, + rocm_kpack_dir, + monkeypatch, + step, + dropped, + with_generic, ): + """A failing concrete compile drops that member; a failing generic pack drops every generic copy.""" root = _root( tmp_path, empty_arch_fixture, @@ -261,22 +242,26 @@ def test_a_failed_generic_pass_leaves_no_member_copy( "g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-g")]), }, ) - real = pipeline.pack_arch + real = getattr(pipeline, step) - def failing(flat, inter, out_dir, *args, **kwargs): - if inter.arch == GENERIC: - raise HkpPackError("synthetic generic pack failure") - return real(flat, inter, out_dir, *args, **kwargs) + def failing(*args, **kwargs): + arch = args[2] if step == "compile_intermediate" else args[1].arch + if arch == (MEMBER_A if step == "compile_intermediate" else GENERIC): + raise HkpPackError("synthetic failure") + return real(*args, **kwargs) - monkeypatch.setattr(pipeline, "pack_arch", failing) - with pytest.raises(HkpPackError, match="synthetic generic pack failure"): + monkeypatch.setattr(pipeline, step, failing) + with pytest.raises(HkpPackError): _pack(root, tmp_path, [MEMBER_A, MEMBER_B], rocm_kpack_dir) out = tmp_path / "out" for member in (MEMBER_A, MEMBER_B): + if member in dropped: + assert not (out / member).exists() + continue assert (out / member / "c.kdp.json").is_file() - assert not (out / member / "g.kdp.json").exists() - assert not (tmp_path / "inter" / ".generic-out" / GENERIC).exists() + assert (out / member / "g.kdp.json").is_file() == (member in with_generic) + assert not [p for p in out.iterdir() if p.name.startswith(".")] @pytest.mark.quick @@ -369,27 +354,18 @@ def _hsaco_ukd(uid, arch): _OVERLAPPING_TABLE, ), "unknown_generic": (["gfx99-generic"], "inline", None, None), - "standalone_narrower_than_generic_kdp": ([GENERIC], "standalone", [MEMBER_B], None), "inline_narrower_than_generic_kdp": ([GENERIC], "inline", [MEMBER_B], None), "inline_unknown_generic": ([GENERIC], "inline", ["gfx99-generic"], None), "inline_generic_beside_member": ([GENERIC], "inline", [GENERIC, MEMBER_B], None), - "mixed_list_ukd_missing_the_generic": ( - ["gfx942", GENERIC], - "inline", - ["gfx942"], - None, - ), "standalone_without_arch_under_concrete_kdp": ( ["gfx942"], "standalone", None, None, ), - "standalone_without_arch_under_generic_kdp": ([GENERIC], "standalone", None, None), "generic_ukd_under_empty_arch_kdp": ([], "inline", [GENERIC], None), "generic_ukd_under_kdp_not_listing_it": ([MEMBER_B], "inline", [GENERIC], None), "rocke_inheriting_a_generic": ([GENERIC], "rocke", None, None), - "rocke_with_own_generic": ([GENERIC], "rocke", [GENERIC], None), } _ACCEPTED = { @@ -423,9 +399,7 @@ def _arch_rule_root(tmp_path, empty_arch_fixture, kdp_arch, form, ukd_arch): @pytest.mark.quick @pytest.mark.parametrize("case", sorted(_REJECTED)) -def test_an_arch_rule_violation_is_an_error_naming_the_kdp( - tmp_path, empty_arch_fixture, case -): +def test_an_arch_rule_violation_is_an_error(tmp_path, empty_arch_fixture, case): kdp_arch, form, ukd_arch, table_doc = _REJECTED[case] root = _arch_rule_root(tmp_path, empty_arch_fixture, kdp_arch, form, ukd_arch) table = GENERIC_TARGETS @@ -433,9 +407,8 @@ def test_an_arch_rule_violation_is_an_error_naming_the_kdp( table_path = tmp_path / "table.json" _write(table_path, table_doc) table = GenericTargets.load(table_path) - with pytest.raises(HkpPackError) as err: + with pytest.raises(HkpPackError): _load(root, table) - assert "g.kdp.json" in str(err.value) @pytest.mark.quick @@ -454,39 +427,6 @@ def _hip_generic_root(dest, empty_arch_fixture): return dest -def _hip_pack(root, out, inter, arches, hipcc, rocm_kpack_dir): - return run_pipeline( - source_root=root, - arches=list(arches), - out_root=out, - hipcc=hipcc, - generic_targets_json=GENERIC_TARGETS_JSON, - rocm_kpack_dir=rocm_kpack_dir, - inter_root=inter, - source_label="generic-test", - ) - - -def test_generic_hip_archive_is_keyed_by_the_generic_spelling( - tmp_path, empty_arch_fixture, hipcc, rocm_kpack_dir -): - root = _hip_generic_root(tmp_path / "src", empty_arch_fixture) - results = _hip_pack( - root, tmp_path / "out", tmp_path / "inter", [MEMBER_A], hipcc, rocm_kpack_dir - ) - - shipped = _read(tmp_path / "out" / MEMBER_A / "solo.kdp.json") - ks = shipped["kernelDescriptors"][0]["kernel_source"] - assert shipped["arch"] == [GENERIC] - assert ks["library"] == f"kpack/hip_kernel_provider_{GENERIC}.kpack" - archive = _load_kpack(rocm_kpack_dir).PackedKernelArchive.read( - tmp_path / "out" / MEMBER_A / ks["library"] - ) - assert archive.get_kernel(ks["toc_key"], GENERIC) is not None - assert archive.get_kernel(ks["toc_key"], MEMBER_A) is None - assert results[MEMBER_A].kpack_path is None - - def test_compiled_agreement_holds_on_a_generic_copy( tmp_path, empty_arch_fixture, hipcc, rocm_kpack_dir ): @@ -494,16 +434,14 @@ def test_compiled_agreement_holds_on_a_generic_copy( from hkp_pack.desk_check import compiled_agreement root = _hip_generic_root(tmp_path / "src", empty_arch_fixture) - _hip_pack( - root, tmp_path / "out", tmp_path / "inter", [MEMBER_A], hipcc, rocm_kpack_dir - ) + _pack(root, tmp_path, [MEMBER_A], rocm_kpack_dir, hipcc=hipcc) failures, _unclaimed, _verified = compiled_agreement( tmp_path / "out" / MEMBER_A / "solo.kdp.json", GENERIC_TARGETS, rocm_kpack_dir ) assert failures == [] -def test_generic_pack_is_byte_identical_across_separate_per_arch_builds( +def test_generic_hip_archive_is_keyed_by_the_generic_spelling_and_byte_identical_across_builds( tmp_path, empty_arch_fixture, hipcc, rocm_kpack_dir, monkeypatch ): """Separate per-arch builds (own roots, cwd, source copy) yield identical generic files.""" @@ -518,9 +456,18 @@ def test_generic_pack_is_byte_identical_across_separate_per_arch_builds( root = _hip_generic_root(base / "deeper" / label / "src", empty_arch_fixture) (base / "cwd").mkdir() monkeypatch.chdir(base / "cwd") - _hip_pack( - root, base / "out", base / "inter" / label, arches, hipcc, rocm_kpack_dir - ) + results = _pack(root, base, arches, rocm_kpack_dir, hipcc=hipcc) + if label == "only_a": + shipped = _read(base / "out" / MEMBER_A / "solo.kdp.json") + ks = shipped["kernelDescriptors"][0]["kernel_source"] + assert shipped["arch"] == [GENERIC] + assert ks["library"] == f"kpack/hip_kernel_provider_{GENERIC}.kpack" + archive = _load_kpack(rocm_kpack_dir).PackedKernelArchive.read( + base / "out" / MEMBER_A / ks["library"] + ) + assert archive.get_kernel(ks["toc_key"], GENERIC) is not None + assert archive.get_kernel(ks["toc_key"], MEMBER_A) is None + assert results[MEMBER_A].kpack_path is None for member in arches: builds[(label, member)] = _files(base / "out" / member) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py index dedd681ceb0a..b57f728a0533 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py @@ -4,13 +4,12 @@ import pytest -from conftest import GENERIC_TARGETS, GENERIC_TARGETS_JSON +from conftest import GENERIC_TARGETS, run_pipeline from hkp_pack import agreement from hkp_pack.hip_compile import hip_variant_key as variant_key from hkp_pack.descriptors import load_flat_input, reachable_generic_ids from hkp_pack.errors import HkpPackError from hkp_pack.kernel_signature import kernel_signature -from hkp_pack.pipeline import run_pipeline ARCHES = ["gfx942", "gfx950", "gfx90a"] @@ -38,7 +37,6 @@ def built(tmp_path_factory, main_fixture, hipcc, rocm_kpack_dir): """Compile + prune + pack the main fixture once for the 3-arch matrix.""" base = tmp_path_factory.mktemp("built") results = run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=main_fixture, arches=ARCHES, out_root=base / "out", @@ -190,7 +188,6 @@ def test_prn3_exact_post_prune_set(built): def test_prn4_empty_arch_skip(tmp_path, empty_arch_fixture, hipcc, rocm_kpack_dir): logs = [] results = run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=empty_arch_fixture, arches=["gfx942", "gfx950"], out_root=tmp_path / "out", @@ -359,7 +356,6 @@ def test_self_describing_ukd(built, rocm_kpack_dir): # --- D. Negatives: compile-spec -------------------------------------------- def _run(source_root, tmp_path, hipcc, rocm_kpack_dir, arches=("gfx942",)): return run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=source_root, arches=list(arches), out_root=tmp_path / "out", @@ -489,7 +485,6 @@ def test_neg_sha256_mismatch(tmp_path, main_fixture, hipcc, rocm_kpack_dir): with pytest.raises(HkpPackError, match="sha256 mismatch"): # An expected digest that cannot match the freshly compiled blob. run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=main_fixture, arches=["gfx942"], out_root=tmp_path / "out", @@ -532,7 +527,6 @@ def test_cli1_single_arch(tmp_path, main_fixture, hipcc, rocm_kpack_dir): @pytest.mark.quick def test_cli2_empty_gpu_targets(tmp_path, main_fixture, hipcc, rocm_kpack_dir): results = run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=main_fixture, arches=[], out_root=tmp_path / "out", @@ -607,7 +601,6 @@ def _shas(out): return result run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=main_fixture, arches=["gfx942"], out_root=tmp_path / "out1", @@ -616,7 +609,6 @@ def _shas(out): inter_root=tmp_path / "inter1", ) run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=main_fixture, arches=["gfx942"], out_root=tmp_path / "out2", @@ -1263,7 +1255,6 @@ def test_empty_pruned_kdp_is_logged(tmp_path, main_fixture, hipcc, rocm_kpack_di p.write_text(json.dumps(doc), encoding="utf-8") logs = [] run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=src, arches=["gfx950"], out_root=tmp_path / "out", @@ -1299,7 +1290,6 @@ def test_nonbare_arch_is_rejected(tmp_path, main_fixture, hipcc, rocm_kpack_dir) p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="feature suffix"): run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=src, arches=["gfx942"], out_root=tmp_path / "out", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hsaco.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hsaco.py index 52706c5c2eb4..f460279336fc 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hsaco.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hsaco.py @@ -16,13 +16,13 @@ import pytest -from conftest import GENERIC_TARGETS, GENERIC_TARGETS_JSON +from conftest import GENERIC_TARGETS, run_pipeline from hkp_pack import pipeline, toolchain from hkp_pack.descriptors import load_flat_input from hkp_pack.errors import HkpPackError from hkp_pack.hsaco_source import hsaco_variant_key from hkp_pack.kernel_signature import kernel_signature -from hkp_pack.pipeline import compile_intermediate, run_pipeline +from hkp_pack.pipeline import compile_intermediate ARCH = "gfx942" OTHER_ARCH = "gfx950" @@ -47,7 +47,6 @@ def _load_kpack(rocm_kpack_dir): def _run(source_root, tmp_path, rocm_kpack_dir, arches=(ARCH,), **kwargs): return run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=source_root, arches=list(arches), out_root=tmp_path / "out", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py index fc4fec07377a..fc60db5c9fbf 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py @@ -20,11 +20,11 @@ import pytest -from conftest import GENERIC_TARGETS, GENERIC_TARGETS_JSON +from conftest import GENERIC_TARGETS, run_pipeline from hkp_pack.descriptors import load_flat_input from hkp_pack.errors import HkpPackError from hkp_pack.hip_compile import hip_variant_key -from hkp_pack.pipeline import _agreement_inputs, compile_intermediate, run_pipeline +from hkp_pack.pipeline import _agreement_inputs, compile_intermediate ARCH = "gfx942" ROCKE_ARCH = "gfx950" @@ -44,7 +44,6 @@ def _load_kpack(rocm_kpack_dir): def _run(source_root, tmp_path, hipcc, rocm_kpack_dir, arches, source_label=None): """Pack one root. A root holding an `embedded_source` descriptor needs a label.""" return run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=source_root, arches=list(arches), out_root=tmp_path / "out", @@ -315,7 +314,6 @@ def _pack_from(parent_name): root = parent / "src" _nest(root, "hip/solo_add", empty_arch_fixture) run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=parent / "out", @@ -435,8 +433,7 @@ def crash(flat, inter, out_arch_dir, *a, **kw): out_root = tmp_path / "out" with pytest.raises(RuntimeError, match="uncaught failure"): - pipeline.run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, + run_pipeline( source_root=root, arches=[ARCH], out_root=out_root, @@ -485,8 +482,7 @@ def flaky_pack(flat, inter, out_arch_dir, *a, **kw): out_root = tmp_path / "out" with pytest.raises(HkpPackError, match="gfx950"): - pipeline.run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, + run_pipeline( source_root=root, arches=["gfx942", "gfx950"], out_root=out_root, @@ -525,8 +521,7 @@ def test_failed_arch_removes_its_previous_good_output( _nest(root, "hip/pointwise", main_fixture) out_root = tmp_path / "out" - pipeline.run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, + run_pipeline( source_root=root, arches=[ARCH], out_root=out_root, @@ -543,8 +538,7 @@ def always_fail(flat, inter, out_arch_dir, *a, **kw): monkeypatch.setattr(pipeline, "pack_arch", always_fail) with pytest.raises(HkpPackError, match=ARCH): - pipeline.run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, + run_pipeline( source_root=root, arches=[ARCH], out_root=out_root, @@ -573,8 +567,7 @@ def always_fail(flat, inter, out_arch_dir, *a, **kw): monkeypatch.setattr(pipeline, "pack_arch", always_fail) with pytest.raises(HkpPackError) as exc: - pipeline.run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, + run_pipeline( source_root=root, arches=["gfx942", "gfx950"], out_root=tmp_path / "out", @@ -602,7 +595,6 @@ def test_example_tree_packs_both_producers( what keeps it honest: if the example rots, this fails. """ results = run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=EXAMPLE_ROOT, arches=[ARCH], out_root=tmp_path / "out", @@ -641,7 +633,6 @@ def test_example_tree_keeps_both_shared_filenames( both. """ run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=EXAMPLE_ROOT, arches=[ARCH], out_root=tmp_path / "out", @@ -684,7 +675,6 @@ def test_provenance_records_the_toolchain_that_built_each_kernel( stamp.write_text("deadbeefcafe\n", encoding="utf-8") run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=EXAMPLE_ROOT, arches=[ARCH], out_root=tmp_path / "out", @@ -796,7 +786,6 @@ def test_library_resolves_from_a_nested_descriptor( _nest(root, "hip/deep/deeper", main_fixture) run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=tmp_path / "out", @@ -826,7 +815,6 @@ def test_library_resolves_for_a_flat_descriptor( shutil.copytree(empty_arch_fixture, root) run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=tmp_path / "out", @@ -991,7 +979,6 @@ def test_library_resolves_for_a_nested_standalone_ukd( _nest(root, "hip/deep", main_fixture) run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=tmp_path / "out", @@ -1043,7 +1030,6 @@ def test_standalone_ukd_anchors_on_its_own_dir_not_the_kdps( ) run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=tmp_path / "out", @@ -1573,7 +1559,6 @@ def _add_sharing_embedded_kdp(folder): def _pack_embedded(root, tmp_path, rocm_kpack_dir, arches, log=print, out="out"): """Pack a root that compiles nothing, so hipcc must never be invoked.""" return run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=list(arches), out_root=tmp_path / out, @@ -1862,7 +1847,6 @@ def test_packing_without_a_source_label_is_refused( with pytest.raises(HkpPackError) as excinfo: run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=root, arches=[ARCH], out_root=tmp_path / "out", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py index 116fd11aa9af..1c42b77dfe5e 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py @@ -8,10 +8,8 @@ copies it verbatim into a tree that has never seen this file. """ -import ast import concurrent.futures import importlib -import inspect import itertools import json import os @@ -518,45 +516,6 @@ def test_prewarm_jobs_are_deduped_on_variant_key(corpus): assert len({j.vk for j in jobs}) == len(jobs) -def _call_sites(callee): - """`callee` call counts in pipeline.py, keyed by enclosing function.""" - tree = ast.parse(inspect.getsource(pipeline)) - counts = {} - for node in tree.body: - if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): - continue - found = 0 - for sub in ast.walk(node): - if not isinstance(sub, ast.Call): - continue - func = sub.func - name = ( - func.id - if isinstance(func, ast.Name) - else func.attr if isinstance(func, ast.Attribute) else None - ) - if name == callee: - found += 1 - if found: - counts[node.name] = found - return counts - - -@pytest.mark.quick -def test_arch_matches_call_sites_are_pinned(): - """The UKD-level selection filters live in the generator and nowhere else.""" - assert _call_sites("arch_matches") == {"_selected_entries": 2} - - -@pytest.mark.quick -def test_kdp_arch_matches_call_sites_are_pinned(): - """The KDP-level filter lives in the generator; `compile_intermediate` only decides disposition.""" - assert _call_sites("kdp_arch_matches") == { - "_selected_entries": 1, - "compile_intermediate": 1, - } - - @pytest.mark.quick def test_pack_jobs_env_parsing(monkeypatch): # Pinned to a fixed budget rather than the runner's core count: restating diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_rocke.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_rocke.py index da64d951fdf2..dcd8aedda8ca 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_rocke.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_rocke.py @@ -10,15 +10,14 @@ from conftest import ( GENERIC_TARGETS, - GENERIC_TARGETS_JSON, _arg, _kernel, _object, requires_msgpack, + run_pipeline, ) from hkp_pack.descriptors import load_flat_input from hkp_pack.errors import HkpPackError -from hkp_pack.pipeline import run_pipeline from hkp_pack.rocke_compile import ( build_spec, compile_rocke_variant, @@ -40,7 +39,6 @@ def _copy_fixture(tmp_path, fixture): def _run(source_root, tmp_path, hipcc, rocm_kpack_dir, arches=(ARCH,)): return run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, source_root=source_root, arches=list(arches), out_root=tmp_path / "out", @@ -758,8 +756,7 @@ def test_rocke_toc_key_collision_is_detected(tmp_path, monkeypatch, rocm_kpack_d (root / "collide.kdp.json").write_text(json.dumps(kdp), encoding="utf-8") with pytest.raises(HkpPackError, match="toc_key collision"): - pipeline.run_pipeline( - generic_targets_json=GENERIC_TARGETS_JSON, + run_pipeline( source_root=root, arches=[ARCH], out_root=tmp_path / "out", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py index 4584c4c783ae..bb1854b99d26 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_arch_probe.py @@ -27,23 +27,17 @@ def _covers_any(kdp_path, arches, table): + """False only when the KDP's well-formed arch list reaches none of @arches.""" try: - doc = json.loads(Path(kdp_path).read_text(encoding="utf-8")) - except (OSError, ValueError): + entries = json.loads(Path(kdp_path).read_text(encoding="utf-8"))["arch"] + if not isinstance(entries, list) or not entries: + return True + # An unknown generic would read as absence; the packer must reach it to report it. + if any(is_generic_shaped(e) and not table.has(e) for e in entries): + return True + return any(admits_target(entries, arch, table) for arch in arches) + except (OSError, ValueError, KeyError, TypeError, AttributeError): return True - if not isinstance(doc, dict): - return True - if "arch" not in doc: - return True - entries = doc["arch"] - if not isinstance(entries, list) or not all(isinstance(e, str) for e in entries): - return True - if not entries: - return True - # An unknown generic would read as absence; the packer must reach it to report it. - if any(is_generic_shaped(e) and not table.has(e) for e in entries): - return True - return any(admits_target(entries, arch, table) for arch in arches) def main(argv=None): diff --git a/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp b/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp index 70502a9693a1..aa1e310e8230 100644 --- a/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp +++ b/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp @@ -178,15 +178,6 @@ TEST(TestKpackModuleCacheSelectArch, FallsBackToAGenericKeyContainingTheDevice) EXPECT_EQ(selected, &keys[2]); } -TEST(TestKpackModuleCacheSelectArch, DeclinesAKeyThatDoesNotNameTheDevice) -{ - // Name shape never implies membership. - EXPECT_EQ(KpackModuleCache::selectArch({"gfx12-generic"}, "gfx1250"), nullptr); - EXPECT_EQ(KpackModuleCache::selectArch({"gfx11-generic", "gfx12-generic"}, "gfx942"), nullptr); - EXPECT_EQ(KpackModuleCache::selectArch({"gfx99-generic"}, "gfx99-generic"), nullptr); - EXPECT_EQ(KpackModuleCache::selectArch({}, "gfx1100"), nullptr); -} - TEST(TestKpackModuleCacheLoad, ASecondOrdinalDoesNotAnswerFromTheFirstOrdinalsEntry) { SKIP_IF_NO_DEVICES(); diff --git a/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake b/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake index 5a28751f8db6..a99f5aef79db 100644 --- a/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake +++ b/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake @@ -3,7 +3,7 @@ # Runs hipdnn_validate_gpu_generic_targets via `cmake -P`, optionally on a mutated copy: # -DGENERIC_TARGETS_JSON= [-DMUTATION_OP= -DMUTATION_PATH= -DMUTATION_VALUE=] -# MUTATION_OP: REMOVE, SET_STRING, SET_JSON, or RAW (replace the whole document). +# MUTATION_OP: SET_STRING, SET_JSON, or RAW (replace the whole document). # Prints "gpu_generic_targets: valid" only if every rule passed. if(NOT GENERIC_TARGETS_JSON) @@ -19,9 +19,7 @@ if(MUTATION_OP) endif() file(READ "${GENERIC_TARGETS_JSON}" json) string(REPLACE "." ";" path "${MUTATION_PATH}") - if(MUTATION_OP STREQUAL "REMOVE") - string(JSON json REMOVE "${json}" ${path}) - elseif(MUTATION_OP STREQUAL "SET_STRING") + if(MUTATION_OP STREQUAL "SET_STRING") string(JSON json SET "${json}" ${path} "\"${MUTATION_VALUE}\"") elseif(MUTATION_OP STREQUAL "SET_JSON") string(JSON json SET "${json}" ${path} "${MUTATION_VALUE}") diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp index f13be243df0f..403ea50005c8 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestArchGate.cpp @@ -67,17 +67,6 @@ TEST(TestIngestorArchGate, AdmitsWhenAnyListedArchMatches) EXPECT_FALSE(archSupports(family, "gfx90a")); } -TEST(TestIngestorArchGate, AdmitsOnlyTheMembersOfAGeneric) -{ - EXPECT_TRUE(archSupports({"gfx11-generic"}, "gfx1151")); - EXPECT_TRUE(archSupports({"gfx942", "gfx12-generic"}, "gfx1201")); - EXPECT_TRUE(archSupports({"gfx11-generic"}, "gfx1151:sramecc+:xnack-")); - // Siblings no table row lists are refused. - EXPECT_FALSE(archSupports({"gfx11-generic"}, "gfx1154")); - EXPECT_FALSE(archSupports({"gfx12-generic"}, "gfx1250:sramecc+")); - EXPECT_FALSE(archSupports({"gfx9-4-generic"}, "gfx942")); -} - TEST(TestIngestorArchGate, PrunesAPackWhoseArchExcludesTheDevice) { const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp index 81163890b540..474f181a18b1 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp @@ -2027,35 +2027,11 @@ std::vector engineNames(const Documents& candidate) } // namespace -TEST(TestDescriptorLoader, AcceptsAGenericTargetBesideAnUnrelatedExplicitId) -{ - const ScopedSymbols symbols; - const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("pack_arch_generic")); - auto documents = makeSetDocuments('1', "test:arch_generic"); - auto& pack = documentOfType(documents, ".kdp.json"); - pack["arch"] = arch({"gfx942", "gfx11-generic"}); - pack.at("kernelDescriptors")[0]["arch"] = arch({"gfx942", "gfx11-generic"}); - writeDocuments(dir.path(), documents); - - const auto sets = loadFrom(dir.path()); - - ASSERT_EQ(sets.size(), 1u); - ASSERT_EQ(sets.front().packs.size(), 1u); - EXPECT_EQ(sets.front().packs.front().arch, - (std::vector{"gfx942", "gfx11-generic"})); - EXPECT_EQ(sets.front().packs.front().kernels.front().arch, - (std::vector{"gfx942", "gfx11-generic"})); -} - /// A generic-shaped name the table does not list matches no device, so its file is dropped. TEST(TestDescriptorLoader, DropsAPackNamingAGenericAbsentFromTheTable) { EXPECT_EQ(engineNames(candidateWithKernelArch(arch({"gfx99-generic"}), arch({}), false)), INTACT_ONLY); - EXPECT_EQ(engineNames(candidateWithKernelArch(arch({}), arch({"gfx99-generic"}), false)), - INTACT_ONLY); - EXPECT_EQ(engineNames(candidateWithKernelArch(arch({}), arch({"gfx99-generic"}), true)), - INTACT_ONLY); } TEST(TestDescriptorLoader, DropsAPackListingAGenericAndOneOfItsMembers) @@ -2063,9 +2039,6 @@ TEST(TestDescriptorLoader, DropsAPackListingAGenericAndOneOfItsMembers) EXPECT_EQ( engineNames(candidateWithKernelArch(arch({"gfx11-generic", "gfx1151"}), arch({}), false)), INTACT_ONLY); - EXPECT_EQ( - engineNames(candidateWithKernelArch(arch({}), arch({"gfx1151", "gfx11-generic"}), false)), - INTACT_ONLY); EXPECT_EQ(engineNames(candidateWithKernelArch( arch({"gfx11-generic", "gfx12-generic", "gfx942"}), arch({}), false)), BOTH); @@ -2087,10 +2060,6 @@ TEST(TestDescriptorLoader, DropsAPackWhoseKernelNamesAGenericThePackDoesNotList) engineNames(candidateWithKernelArch(members, arch({"gfx11-generic"}), standalone)), INTACT_ONLY) << (standalone ? "standalone" : "inline"); - EXPECT_EQ(engineNames(candidateWithKernelArch( - arch({"gfx11-generic", "gfx942"}), arch({"gfx11-generic"}), standalone)), - BOTH) - << (standalone ? "standalone" : "inline"); } } @@ -2114,24 +2083,6 @@ TEST(TestDescriptorLoader, DropsAPackWhoseKernelOmitsOrExceedsTheGenericPackArch EXPECT_EQ(engineNames(candidateWithKernelArch(arch({"gfx11-generic"}), arch({}), false)), BOTH); } -/// A standalone kernel id defined for a generic and for one of its members is ambiguous. -TEST(TestDescriptorLoader, StillRejectsAStandaloneKernelDefinedForAGenericAndForOneOfItsMembers) -{ - const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("generic_and_member")); - auto documents = makeSetDocuments('1', "test:generic_and_member"); - referenceLastKernel(documents); - documentOfType(documents, ".kdp.json")["arch"] = nlohmann::json::array({"gfx11-generic"}); - documentOfType(documents, ".ukd.json")["arch"] = nlohmann::json::array({"gfx11-generic"}); - - auto member = documentOfType(documents, ".ukd.json"); - member["arch"] = nlohmann::json::array({"gfx1151"}); - member["name"] = "the gfx1151 build"; - writeDocuments(dir.path(), documents); - writeDocuments(dir.path() / "gfx1151", {TestDocument{".ukd.json", member}}); - - EXPECT_TRUE(loadFrom(dir.path()).empty()); -} - /// The default: a pack naming no architecture applies everywhere, so absence must parse /// as empty rather than as a constraint nothing satisfies. TEST(TestDescriptorLoader, APackWithNoDeclaredArchIsArchIndependent) diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp index 8ba6dd02bec9..1813a9604a02 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp @@ -28,27 +28,13 @@ TEST(TestGenericTargets, FindsAGenericOnlyByItsExactName) EXPECT_EQ(row->name, "gfx11-generic"); EXPECT_EQ(findGenericTarget("gfx11"), nullptr); - EXPECT_EQ(findGenericTarget("gfx11-generic "), nullptr); - EXPECT_EQ(findGenericTarget("GFX11-GENERIC"), nullptr); EXPECT_EQ(findGenericTarget("gfx9-4-generic"), nullptr); - EXPECT_EQ(findGenericTarget(""), nullptr); EXPECT_TRUE(isGenericShapedArchName("gfx9-4-generic")); EXPECT_FALSE(isGenericShapedArchName("gfx942")); EXPECT_FALSE(isGenericShapedArchName("generic")); } -TEST(TestGenericTargets, ContainsReportsMembership) -{ - EXPECT_TRUE(genericTargetContains("gfx11-generic", "gfx1100")); - EXPECT_TRUE(genericTargetContains("gfx11-generic", "gfx1153")); - EXPECT_FALSE(genericTargetContains("gfx11-generic", "gfx1154")); - EXPECT_FALSE(genericTargetContains("gfx11-generic", "gfx1200")); - EXPECT_TRUE(genericTargetContains("gfx12-generic", "gfx1201")); - EXPECT_FALSE(genericTargetContains("gfx12-generic", "gfx1250")); - EXPECT_FALSE(genericTargetContains("gfx9-4-generic", "gfx942")); -} - TEST(TestGenericTargets, EntryTierRanksExplicitAboveGeneric) { EXPECT_EQ(entryTier("gfx1151", "gfx1151"), ArchTier::EXPLICIT); @@ -72,13 +58,6 @@ TEST(TestGenericTargets, ListTierIsTheBestOfItsEntries) EXPECT_EQ(archTier({"gfx11-generic"}, "gfx1151:sramecc+"), ArchTier::GENERIC); } -TEST(TestGenericTargets, EmptyListIsUnrestricted) -{ - EXPECT_EQ(archTier({}, "gfx1151"), ArchTier::UNRESTRICTED); - EXPECT_EQ(archTier({}, ""), ArchTier::UNRESTRICTED); - EXPECT_TRUE(archSupports({}, "gfx1151")); -} - TEST(TestGenericTargets, OverlapIsOverExpandedMemberSets) { EXPECT_TRUE(archOverlaps({"gfx11-generic"}, {"gfx1151"})); @@ -114,18 +93,6 @@ TEST(TestGenericTargets, CompeteIsSameTierOverlap) EXPECT_FALSE(archesCompete({"gfx11-generic"}, {"gfx12-generic"})); } -TEST(TestGenericTargets, UnknownGenericExpandsToNothing) -{ - const Arch unknown{"gfx9-4-generic"}; - EXPECT_FALSE(archSupports(unknown, "gfx942")); - EXPECT_FALSE(archSupports(unknown, "gfx9-4-generic")); - EXPECT_FALSE(archOverlaps(unknown, unknown)); - EXPECT_TRUE(archOverlaps(unknown, {})); - EXPECT_TRUE(archCovers({"gfx11-generic"}, unknown)); - EXPECT_FALSE(archCovers(unknown, {"gfx1151"})); - EXPECT_FALSE(archesCompete(unknown, unknown)); -} - } // namespace #endif // HIPDNN_ENABLE_KERNEL_INGESTOR diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py index 713726780a03..f75395e85662 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py @@ -224,9 +224,7 @@ def test_accepted_shapes(self, pack_arch, kernel_arch): (["gfx942", "gfx11-generic"], ["gfx1151", "gfx11-generic"]), ([], ["gfx11-generic"]), (["gfx942"], ["gfx11-generic"]), - (["gfx942", "gfx12-generic"], ["gfx11-generic"]), (["gfx11-generic"], ["gfx1151"]), - (["gfx942", "gfx11-generic"], ["gfx942"]), (["gfx11-generic", "gfx12-generic"], ["gfx11-generic"]), ], ) diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py index f47dfb5cced0..45d4206e0eb8 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py @@ -1032,7 +1032,7 @@ def test_two_distinct_arches_each_count_the_descriptor_once(self, tmp_path): assert inventory["total_descriptor_count"] == 1 def test_inventory_gives_every_member_the_generic_rows(self): - """Each member's row holds the generic's descriptors, its own, and the wildcard's.""" + """Each member's row holds the generic's descriptors and its own.""" generic = make_pack( name="generic", arch=["gfx11-generic"], @@ -1048,24 +1048,18 @@ def test_inventory_gives_every_member_the_generic_rows(self): arch=["gfx942"], kernels=[make_kernel(name="other.k", metadata={"block_size": 256})], ) - everywhere = make_pack( - name="everywhere", - arch=[], - kernels=[make_kernel(name="any.k", metadata={"block_size": 512})], - ) - config = make_minimal_config(packs=[generic, explicit, other, everywhere]) + config = make_minimal_config(packs=[generic, explicit, other]) documents = build_kdp_documents(config, mint_ids(config)) arches = emitted_inventory(config, documents)["arches"] names = {a: set(row["descriptor_names"]) for a, row in arches.items()} - assert names["gfx1100"] == {"generic.k", "any.k"} - assert names["gfx1153"] == {"generic.k", "any.k"} - assert names["gfx1151"] == {"generic.k", "explicit.k", "any.k"} - assert names["gfx942"] == {"other.k", "any.k"} - assert names["gfx11-generic"] == {"generic.k", "any.k"} + assert names["gfx1100"] == {"generic.k"} + assert names["gfx1153"] == {"generic.k"} + assert names["gfx1151"] == {"generic.k", "explicit.k"} + assert names["gfx942"] == {"other.k"} + assert names["gfx11-generic"] == {"generic.k"} assert "gfx1154" not in names - stems = {config.kdp_stem(p) for p in (generic, everywhere)} - assert set(arches["gfx1100"]["pack_names"]) == stems + assert set(arches["gfx1100"]["pack_names"]) == {config.kdp_stem(generic)} def test_the_packaged_dialect_reports_the_kind_that_actually_ships(self): """hkp_pack lowers ``rocke``/``hip`` to ``kpack`` before the loader reads it, so diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py index 561853ad4f3f..07ca309f7c9f 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py @@ -29,14 +29,11 @@ ("arch", "device", "expect"), [ ([], "gfx1151", _UNRESTRICTED), - ([], "gfx942:sramecc+:xnack-", _UNRESTRICTED), (["gfx1151"], "gfx1151", _EXPLICIT), - (["gfx942"], "gfx942:sramecc+:xnack-", _EXPLICIT), ([_G11], "gfx1100", _GENERIC), ([_G11], "gfx1151:sramecc+", _GENERIC), (["gfx1151", _G11], "gfx1151", _EXPLICIT), (["gfx1151", _G11], "gfx1100", _GENERIC), - (["gfx942", _G11], "gfx1100", _GENERIC), ([_G11], "gfx1154", None), ([_G12], "gfx1100", None), (["gfx942"], "gfx950", None), @@ -44,8 +41,6 @@ (["gfx1250"], "gfx1250-strict", None), (["gfx1250-strict"], "gfx1250-strict", _EXPLICIT), ([_UNKNOWN], "gfx942", None), - ([_UNKNOWN], _UNKNOWN, None), - ([_UNKNOWN, "gfx942"], "gfx942", _EXPLICIT), ], ) def test_list_tier(arch, device, expect): @@ -60,14 +55,11 @@ def test_list_tier(arch, device, expect): (["gfx942", "gfx950"], ["gfx942"], True), (["gfx942"], ["gfx942", "gfx950"], False), ([_G11], ["gfx1151"], True), - ([_G11], ["gfx1100", "gfx1151"], True), ([_G11], ["gfx1154"], False), (["gfx1151"], [_G11], False), (["gfx1100", "gfx1151"], [_G11], True), ([_G11], [_G11], True), ([_G11], [_G12], False), - ([_UNKNOWN], [_UNKNOWN], True), - ([_UNKNOWN], ["gfx942"], False), ], ) def test_covers(outer, inner, expect): @@ -79,17 +71,13 @@ def test_covers(outer, inner, expect): [ ([], [], True), ([], ["gfx942"], False), - ([_G11], [], False), (["gfx942"], ["gfx942"], True), (["gfx942"], ["gfx950"], False), ([_G11], ["gfx1151"], False), - ([_G11], ["gfx1151", "gfx1100"], False), ([_G11], [_G11], True), ([_G11], [_G12], False), (["gfx1151", _G11], [_G11], True), ([_G11], [_G11, "gfx942"], True), - ([_UNKNOWN], [_UNKNOWN], False), - ([_UNKNOWN], [], False), ], ) def test_compete(a, b, expect): diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py index df4c3817fda1..ca0da2ba1df0 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py @@ -334,7 +334,6 @@ def test_equal_tuples_on_disjoint_arches_are_both_accepted(self, gate): self._two_packs(gate, "disjoint", ["gfx942"], ["gfx950"]) result = gate.run("d", "disjoint", profiled=False) assert result.returncode == 0, result.stdout + result.stderr - assert "loader-tuple" not in result.stdout def test_equal_tuples_overlapping_on_one_arch_collide(self, gate): self._two_packs(gate, "overlap", ["gfx942", "gfx950"], ["gfx950"]) @@ -356,7 +355,6 @@ def test_a_better_tier_does_not_collide_with_a_worse_one( self._two_packs(gate, "outranked", left_arch, right_arch) result = gate.run("o", "outranked", profiled=False) assert result.returncode == 0, result.stdout + result.stderr - assert "loader-tuple" not in result.stdout @pytest.mark.parametrize( ("left_arch", "right_arch"), @@ -1030,18 +1028,18 @@ def real_archive(): kpack, compression = load_kpack(python_dir) - def write(root): + def write(root, arch=_ARCH): path = root / "kpack" / "test.kpack" path.parent.mkdir(parents=True, exist_ok=True) archive = kpack.PackedKernelArchive( group_name="test", - gfx_arch_family=_ARCH, - gfx_arches=[_ARCH], + gfx_arch_family=arch, + gfx_arches=[arch], compressor=compression.ZstdCompressor(compression_level=3), ) archive.add_kernel( archive.prepare_kernel( - relative_path="v0", gfx_arch=_ARCH, hsaco_data=_PAYLOAD, metadata={} + relative_path="v0", gfx_arch=arch, hsaco_data=_PAYLOAD, metadata={} ) ) archive.finalize_archive() @@ -1237,27 +1235,11 @@ def _entry(arch): "Entry", (), {"ukd": ukd, "arch": arch, "origin_dir": None, "inline": False} ) - def test_a_generic_document_is_read_with_its_generic_archive_key(self, tmp_path): - python_dir = _kpack_python_dir() - kpack, compression = load_kpack(python_dir) - path = tmp_path / "kpack" / "test.kpack" - path.parent.mkdir(parents=True) - archive = kpack.PackedKernelArchive( - group_name="test", - gfx_arch_family="gfx11-generic", - gfx_arches=["gfx11-generic"], - compressor=compression.ZstdCompressor(compression_level=3), - ) - archive.add_kernel( - archive.prepare_kernel( - relative_path="v0", - gfx_arch="gfx11-generic", - hsaco_data=_PAYLOAD, - metadata={}, - ) - ) - archive.finalize_archive() - archive.write(path) + def test_a_generic_document_is_read_with_its_generic_archive_key( + self, tmp_path, real_archive + ): + write_archive, python_dir = real_archive + write_archive(tmp_path, "gfx11-generic") table = gate_module.gtmod.GenericTargets.load( gate_module.gtmod.DEFAULT_TABLE_PATH diff --git a/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py b/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py index 577ff57525b4..69e4d06a085e 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py +++ b/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py @@ -209,34 +209,15 @@ def _specialization_twins(order: list, by_label: dict, knobs: set) -> list: return violations -_TIER_NAMES = { - gtmod.TIER_EXPLICIT: "explicit", - gtmod.TIER_GENERIC: "generic", - gtmod.TIER_UNRESTRICTED: "unrestricted", -} - - -def _tie(left: list, right: list, table) -> tuple[str, str]: - """Devices and tier name where two competing arch lists tie.""" - if not left and not right: - return "every arch", _TIER_NAMES[gtmod.TIER_UNRESTRICTED] - candidates = set() - for entry in (*left, *right): - if gtmod.is_generic_shaped(entry): - if table.has(entry): - candidates.update(table.members(entry)) - else: - candidates.add(entry) - ties = {} - for device in candidates: - tier = gtmod.list_tier(left, device, table) - if tier is not None and tier == gtmod.list_tier(right, device, table): - ties[device] = tier - best = min(ties.values()) - return ( - ", ".join(sorted(d for d, t in ties.items() if t == best)), - _TIER_NAMES[best], - ) +def _shared_devices(left: list, right: list, table) -> str: + """The devices both competing arch lists admit.""" + left_devices = gtmod.expand(left, table) + right_devices = gtmod.expand(right, table) + if left_devices is None or right_devices is None: + shared = left_devices or right_devices + else: + shared = left_devices & right_devices + return ", ".join(sorted(shared)) if shared else "every arch" def effective_arch( @@ -396,10 +377,10 @@ def check( left.arch, right.arch, generic_targets ): continue - where, tier = _tie(left.arch, right.arch, generic_targets) + where = _shared_devices(left.arch, right.arch, generic_targets) collisions.append( f"{left.ukd.get('name')} and {right.ukd.get('name')} complete to one " - f"tuple on {where} at the {tier} tier" + f"tuple on {where}" ) if collisions: failures.append( From 2540f004c37e23a03ca07148eb5533e9c2033ccf Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 11:51:42 -0400 Subject: [PATCH 38/52] hkp probes: sweep each spec value, probe every named arch, reuse the packer loader --- .github/scripts/check_probe_junit.py | 11 +- .../scripts/tests/test_check_probe_junit.py | 6 +- dnn-providers/hip-kernel-provider/README.md | 2 +- .../descriptor-packaging/README.md | 122 ++++--- .../cmake/HkpPackagingProbes.cmake | 91 ++++-- .../descriptor-packaging/probes/probes.cmake | 26 +- .../probes/tests/probe_support.py | 4 - .../tests/test_hkp_probe_derive_root.py | 297 ++++++++++++------ .../tools/hkp_probe_derive_root.py | 221 ++++++++----- .../tools/hkp_probe_kinds.py | 20 +- .../descriptors/README.md | 11 +- .../skills/hipdnn-ingestor-engine/RUNBOOK.md | 18 +- .../ai/skills/hipdnn-ingestor-engine/SKILL.md | 6 +- .../skills/hipdnn-ingestor-engine/extend.md | 7 +- .../ai/skills/hipdnn-superbuild-test/SKILL.md | 6 +- .../ai/skills/hipdnn-superbuild/SKILL.md | 2 +- 16 files changed, 581 insertions(+), 269 deletions(-) diff --git a/.github/scripts/check_probe_junit.py b/.github/scripts/check_probe_junit.py index 161c6e76192d..2021e5d2d4d9 100755 --- a/.github/scripts/check_probe_junit.py +++ b/.github/scripts/check_probe_junit.py @@ -9,6 +9,11 @@ and checks it against the manifest the CMake configure wrote (one ctest test name per line). Testcases not named in the manifest are ignored. +The manifest always names the probe tooling test "hkp-probe-tools", plus one entry +per declared probe. A lane whose GPU_TARGETS cover every architecture the +descriptors name declares no probe, so "hkp-probe-tools" alone is a valid manifest; +an empty one means the configure did not write it properly. + For every manifest name, the junit must hold exactly one testcase with that name, and that testcase must have run (status "run" or "fail"; without a status attribute, no child) and must have no or child. @@ -30,8 +35,8 @@ import xml.etree.ElementTree as ET from pathlib import Path -# One probe plus the "hkp-probe-tools" test. -MIN_MANIFEST_ENTRIES = 2 +# The "hkp-probe-tools" test; probes are optional. +MIN_MANIFEST_ENTRIES = 1 def read_manifest(path: Path) -> list[str]: @@ -63,7 +68,7 @@ def check(junit_path: Path, manifest_path: Path) -> list[tuple[str, str]]: ( "manifest-too-small", f"{manifest_path} lists {len(names)} test(s), expected at least " - f"{MIN_MANIFEST_ENTRIES} (one probe plus hkp-probe-tools)", + f"{MIN_MANIFEST_ENTRIES} (hkp-probe-tools)", ) ) diff --git a/.github/scripts/tests/test_check_probe_junit.py b/.github/scripts/tests/test_check_probe_junit.py index 5d5f61476abe..40ec0aa186ef 100644 --- a/.github/scripts/tests/test_check_probe_junit.py +++ b/.github/scripts/tests/test_check_probe_junit.py @@ -96,11 +96,9 @@ def test_empty_manifest_fails(self): xml = junit_xml([case_xml(n) for n in MANIFEST]) self.assertIn("manifest-too-small", self.run_check(xml, [])) - def test_manifest_without_probe_fails(self): + def test_manifest_without_probe_passes(self): xml = junit_xml([case_xml("hkp-probe-tools")]) - self.assertEqual( - self.run_check(xml, ["hkp-probe-tools"]), ["manifest-too-small"] - ) + self.assertEqual(self.run_check(xml, ["hkp-probe-tools"]), []) def test_failure_fails(self): xml = junit_xml([case_xml(MANIFEST[0], "failure"), case_xml(MANIFEST[1])]) diff --git a/dnn-providers/hip-kernel-provider/README.md b/dnn-providers/hip-kernel-provider/README.md index 116aa0dfc9b4..e9cc9a17427b 100644 --- a/dnn-providers/hip-kernel-provider/README.md +++ b/dnn-providers/hip-kernel-provider/README.md @@ -78,7 +78,7 @@ Beyond the shared third-party set, the prefixes must provide HIP/HIPRTC, `hipdnn Descriptor packaging (`HIPDNN_ENABLE_KERNEL_INGESTOR=ON`) additionally requires a supplied `Python3_EXECUTABLE` with pip, `msgpack`, and `zstandard`, a kpack source tree, and local rocKE wheels. The `hkp_rocke_wheel_python_interp` target installs those wheels only into build-owned storage with no index access or dependency resolution, then uses the supplied interpreter with a subprocess-scoped private import path. It does not install rocKE into the parent Python environment. See [Kernel packing](../../projects/hipdnn/docs/Building.md#kernel-packing-rocm_kpack) for wheel supply modes and prerequisites. -Packaging probes pack descriptors with the real packer for one explicit architecture, independent of the build's `GPU_TARGETS`, and assert the output. Each integration declares its own probe in `descriptor-packaging/probes/probes.cmake`, `hkp_add_packaging_probe(ARCH NAME UKDS ...)`, naming a representative set of UKDs; without `UKDS` a probe packs one UKD per compile group. Probes are superbuild-only: the `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` option is `OFF` by default. They do not cover device behavior, install staging, or standalone-UKD references (probe derive fails configure on those). See the [descriptor-packaging README](descriptor-packaging/README.md#packaging-probes). +Packaging probes pack descriptors with the real packer for one explicit architecture, independent of the build's `GPU_TARGETS`, and assert the output. Every architecture named by an explicit `arch` on a production KDP or UKD gets an automatic probe in each build that does not target it; wildcard descriptors (no `arch`) are excluded, since every build's own pack covers them. An automatic probe packs, per compile group, one UKD, or for rocKE enough UKDs that each value of each varying `spec` field is packed once (value combinations are not guaranteed). An integration that needs specific UKDs packed adds a named probe, `hkp_add_packaging_probe(ARCH NAME UKDS ...)`, in `descriptor-packaging/probes/probes.cmake`. Probes are superbuild-only: the `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` option is `OFF` by default. They do not cover device behavior, install staging, or standalone-UKD references (probe derive fails configure on those). See the [descriptor-packaging README](descriptor-packaging/README.md#packaging-probes). ### Testing diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 4d236a6e8b1a..4e560b047e28 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -272,8 +272,8 @@ a ctest entry then asserts the packed output. The packer's architecture list is does not build. A probe may use any architecture, built by the lane or not. Probes are superbuild-only and exist only under `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` -(default `OFF`). They are declared in `probes/probes.cmake`. With the option `ON`, the -probe pack targets are part of `all`, and the aggregate target `hkp_packaging_probes` +(default `OFF`). Architecture probes are automatic (below); `probes/probes.cmake` declares +only named probes. With the option `ON`, the probe pack targets are part of `all`, and the aggregate target `hkp_packaging_probes` builds them alone. Probe output is written under `/hkp-probes//`, outside both shipped descriptor trees, and is never installed. With the option `OFF` the only difference is that cache entry; the probe module is never read. @@ -294,37 +294,62 @@ is that cache entry; the probe module is never read. **What it does not prove** -- UKDs it does not pack: the other variants of a compile group in the default sample, or - the UKDs not listed in `UKDS`. +- UKDs it does not pack: the variants of a compile group the default sample leaves out, + so in particular combinations of spec values (see the sweep below), or the UKDs not + listed in `UKDS`. - Anything outside the descriptor root and the packer, for example the gfx950 ASM SDPA (prebuilt `.co` files) and C++/runtime paths, and install staging. - Device behavior: no kernel is launched, no numerics are checked. -- Integrations and architectures without a probe. +- Wildcard descriptors on architectures no lane builds (see below for why none ship). - That the CI job runs: that is the job of the junit check below. +### Which architectures are probed + +Every architecture named explicitly under the production root gets an **automatic probe** +in every lane that does not build it. At configure, `hkp_load_packaging_probes()` runs +`tools/hkp_probe_derive_root.py --list-arches` on +`HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT`: the union of the explicit `arch` +entries of every KDP and UKD (inline and standalone), read with the packer's own loader. +It subtracts the lane's own architectures (`hkp_selected_arches()`, i.e. `GPU_TARGETS`) +and declares `hkp_add_packaging_probe(ARCH )` for each one left: probe ``, +ctest entry `hkp-probe-`, packing the default sample. Configure prints the result: + +``` +hkp: automatic probe arches: [gfx950] (explicit arches under : [gfx950]; build targets: [gfx942]) +hkp: probe gfx950: derived root /hkp-probes/gfx950/root (17 UKD(s) for gfx950) +``` + +A new architecture, or a new pack under one, is therefore probed with no edit anywhere. A +lane whose `GPU_TARGETS` cover every named architecture declares no probe; that is valid, +and the manifest then lists `hkp-probe-tools` alone. + +**Wildcards are excluded.** A descriptor with no `arch` (absent or empty) names no +architecture and gets no automatic probe. That is safe: a wildcard ships for whatever the +build targets, so each lane's own production pack packs it, and its GPU test lanes run it, +on every PR. Only an architecture the lane does not build needs a probe, and only an +explicit `arch` entry can make a descriptor ship there and nowhere else. + ### How to use -Probing is author-controlled: each integration declares what to probe, one -`hkp_add_packaging_probe()` line in `probes/probes.cmake`. +`probes/probes.cmake` is for **named `UKDS` probes** only. Add a line when an integration +needs specific UKDs packed on every PR, beyond the default sample: a combination of spec +values the sweep does not guarantee, or a variant with its own known risk. ```cmake -# Default sample: one UKD per compile group of every KDP in the root shipping for gfx950. -hkp_add_packaging_probe(ARCH gfx950) # One integration, a chosen representative set. hkp_add_packaging_probe(ARCH gfx950 NAME attention_dense - UKDS attention_dense.bf16_d64_hq8_kv1_c1_bm128_bn64.gfx950 + UKDS attention_dense.fp16_d64_hq8_kv1_c1_bm128_bn64.gfx950 attention_dense.bf16_d64_hq8_kv1_c1_bm256_bn32.gfx950) ``` - **With `UKDS`**, the probe packs exactly the listed UKDs, selected by UKD `name`. Pick a - minimal representative set: one UKD per compile path you want checked on every PR. Probe - as much as you consider necessary; every listed UKD is packed, and compiled for a - compiled kind, in every CI run. A KDP that keeps none of the listed UKDs is left out of - the derived root. Give the line a `NAME`, since the default name is shared with the - default-sample probe of the same architecture. -- **Without `UKDS`**, the probe packs the default sample: one UKD per compile group of - every KDP in the root that ships for `ARCH` (see the compile-group table below). A new - pack under that architecture is then covered with no new line. + minimal representative set; every listed UKD is packed, and compiled for a compiled + kind, in every CI run. A KDP that keeps none of the listed UKDs is left out of the + derived root. Give the line a `NAME`: the default name is the bare architecture, which + that architecture's automatic probe uses, so the two would be declared twice. +- **Without `UKDS`**, a probe packs the default sample of every KDP in the root that ships + for `ARCH` (see the derived root below). The automatic probes are of this form; a line + without `UKDS` is only useful with another `ROOT`. A listed name that is not kept fails configure: no KDP shipping for `ARCH` holds an inline UKD with that name that itself ships for `ARCH` (a misspelled name, a UKD for another @@ -354,22 +379,23 @@ pass-through UKDs ships descriptors and no `kpack/` directory; the archive is th required and an archive that happens to exist is ignored. Configuration fails, never skips, when: `ARCH` is missing or malformed; `NAME` is malformed, -`tools` or declared twice; `ROOT` is not a directory; no KDP under the root ships for -`ARCH`; a `UKDS` entry is empty, repeated or not kept; a KDP the probe keeps references a +`tools` or declared twice (a named probe whose `NAME` is an automatically probed +architecture); `ROOT` is not a directory; the packer's loader (`load_flat_input`) rejects +the root or the production root; no KDP under the root ships for `ARCH`; a `UKDS` entry is empty, repeated or not kept; a KDP the probe keeps references a standalone UKD (a `kernelDescriptors` entry that is not an object; derive supports inline UKDs only); a kept UKD has a kind not registered in `tools/hkp_probe_kinds.py`; two kept UKDs of one KDP share a name; in the default sample, a UKD's `kernel_source` lacks one of its kind's compile-group fields; the probe output root would sit under a shipped -descriptor tree; no probe is declared; or pytest is not importable by `Python3_EXECUTABLE`. +descriptor tree; or pytest is not importable by `Python3_EXECUTABLE`. ### The derived root and the default sample At configure, `tools/hkp_probe_derive_root.py` copies the root into `/hkp-probes//root`. Every KDP shipping for `ARCH` has its `kernelDescriptors` reduced to the probed UKDs, among the UKDs that themselves ship for -`ARCH`: the `UKDS` list (one `--ukd` per entry), or else one UKD per **compile group**. -With `UKDS`, a KDP that keeps nothing is left out; other files are copied unchanged. The -compile group is the part of a UKD that selects its compile path: +`ARCH`: the `UKDS` list (one `--ukd` per entry), or else the **default sample**, chosen per +**compile group**. With `UKDS`, a KDP that keeps nothing is left out; other files are copied +unchanged. The compile group is the part of a UKD that selects its compile path: | Producer kind | Compile group | |---|---| @@ -378,15 +404,26 @@ compile group is the part of a UKD that selects its compile path: | `hsaco` | one per KDP (list more with `UKDS`) | | `embedded_source` | one per KDP (list more with `UKDS`; ships as authored, no archive entry) | -The kept UKD is the first of its group by sorted `name`, so a KDP whose UKDs share one -compile path keeps one UKD. - -- **Variants inside a group are not compiled** by the default sample. Breakage specific to - one spec of a shared builder is caught only if that UKD is listed in `UKDS`. +Within a group, a kind with a **sweep field** (`sweep_field` in `tools/hkp_probe_kinds.py`; +`rocke`: `spec`) keeps a **value sweep**: every `(field, value)` pair of `spec` whose field +takes more than one value within the group must appear in at least one kept UKD (values +compared as canonical JSON; a field absent from some UKDs counts absence as a value). +Greedily, the UKD covering the most uncovered pairs is kept, ties broken by sorted `name`, +until every pair is covered; the result is deterministic. A group in which no field varies, +and every group of a kind without a sweep field (`hip`, `hsaco`, `embedded_source`), keeps +one UKD, the first by sorted `name`. Kept UKDs keep their authored order. The gfx950 dense +attention KDP (840 UKDs in one group, 7 varying spec fields) keeps 17. + +- **Each value is packed; combinations are not.** The sweep guarantees, say, that some fp16 + UKD and some causal UKD are packed, not that an fp16 causal one is. Breakage specific to + a combination is caught only if a UKD with it is listed in `UKDS`. - **The key is the UKD `name`, not its `id`.** Ids are `uuid4` values regenerated on every ingestor run; names encode the specialization and are unique. -- **Arch matching follows the packer** (`arch_matches`, `_arch_subset_ok`): an empty or - absent `arch` list is a wildcard. +- **Derive reads the root with the packer's own loader** (`load_flat_input`): it sees + the descriptors the packer sees (hidden paths skipped) and refuses what the packer + refuses. Which KDPs ship is the packer's `kdp_survives`, which UKDs ship its + `arch_matches`: an empty or absent `arch` list is a wildcard. Standalone UKDs are out of + probe scope by policy. - Derive also writes `expect.json` (kept KDP, UKD name and kind; pass-through kinds also carry the authored `kernel_source`) beside the root; the assertion expects exactly that set. @@ -463,12 +500,13 @@ compiles hip and rocke fixtures. There is no local Windows reproduction. ### How to extend -No CMake guard requires a probe: an integration without one configures, builds and passes -CI, and its packaging for an architecture the lanes do not build is then unchecked. +Every architecture named explicitly under the production root is probed in each lane that +does not build it, with the default sample; nothing needs declaring. -- **New architecture:** one `hkp_add_packaging_probe(ARCH )` line. -- **New integration:** one line with `NAME ` and `UKDS` naming its - representative UKDs. +- **New architecture or new integration:** no edit. Reconfigure and check the + `hkp: automatic probe arches` and `hkp: probe ` STATUS lines. +- **Specific UKDs packed on every PR** (a combination the sweep does not guarantee): one + line with `NAME ` and `UKDS` naming them. - **Another descriptor root:** one line with `ROOT `. - **New producer kind:** the packer must support the `kernel_source.kind` first. Then one `KINDS` entry in `tools/hkp_probe_kinds.py` (compile-group fields, provenance checks by @@ -480,9 +518,9 @@ CI, and its packaging for an architecture the lanes do not build is then uncheck the derive and assert sites that branch on the output type (`expect.json` content, the archive rule). This is a design decision, not an entry. -After adding a probe line, reconfigure and run the local block above: the new -`hkp-probe-` entry and its manifest line must appear. Then run the mutation checks -below. +After adding an architecture or a probe line, reconfigure and run the local block above: +the new `hkp-probe-` entry and its manifest line must appear. Then run the +mutation checks below. ### Mutation checks when adding a probe @@ -496,13 +534,13 @@ ctest --test-dir build-probe -R '^hkp-probe-$' --output-on-failure ``` 1. **Derived descriptors reach the compiler.** - - Replace the `ARCH` of the probe with an architecture no KDP in the root ships for: + - Replace the `ARCH` of a named probe with an architecture no KDP in the root ships for: configure must fail (no KDP under the root ships for it). - Add a standalone-UKD reference (a non-object `kernelDescriptors` entry) to a KDP the probe keeps: configure must fail with `standalone UKD references unsupported by probe derive`. - - For a compiled kind, set a compile-affecting spec field of a probed UKD to a value - the compiler rejects: the build must fail in the packer. rocKE dense attention, for + - For a compiled kind, set a compile-affecting spec field of a probed UKD (one in + `expect.json`) to a value the compiler rejects: the build must fail in the packer. rocKE dense attention, for example: `block_n` in `kernel_source.spec` from `64` to `7` fails with `invalid spec ...`. - Make a probed UKD's `metadata` disagree with the compiled specialization: the build diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake index f7945008d3cf..4060ccbf3c4c 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -38,8 +38,8 @@ endfunction() # Derive the probe root at configure time (FATAL with the tool's stderr when the root # cannot be derived, e.g. a kept KDP references a standalone UKD, or a listed UKD is # kept nowhere) and re-run configure when a source file or the derive tooling -# changes. Each is passed as one --ukd; with none, derive keeps one UKD per -# compile group. The tool writes /../expect.json, the list of kept UKDs the +# changes. Each is passed as one --ukd; with none, derive selects per compile +# group (one UKD, or a value sweep for kinds with a sweep field). The tool writes /../expect.json, the list of kept UKDs the # assertion expects; receives its path. FATAL when that list is empty: no # KDP under ships for , so the pack would prune everything. Deriving # is idempotent, so a reconfigure leaves the derived root's mtimes alone and does not @@ -140,9 +140,11 @@ endfunction() # ROOT defaults to the production descriptors, HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_ # SOURCE_ROOT; a relative ROOT is resolved against the directory of the probes file. # It is copied at configure time into the build tree with every KDP that ships for -# ARCH trimmed (see hkp_probe_derive_root.py). Without UKDS, each such KDP keeps one -# UKD per compile group, so the probe packs the real descriptors but compiles each -# distinct compile path once. With UKDS, the probe packs exactly the listed UKDs (by +# ARCH trimmed (see hkp_probe_derive_root.py). Without UKDS, each compile group of +# such a KDP keeps one UKD, or for a kind with a sweep field (rocke: spec) enough UKDs +# that each value of each spec field varying within the group is packed at least once +# (combinations of values are not guaranteed). So the probe packs the real +# descriptors but compiles each compile path, and each value, once. With UKDS, the probe packs exactly the listed UKDs (by # UKD name): KDPs keeping none of them are left out of the derived root, and # configure fails when a listed name is kept by no KDP shipping for ARCH. The # assertion expects exactly the kept UKDs, each with the provenance of its producer @@ -158,7 +160,8 @@ endfunction() # ${CMAKE_BINARY_DIR}/hkp-probes//out) and ctest entry hkp-probe-. The # pack targets are part of `all` (hkp_wire_pack_target declares them so); the # aggregate hkp_packaging_probes builds every probe's pack. Only callable from a -# probes file loaded by hkp_load_packaging_probes(). +# probes file loaded by hkp_load_packaging_probes(), which also calls it for the +# automatic arches. # --------------------------------------------------------------------------- function(hkp_add_packaging_probe) cmake_parse_arguments(PARSE_ARGV 0 ARG "" "ARCH;ROOT;NAME;PACK_JOBS" "UKDS") @@ -247,13 +250,63 @@ function(hkp_add_packaging_probe) set_property(GLOBAL APPEND PROPERTY HKP_PROBE_NAMES "${ARG_NAME}") endfunction() +# --------------------------------------------------------------------------- +# _hkp_probe_automatic_arches() +# The architectures to probe automatically: every explicit `arch` entry on a KDP or +# UKD under HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT, minus the build's own +# targets (hkp_selected_arches). Descriptors with no `arch` (wildcards) name no arch: +# they ship for every build target, so the build's own production pack covers them. +# FATAL with the tool's stderr when the packer's loader rejects the root. Configure +# re-runs when a file under the root or the derive tooling changes. +# --------------------------------------------------------------------------- +function(_hkp_probe_automatic_arches out_var) + set(_root "${HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT}") + file(GLOB_RECURSE _root_inputs CONFIGURE_DEPENDS "${_root}/*") + set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS ${_root_inputs} + "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" + "${HKP_PKG_DIR}/tools/hkp_probe_kinds.py") + execute_process( + COMMAND "${Python3_EXECUTABLE}" + "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" + --list-arches --from "${_root}" + RESULT_VARIABLE _rc + OUTPUT_VARIABLE _out + ERROR_VARIABLE _err) + if(NOT _rc EQUAL 0) + message(FATAL_ERROR + "hkp probe: listing the architectures of the production root ${_root} " + "failed (exit ${_rc}).\n${_err}") + endif() + string(REGEX REPLACE "\n+$" "" _out "${_out}") + string(REPLACE "\n" ";" _named "${_out}") + + hkp_selected_arches(_targets _targets_source) + set(_auto "${_named}") + if(_targets) + list(REMOVE_ITEM _auto ${_targets}) + endif() + list(JOIN _auto ", " _auto_text) + list(JOIN _named ", " _named_text) + list(JOIN _targets ", " _targets_text) + message(STATUS + "hkp: automatic probe arches: [${_auto_text}] (explicit arches under ${_root}: " + "[${_named_text}]; build targets: [${_targets_text}])") + set(${out_var} "${_auto}" PARENT_SCOPE) +endfunction() + # --------------------------------------------------------------------------- # hkp_load_packaging_probes( [...]) -# Store the toolchain hkp_add_packaging resolved, load the declared probes from -# probes/probes.cmake, register the probe tooling tests, write the manifest of ctest -# names (${CMAKE_BINARY_DIR}/hkp-probes/manifest.txt) and define the aggregate target -# hkp_packaging_probes. Configuration fails when no probe is declared or pytest is not -# importable by Python3_EXECUTABLE. +# Store the toolchain hkp_add_packaging resolved, load the named probes from +# probes/probes.cmake, then declare one automatic probe per architecture the production +# root names that the build does not target: the explicit `arch` entries of its KDPs +# and UKDs (hkp_probe_derive_root.py --list-arches; wildcard descriptors name none) +# minus hkp_selected_arches(), each as hkp_add_packaging_probe(ARCH ). Register +# the probe tooling tests, write the manifest of ctest names +# (${CMAKE_BINARY_DIR}/hkp-probes/manifest.txt) and define the aggregate target +# hkp_packaging_probes. Zero probes is valid: every explicit arch is a build target, +# so the production pack covers it. Configuration fails when the production root +# cannot be read or pytest is not importable by Python3_EXECUTABLE. # --------------------------------------------------------------------------- function(hkp_load_packaging_probes rocm_kpack_dir hipcc rocke_comgr_lib) set_property(GLOBAL PROPERTY HKP_PROBE_ROCM_KPACK_DIR "${rocm_kpack_dir}") @@ -264,15 +317,15 @@ function(hkp_load_packaging_probes rocm_kpack_dir hipcc rocke_comgr_lib) set_property(GLOBAL PROPERTY HKP_PROBE_TOOLCHAIN_SET TRUE) include("${HKP_PKG_DIR}/probes/probes.cmake") - # Calls from anywhere but the probes file hit the guard in hkp_add_packaging_probe. + _hkp_probe_automatic_arches(_auto_arches) + foreach(_arch IN LISTS _auto_arches) + hkp_add_packaging_probe(ARCH "${_arch}") + endforeach() + # Calls from anywhere but the probes file and the automatic probes above hit the + # guard in hkp_add_packaging_probe. set_property(GLOBAL PROPERTY HKP_PROBE_TOOLCHAIN_SET FALSE) get_property(_names GLOBAL PROPERTY HKP_PROBE_NAMES) - if(NOT _names) - message(FATAL_ERROR - "hkp probe: HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES is ON but " - "${HKP_PKG_DIR}/probes/probes.cmake declared no probe.") - endif() execute_process( COMMAND "${Python3_EXECUTABLE}" -c "import pytest" @@ -310,5 +363,7 @@ function(hkp_load_packaging_probes rocm_kpack_dir hipcc rocke_comgr_lib) file(WRITE "${CMAKE_BINARY_DIR}/hkp-probes/manifest.txt" "${_manifest}\n") add_custom_target(hkp_packaging_probes COMMENT "hkp: packaging probes") - add_dependencies(hkp_packaging_probes ${_targets}) + if(_targets) + add_dependencies(hkp_packaging_probes ${_targets}) + endif() endfunction() diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake index f84c9fed75f3..690ac5e14c09 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake @@ -1,14 +1,24 @@ # Copyright © Advanced Micro Devices, Inc., or its affiliates. # SPDX-License-Identifier: MIT # -# Packaging probes. Loaded by hkp_load_packaging_probes() under +# Named packaging probes. Loaded by hkp_load_packaging_probes() under # HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES; see HkpPackagingProbes.cmake for the # arguments and descriptor-packaging/README.md, "Packaging probes", for usage. # -# A probe packs a descriptor root for ARCH, independent of the build's GPU_TARGETS; ARCH -# may be any architecture. Without UKDS it packs one UKD per compile group of every KDP -# shipping for ARCH, so a new pack under ARCH needs no new line. An integration adds its -# own line, hkp_add_packaging_probe(ARCH NAME UKDS ...), -# naming a representative set of UKDs that is packed on every PR. - -hkp_add_packaging_probe(ARCH gfx950) +# Architectures need no line here. After this file, hkp_load_packaging_probes() declares +# one automatic probe, hkp-probe-, for every explicit `arch` entry on a KDP or UKD +# under the production descriptor root that is not one of the build's GPU_TARGETS. A new +# architecture, or a new pack under one, is probed with no edit to this file. +# +# Wildcard descriptors (no `arch`, or an empty list) name no architecture and get no +# automatic probe. That is safe: a wildcard ships for whatever the build targets, so +# every lane's own production pack, and its GPU test lanes, exercise it on every PR. +# +# Without UKDS a probe packs, per compile group of every KDP shipping for its ARCH, one +# UKD, or for rocke enough UKDs that each value of each varying spec field is packed +# once; combinations of values are not guaranteed. This file is for named UKDS probes +# only: an integration that needs specific UKDs (a combination the sweep may miss) +# packed on every PR adds +# hkp_add_packaging_probe(ARCH NAME UKDS ...) +# Automatic probes are named by their bare arch, so a NAME equal to an automatically +# probed arch is declared twice and fails configure. diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py index 8408b2f84334..1d41cf97527a 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py @@ -19,10 +19,6 @@ _PKG_DIR = _PROBES_TESTS_DIR.parent.parent _HKP_DIR = _PKG_DIR.parent -# hkp_pack is imported to pin the probe tools' arch rules to the packer's. -if str(_PKG_DIR / "python") not in sys.path: - sys.path.insert(0, str(_PKG_DIR / "python")) - ARCH = "gfx950" STAMP_NAME = ".hkp-packed.stamp" HKP_PACK = _PKG_DIR / "tools" / "hkp_pack.py" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py index 2ebcd40419ef..800845fbb84a 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py @@ -5,6 +5,7 @@ import itertools import json import os +import subprocess import sys from pathlib import Path @@ -19,50 +20,94 @@ def _derive(src: Path, arch: str, out: Path, ukds=()): return json.loads((out.parent / "expect.json").read_text()) -def _rocke(name, builder="b1", arch=None, source="s.py"): +def _ukd(name, kernel_source, arch=None, ukd_id=None): ukd = { - "id": f"id-{name}", + "version": "1.0", + "id": ukd_id or f"id-{name}", "name": name, - "kernel_source": { - "kind": "rocke", - "source": source, - "builder": builder, - "spec": {"n": len(name)}, - }, + "kernel_source": kernel_source, + "metadata": {}, + "priority": 0, } if arch is not None: ukd["arch"] = arch return ukd +def _rocke(name, builder="b1", arch=None, source="s.py", spec=None, ukd_id=None): + ks = { + "kind": "rocke", + "source": source, + "builder": builder, + "spec": spec if spec is not None else {"block": 64}, + } + return _ukd(name, ks, arch, ukd_id) + + def _hip(name, source="a.cpp", build=None, arch=None): - ukd = { - "id": f"id-{name}", - "name": name, - "kernel_source": { - "kind": "hip", - "source": source, - "entry": name, - "build": build if build is not None else {}, - }, + ks = { + "kind": "hip", + "source": source, + "entry": name, + "build": build if build is not None else {}, } - if arch is not None: - ukd["arch"] = arch - return ukd + return _ukd(name, ks, arch) + + +def _hsaco(name, arch=("gfx950",)): + ks = {"kind": "hsaco", "file": f"{name}.hsaco", "symbol": name} + return _ukd(name, ks, list(arch)) + + +def _kpack(name, arch=None): + ks = { + "kind": "kpack", + "library": "kpack/x.kpack", + "toc_key": name, + "symbol": name, + "sha256": "0" * 64, + "signature": "s", + } + return _ukd(name, ks, arch) + +def _embedded(name, ukd_id=None): + ks = {"kind": "embedded_source", "source_file": "k/a.cpp", "entry_point": "Entry"} + return _ukd(name, ks, ukd_id=ukd_id) -def _other(kind, name): - return {"id": f"id-{name}", "name": name, "kernel_source": {"kind": kind}} + +# The generic descriptors every KDP of _write_root references. +_GENERICS = { + "x.umd.json": {"name": "m", "scope": "kernel", "match_symbol": "m"}, + "x.ued.json": {"name": "t:e", "heuristic": "uhd", "metadata": "kmd"}, + "x.udd.json": {"name": "d", "dispatch_symbol": "d"}, + "x.uhd.json": {"name": "h", "kind": "native", "payload": "h"}, + "x.kmd.json": {"name": "k", "fields": []}, +} def _write_root(root: Path, ukds, kdp_arch=("gfx950",), kdp_file="x.kdp.json"): root.mkdir(parents=True, exist_ok=True) - kdp = {"version": "1.0", "id": "kdp-id", "kernelDescriptors": list(ukds)} - if kdp_arch is not None: - kdp["arch"] = list(kdp_arch) + kdp = { + "version": "1.0", + "id": f"kdp-{kdp_file}", + "name": "x", + "arch": list(kdp_arch) if kdp_arch is not None else [], + "matchers": ["umd"], + "engine": "ued", + "dispatch": "udd", + "kernelDescriptors": list(ukds), + } (root / kdp_file).parent.mkdir(parents=True, exist_ok=True) (root / kdp_file).write_text(json.dumps(kdp, indent=2) + "\n") - (root / "x.kmd.json").write_text('{"id": "kmd"}\n') + for file, doc in _GENERICS.items(): + doc = {"version": "1.0", "id": file.split(".")[1], **doc} + (root / file).write_text(json.dumps(doc) + "\n") + + +def _write_standalone(root: Path, ukd_id="ukd-standalone-id", arch=None): + ukd = _rocke("standalone", builder="b9", arch=arch, ukd_id=ukd_id) + (root / "s.ukd.json").write_text(json.dumps(ukd) + "\n") def _kept(out: Path, kdp_file="x.kdp.json"): @@ -106,7 +151,7 @@ def test_rocke_group_key_includes_source(tmp_path): def test_hsaco_compiles_nothing_so_one_per_kdp(tmp_path): - ukds = [_other("hsaco", "z_hsaco"), _other("hsaco", "y_hsaco"), _rocke("r")] + ukds = [_hsaco("z_hsaco"), _hsaco("y_hsaco"), _rocke("r")] _write_root(tmp_path / "src", ukds) expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") assert {(e["kind"], e["name"]) for e in expect} == { @@ -117,8 +162,8 @@ def test_hsaco_compiles_nothing_so_one_per_kdp(tmp_path): def test_hsaco_ukd_for_other_arch_is_ignored(tmp_path): ukds = [ - dict(_other("hsaco", "a_942"), arch=["gfx942"]), - dict(_other("hsaco", "b_950"), arch=["gfx950"]), + _hsaco("a_942", arch=["gfx942"]), + _hsaco("b_950", arch=["gfx950"]), ] _write_root(tmp_path / "src", ukds, kdp_arch=("gfx942", "gfx950")) expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") @@ -126,16 +171,8 @@ def test_hsaco_ukd_for_other_arch_is_ignored(tmp_path): def test_passthrough_kind_expect_carries_authored_kernel_source(tmp_path): - emb = { - "id": "id-e", - "name": "e", - "kernel_source": { - "kind": "embedded_source", - "source_file": "k/a.cpp", - "entry_point": "Entry", - }, - } - other = dict(emb, id="id-e2", name="e2") + emb = _embedded("e") + other = _embedded("e2") _write_root(tmp_path / "src", [other, emb, _rocke("r")]) expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") by_name = {e["name"]: e for e in expect} @@ -146,7 +183,8 @@ def test_passthrough_kind_expect_carries_authored_kernel_source(tmp_path): assert set(by_name["r"]) == {"kdp", "name", "kind"} -def test_pick_is_independent_of_authored_order(tmp_path): +def test_non_varying_group_keeps_first_by_name(tmp_path): + # Every spec is equal, so the group keeps one UKD whatever the authored order. ukds = [_rocke(n) for n in ("m", "c", "x", "a2", "a1")] # Ids sort opposite to names, so a pick by id would choose differently. for i, u in enumerate(sorted(ukds, key=lambda u: u["name"])): @@ -160,6 +198,63 @@ def test_pick_is_independent_of_authored_order(tmp_path): assert _kept(out) == [want] +def _sweep_group(): + """A rocke group: dtype x causal x block_n vary, head does not.""" + ukds = [] + for dtype, causal, block_n in itertools.product( + ("bf16", "fp16"), (False, True), (64, 128, 256) + ): + name = f"k_{dtype}_c{int(causal)}_n{block_n}" + spec = {"dtype": dtype, "causal": causal, "block_n": block_n, "head": 128} + ukds.append(_rocke(name, spec=spec)) + # An authored order that is neither sorted nor reverse-sorted. + return ukds[3:] + ukds[:3] + + +def _pairs(ukd): + return {(k, json.dumps(v)) for k, v in ukd["kernel_source"]["spec"].items()} + + +def test_sweep_covers_every_varying_value(tmp_path): + ukds = _sweep_group() + _write_root(tmp_path / "src", ukds) + out = tmp_path / "w" / "root" + kept = [e["name"] for e in _derive(tmp_path / "src", "gfx950", out)] + by_name = {u["name"]: u for u in ukds} + covered = set().union(*(_pairs(by_name[n]) for n in kept)) + varying = {p for u in ukds for p in _pairs(u) if p[0] != "head"} + assert varying <= covered + # block_n has three values, so three UKDs cover the group; the greedy pick finds + # that cover instead of packing all twelve. + assert len(kept) == 3 + # Written in authored order. + assert _kept(out) == [u["name"] for u in ukds if u["name"] in kept] + + +def test_sweep_is_deterministic(tmp_path): + ukds = _sweep_group() + picks = [] + for i, order in enumerate([ukds, ukds, ukds[::-1]]): + src = tmp_path / f"src{i}" + _write_root(src, order) + picks.append( + sorted(e["name"] for e in _derive(src, "gfx950", tmp_path / f"w{i}/root")) + ) + assert picks[0] == picks[1] == picks[2] + + +def test_sweep_is_per_compile_group(tmp_path): + # A value varying across groups but not within one is not swept. + ukds = [ + _rocke("a1", "b1", spec={"n": 1}), + _rocke("a2", "b1", spec={"n": 1}), + _rocke("b1", "b2", spec={"n": 2}), + ] + _write_root(tmp_path / "src", ukds) + expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") + assert [e["name"] for e in expect] == ["a1", "b1"] + + def test_hip_build_key_ignores_dict_key_order(tmp_path): ukds = [ _hip("h1", build={"defines": {"A": 1, "B": 2}}), @@ -224,10 +319,12 @@ def test_dot_prefixed_kdp_is_copied_but_not_expected(tmp_path): # --- configure-time refusals (exit 2, nothing written) ---------------------- def _refused(tmp_path, ukds, message, kdp_arch=("gfx950",)): _write_root(tmp_path / "src", ukds, kdp_arch=kdp_arch) + _write_standalone(tmp_path / "src") r = run_derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") assert r.returncode == 2, r.stderr assert r.stderr.startswith("hkp_probe_derive:") assert message in r.stderr + assert "Traceback" not in r.stderr assert not (tmp_path / "w").exists() @@ -253,20 +350,17 @@ def test_standalone_reference_in_other_arch_kdp_is_ignored(tmp_path): _write_root( tmp_path / "src", [_rocke("a"), "ukd-standalone-id"], kdp_arch=("gfx942",) ) + _write_standalone(tmp_path / "src") assert _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") == [] -@pytest.mark.parametrize("kind", ["kpack", "mystery"]) -def test_kind_without_probe_support_exits_2(tmp_path, kind): - _refused( - tmp_path, - [_rocke("a"), _other(kind, "m")], - f"kind {kind!r} has no probe support", - ) +def test_kind_without_probe_support_exits_2(tmp_path): + # kpack is a kind the packer accepts but the probe has no entry for. + _refused(tmp_path, [_rocke("a"), _kpack("m")], "kind 'kpack' has no probe support") def test_unsupported_kind_for_other_arch_is_ignored(tmp_path): - ukds = [_rocke("a"), dict(_other("kpack", "m"), arch=["gfx942"])] + ukds = [_rocke("a"), _kpack("m", arch=["gfx942"])] _write_root(tmp_path / "src", ukds, kdp_arch=("gfx942", "gfx950")) assert [ e["name"] for e in _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") @@ -274,25 +368,31 @@ def test_unsupported_kind_for_other_arch_is_ignored(tmp_path): @pytest.mark.parametrize( - "ukd, field", + "ukd, message", [ - ({**_hip("h"), "kernel_source": {"kind": "hip", "source": "a.cpp"}}, "build"), - ({**_rocke("r"), "kernel_source": {"kind": "rocke", "builder": "b"}}, "source"), + (_ukd("m", {"kind": "mystery"}), "unsupported kind 'mystery'"), + (_ukd("h", {"kind": "hip", "source": "a.cpp", "entry": "h"}), "invalid build"), + ( + _ukd("r", {"kind": "rocke", "builder": "b", "spec": {}}), + "missing required field 'source'", + ), ], - ids=["hip-build", "rocke-source"], + ids=["unknown-kind", "hip-build", "rocke-source"], ) -def test_group_key_field_absent_exits_2(tmp_path, ukd, field): - # Absent is not null: a typo in a group_by field would otherwise merge groups. - _refused(tmp_path, [ukd], f"has no kernel_source field {field!r}") +def test_packer_rejection_exits_2(tmp_path, ukd, message): + # Derive loads the root with the packer's loader, so what the packer rejects + # fails configure, with the packer's message. + _refused(tmp_path, [_rocke("a"), ukd], message) def test_ukd_arch_outside_kdp_arch_exits_2(tmp_path): # The packer rejects this root; derive must not hide it by dropping the UKD. - _refused(tmp_path, [_rocke("a", arch=["gfx942"])], "not a subset of the KDP arch") + _refused(tmp_path, [_rocke("a", arch=["gfx942"])], "is not a subset of KDP") def test_kept_ukds_with_equal_names_exit_2(tmp_path): - _refused(tmp_path, [_rocke("same", "b1"), _rocke("same", "b2")], "share a name") + ukds = [_rocke("same", "b1"), _rocke("same", "b2", ukd_id="id-same-2")] + _refused(tmp_path, ukds, "share a name") def test_malformed_kdp_exits_2(tmp_path): @@ -301,7 +401,7 @@ def test_malformed_kdp_exits_2(tmp_path): (src / "bad.kdp.json").write_text("{ not json") r = run_derive(src, "gfx950", tmp_path / "w" / "root") assert r.returncode == 2 - assert "bad.kdp.json is unreadable" in r.stderr + assert "malformed descriptor JSON in bad.kdp.json" in r.stderr assert "Traceback" not in r.stderr @@ -343,6 +443,7 @@ def test_ukd_list_copies_kdps_not_shipping_for_arch(tmp_path): def _ukd_refused(tmp_path, ukds, listed, message): _write_root(tmp_path / "src", ukds, kdp_arch=("gfx942", "gfx950")) + _write_standalone(tmp_path / "src") r = run_derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root", ukds=listed) assert r.returncode == 2, r.stderr assert r.stderr.startswith("hkp_probe_derive:") @@ -376,6 +477,7 @@ def test_ukd_list_ignores_standalone_ref_in_kdp_keeping_nothing(tmp_path): src = tmp_path / "src" _write_root(src, [_rocke("a")], kdp_file="keep.kdp.json") _write_root(src, [_rocke("b"), "ukd-standalone-id"], kdp_file="drop.kdp.json") + _write_standalone(src) expect = _derive(src, "gfx950", tmp_path / "w" / "root", ukds=["a"]) assert [e["name"] for e in expect] == ["a"] @@ -391,23 +493,21 @@ def test_ukd_list_standalone_ref_in_kept_kdp_exits_2(tmp_path): def test_ukd_list_arch_outside_kdp_arch_exits_2(tmp_path): ukds = [_rocke("a", arch=["gfx90a", "gfx950"])] - _ukd_refused(tmp_path, ukds, ["a"], "not a subset of the KDP arch") + _ukd_refused(tmp_path, ukds, ["a"], "is not a subset of KDP") def test_ukd_list_kept_ukds_with_equal_names_exit_2(tmp_path): - ukds = [_rocke("same", "b1"), _rocke("same", "b2")] + ukds = [_rocke("same", "b1"), _rocke("same", "b2", ukd_id="id-same-2")] _ukd_refused(tmp_path, ukds, ["same"], "share a name") def test_ukd_list_unregistered_kind_of_kept_ukd_exits_2(tmp_path): - _ukd_refused( - tmp_path, [_rocke("a"), _other("mystery", "m")], ["m"], "has no probe support" - ) + _ukd_refused(tmp_path, [_rocke("a"), _kpack("m")], ["m"], "has no probe support") def test_ukd_list_ignores_unlisted_ukds_of_kept_kdp(tmp_path): - # Neither the kind nor the group key of an unlisted UKD matters. - ukds = [_rocke("a"), _other("mystery", "m"), _other("hip", "h_no_build")] + # The kind of an unlisted UKD does not matter. + ukds = [_rocke("a"), _kpack("m")] _write_root(tmp_path / "src", ukds) expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root", ukds=["a"]) assert [e["name"] for e in expect] == ["a"] @@ -453,45 +553,56 @@ def test_rederive_follows_source_changes(tmp_path): (out / "stale_dir").mkdir() (out / "stale_dir" / "f.json").write_text("{}") _write_root(src, [_rocke("b"), _rocke("0first")]) - (src / "x.kmd.json").write_text('{"id": "kmd2"}\n') + kmd = '{"version": "1.0", "id": "kmd", "name": "k2", "fields": []}\n' + (src / "x.kmd.json").write_text(kmd) expect = _derive(src, "gfx950", out) assert [e["name"] for e in expect] == ["0first"] assert _kept(out) == ["0first"] - assert (out / "x.kmd.json").read_text() == '{"id": "kmd2"}\n' - assert sorted(p.name for p in out.rglob("*")) == ["x.kdp.json", "x.kmd.json"] - - -# --- arch rules pinned to the packer's -------------------------------------- -_ARCH_LISTS = [None, [], ["gfx942"], ["gfx950"], ["gfx942", "gfx950"], ["gfx90a"]] + assert (out / "x.kmd.json").read_text() == kmd + assert sorted(p.name for p in out.rglob("*")) == sorted(["x.kdp.json", *_GENERICS]) -def _derive_module(): - sys.path.insert(0, str(PROBE_DERIVE.parent)) - try: - import hkp_probe_derive_root - finally: - sys.path.pop(0) - return hkp_probe_derive_root +# --- --list-arches ---------------------------------------------------------- +def _list_arches(src: Path): + r = subprocess.run( + [sys.executable, str(PROBE_DERIVE), "--list-arches", "--from", str(src)], + capture_output=True, + text=True, + check=False, + ) + assert r.returncode == 0, r.stderr + return r.stdout.splitlines() -@pytest.mark.parametrize("doc_arch", _ARCH_LISTS) -@pytest.mark.parametrize("arch", ["gfx942", "gfx950"]) -def test_arch_matches_agrees_with_packer(doc_arch, arch): - from hkp_pack import descriptors +def test_list_arches_unions_explicit_kdp_and_ukd_arches(tmp_path): + src = tmp_path / "src" + _write_root(src, [_rocke("a", arch=["gfx942"])], kdp_arch=("gfx950", "gfx942")) + # A wildcard KDP contributes nothing itself; its explicit UKD arches do, inline + # and standalone. + wild = [_rocke("b", arch=["gfx1201"]), _rocke("c"), "ukd-standalone-id"] + _write_root(src, wild, kdp_arch=None, kdp_file="wild.kdp.json") + _write_standalone(src, arch=["gfx90a"]) + assert _list_arches(src) == ["gfx1201", "gfx90a", "gfx942", "gfx950"] - doc = {} if doc_arch is None else {"arch": doc_arch} - assert _derive_module().arch_matches(doc, arch) == descriptors.arch_matches( - doc, arch - ) +def test_list_arches_excludes_wildcards(tmp_path): + src = tmp_path / "src" + _write_root(src, [_rocke("a"), _rocke("b", arch=[])], kdp_arch=None) + assert _list_arches(src) == [] -@pytest.mark.parametrize("kdp_arch", _ARCH_LISTS) -@pytest.mark.parametrize("ukd_arch", _ARCH_LISTS) -def test_arch_subset_agrees_with_packer(ukd_arch, kdp_arch): - from hkp_pack import descriptors - got = _derive_module().arch_subset_ok(ukd_arch or [], kdp_arch or []) - assert got == descriptors._arch_subset_ok(ukd_arch or [], kdp_arch or []) +def test_list_arches_packer_rejection_exits_2(tmp_path): + src = tmp_path / "src" + _write_root(src, [_rocke("a", arch=["gfx942"])]) + r = subprocess.run( + [sys.executable, str(PROBE_DERIVE), "--list-arches", "--from", str(src)], + capture_output=True, + text=True, + check=False, + ) + assert r.returncode == 2 + assert r.stderr.startswith("hkp_probe_derive:") + assert "is not a subset of KDP" in r.stderr # --- the kinds registry ----------------------------------------------------- diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py index 9a4e82276088..622161bdfc35 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py @@ -5,16 +5,31 @@ `--arch` has its inline `kernelDescriptors` reduced to the UKDs the probe packs, among the UKDs that themselves ship for `--arch`. -Default mode (no `--ukd`): one UKD per compile group, where the group key is the -kind plus the kernel_source fields hkp_probe_kinds.KINDS names for it: +The root is read with the packer's own loader (`hkp_pack.descriptors.load_flat_input`), +so derive sees exactly the descriptors the packer sees and rejects what the packer +rejects. Which KDPs ship for `--arch` is the packer's `kdp_survives`, and which of their +UKDs ship is the packer's `arch_matches`: an empty or absent `arch` list is a wildcard. + +Default mode (no `--ukd`): UKDs are partitioned into compile groups, where the group +key is the kind plus the kernel_source fields hkp_probe_kinds.KINDS names for it: rocke (kind, source, builder), hip (kind, source, build), hsaco and - embedded_source (kind: compile nothing, so one UKD per KDP) + embedded_source (kind: compile nothing, so one group per KDP) + +Every field of the key must be present in kernel_source. Within a group: + + - a kind with no `sweep_field` keeps one UKD, the first by sorted UKD `name`; + - a kind with a `sweep_field` (rocke: `spec`) keeps a value sweep. The universe is + every (field, value) pair of the sweep block whose field takes more than one value + within the group (values compared as canonical JSON; a field absent from some UKDs + counts absence as one of its values). Greedily, the UKD covering the most + uncovered pairs is kept, ties broken by sorted UKD `name`, until every pair is + covered. A group in which no field varies keeps one UKD, the first by sorted name. -Every field of the key must be present in kernel_source. The pick within a group is -the first by sorted UKD `name`. Packing the derived root therefore exercises every -compile path once instead of every variant. Authors who want more than one UKD of a -group list them with `--ukd`. +The sweep guarantees that each value of each varying field is packed at least once; it +does not guarantee any combination of values (e.g. fp16 together with causal). Kept +UKDs are written in authored order. Authors who want specific UKDs packed list them +with `--ukd`. `--ukd ` mode (repeatable): each KDP keeps exactly the UKDs whose `name` is listed. A KDP shipping for `--arch` that keeps nothing is omitted from the derived @@ -33,15 +48,20 @@ derived set are removed, so deriving twice leaves every mtime of the derived root alone and the pack stamp stays fresh across a reconfigure. -Arch rules are the packer's (`hkp_pack.descriptors.arch_matches` and -`_arch_subset_ok`): an empty or absent `arch` list is a wildcard. +`--list-arches --from ` prints, one per line and sorted, the union of the +explicit `arch` entries of every KDP and UKD (inline and standalone) under the root. +Descriptors with no `arch` (wildcards) contribute nothing. + +Standalone UKDs (a `kernelDescriptors` entry that is a bare id) are out of probe +scope by policy: a KDP that would be trimmed and references one is refused. Exit code 0 on success. Exit code 2, with a `hkp_probe_derive: ...` message on -stderr, when the root cannot be derived: a KDP that keeps UKDs (or, in default mode, -any KDP shipping for `--arch`) references a standalone UKD (`kernelDescriptors` entry -that is not an object), a kept UKD has a kind not in hkp_probe_kinds.KINDS, a -default-mode candidate lacks a group key field, a UKD is malformed, the source is -unreadable, or a `--ukd` name is listed twice or kept by no KDP. +stderr, when the root cannot be derived: the packer's loader rejects the root +(malformed or unreadable descriptor, dangling reference, UKD arch outside its KDP's), +a KDP that keeps UKDs (or, in default mode, any KDP shipping for `--arch`) references +a standalone UKD, a kept UKD has a kind not in hkp_probe_kinds.KINDS, a default-mode +candidate lacks a group key field, kept UKDs share a name, or a `--ukd` name is listed +twice or kept by no KDP. """ from __future__ import annotations @@ -53,28 +73,39 @@ import sys from pathlib import Path -_PREFIX = "hkp_probe_derive:" +# Put the package dir ahead of everything, including this script's own dir: +# tools/ also holds a module literally named hkp_pack (hkp_pack.py), which would +# otherwise shadow the hkp_pack package when tools/ is sys.path[0]. +_PKG_ROOT = str(Path(__file__).resolve().parent.parent / "python") +while _PKG_ROOT in sys.path: + sys.path.remove(_PKG_ROOT) +sys.path.insert(0, _PKG_ROOT) + +from hkp_pack.descriptors import ( + arch_matches, + kdp_survives, + load_flat_input, +) # noqa: E402 +from hkp_pack.errors import HkpPackError # noqa: E402 +from hkp_probe_kinds import KINDS # noqa: E402 -from hkp_probe_kinds import KINDS +_PREFIX = "hkp_probe_derive:" class DeriveError(Exception): pass -def arch_matches(doc, arch): - """Port of hkp_pack.descriptors.arch_matches: empty or absent list = wildcard.""" - archs = doc.get("arch") - if not archs: - return True - return arch in archs +def _discard(*_args, **_kwargs): + pass -def arch_subset_ok(ukd_arch, kdp_arch): - """Port of hkp_pack.descriptors._arch_subset_ok: empty on either side = wildcard.""" - if not ukd_arch or not kdp_arch: - return True - return set(ukd_arch) <= set(kdp_arch) +def _load(src): + """The packer's view of `src`; DeriveError when the packer rejects it.""" + try: + return load_flat_input(src, log=_discard) + except HkpPackError as exc: + raise DeriveError(str(exc)) from exc def _check_kind(ukd, where): @@ -121,43 +152,68 @@ def _check_unique_names(rel, kept): ) -def _check_arch_subset(ukd, kdp, where): - if not arch_subset_ok(ukd.get("arch") or [], kdp.get("arch") or []): +# A field missing from a UKD's sweep block; distinct from every canonical JSON value. +_ABSENT = object() + + +def _sweep_values(ukd, field, where): + """Map each field of `ukd`'s kernel_source[field] object to its canonical JSON.""" + block = ukd["kernel_source"].get(field) + if not isinstance(block, dict): raise DeriveError( - f"{where} arch {ukd.get('arch')} is not a subset of the KDP " - f"arch {kdp.get('arch')}" + f"{where} kernel_source field {field!r} is not an object, so its values " + "cannot be swept" ) + return {k: json.dumps(v, sort_keys=True) for k, v in block.items()} + + +def _select(members, field, rel): + """Return the UKDs a compile group keeps; `members` is sorted by name.""" + if field is None: + return [members[0]] + values = [ + _sweep_values(u, field, f"{rel}: UKD '{u.get('id', '?')}'") for u in members + ] + keys = set().union(*values) + pairs = [{(k, v.get(k, _ABSENT)) for k in keys} for v in values] + varying = {k for k in keys if len({p.get(k, _ABSENT) for p in values}) > 1} + uncovered = {(k, x) for p in pairs for (k, x) in p if k in varying} + if not uncovered: + return [members[0]] + kept = [] + while uncovered: + # max() keeps the first of equal gains, i.e. the first by sorted name. + best = max(range(len(members)), key=lambda i: len(pairs[i] & uncovered)) + kept.append(members[best]) + uncovered -= pairs[best] + return kept def _derive_kdp(kdp, rel, arch): """Return the kept UKDs of a KDP shipping for `arch`, in authored order.""" - entries = kdp.get("kernelDescriptors") - if not isinstance(entries, list): - raise DeriveError(f"{rel} 'kernelDescriptors' is not a list") - candidates = [] + entries = kdp["kernelDescriptors"] + groups = {} for entry in entries: if not isinstance(entry, dict): raise _standalone_error(rel, entry) where = f"{rel}: UKD '{entry.get('id', '?')}'" if not isinstance(entry.get("name"), str): raise DeriveError(f"{where} has no string 'name'") - _check_arch_subset(entry, kdp, where) if arch_matches(entry, arch): - candidates.append((_group_key(entry, where), entry)) + groups.setdefault(_group_key(entry, where), []).append(entry) - picks = {} - for key, entry in sorted(candidates, key=lambda c: c[1]["name"]): - picks.setdefault(key, entry) - kept = [e for e in entries if any(e is p for p in picks.values())] + picked = [] + for key, members in groups.items(): + members.sort(key=lambda e: e["name"]) + picked += _select(members, KINDS[key[0]].sweep_field, rel) + kept = [e for e in entries if any(e is p for p in picked)] _check_unique_names(rel, kept) return kept def _derive_kdp_listed(kdp, rel, arch, wanted): """Return the UKDs of a KDP shipping for `arch` whose name is in `wanted`.""" - entries = kdp.get("kernelDescriptors") - if not isinstance(entries, list): - raise DeriveError(f"{rel} 'kernelDescriptors' is not a list") + entries = kdp["kernelDescriptors"] kept = [ e for e in entries @@ -169,10 +225,7 @@ def _derive_kdp_listed(kdp, rel, arch, wanted): if not isinstance(entry, dict): raise _standalone_error(rel, entry) for entry in kept: - where = f"{rel}: UKD '{entry.get('id', '?')}'" - # The packer rejects this root; a listed UKD must not be dropped silently. - _check_arch_subset(entry, kdp, where) - _check_kind(entry, where) + _check_kind(entry, f"{rel}: UKD '{entry.get('id', '?')}'") _check_unique_names(rel, kept) return kept @@ -180,7 +233,7 @@ def _derive_kdp_listed(kdp, rel, arch, wanted): def _plan(src, arch, ukds=None): """Return (files, expect): files maps relative posix path -> source Path or bytes. - `ukds` is the list of UKD names to keep, or None for one UKD per compile group. + `ukds` is the list of UKD names to keep, or None for the default selection. """ if ukds is not None: dupes = sorted({n for n in ukds if ukds.count(n) > 1}) @@ -188,38 +241,27 @@ def _plan(src, arch, ukds=None): raise DeriveError(f"--ukd lists {dupes} more than once") wanted = set(ukds) + flat = _load(src) files = {} for p in sorted(src.rglob("*")): if p.is_file(): files[p.relative_to(src).as_posix()] = p expect = [] - for rel in sorted(files): - if not rel.endswith(".kdp.json"): - continue - # The packer skips dot-prefixed paths; so does derive. - if any(part.startswith(".") for part in rel.split("/")): - continue - try: - kdp = json.loads(files[rel].read_text(encoding="utf-8")) - except (OSError, ValueError) as exc: - raise DeriveError(f"{rel} is unreadable: {type(exc).__name__}: {exc}") - if not isinstance(kdp, dict): - raise DeriveError(f"{rel} is not a JSON object") - if not arch_matches(kdp, arch): + kdps = {d.path.relative_to(src).as_posix(): d.doc for d in flat.kdps()} + for rel in sorted(kdps): + kdp = kdps[rel] + if not kdp_survives(kdp, flat, arch): continue if ukds is None: kept = _derive_kdp(kdp, rel, arch) else: kept = _derive_kdp_listed(kdp, rel, arch, wanted) if not kept: - if ukds is not None: - # Only listed UKDs are packed, so this KDP must not ship in full. - del files[rel] - # Default mode: every UKD filters out for this arch, so the packer drops - # the whole KDP. + # Only listed UKDs are packed, so this KDP must not ship in full. + del files[rel] continue - kdp["kernelDescriptors"] = kept + kdp = dict(kdp, kernelDescriptors=kept) files[rel] = (json.dumps(kdp, indent=2) + "\n").encode("utf-8") for u in kept: entry = {"kdp": rel, "name": u["name"], "kind": u["kernel_source"]["kind"]} @@ -239,6 +281,20 @@ def _plan(src, arch, ukds=None): return files, expect +def list_arches(src: Path) -> list[str]: + """Sorted union of the explicit `arch` entries of every KDP and UKD under `src`.""" + flat = _load(src) + arches = set() + for kdp in flat.kdps(): + arches.update(kdp.doc.get("arch") or []) + for entry in kdp.doc["kernelDescriptors"]: + if isinstance(entry, dict): + arches.update(entry.get("arch") or []) + for ukd in flat.ukds(): + arches.update(ukd.doc.get("arch") or []) + return sorted(arches) + + def _same(dest, content): if not dest.is_file() or dest.is_symlink(): return False @@ -282,7 +338,7 @@ def derive_root( ) -> str | None: """Write the derived root to `out`; return an error message, or None on success. - `ukds` lists the UKD names to keep (`--ukd`); None keeps one per compile group. + `ukds` lists the UKD names to keep (`--ukd`); None selects per compile group. """ if not src.is_dir(): return f"{_PREFIX} {src} is not a directory" @@ -306,18 +362,39 @@ def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--from", dest="src", type=Path, required=True) parser.add_argument( - "--arch", required=True, help="architecture to probe, e.g. gfx950" + "--list-arches", + action="store_true", + help="print the explicit arches of the root's KDPs and UKDs, one per line", ) - parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--arch", help="architecture to probe, e.g. gfx950") + parser.add_argument("--out", type=Path) parser.add_argument( "--ukd", dest="ukds", action="append", metavar="NAME", - help="keep the UKD with this name (repeatable); default: one per compile group", + help="keep the UKD with this name (repeatable); default: per compile group, " + "one UKD or a value sweep (see the module docstring)", ) args = parser.parse_args(argv) + if args.list_arches: + if args.arch or args.out or args.ukds: + parser.error("--list-arches takes only --from") + if not args.src.is_dir(): + print(f"{_PREFIX} {args.src} is not a directory", file=sys.stderr) + return 2 + try: + arches = list_arches(args.src) + except DeriveError as exc: + print(f"{_PREFIX} {exc}", file=sys.stderr) + return 2 + for a in arches: + print(a) + return 0 + + if not args.arch or args.out is None: + parser.error("--arch and --out are required unless --list-arches is given") error = derive_root(args.src, args.arch, args.out, args.ukds) if error is not None: print(error, file=sys.stderr) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py index 51aa85b52f03..f0f3f40a3386 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_kinds.py @@ -27,20 +27,30 @@ @dataclass(frozen=True) class Kind: # kernel_source fields that define a compile group within a KDP: UKDs of one kind - # agreeing on all of them share a compile path, so by default one stands for the - # group. Every field must be present in kernel_source. Empty means the kind compiles - # nothing and one UKD per KDP stands for it; authors who want more UKDs of a group - # packed list them by name (hkp_add_packaging_probe UKDS). + # agreeing on all of them share a compile path. Every field must be present in + # kernel_source. Empty means the kind compiles nothing and the whole KDP is one + # group. Authors who want specific UKDs of a group packed list them by name + # (hkp_add_packaging_probe UKDS). group_by: tuple[str, ...] # Names of provenance checks (hkp_probe_assert._PROVENANCE_CHECKS) a shipped UKD of # this kind must satisfy, beyond provenance.origin_kind == the kind itself. provenance: tuple[str, ...] # Packed output type: a key of hkp_probe_assert._OUTPUT_CHECKS. output: str = "kpack" + # kernel_source field holding an object whose values select code within one + # compile group (rocke: `spec`, which the builder bakes into the kernel). With a + # sweep field, a group keeps UKDs until each value of each field that varies within + # the group is packed at least once (greedy, ties by sorted name); combinations of + # values are not guaranteed. None keeps one UKD per group, the first by sorted name. + sweep_field: str | None = None KINDS = { - "rocke": Kind(group_by=("source", "builder"), provenance=("wheel", "comgr")), + "rocke": Kind( + group_by=("source", "builder"), + provenance=("wheel", "comgr"), + sweep_field="spec", + ), "hip": Kind(group_by=("source", "build"), provenance=()), "hsaco": Kind(group_by=(), provenance=()), "embedded_source": Kind(group_by=(), provenance=(), output="passthrough"), diff --git a/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/descriptors/README.md b/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/descriptors/README.md index 08d8404efb3a..54799ab2b7da 100644 --- a/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/descriptors/README.md +++ b/dnn-providers/hip-kernel-provider/src/engines/kernel_ingestor_engine/descriptors/README.md @@ -45,10 +45,13 @@ architecture is a hard failure for a root the build NAMED, and dormancy for this reached as the built-in default. Packaging probes read this root too (superbuild-only, -`HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES`, `OFF` by default). A bundle's integration -declares its probe with `hkp_add_packaging_probe(ARCH NAME UKDS -...)`, naming UKDs of this root by `name`; a probe without `UKDS` packs one UKD -per compile group of every KDP shipping for its architecture. A KDP a probe keeps must +`HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES`, `OFF` by default). Every architecture an +explicit `arch` of a KDP or UKD here names is probed automatically in each build that does +not target it, with the default sample (one UKD per compile group, or for rocKE a sweep +packing each value of each varying `spec` field once); wildcard descriptors get no probe. +An integration that needs specific UKDs packed adds a named probe, +`hkp_add_packaging_probe(ARCH NAME UKDS ...)`, naming UKDs of +this root by `name`. A KDP a probe keeps must keep its `kernelDescriptors` inline: a standalone-UKD reference fails probe configure. See "Packaging probes" in `descriptor-packaging/README.md`. diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md index 33c01b466c9c..b6a070f0d2d3 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md @@ -681,12 +681,18 @@ accounting. ambiguous, erroneous or unexplained in-scope outcomes. Changed installed artifacts invalidate old evidence and return to stages 3–5. -**Final step: packaging probe.** Add -`hkp_add_packaging_probe(ARCH NAME UKDS )` -to `dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake`, configure -with `-DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON`, build -(`cmake --build --target hkp_packaging_probes`), and run -`ctest --test-dir -R '^hkp-probe-' --no-tests=error`; `hkp-probe-` must +**Final step: packaging probe.** The architecture probe is automatic and needs no edit: +every explicit `arch` of the bundle's KDPs/UKDs that the build does not target gets probe +``, packing the default sample (for rocKE, each value of each varying `spec` field +once; combinations are not guaranteed). Optionally, when specific UKDs must be packed on +every PR, add a named probe, +`hkp_add_packaging_probe(ARCH NAME UKDS )`, +to `dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake`. Configure +with `-DHIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON` and a `GPU_TARGETS` that excludes the +bundle's architecture, check the configure STATUS lines `hkp: automatic probe arches: +[...]` (the architecture is listed) and `hkp: probe : ... ( UKD(s) for )`, +build (`cmake --build --target hkp_packaging_probes`), and run +`ctest --test-dir -R '^hkp-probe-' --no-tests=error`; every `hkp-probe-*` entry must pass. See "Packaging probes" in the descriptor-packaging README. ## 8. Handoff diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md index 5bfafeec1b41..86262dfabc38 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md @@ -69,9 +69,9 @@ Completion requires: - Exact-engine quick/standard numerics and required negative cases, with selected, served, skipped and failed counts. Zero selected, all-skipped or another engine's work is not correctness evidence. An extension must dispatch its new candidate. -- A packaging probe: `hkp_add_packaging_probe(ARCH NAME UKDS - )` in `descriptor-packaging/probes/probes.cmake`, with its - ctest entry `hkp-probe-` run and passing (RUNBOOK's final step). +- Packaging probes run and passing (RUNBOOK's final step): the automatic + `hkp-probe-` for the bundle's architecture (no edit), plus any named `UKDS` probe + the integration adds in `descriptor-packaging/probes/probes.cmake`. - Rebuilt/reinstalled artifacts after tuning or regeneration, then fresh final gates. Every final corpus input has an attributable runtime outcome and the complete join required by [workloads.md](workloads.md); missing, ambiguous or error outcomes block diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md index 73ebb78ee757..55208e0c33cf 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md @@ -100,9 +100,10 @@ through that suite's ordinary host run. [native-pack.md](native-pack.md) owns eligibility. Device proof must explicitly select and numerically verify the new candidate; passing the unchanged default is not extension acceptance. -When the addition adds a compile path or an architecture that its integration's packaging -probe does not pack, add a representative UKD name to that probe's `UKDS` (or a probe line -for the new architecture) and run `hkp-probe-`, as in RUNBOOK's final step. +A new architecture is probed automatically, and a new compile path or `spec` value is +packed by the automatic probe's default sample. When the addition needs a specific UKD +(a value combination the sweep does not guarantee) packed on every PR, add its name to the +integration's named probe `UKDS`. Run the `hkp-probe-*` entries as in RUNBOOK's final step. The handoff identifies retained IDs/references, changed/new files, baseline/final installations, whole-engine results and the addition's actual dispatch. RUNBOOK stage 5 diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild-test/SKILL.md b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild-test/SKILL.md index 07002ec9d20d..7aa9d49ddc59 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild-test/SKILL.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild-test/SKILL.md @@ -120,8 +120,10 @@ proving a named engine; a generic `command:` line is not that proof. ## Packaging probes When the build was configured with `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON` -(superbuild-only, OFF by default), the probe ctest entries are `hkp-probe-`, one per -`hkp_add_packaging_probe()` line, plus `hkp-probe-tools`. A full ctest run includes them; +(superbuild-only, OFF by default), the probe ctest entries are `hkp-probe-`, one per +architecture explicitly named under the production descriptor root that `GPU_TARGETS` +does not include (automatic; none when the build targets them all), `hkp-probe-`, +one per named `hkp_add_packaging_probe()` line in `probes.cmake`, and `hkp-probe-tools`. A full ctest run includes them; to run them alone, select with the anchored `-R '^hkp-probe-'` and `--no-tests=error`. `/hkp-probes/manifest.txt` lists the entries that must run. Check a ctest junit (`--output-junit`) against it with `.github/scripts/check_probe_junit.py --junit diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md index 7221f6bd9cdb..743d6953b6e2 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md @@ -79,7 +79,7 @@ on Windows the wheel venv's `_rocm_sdk_devel/bin`. | `HIPKERNELPROVIDER_PRODUCTION_SOURCE_ROOT` | the in-tree `.../kernel_ingestor_engine/descriptors` | `CACHE PATH` naming the authored tree production packaging packages from. Packaging requires at least one non-hidden `*.kdp.json`; with none it is dormant. The default root is also dormant when no KDP declares any selected packaging architecture. Dormancy removes any stale product tree and is not an error. Set but not a directory is fatal. | | `HIPKERNELPROVIDER_KPACK_PYTHON_DIR` | unset | Directory **containing** `rocm_kpack/`; this locates a package, not a compiler interpreter. | | `Python3_EXECUTABLE` | system | Explicit environment for packaging dependencies such as `msgpack` and `zstandard`; production compilation retains its selected hermetic wheel interpreter. | - | `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` | OFF | Packaging probes declared in `descriptor-packaging/probes/probes.cmake` (`hkp_add_packaging_probe(ARCH [NAME ] [UKDS ...])`); superbuild-only, ON in the superbuild CI lanes. Each probe packs its descriptors for `ARCH` (any architecture) independent of `GPU_TARGETS`. When ON, the probe pack targets are part of `all`; `cmake --build --target hkp_packaging_probes` builds them alone. The ctest entries are `hkp-probe-` plus `hkp-probe-tools`. | + | `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` | OFF | Packaging probes: one automatic probe per architecture explicitly named under the production descriptor root and absent from `GPU_TARGETS`, plus named probes in `descriptor-packaging/probes/probes.cmake` (`hkp_add_packaging_probe(ARCH NAME UKDS ...)`); superbuild-only, ON in the superbuild CI lanes. Each probe packs its descriptors for `ARCH` (any architecture) independent of `GPU_TARGETS`. When ON, the probe pack targets are part of `all`; `cmake --build --target hkp_packaging_probes` builds them alone. The ctest entries are `hkp-probe-` plus `hkp-probe-tools`. | These flags only do anything on a preset that actually builds hip-kernel-provider. The default `hipdnn-providers` preset does **not** include it; the presets that do are From 17799893437b9c0e922beb78f0dd56caf1bd88e6 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 11:52:59 -0400 Subject: [PATCH 39/52] generic targets: restore coverage for the no-member generic pass, mixed-list rules, wildcard fill, probe and covers rows --- .../python/hkp_pack/generic_targets.py | 2 ++ .../tests/test_hkp_arch_probe.py | 1 + .../tests/test_hkp_generic_targets.py | 10 ++++++++- .../tests/test_hkp_pack_generic.py | 20 ++++++++++++++++- .../tests/ingestor/TestDescriptorLoader.cpp | 3 +++ .../IngestorGenerator/tests/test_generator.py | 22 ++++++++++++------- .../tests/test_generic_targets.py | 2 ++ 7 files changed, 50 insertions(+), 10 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py index 720e997a3df9..16fc4cf25f45 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py @@ -34,6 +34,8 @@ def load(cls, path): path = Path(path) try: doc = json.loads(path.read_text(encoding="utf-8")) + if doc["schemaVersion"] != 1: + raise ValueError(f"unsupported schemaVersion {doc['schemaVersion']!r}") generics = {n: tuple(m) for n, m in doc["generics"].items()} except (OSError, ValueError, KeyError, TypeError, AttributeError) as exc: raise HkpPackError(f"{path}: unusable generic target table: {exc!r}") diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py index c6f879dadbd8..af2e495a9db4 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_arch_probe.py @@ -46,6 +46,7 @@ def test_probe_false_when_no_entry_reaches_any_selected_target(tmp_path): def test_probe_true_for_a_generic_absent_from_the_table(tmp_path): assert _probe(tmp_path, ["gfx99-generic"], "gfx942") == "TRUE" + assert _probe(tmp_path, ["gfx99-generic", "gfx950"], "gfx942") == "TRUE" @pytest.mark.parametrize( diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py index ef35967c2766..79d3032b1dd8 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py @@ -29,7 +29,13 @@ def table(): @pytest.mark.parametrize( "doc", - [{"schemaVersion": 1}, {"generics": []}, {"generics": {"gfx11-generic": 5}}, []], + [ + {"schemaVersion": 1}, + {"schemaVersion": 2, "generics": {"gfx11-generic": ["gfx1100"]}}, + {"generics": []}, + {"generics": {"gfx11-generic": 5}}, + [], + ], ) def test_rejects_a_malformed_table(tmp_path, doc): path = tmp_path / "table.json" @@ -70,6 +76,8 @@ def test_list_tier(table, arch, device, expect): (["gfx11-generic"], ["gfx11-generic"], True), (["gfx11-generic"], ["gfx12-generic"], False), (["gfx9-4-generic"], ["gfx1151"], False), + (["gfx942", "gfx11-generic"], ["gfx942", "gfx1100"], True), + (["gfx942", "gfx11-generic"], ["gfx950", "gfx1100"], False), ], ) def test_covers(table, outer, inner, expect): diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py index 749e3aa224e3..e618b7617bb7 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py @@ -179,8 +179,18 @@ def test_generic_entries_land_in_every_selected_member_folder_only( @pytest.mark.quick def test_generic_entry_is_not_materialized_when_no_member_is_selected( - tmp_path, empty_arch_fixture, rocm_kpack_dir + tmp_path, empty_arch_fixture, rocm_kpack_dir, monkeypatch ): + seen = [] + for step in ("compile_intermediate", "pack_arch"): + real = getattr(pipeline, step) + + def spy(*args, _real=real, _step=step, **kwargs): + seen.append(args[2] if _step == "compile_intermediate" else args[1].arch) + return _real(*args, **kwargs) + + monkeypatch.setattr(pipeline, step, spy) + root = _root( tmp_path, empty_arch_fixture, @@ -194,6 +204,8 @@ def test_generic_entry_is_not_materialized_when_no_member_is_selected( out = tmp_path / "out" assert sorted(p.name for p in out.iterdir()) == ["gfx942"] assert not (out / "gfx942" / "g.kdp.json").exists() + assert seen + assert GENERIC not in seen @pytest.mark.quick @@ -357,6 +369,12 @@ def _hsaco_ukd(uid, arch): "inline_narrower_than_generic_kdp": ([GENERIC], "inline", [MEMBER_B], None), "inline_unknown_generic": ([GENERIC], "inline", ["gfx99-generic"], None), "inline_generic_beside_member": ([GENERIC], "inline", [GENERIC, MEMBER_B], None), + "mixed_list_ukd_missing_the_generic": ( + ["gfx942", GENERIC], + "inline", + ["gfx942"], + None, + ), "standalone_without_arch_under_concrete_kdp": ( ["gfx942"], "standalone", diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp index 474f181a18b1..869ecfae4db8 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp @@ -2039,6 +2039,9 @@ TEST(TestDescriptorLoader, DropsAPackListingAGenericAndOneOfItsMembers) EXPECT_EQ( engineNames(candidateWithKernelArch(arch({"gfx11-generic", "gfx1151"}), arch({}), false)), INTACT_ONLY); + EXPECT_EQ( + engineNames(candidateWithKernelArch(arch({"gfx1151", "gfx11-generic"}), arch({}), false)), + INTACT_ONLY); EXPECT_EQ(engineNames(candidateWithKernelArch( arch({"gfx11-generic", "gfx12-generic", "gfx942"}), arch({}), false)), BOTH); diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py index 45d4206e0eb8..f47dfb5cced0 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py @@ -1032,7 +1032,7 @@ def test_two_distinct_arches_each_count_the_descriptor_once(self, tmp_path): assert inventory["total_descriptor_count"] == 1 def test_inventory_gives_every_member_the_generic_rows(self): - """Each member's row holds the generic's descriptors and its own.""" + """Each member's row holds the generic's descriptors, its own, and the wildcard's.""" generic = make_pack( name="generic", arch=["gfx11-generic"], @@ -1048,18 +1048,24 @@ def test_inventory_gives_every_member_the_generic_rows(self): arch=["gfx942"], kernels=[make_kernel(name="other.k", metadata={"block_size": 256})], ) - config = make_minimal_config(packs=[generic, explicit, other]) + everywhere = make_pack( + name="everywhere", + arch=[], + kernels=[make_kernel(name="any.k", metadata={"block_size": 512})], + ) + config = make_minimal_config(packs=[generic, explicit, other, everywhere]) documents = build_kdp_documents(config, mint_ids(config)) arches = emitted_inventory(config, documents)["arches"] names = {a: set(row["descriptor_names"]) for a, row in arches.items()} - assert names["gfx1100"] == {"generic.k"} - assert names["gfx1153"] == {"generic.k"} - assert names["gfx1151"] == {"generic.k", "explicit.k"} - assert names["gfx942"] == {"other.k"} - assert names["gfx11-generic"] == {"generic.k"} + assert names["gfx1100"] == {"generic.k", "any.k"} + assert names["gfx1153"] == {"generic.k", "any.k"} + assert names["gfx1151"] == {"generic.k", "explicit.k", "any.k"} + assert names["gfx942"] == {"other.k", "any.k"} + assert names["gfx11-generic"] == {"generic.k", "any.k"} assert "gfx1154" not in names - assert set(arches["gfx1100"]["pack_names"]) == {config.kdp_stem(generic)} + stems = {config.kdp_stem(p) for p in (generic, everywhere)} + assert set(arches["gfx1100"]["pack_names"]) == stems def test_the_packaged_dialect_reports_the_kind_that_actually_ships(self): """hkp_pack lowers ``rocke``/``hip`` to ``kpack`` before the loader reads it, so diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py index 07ca309f7c9f..d5c372fce09f 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py @@ -60,6 +60,8 @@ def test_list_tier(arch, device, expect): (["gfx1100", "gfx1151"], [_G11], True), ([_G11], [_G11], True), ([_G11], [_G12], False), + (["gfx942", _G11], ["gfx942", "gfx1100"], True), + (["gfx942", _G11], ["gfx950", "gfx1100"], False), ], ) def test_covers(outer, inner, expect): From 10231247191f6a3840b83292a7b251978082f065 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 12:19:56 -0400 Subject: [PATCH 40/52] docs: state the arch-tier shadowing rule and its justification once in RFC 0017 --- .../descriptor-packaging/README.md | 3 ++- .../rfcs/0017_UniversalKernelDescriptor.md | 22 ++++++++++++++----- .../hipdnn/tools/IngestorGenerator/README.md | 4 ++-- .../skills/hipdnn-ingestor-engine/RUNBOOK.md | 2 +- .../skills/hipdnn-ingestor-engine/extend.md | 4 ++-- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 759d555b9f69..a0ea7430d1aa 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -16,7 +16,8 @@ Copies depend only on the source tree, the generic's spelling, hipcc and the tab identical across member folders and separate per-arch builds, and the loader collapses them to one entry. Copies from different compilers differ in `provenance`, and the loader drops both. -A device ranks an `arch` list explicit, then generic, then empty; duplicate tuples conflict within one tier. +Kernels sharing a metadata tuple are ranked per device by arch tier; see +[RFC 0017](../../../projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md#4-resolve-the-kernel-packs-that-name-this-engine-and-apply-the-arch-gate). Authoring rules. Each violation fails the packer, raises a generator ConfigError, and makes the loader log an ERROR and drop the pack: diff --git a/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md b/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md index c76d6da479a3..2b8d84d78aa8 100644 --- a/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md +++ b/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md @@ -575,7 +575,9 @@ basis on which to prefer one over the other, and no answer to give. The catalog therefore keyed on the tuple: a duplicate key is logged as an error and the colliding kernel dropped, rather than silently admitted into a set where selection cannot choose between its entries. Uniqueness is engine-wide, not per-pack, because the catalog spans -every KDP that names the engine. +every KDP that names the engine. A device sees at most one kernel per tuple: kernels sharing a +tuple are resolved by arch tier (step 4 of the load flow, below), and only same-tier +duplicates are errors. The remedy when two kernels must coexist is to add the KMD field that distinguishes them, so the schema grows to describe the variants the engine spans. This is additive and @@ -1422,11 +1424,21 @@ provider's descriptor cache, reused by every later graph. **4. Resolve the kernel packs that name this engine, and apply the arch gate.** Each pack (**KDP**) contributes a matcher set, one dispatch-descriptor (**UDD**) id, and a kernel vector with each -kernel's metadata values. A pack whose `arch` list excludes this device is dropped here. After its -matchers admit it, explicit kernels outrank kernels of an LLVM generic target containing the -device (e.g. `gfx11-generic`), which outrank unrestricted kernels; kernels with one metadata -tuple coexist at different tiers and the highest applicable tier wins. The +kernel's metadata values. A pack whose `arch` list excludes this device is dropped here. The dispatch descriptor is named but **not** loaded; nothing dispatches yet. + +*Arch tiers.* For one device, each kernel sits in one tier: an explicit arch id (e.g. `gfx1151`) +outranks an LLVM generic target containing the device (e.g. `gfx11-generic`), which outranks an +empty `arch` (unrestricted). When kernels sharing one metadata tuple are admitted for the device, +the loader keeps the one in the best tier, so the tuple stays unique in the catalog. Two kernels +with the same tuple in the *same* tier on a device (two explicit lists naming `gfx1151`, two +generics sharing a member, two empty-`arch` kernels) are an identical definition with no basis to +pick one, and the collision is an error. A family may ship a generic-target +build for portability and a member-specialized build with identical metadata; the specialized build +is expected to be better tuned for its device, so it wins there, and the generic remains the +fallback on the other members. The same ranking picks which arch key of one kernel's archive is +loaded: explicit before generic. + *Stored:* the parsed packs, matchers, and kernel metadata, in the same descriptor cache. **5. Run the engine's `graph_match`, lazily, on the first pack that cleared the gate.** It either diff --git a/projects/hipdnn/tools/IngestorGenerator/README.md b/projects/hipdnn/tools/IngestorGenerator/README.md index f821f7335b90..9ef547c68079 100644 --- a/projects/hipdnn/tools/IngestorGenerator/README.md +++ b/projects/hipdnn/tools/IngestorGenerator/README.md @@ -506,8 +506,8 @@ Run, in this order, **before any UUID is minted**: Configs accept generic `arch` targets under the [packaging rules](../../../../dnn-providers/hip-kernel-provider/descriptor-packaging/README.md#generic-gpu-targets). -`build_kdp` refuses a tuple shared by two kernels only at the same tier, so an explicit kernel -may override a generic kernel with the same tuple. The generated census gives every member of +`build_kdp` refuses a tuple shared by two kernels only at the same arch tier +([RFC 0017](../../docs/rfcs/0017_UniversalKernelDescriptor.md#4-resolve-the-kernel-packs-that-name-this-engine-and-apply-the-arch-gate)). The generated census gives every member of a generic row the generic's descriptors. ## Source adapters (`codegen/sources/`) diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md index 2d77b84ff01f..43ca622df32c 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md @@ -166,7 +166,7 @@ descriptor index expands as the recursive glob `/**/*.json` — a walk of the wh filesystem. Review the finalized inventory after deduplication. Resolve KDP → UED → KMD by UUID; -tuple identity includes schema types/defaults and effective architecture overlap. +tuple identity includes schema types/defaults and the arch tier at which kernels compete. Structural mode reports compiled agreement as `NOT CHECKED` and can exit 0 with unrun checks. `--profile` optionally selects the bundle and supplies vocabulary, never compiler evidence. Full artifact checking belongs after packing. diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md index 7113f7bc2b71..400866e280c8 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md @@ -12,8 +12,8 @@ KMD whose filename does not follow the individual packs. Preserve old names, UUIDs, symbols, references and native hooks; only genuinely new objects get new IDs. Compare completed, KMD-typed metadata across the whole engine: -conflicting tuples on overlapping effective architectures fail, equal tuples on disjoint -architectures are legal. New topology, layouts, attributes or feature combinations +equal tuples fail only when they compete at the same arch tier on a device; disjoint +architectures or different tiers (explicit over generic over empty) are legal. New topology, layouts, attributes or feature combinations reopen [graph-contract.md](graph-contract.md), even for a "variant". A new variant can be one added KDP entry or a standalone UKD reference, with no new From d95bdd97ed007e9f4f54a28cca3180e2646d3402 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 13:12:55 -0400 Subject: [PATCH 41/52] hkp probes: packer-default pack jobs; probe arches follow the GPU_TARGETS concrete-arch rule --- .../descriptor-packaging/README.md | 12 ++++- .../cmake/HkpPackaging.cmake | 25 ++++++++-- .../cmake/HkpPackagingProbes.cmake | 49 +++++++++++++------ .../descriptor-packaging/probes/probes.cmake | 2 + 4 files changed, 67 insertions(+), 21 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 4e560b047e28..76ed5cc3f84f 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -329,6 +329,14 @@ build targets, so each lane's own production pack packs it, and its GPU test lan on every PR. Only an architecture the lane does not build needs a probe, and only an explicit `arch` entry can make a descriptor ship there and nowhere else. +**Non-concrete names are skipped.** An explicit `arch` entry that is not a concrete +target, such as a generic target (`gfx9-4-generic`) or a TheRock family name, gets no +automatic probe; configure prints which ones it skipped. No build packs for such a name: +`GPU_TARGETS` drops it, and the packer matches arch names exactly, so it never selects a +family member in its place either. Concrete names with a distinct-target word +(`gfx1250-strict`) are probed like any other arch. The rule is `hkp_is_concrete_arch()` +in `cmake/HkpPackaging.cmake`, the same one `GPU_TARGETS` goes through. + ### How to use `probes/probes.cmake` is for **named `UKDS` probes** only. Add a line when an integration @@ -358,11 +366,11 @@ also fails configure. | Argument | Meaning | |---|---| -| `ARCH` | Required. The one architecture to pack, e.g. `gfx950`; one concrete target matching `^gfx[0-9a-f]+$`. Passed to the packer as `--arches`. | +| `ARCH` | Required. The one architecture to pack, e.g. `gfx950` or `gfx1250-strict`; one concrete target as `hkp_is_concrete_arch()` defines it (not a generic target or TheRock family name). Passed to the packer as `--arches`. | | `ROOT` | Optional descriptor root to probe. Defaults to the production root (`HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT`). A relative path is resolved against `probes/`; a trailing `/` is ignored. | | `NAME` | Optional probe name; must match `^[A-Za-z0-9_.+-]+$`, must not be dots only, and must not be `tools`. Defaults to `` for the production root and `_` for another `ROOT`. Pack target `hkp_packaging_probe_`, ctest entry `hkp-probe-`. Declaring a name twice is a configure error. | | `UKDS` | Optional UKD `name`s to pack instead of the default sample. Each entry is non-empty and listed once. | -| `PACK_JOBS` | Optional prewarm worker count; defaults to `1`. | +| `PACK_JOBS` | Optional prewarm worker count. Omitted, the packer uses its default, `min(32, ncpu)`, as the production root does. | A probe has no producer-kind argument: the kinds present are discovered from the descriptors, and the assertion expects each UKD's provenance by its authored diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake index 1e1e1f42bf84..23217e8f3a9a 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake @@ -58,15 +58,30 @@ function(hkp_resolve_kpack out_var python_exe) set(${out_var} "${_python_dir}" PARENT_SCOPE) endfunction() +# --------------------------------------------------------------------------- +# hkp_is_concrete_arch( ) +# TRUE when (already stripped of any :feature suffix) is a concrete gfx target +# name: a lowercase processor id (gfx942) optionally followed by lowercase +# hyphen-separated words that name a distinct target (gfx1250-strict), and none of +# those words is generic, all, dcgpu, dgpu or igpu (generic targets and TheRock family +# names, which no device reports). Only the shape is checked. +# --------------------------------------------------------------------------- +function(hkp_is_concrete_arch out_var arch) + if(arch MATCHES "^gfx[0-9a-f]+(-[a-z]+)*$" + AND NOT arch MATCHES "-(generic|all|dcgpu|dgpu|igpu)(-|$)") + set(${out_var} TRUE PARENT_SCOPE) + else() + set(${out_var} FALSE PARENT_SCOPE) + endif() +endfunction() + # --------------------------------------------------------------------------- # hkp_selected_arches( ) # Normalize GPU_TARGETS (or AMDGPU_TARGETS) into a bare gfx arch list, # stripping feature suffixes (gfx942:xnack-) and dropping anything that is not # a concrete gfx target name. # -# A concrete name is a lowercase processor id (gfx942) optionally followed by -# lowercase hyphen-separated words that name a distinct target (gfx1250-strict). -# Dropped: +# A concrete name is one hkp_is_concrete_arch() accepts. Dropped: # - TheRock family names, recognised by any hyphen-separated word of # dcgpu, dgpu, igpu or all, so a variant such as gfx950-dcgpu-asan is # dropped with its family (gfx94X-dcgpu, gfx950-dcgpu, gfx906-dgpu, @@ -109,8 +124,8 @@ function(hkp_selected_arches out_var out_source_var) if(NOT _bare) continue() endif() - if(NOT _bare MATCHES "^gfx[0-9a-f]+(-[a-z]+)*$" - OR _bare MATCHES "-(generic|all|dcgpu|dgpu|igpu)(-|$)") + hkp_is_concrete_arch(_concrete "${_bare}") + if(NOT _concrete) message(WARNING "hkp: ignoring '${_arch}' from ${_source}; it is not a concrete gfx " "target name (a TheRock family, a generic target, or an unrecognised " diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake index 4060ccbf3c4c..f77545669e5d 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -151,11 +151,15 @@ endfunction() # kind (see hkp_probe_assert.py). Adding a pack under an already probed ARCH needs no # new declaration unless the probe lists UKDS. # -# ARCH must be one concrete target, gfx[0-9a-f]+. NAME defaults to for the +# ARCH must be one concrete target as hkp_is_concrete_arch() defines it, the same +# rule GPU_TARGETS goes through (gfx950, gfx1250-strict; not gfx9-4-generic or a +# TheRock family name). NAME defaults to for the # production root and _ for another ROOT; it must match # [A-Za-z0-9_.+-]+, must not be dots only, and must not be `tools` (hkp-probe-tools # is the probe tooling test). -# UKDS entries must be non-empty and unique. Creates pack target +# UKDS entries must be non-empty and unique. PACK_JOBS caps the pack's worker +# processes; omitted, the packer sizes its pool against the machine, min(32, ncpu), +# as the production root does. Creates pack target # hkp_packaging_probe_ (stamp and output under # ${CMAKE_BINARY_DIR}/hkp-probes//out) and ctest entry hkp-probe-. The # pack targets are part of `all` (hkp_wire_pack_target declares them so); the @@ -173,10 +177,12 @@ function(hkp_add_packaging_probe) if(NOT ARG_ARCH) message(FATAL_ERROR "hkp probe: hkp_add_packaging_probe requires ARCH.") endif() - if(NOT ARG_ARCH MATCHES "^gfx[0-9a-f]+$") + hkp_is_concrete_arch(_concrete "${ARG_ARCH}") + if(NOT _concrete) message(FATAL_ERROR - "hkp probe: ARCH '${ARG_ARCH}' is not one concrete architecture of the " - "form gfx[0-9a-f]+.") + "hkp probe: ARCH '${ARG_ARCH}' is not one concrete architecture (a " + "processor id such as gfx950, optionally with a distinct-target word such " + "as gfx1250-strict; not a generic target or TheRock family name).") endif() if(NOT ARG_ROOT) set(ARG_ROOT "${HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT}") @@ -218,10 +224,7 @@ function(hkp_add_packaging_probe) _hkp_probe_derive_root("${ARG_NAME}" "${ARG_ROOT}" "${ARG_ARCH}" "${_root}" _expect_file ${ARG_UKDS}) - set(_pack_jobs 1) - if(ARG_PACK_JOBS) - set(_pack_jobs "${ARG_PACK_JOBS}") - endif() + # Empty PACK_JOBS passes nothing to the packer, which then uses its own default. hkp_wire_pack_target( NAME "probe_${ARG_NAME}" SOURCE_ROOT "${_root}" @@ -230,7 +233,7 @@ function(hkp_add_packaging_probe) ROCM_KPACK_DIR "${_kpack_dir}" OUT_ROOT "${_out_root}" ${_rocke_args} - PACK_JOBS "${_pack_jobs}") + PACK_JOBS "${ARG_PACK_JOBS}") set(_assert_args --out-root "${_out_root}" @@ -253,9 +256,12 @@ endfunction() # --------------------------------------------------------------------------- # _hkp_probe_automatic_arches() # The architectures to probe automatically: every explicit `arch` entry on a KDP or -# UKD under HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT, minus the build's own -# targets (hkp_selected_arches). Descriptors with no `arch` (wildcards) name no arch: -# they ship for every build target, so the build's own production pack covers them. +# UKD under HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT that is a concrete +# target (hkp_is_concrete_arch), minus the build's own targets (hkp_selected_arches). +# Descriptors with no `arch` (wildcards) name no arch: they ship for every build +# target, so the build's own production pack covers them. A non-concrete entry +# (gfx9-4-generic) is skipped with a STATUS line: no build packs for it, because +# GPU_TARGETS drops it and the packer matches arch names exactly. # FATAL with the tool's stderr when the packer's loader rejects the root. Configure # re-runs when a file under the root or the derive tooling changes. # --------------------------------------------------------------------------- @@ -282,7 +288,22 @@ function(_hkp_probe_automatic_arches out_var) string(REPLACE "\n" ";" _named "${_out}") hkp_selected_arches(_targets _targets_source) - set(_auto "${_named}") + set(_auto "") + set(_skipped "") + foreach(_arch IN LISTS _named) + hkp_is_concrete_arch(_concrete "${_arch}") + if(_concrete) + list(APPEND _auto "${_arch}") + else() + list(APPEND _skipped "${_arch}") + endif() + endforeach() + if(_skipped) + list(JOIN _skipped ", " _skipped_text) + message(STATUS + "hkp: no automatic probe for non-concrete arches [${_skipped_text}]: no " + "build packs for them.") + endif() if(_targets) list(REMOVE_ITEM _auto ${_targets}) endif() diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake index 690ac5e14c09..9cda0219bcd2 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake @@ -13,6 +13,8 @@ # Wildcard descriptors (no `arch`, or an empty list) name no architecture and get no # automatic probe. That is safe: a wildcard ships for whatever the build targets, so # every lane's own production pack, and its GPU test lanes, exercise it on every PR. +# A non-concrete arch name (gfx9-4-generic, a TheRock family) is skipped with a STATUS +# line: no build packs for it. # # Without UKDS a probe packs, per compile group of every KDP shipping for its ARCH, one # UKD, or for rocke enough UKDs that each value of each varying spec field is packed From 73cf876735dadfd42b32394b69fa41ef6e38bf3f Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 13:17:42 -0400 Subject: [PATCH 42/52] hipdnn: qualify the metadata-tuple uniqueness comments and error by arch tier --- .../include/hipdnn_plugin_sdk/ingestor/Descriptors.hpp | 3 ++- .../ingestor/KernelIngestorStateManager.hpp | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/Descriptors.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/Descriptors.hpp index 0cae60704112..55a78c7e594e 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/Descriptors.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/Descriptors.hpp @@ -105,7 +105,8 @@ static_assert( std::vector>, "MetadataType::INT_LIST no longer indexes MetadataValue's vector alternative."); -/// A kernel's complete metadata tuple; must be unique per kernel within an engine. +/// A kernel's complete metadata tuple. Kernels of one engine sharing a tuple must not compete at +/// the same arch tier on any device; a better tier shadows a worse one. using MetadataValues = std::map; /// One field a kernel may vary along, as declared by an engine's KMD. diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp index 01b0b994a6b9..b0610e33f332 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp @@ -97,7 +97,8 @@ class KernelIngestorStateManager /// logs once and changes nothing. static constexpr size_t WINNER_CACHE_WARNING_THRESHOLD = 4096; - /// @throws std::invalid_argument bad pack reference, or duplicate metadata tuple. + /// @throws std::invalid_argument bad pack reference, or two kernels sharing a metadata tuple + /// that compete at the same arch tier on a device. /// @throws std::runtime_error a UMD or the engine's graph_match names a symbol this /// build does not ship. /// @@ -447,8 +448,8 @@ class KernelIngestorStateManager "kernel '" + toString(kernel.id) + "' duplicates the metadata tuple of another kernel under schema '" + _schema.name - + "' on an arch both reach; the tuple is the catalog key " - + "and must be unique per device at the same arch tier"); + + "' at the same arch tier on a device both reach; a better tier " + + "shadows a worse one, so equal tiers cannot be told apart"); } } claimants.push_back(kernelArch); From 9c2434fb4be8b3d175592c912ff74515a814d131 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 16:41:03 -0400 Subject: [PATCH 43/52] hkp probes: comments state that probe packs use the packer's default pool --- .../hip-kernel-provider/descriptor-packaging/README.md | 6 +++--- .../descriptor-packaging/cmake/HkpPackaging.cmake | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 76ed5cc3f84f..20f9cdcdca65 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -259,9 +259,9 @@ export AMD_COMGR_CACHE_DIR=/tmp/comgr-cache # RAM disk or local disk `HKP_PACK_JOBS` is read by a **direct child run** of `hkp_pack`. Inside the build it comes from the `PACK_JOBS ` argument of each `hkp_wire_pack_target()` call: the test roots -pass `1`, or `2` for `integration`, and the production root passes none, so the packer uses -its default. The reason is in the `hkp_wire_pack_target()` header in -`cmake/HkpPackaging.cmake`. +pass `1`, or `2` for `integration`, and the production root and the packaging probes pass +none, so the packer uses its default. The reason is in the `hkp_wire_pack_target()` header +in `cmake/HkpPackaging.cmake`. ## Packaging probes diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake index 23217e8f3a9a..df29b13cc9fb 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake @@ -183,8 +183,9 @@ endfunction() # PACK_JOBS caps the worker processes one pack may spawn; 1 selects the packer's # serial path. Omitted, the packer sizes its pool against the machine. Roots have # no ordering edge between them, so the generator runs them at once: the test -# roots name a small cap so their pools do not multiply, and the product root -# omits it because it is the root expected to be large enough to repay a full pool. +# roots name a small cap so their pools do not multiply, while the production root +# and the packaging probes omit it and use the packer's default. A probe pack is +# bounded by the descriptors it keeps, so a full pool costs it little. # # NAME is the source label written into every descriptor's provenance. NAME, the # absolute SOURCE_ROOT, OUT_ROOT and ARCHES go into a global registry read by From 8e08eeceb275bb36fd9dbeb53f7d71bf55bed4b9 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 16:53:52 -0400 Subject: [PATCH 44/52] hkp probes: probe generic targets through their first member --- .../descriptor-packaging/README.md | 40 ++++- .../cmake/HkpPackagingProbes.cmake | 62 +++++-- .../descriptor-packaging/probes/probes.cmake | 8 +- .../probes/tests/probe_support.py | 32 +++- .../probes/tests/test_hkp_probe_assert.py | 1 + .../tests/test_hkp_probe_derive_root.py | 159 ++++++++++++++++-- .../probes/tests/test_hkp_probe_end_to_end.py | 126 +++++++++++++- .../python/hkp_pack/pipeline.py | 20 ++- .../tools/hkp_probe_assert.py | 99 +++++++---- .../tools/hkp_probe_derive_root.py | 121 ++++++++++--- 10 files changed, 545 insertions(+), 123 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 052df248a858..39c8602e3a06 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -344,8 +344,8 @@ Every architecture named explicitly under the production root gets an **automati in every lane that does not build it. At configure, `hkp_load_packaging_probes()` runs `tools/hkp_probe_derive_root.py --list-arches` on `HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT`: the union of the explicit `arch` -entries of every KDP and UKD (inline and standalone), read with the packer's own loader. -It subtracts the lane's own architectures (`hkp_selected_arches()`, i.e. `GPU_TARGETS`) +entries of every KDP and UKD (inline and standalone), read with the packer's own loader and +the generic target table, generic names mapped as below. It subtracts the lane's own architectures (`hkp_selected_arches()`, i.e. `GPU_TARGETS`) and declares `hkp_add_packaging_probe(ARCH )` for each one left: probe ``, ctest entry `hkp-probe-`, packing the default sample. Configure prints the result: @@ -364,13 +364,35 @@ build targets, so each lane's own production pack packs it, and its GPU test lan on every PR. Only an architecture the lane does not build needs a probe, and only an explicit `arch` entry can make a descriptor ship there and nowhere else. -**Non-concrete names are skipped.** An explicit `arch` entry that is not a concrete -target, such as a generic target (`gfx9-4-generic`) or a TheRock family name, gets no -automatic probe; configure prints which ones it skipped. No build packs for such a name: -`GPU_TARGETS` drops it, and the packer matches arch names exactly, so it never selects a -family member in its place either. Concrete names with a distinct-target word -(`gfx1250-strict`) are probed like any other arch. The rule is `hkp_is_concrete_arch()` -in `cmake/HkpPackaging.cmake`, the same one `GPU_TARGETS` goes through. +**Generic targets are probed through one member.** A generic target in the generic target +table (`projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json`) that a descriptor names +gets its probe through the first member the table lists for it: `gfx11-generic` through +`gfx1100`, `gfx12-generic` through `gfx1200`. The packer compiles the generic once, for the +generic target, and merges the result into each requested member's folder, so which member +carries the probe only names the folder; the table's order picks it, with no configuration. +The probe for `gfx1100` packs every pass that applies to `gfx1100`, its own and the +generic's: a KDP that ships only for the generic is trimmed like any other, and the +assertion expects its UKDs with `arch: gfx11-generic` and the generic's archive +(`kpack/hip_kernel_provider_gfx11-generic.kpack`) in the `gfx1100` folder. + +- A lane that targets any member of the generic gets no probe for it: its production pack + compiles the generic already. Linux (`gfx942`) probes `gfx11-generic` through `gfx1100`; + Windows (`gfx1151`, a member) does not. +- One probe per member arch: a member that is also named explicitly, or chosen for two + generics, is probed once, because `--arches ` runs every pass that applies to it. + +``` +hkp: generic targets probed through their first member: [gfx11-generic -> gfx1100] +hkp: automatic probe arches: [gfx1100, gfx950] (probe arches of the explicit arches under : [gfx1100, gfx950]; build targets: [gfx942]) +``` + +**TheRock family names are skipped; an unlisted generic fails configure.** An explicit `arch` +entry that is a TheRock family name gets no automatic probe; configure prints which ones it +skipped. No build packs for such a name: `GPU_TARGETS` drops it. A `*-generic` entry the table +does not list fails configure with the packer loader's message, since the packer rejects it +too. Concrete names with a distinct-target word (`gfx1250-strict`) are +probed like any other arch. The rule is `hkp_is_concrete_arch()` in +`cmake/HkpPackaging.cmake`, the same one `GPU_TARGETS` goes through. ### How to use diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake index f77545669e5d..d7d66ba390b7 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -39,8 +39,9 @@ endfunction() # cannot be derived, e.g. a kept KDP references a standalone UKD, or a listed UKD is # kept nowhere) and re-run configure when a source file or the derive tooling # changes. Each is passed as one --ukd; with none, derive selects per compile -# group (one UKD, or a value sweep for kinds with a sweep field). The tool writes /../expect.json, the list of kept UKDs the -# assertion expects; receives its path. FATAL when that list is empty: no +# group (one UKD, or a value sweep for kinds with a sweep field). The tool writes +# /../expect.json, the list of kept UKDs the assertion expects, each with the +# pass it packs under (, or a generic target containing it); receives its path. FATAL when that list is empty: no # KDP under ships for , so the pack would prune everything. Deriving # is idempotent, so a reconfigure leaves the derived root's mtimes alone and does not # re-pack. @@ -50,7 +51,8 @@ function(_hkp_probe_derive_root name from_dir arch out_dir expect_var) set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_derive_inputs} "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" - "${HKP_PKG_DIR}/tools/hkp_probe_kinds.py") + "${HKP_PKG_DIR}/tools/hkp_probe_kinds.py" + "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}") set(_ukd_args "") foreach(_ukd IN LISTS ARGN) list(APPEND _ukd_args --ukd "${_ukd}") @@ -59,6 +61,7 @@ function(_hkp_probe_derive_root name from_dir arch out_dir expect_var) COMMAND "${Python3_EXECUTABLE}" "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" --from "${from_dir}" + --generic-targets-json "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}" --arch "${arch}" --out "${out_dir}" ${_ukd_args} @@ -259,11 +262,16 @@ endfunction() # UKD under HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT that is a concrete # target (hkp_is_concrete_arch), minus the build's own targets (hkp_selected_arches). # Descriptors with no `arch` (wildcards) name no arch: they ship for every build -# target, so the build's own production pack covers them. A non-concrete entry -# (gfx9-4-generic) is skipped with a STATUS line: no build packs for it, because -# GPU_TARGETS drops it and the packer matches arch names exactly. -# FATAL with the tool's stderr when the packer's loader rejects the root. Configure -# re-runs when a file under the root or the derive tooling changes. +# target, so the build's own production pack covers them. A generic target in +# HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON is probed through its first member (the +# packer compiles the generic for any member and merges it into the member's folder), +# unless the build targets one of its members, whose production pack compiles it +# already; the STATUS line shows each generic -> member mapping. A TheRock family name +# is skipped with a STATUS line: no build packs for it, because GPU_TARGETS drops it. +# FATAL with the tool's stderr when the packer's loader rejects the root, including a +# `*-generic` entry absent from the table. Configure +# re-runs when a file under the root, the generic target table or the derive tooling +# changes. # --------------------------------------------------------------------------- function(_hkp_probe_automatic_arches out_var) set(_root "${HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT}") @@ -271,11 +279,19 @@ function(_hkp_probe_automatic_arches out_var) set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_root_inputs} "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" - "${HKP_PKG_DIR}/tools/hkp_probe_kinds.py") + "${HKP_PKG_DIR}/tools/hkp_probe_kinds.py" + "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}") + hkp_selected_arches(_targets _targets_source) + set(_exclude_args "") + foreach(_target IN LISTS _targets) + list(APPEND _exclude_args --exclude "${_target}") + endforeach() execute_process( COMMAND "${Python3_EXECUTABLE}" "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" --list-arches --from "${_root}" + --generic-targets-json "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}" + ${_exclude_args} RESULT_VARIABLE _rc OUTPUT_VARIABLE _out ERROR_VARIABLE _err) @@ -285,19 +301,32 @@ function(_hkp_probe_automatic_arches out_var) "failed (exit ${_rc}).\n${_err}") endif() string(REGEX REPLACE "\n+$" "" _out "${_out}") - string(REPLACE "\n" ";" _named "${_out}") + string(REPLACE "\n" ";" _lines "${_out}") - hkp_selected_arches(_targets _targets_source) + # Each line is an arch followed by the generic targets it stands for. + set(_named "") set(_auto "") set(_skipped "") - foreach(_arch IN LISTS _named) + set(_mapped "") + foreach(_line IN LISTS _lines) + separate_arguments(_words UNIX_COMMAND "${_line}") + list(POP_FRONT _words _arch) + list(APPEND _named "${_arch}") hkp_is_concrete_arch(_concrete "${_arch}") if(_concrete) list(APPEND _auto "${_arch}") + foreach(_generic IN LISTS _words) + list(APPEND _mapped "${_generic} -> ${_arch}") + endforeach() else() list(APPEND _skipped "${_arch}") endif() endforeach() + if(_mapped) + list(JOIN _mapped ", " _mapped_text) + message(STATUS + "hkp: generic targets probed through their first member: [${_mapped_text}]") + endif() if(_skipped) list(JOIN _skipped ", " _skipped_text) message(STATUS @@ -311,8 +340,8 @@ function(_hkp_probe_automatic_arches out_var) list(JOIN _named ", " _named_text) list(JOIN _targets ", " _targets_text) message(STATUS - "hkp: automatic probe arches: [${_auto_text}] (explicit arches under ${_root}: " - "[${_named_text}]; build targets: [${_targets_text}])") + "hkp: automatic probe arches: [${_auto_text}] (probe arches of the explicit " + "arches under ${_root}: [${_named_text}]; build targets: [${_targets_text}])") set(${out_var} "${_auto}" PARENT_SCOPE) endfunction() @@ -321,8 +350,9 @@ endfunction() # Store the toolchain hkp_add_packaging resolved, load the named probes from # probes/probes.cmake, then declare one automatic probe per architecture the production # root names that the build does not target: the explicit `arch` entries of its KDPs -# and UKDs (hkp_probe_derive_root.py --list-arches; wildcard descriptors name none) -# minus hkp_selected_arches(), each as hkp_add_packaging_probe(ARCH ). Register +# and UKDs (hkp_probe_derive_root.py --list-arches; wildcard descriptors name none, +# generic targets map to a member, see _hkp_probe_automatic_arches) minus +# hkp_selected_arches(), each as hkp_add_packaging_probe(ARCH ). Register # the probe tooling tests, write the manifest of ctest names # (${CMAKE_BINARY_DIR}/hkp-probes/manifest.txt) and define the aggregate target # hkp_packaging_probes. Zero probes is valid: every explicit arch is a build target, diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake index 9cda0219bcd2..288011fa3b36 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake @@ -13,8 +13,12 @@ # Wildcard descriptors (no `arch`, or an empty list) name no architecture and get no # automatic probe. That is safe: a wildcard ships for whatever the build targets, so # every lane's own production pack, and its GPU test lanes, exercise it on every PR. -# A non-concrete arch name (gfx9-4-generic, a TheRock family) is skipped with a STATUS -# line: no build packs for it. +# A generic target in the generic target table (gfx11-generic) is probed through its +# first member in table order (gfx1100): the packer compiles the generic once and merges +# it into each requested member's folder, so the probe for the member covers it. A build +# that targets any member of the generic (gfx1151) already compiles it and gets no probe +# for it. A TheRock family name is skipped with a STATUS line: no build packs for it. A +# `*-generic` absent from the table fails configure, as it fails the packer. # # Without UKDS a probe packs, per compile group of every KDP shipping for its ARCH, one # UKD, or for rocke enough UKDs that each value of each varying spec field is packed diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py index 1d41cf97527a..d74000ad229e 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py @@ -24,6 +24,14 @@ HKP_PACK = _PKG_DIR / "tools" / "hkp_pack.py" PROBE_ASSERT = _PKG_DIR / "tools" / "hkp_probe_assert.py" PROBE_DERIVE = _PKG_DIR / "tools" / "hkp_probe_derive_root.py" +GENERIC_TARGETS_JSON = ( + _HKP_DIR.parent.parent + / "projects" + / "hipdnn" + / "plugin_sdk" + / "data" + / "gpu_generic_targets.json" +) FIXTURES = _PKG_DIR / "tests" / "fixtures" ROCKE_FIXTURE = FIXTURES / "rocke" MAIN_FIXTURE = FIXTURES / "main" @@ -61,8 +69,8 @@ def comgr_lib(): return os.environ.get("ROCKE_COMGR_LIB") or None -def pack_root(src, work, rocm_kpack_dir, hipcc, comgr_lib): - """Pack `src` for ARCH with the real packer; return the output root. +def pack_root(src, work, rocm_kpack_dir, hipcc, comgr_lib, arch=ARCH): + """Pack `src` for `arch` with the real packer; return the output root. The stamp file is written by CMake in production; it is created here so the stamp assertion has something to find. @@ -85,9 +93,11 @@ def pack_root(src, work, rocm_kpack_dir, hipcc, comgr_lib): "--out-root", str(out), "--arches", - ARCH, + arch, "--hipcc", hipcc, + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), "--inter-root", str(work / "inter"), "--kpack-python-dir", @@ -114,14 +124,14 @@ def packed_root(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): return pack_root(ROCKE_FIXTURE, work, rocm_kpack_dir, hipcc, comgr_lib) -def run_assert(out_root, expect_path, rocm_kpack_dir, expect_comgr=None): +def run_assert(out_root, expect_path, rocm_kpack_dir, expect_comgr=None, arch=ARCH): cmd = [ sys.executable, str(PROBE_ASSERT), "--out-root", str(out_root), "--arch", - ARCH, + arch, "--expect", str(expect_path), "--kpack-python-dir", @@ -141,6 +151,8 @@ def run_derive(src, arch, out, ukds=()): str(PROBE_DERIVE), "--from", str(src), + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), "--arch", arch, "--out", @@ -163,9 +175,11 @@ def hip_source(dst): return dst -def derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib, ukds=()): - """Derive `src` for ARCH, pack the derived root; return (out root, expect path).""" - r = run_derive(src, ARCH, work / "probe" / "root", ukds) +def derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib, ukds=(), arch=ARCH): + """Derive `src` for `arch`, pack it; return (out root, expect path).""" + r = run_derive(src, arch, work / "probe" / "root", ukds) assert r.returncode == 0, r.stderr - out = pack_root(work / "probe" / "root", work, rocm_kpack_dir, hipcc, comgr_lib) + out = pack_root( + work / "probe" / "root", work, rocm_kpack_dir, hipcc, comgr_lib, arch + ) return out, work / "probe" / "expect.json" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py index 4046c0474a7d..457500cee154 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py @@ -33,6 +33,7 @@ def entry(self, name=None, kind="rocke"): "kdp": KDP_IN_ARCH_DIR, "name": self.ukd_name if name is None else name, "kind": kind, + "arch": ARCH, } def write_expect(self, entries): diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py index 800845fbb84a..1351ac63b86e 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py @@ -11,7 +11,7 @@ import pytest -from probe_support import MAIN_FIXTURE, PROBE_DERIVE, run_derive +from probe_support import GENERIC_TARGETS_JSON, MAIN_FIXTURE, PROBE_DERIVE, run_derive def _derive(src: Path, arch: str, out: Path, ukds=()): @@ -105,8 +105,8 @@ def _write_root(root: Path, ukds, kdp_arch=("gfx950",), kdp_file="x.kdp.json"): (root / file).write_text(json.dumps(doc) + "\n") -def _write_standalone(root: Path, ukd_id="ukd-standalone-id", arch=None): - ukd = _rocke("standalone", builder="b9", arch=arch, ukd_id=ukd_id) +def _write_standalone(root: Path, ukd_id="ukd-standalone-id", arch=("gfx950",)): + ukd = _rocke("standalone", builder="b9", arch=list(arch), ukd_id=ukd_id) (root / "s.ukd.json").write_text(json.dumps(ukd) + "\n") @@ -179,8 +179,8 @@ def test_passthrough_kind_expect_carries_authored_kernel_source(tmp_path): # One passthrough UKD per KDP (nothing compiles); it records its authored block. assert set(by_name) == {"e", "r"} assert by_name["e"]["kernel_source"] == emb["kernel_source"] - # kpack-output entries keep the three-field schema. - assert set(by_name["r"]) == {"kdp", "name", "kind"} + # kpack-output entries carry no authored block. + assert set(by_name["r"]) == {"kdp", "name", "kind", "arch"} def test_non_varying_group_keeps_first_by_name(tmp_path): @@ -301,7 +301,9 @@ def test_wildcard_kdp_ships_everywhere(tmp_path): def test_expect_records_kdp_relative_path(tmp_path): _write_root(tmp_path / "src", [_rocke("a")], kdp_file="sub/dir/x.kdp.json") expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") - assert expect == [{"kdp": "sub/dir/x.kdp.json", "name": "a", "kind": "rocke"}] + assert expect == [ + {"kdp": "sub/dir/x.kdp.json", "name": "a", "kind": "rocke", "arch": "gfx950"} + ] def test_dot_prefixed_kdp_is_copied_but_not_expected(tmp_path): @@ -350,7 +352,7 @@ def test_standalone_reference_in_other_arch_kdp_is_ignored(tmp_path): _write_root( tmp_path / "src", [_rocke("a"), "ukd-standalone-id"], kdp_arch=("gfx942",) ) - _write_standalone(tmp_path / "src") + _write_standalone(tmp_path / "src", arch=("gfx942",)) assert _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") == [] @@ -563,9 +565,24 @@ def test_rederive_follows_source_changes(tmp_path): # --- --list-arches ---------------------------------------------------------- -def _list_arches(src: Path): +def _list_cmd(src: Path, exclude=()): + cmd = [ + sys.executable, + str(PROBE_DERIVE), + "--list-arches", + "--from", + str(src), + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), + ] + for arch in exclude: + cmd += ["--exclude", arch] + return cmd + + +def _list_arches(src: Path, exclude=()): r = subprocess.run( - [sys.executable, str(PROBE_DERIVE), "--list-arches", "--from", str(src)], + _list_cmd(src, exclude), capture_output=True, text=True, check=False, @@ -581,7 +598,7 @@ def test_list_arches_unions_explicit_kdp_and_ukd_arches(tmp_path): # and standalone. wild = [_rocke("b", arch=["gfx1201"]), _rocke("c"), "ukd-standalone-id"] _write_root(src, wild, kdp_arch=None, kdp_file="wild.kdp.json") - _write_standalone(src, arch=["gfx90a"]) + _write_standalone(src, arch=("gfx90a",)) assert _list_arches(src) == ["gfx1201", "gfx90a", "gfx942", "gfx950"] @@ -594,17 +611,127 @@ def test_list_arches_excludes_wildcards(tmp_path): def test_list_arches_packer_rejection_exits_2(tmp_path): src = tmp_path / "src" _write_root(src, [_rocke("a", arch=["gfx942"])]) - r = subprocess.run( - [sys.executable, str(PROBE_DERIVE), "--list-arches", "--from", str(src)], - capture_output=True, - text=True, - check=False, - ) + r = subprocess.run(_list_cmd(src), capture_output=True, text=True, check=False) assert r.returncode == 2 assert r.stderr.startswith("hkp_probe_derive:") assert "is not a subset of KDP" in r.stderr +# --- generic targets --------------------------------------------------------- +# gpu_generic_targets.json lists gfx1100 first for gfx11-generic, and gfx1151 among its +# members. +_GENERIC = "gfx11-generic" +_FIRST_MEMBER = "gfx1100" + + +def _generic_hsaco(name): + ks = {"kind": "hsaco", "file": f"{name}.hsaco", "symbol": name} + return _ukd(name, ks, [_GENERIC]) + + +def test_list_arches_maps_generic_to_its_first_member(tmp_path): + src = tmp_path / "src" + _write_root(src, [_generic_hsaco("g")], kdp_arch=(_GENERIC,)) + assert _list_arches(src) == [f"{_FIRST_MEMBER} {_GENERIC}"] + + +def test_list_arches_drops_a_generic_when_a_member_is_a_build_target(tmp_path): + src = tmp_path / "src" + _write_root(src, [_generic_hsaco("g")], kdp_arch=(_GENERIC,)) + _write_root( + src, [_hsaco("o", ("gfx942",))], kdp_arch=("gfx942",), kdp_file="o.kdp.json" + ) + # gfx1151 is a member that is not the first one; any member covers the generic. + assert _list_arches(src, exclude=["gfx1151"]) == ["gfx942"] + # A build target that is no member leaves the generic probed. + assert _list_arches(src, exclude=["gfx942"]) == [ + f"{_FIRST_MEMBER} {_GENERIC}", + "gfx942", + ] + + +def test_list_arches_gives_one_probe_to_a_member_that_is_also_explicit(tmp_path): + src = tmp_path / "src" + _write_root(src, [_generic_hsaco("g")], kdp_arch=(_GENERIC,)) + _write_root( + src, + [_hsaco("m", (_FIRST_MEMBER,))], + kdp_arch=(_FIRST_MEMBER,), + kdp_file="m.kdp.json", + ) + assert _list_arches(src) == [f"{_FIRST_MEMBER} {_GENERIC}"] + + +def test_list_arches_probes_each_generic_through_its_own_first_member(tmp_path): + src = tmp_path / "src" + _write_root(src, [_generic_hsaco("g")], kdp_arch=(_GENERIC,)) + twelve = _ukd( + "h", + {"kind": "hsaco", "file": "h.hsaco", "symbol": "h"}, + ["gfx12-generic"], + ) + _write_root(src, [twelve], kdp_arch=("gfx12-generic",), kdp_file="h.kdp.json") + assert _list_arches(src) == [ + f"gfx1100 {_GENERIC}", + "gfx1200 gfx12-generic", + ] + + +def test_list_arches_keeps_family_names_and_rejects_unknown_generics(tmp_path): + src = tmp_path / "src" + _write_root(src, [_hsaco("f", ("gfx110x-all",))], kdp_arch=("gfx110x-all",)) + # A family name is not in the table: it prints as itself for the caller to skip. + assert _list_arches(src) == ["gfx110x-all"] + + unknown = tmp_path / "unknown" + _write_root(unknown, [_hsaco("u", ("gfx99-generic",))], kdp_arch=("gfx99-generic",)) + r = subprocess.run(_list_cmd(unknown), capture_output=True, text=True, check=False) + # The packer's loader refuses a generic absent from the table. + assert r.returncode == 2 + assert "gfx99-generic" in r.stderr + assert r.stdout == "" + + +def test_derive_trims_a_generic_only_kdp_for_a_member_arch(tmp_path): + src = tmp_path / "src" + _write_root( + src, + [_generic_hsaco(n) for n in ("g_b", "g_a", "g_c")], + kdp_arch=(_GENERIC,), + kdp_file="generic.kdp.json", + ) + _write_root(src, [_hsaco("m", (_FIRST_MEMBER,))], kdp_arch=(_FIRST_MEMBER,)) + twelve = [ + _ukd(n, {"kind": "hsaco", "file": f"{n}.hsaco", "symbol": n}, ["gfx12-generic"]) + for n in ("o1", "o2") + ] + _write_root(src, twelve, kdp_arch=("gfx12-generic",), kdp_file="twelve.kdp.json") + + out = tmp_path / "w" / "root" + expect = _derive(src, _FIRST_MEMBER, out) + + # One UKD per compile group under each pass; the packed archive of the generic + # pass is the generic's, so its entry names the generic. + assert expect == [ + {"kdp": "generic.kdp.json", "name": "g_a", "kind": "hsaco", "arch": _GENERIC}, + {"kdp": "x.kdp.json", "name": "m", "kind": "hsaco", "arch": _FIRST_MEMBER}, + ] + assert _kept(out, "generic.kdp.json") == ["g_a"] + # A generic of another family does not ship for this member: copied unchanged. + assert (out / "twelve.kdp.json").read_bytes() == ( + src / "twelve.kdp.json" + ).read_bytes() + + +def test_derive_for_a_member_outside_the_generic_ignores_its_kdps(tmp_path): + src = tmp_path / "src" + _write_root(src, [_generic_hsaco("g")], kdp_arch=(_GENERIC,)) + out = tmp_path / "w" / "root" + # gfx1200 is no member of gfx11-generic: no pass for it, nothing to expect. + assert _derive(src, "gfx1200", out) == [] + assert (out / "x.kdp.json").read_bytes() == (src / "x.kdp.json").read_bytes() + + # --- the kinds registry ----------------------------------------------------- def _tool_module(name): sys.path.insert(0, str(PROBE_DERIVE.parent)) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py index 16470c04467d..3c44c5483870 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py @@ -27,11 +27,17 @@ # gfx950) holds two hip UKDs in one compile group and the wildcard KDP one more in # another; the gfx942-only KDPs ship nothing. _HIP_EXPECT = [ - {"kdp": "pointwise.kdp.json", "name": "PointwiseAdd f32 block64", "kind": "hip"}, + { + "kdp": "pointwise.kdp.json", + "name": "PointwiseAdd f32 block64", + "kind": "hip", + "arch": ARCH, + }, { "kdp": "pointwise_wild.kdp.json", "name": "PointwiseAdd f32 block256", "kind": "hip", + "arch": ARCH, }, ] @@ -158,3 +164,121 @@ def test_listed_ukds_pack_and_pass(tmp_path, rocm_kpack_dir, hipcc, comgr_lib): assert not (out / ARCH / "pointwise_wild.kdp.json").exists() result = run_assert(out, expect, rocm_kpack_dir) assert result.returncode == 0, result.stderr + + +# --- generic targets ---------------------------------------------------------- +# gfx11-generic's first member in the generic target table; the probe is for it. +_MEMBER = "gfx1100" +_GENERIC = "gfx11-generic" + + +def _generic_source(dst): + """A generic hsaco KDP beside a KDP for the member itself, both authored hsaco.""" + _hsaco_source(dst, arch="gfx942") + kdp = json.loads((dst / "solo.kdp.json").read_text()) + kdp["arch"] = [_GENERIC] + kdp["kernelDescriptors"][0]["arch"] = [_GENERIC] + (dst / "solo.kdp.json").write_text(json.dumps(kdp, indent=2)) + + member = json.loads(json.dumps(kdp)) + member.update(id="kdp-member", arch=[_MEMBER]) + ukd = member["kernelDescriptors"][0] + ukd.update(id="ukd-member", name="member hsaco", arch=[_MEMBER]) + ukd["metadata"]["block_size"] = 128 + (dst / "member.kdp.json").write_text(json.dumps(member, indent=2)) + return dst + + +@pytest.fixture(scope="module") +def generic_probe(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): + work = tmp_path_factory.mktemp("generic_probe") + return derive_and_pack( + _generic_source(work / "src"), + work, + rocm_kpack_dir, + hipcc, + comgr_lib, + arch=_MEMBER, + ) + + +def _generic_assert(out, expect, rocm_kpack_dir): + return run_assert(out, expect, rocm_kpack_dir, arch=_MEMBER) + + +@pytest.fixture +def generic_tree(generic_probe, tmp_path): + out, expect = generic_probe + copy = tmp_path / "out" + shutil.copytree(out, copy) + return copy, expect + + +def test_generic_root_expects_each_ukd_under_its_pass_and_passes( + generic_probe, rocm_kpack_dir +): + out, expect = generic_probe + assert {e["kdp"]: e["arch"] for e in json.loads(expect.read_text())} == { + "solo.kdp.json": _GENERIC, + "member.kdp.json": _MEMBER, + } + # Both archives sit in the member's folder, the generic's named by the generic. + kpacks = sorted(p.name for p in (out / _MEMBER / "kpack").iterdir()) + assert kpacks == [ + f"hip_kernel_provider_{_GENERIC}.kpack", + f"hip_kernel_provider_{_MEMBER}.kpack", + ] + result = _generic_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 0, result.stderr + + +def test_assert_fails_when_member_folder_lacks_the_generic_archive( + generic_tree, rocm_kpack_dir +): + out, expect = generic_tree + (out / _MEMBER / "kpack" / f"hip_kernel_provider_{_GENERIC}.kpack").unlink() + result = _generic_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 1 + assert f"FAIL kpack-missing: {out / _MEMBER / 'kpack'}" in result.stderr + assert f"hip_kernel_provider_{_GENERIC}.kpack" in result.stderr + + +def test_assert_fails_when_member_archive_stands_in_for_the_generic_one( + generic_tree, rocm_kpack_dir +): + # The generic UKD's blob is looked up under the generic spelling in the generic + # archive: the member's archive, copied over it, holds no such entry. + out, expect = generic_tree + kpack = out / _MEMBER / "kpack" + shutil.copyfile( + kpack / f"hip_kernel_provider_{_MEMBER}.kpack", + kpack / f"hip_kernel_provider_{_GENERIC}.kpack", + ) + result = _generic_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 1 + assert "FAIL kpack-toc:" in result.stderr + assert "solo.kdp.json" in result.stderr + + +@pytest.mark.parametrize( + "kdp, edit_ukd, wrong", + [ + ("solo.kdp.json", True, [_MEMBER]), + ("solo.kdp.json", False, [_MEMBER]), + ("member.kdp.json", True, [_GENERIC]), + ], +) +def test_assert_fails_when_arch_is_not_the_ukds_pass( + generic_tree, rocm_kpack_dir, kdp, edit_ukd, wrong +): + out, expect = generic_tree + path = out / _MEMBER / kdp + doc = json.loads(path.read_text()) + if edit_ukd: + doc["kernelDescriptors"][0]["arch"] = wrong + else: + doc["arch"] = wrong + path.write_text(json.dumps(doc, indent=2)) + result = _generic_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 1 + assert "FAIL arch-field:" in result.stderr diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py index ca5f85251e85..d5e546c9f33c 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py @@ -1507,6 +1507,20 @@ def _merge_generic_into_member(generic_dir, member_dir, staging_dir): ) from exc +def generic_passes(flat, arches): + """The generics `run_pipeline` compiles for @arches, in table order. + + A generic is compiled when one of @arches is its member and a KDP ships for it. + """ + table = flat.generic_targets + return [ + generic + for generic in table.names() + if any(a in table.members(generic) for a in arches) + and any(kdp_survives(k.doc, flat, generic) for k in flat.kdps()) + ] + + def run_pipeline( source_root, arches, @@ -1628,12 +1642,8 @@ def run_pipeline( arch=arch, out_dir=out_arch_dir, kpack_path=None, skipped=True ) - for generic in generic_targets.names(): + for generic in generic_passes(flat, arches): members = [a for a in arches if a in generic_targets.members(generic)] - if not members: - continue - if not any(kdp_survives(k.doc, flat, generic) for k in flat.kdps()): - continue generic_out = inter_root / ".generic-out" / generic try: inter = compile_intermediate( diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py index b96d1b6cf65d..6542e6fcff89 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py @@ -6,12 +6,16 @@ failure, unless the shipped set is the one `--expect` lists (hkp_probe_derive_root.py writes it) and every UKD is well formed for its kind: a JSON list of {"kdp": , "name": , "kind": -}, plus the authored "kernel_source" for kinds whose -output is a pass-through. The checks applied to a UKD follow its EXPECTED kind's output -type (hkp_probe_kinds.KINDS): `kpack` UKDs are checked against the archive; -`passthrough` UKDs must ship as authored. The archive checks (kpack-missing, -kpack-empty, kpack-toc, sha256, symbol, signature) run only when the expect list holds -at least one kpack-output UKD; a pass-through-only root ships no archive. +, "arch": }, plus the +authored "kernel_source" for kinds whose output is a pass-through. The pass is `--arch`, +or a generic target containing it: the packer compiles a generic once and merges its +descriptors and archive into the member folder, so the UKD ships under `//` +with `arch` []. The checks applied to a UKD follow its EXPECTED kind's output +type (hkp_probe_kinds.KINDS): `kpack` UKDs are checked against the archive of their +pass; `passthrough` UKDs must ship as authored. The archive of a pass +(//kpack/hip_kernel_provider_.kpack) is checked (kpack-missing, +kpack-empty, kpack-toc, sha256, symbol, signature) for each pass that has at least one +kpack-output UKD in the expect list; a pass-through-only root ships no archive. hkp_probe_assert: FAIL : @@ -25,8 +29,8 @@ stamp-missing / does not exist arch-dir-missing // does not exist extra-arch-dir another gfx* directory sits beside // - kpack-missing kpack output expected but //kpack/hip_kernel_provider_ - .kpack is absent + kpack-missing kpack output expected for a pass but //kpack/ + hip_kernel_provider_.kpack is absent kpack-empty that archive has size 0 no-kdp no *.kdp.json under //, or one is not valid JSON ukd-count the number of shipped UKDs found in the --expect list differs @@ -34,13 +38,15 @@ ukd-kind a UKD's shipped kernel_source.kind is not what its expected kind's output type requires ("kpack", or the authored kind for a pass-through) - arch-field a KDP `arch` present and not [], or a UKD `arch` absent - or not [] + arch-field a KDP `arch` present and not [], or a UKD `arch` absent + or not [], where is the expected UKD's `arch` (for + a KDP, that of its expected UKDs; --arch when it has none) passthrough-source a pass-through UKD's kernel_source differs from the authored one passthrough-provenance a pass-through UKD's provenance.source_label is empty or its provenance.source_file differs from kernel_source.source_file - kpack-toc the archive is unreadable or has no entry for a UKD's toc_key + kpack-toc an archive is unreadable or has no entry, under its pass, for a + UKD's toc_key sha256 the archive blob's sha256 differs from kernel_source.sha256 signature a UKD's signature is not a non-empty list symbol a UKD's symbol does not appear in its archive blob @@ -59,7 +65,16 @@ import sys from pathlib import Path -from hkp_probe_kinds import KINDS +# Put the package dir ahead of everything, including this script's own dir: tools/ +# also holds a module literally named hkp_pack (hkp_pack.py), which would otherwise +# shadow the hkp_pack package when tools/ is sys.path[0]. +_PKG_ROOT = str(Path(__file__).resolve().parent.parent / "python") +while _PKG_ROOT in sys.path: + sys.path.remove(_PKG_ROOT) +sys.path.insert(0, _PKG_ROOT) + +from hkp_pack.pipeline import _kpack_filename # noqa: E402 +from hkp_probe_kinds import KINDS # noqa: E402 _PREFIX = "hkp_probe_assert" @@ -93,7 +108,7 @@ class _UsageError(Exception): def _load_expect(path): - """Read --expect into {(kdp, name): entry}; entry keeps `kind` and `kernel_source`.""" + """Read --expect into {(kdp, name): entry}; entry keeps its other fields.""" try: entries = json.loads(path.read_text(encoding="utf-8")) except (OSError, ValueError) as exc: @@ -103,10 +118,10 @@ def _load_expect(path): expected = {} for entry in entries: if not isinstance(entry, dict) or not all( - isinstance(entry.get(k), str) for k in ("kdp", "name", "kind") + isinstance(entry.get(k), str) for k in ("kdp", "name", "kind", "arch") ): raise _UsageError( - f"--expect {path} entry {entry!r} lacks string kdp, name and kind" + f"--expect {path} entry {entry!r} lacks string kdp, name, kind and arch" ) if _output_of(entry["kind"]) == "passthrough" and not isinstance( entry.get("kernel_source"), dict @@ -165,9 +180,9 @@ def _check_layout(args, failures): return arch_dir -def _check_kpack(args, arch_dir, failures): - """Check 4. Returns the archive path when it exists and is non-empty.""" - kpack_path = arch_dir / "kpack" / f"hip_kernel_provider_{args.arch}.kpack" +def _check_kpack(arch_dir, pass_arch, failures): + """Check 4 for one pass. Returns the archive path when it is non-empty.""" + kpack_path = arch_dir / "kpack" / _kpack_filename(pass_arch) if not kpack_path.is_file(): failures.add("kpack-missing", f"{kpack_path} does not exist") return None @@ -211,7 +226,7 @@ def _check_provenance(args, label, ukd, kind, failures): _PROVENANCE_CHECKS[name](args, label, prov, failures) -def _check_kpack_ukd(args, label, ukd, expected, archive, failures): +def _check_kpack_ukd(args, label, ukd, expected, archives, failures): source = ukd.get("kernel_source") or {} kind = source.get("kind") if kind != "kpack": @@ -222,7 +237,8 @@ def _check_kpack_ukd(args, label, ukd, expected, archive, failures): ) return - if ukd.get("arch") != [args.arch]: + pass_arch = expected["arch"] + if ukd.get("arch") != [pass_arch]: failures.add("arch-field", f"{label} arch is {ukd.get('arch')!r}") signature = source.get("signature") @@ -231,13 +247,14 @@ def _check_kpack_ukd(args, label, ukd, expected, archive, failures): _check_provenance(args, label, ukd, expected["kind"], failures) + archive = archives.get(pass_arch) if archive is None: return toc_key = source.get("toc_key") - blob = archive.get_kernel(toc_key, args.arch) if toc_key else None + blob = archive.get_kernel(toc_key, pass_arch) if toc_key else None if blob is None: failures.add( - "kpack-toc", f"{label} toc_key {toc_key!r} has no {args.arch} archive entry" + "kpack-toc", f"{label} toc_key {toc_key!r} has no {pass_arch} archive entry" ) return digest = hashlib.sha256(blob).hexdigest() @@ -252,7 +269,7 @@ def _check_kpack_ukd(args, label, ukd, expected, archive, failures): failures.add("symbol", f"{label} symbol {symbol!r} not found in archive blob") -def _check_passthrough_ukd(args, label, ukd, expected, archive, failures): +def _check_passthrough_ukd(args, label, ukd, expected, archives, failures): source = ukd.get("kernel_source") or {} kind = source.get("kind") if kind != expected["kind"]: @@ -263,7 +280,7 @@ def _check_passthrough_ukd(args, label, ukd, expected, archive, failures): ) return - if ukd.get("arch") != [args.arch]: + if ukd.get("arch") != [expected["arch"]]: failures.add("arch-field", f"{label} arch is {ukd.get('arch')!r}") if source != expected["kernel_source"]: @@ -293,7 +310,7 @@ def _check_passthrough_ukd(args, label, ukd, expected, archive, failures): _OUTPUT_CHECKS = {"kpack": _check_kpack_ukd, "passthrough": _check_passthrough_ukd} -def _check_ukd(args, kdp_name, ukd, archive, failures): +def _check_ukd(args, kdp_name, ukd, archives, failures): """Check one shipped UKD against its expectation. True when it was expected.""" label = f"{kdp_name}:{ukd.get('name', ukd.get('id', '?'))}" expected = args.expected.get((kdp_name, ukd.get("name"))) @@ -309,11 +326,17 @@ def _check_ukd(args, kdp_name, ukd, archive, failures): f"{label} expected kind {expected['kind']!r} has no rules", ) return True - _OUTPUT_CHECKS[output](args, label, ukd, expected, archive, failures) + _OUTPUT_CHECKS[output](args, label, ukd, expected, archives, failures) return True -def _check_descriptors(args, arch_dir, archive, failures): +def _kdp_arch(args, kdp_name): + """The arch a shipped KDP carries: the pass of its expected UKDs, else --arch.""" + passes = sorted({e["arch"] for (k, _), e in args.expected.items() if k == kdp_name}) + return passes if passes else [args.arch] + + +def _check_descriptors(args, arch_dir, archives, failures): """Checks 5-7: every UKD of every shipped KDP.""" kdps = sorted(arch_dir.rglob("*.kdp.json")) if not kdps: @@ -329,10 +352,10 @@ def _check_descriptors(args, arch_dir, archive, failures): if not isinstance(kdp, dict): failures.add("no-kdp", f"{name} is not a JSON object") continue - if "arch" in kdp and kdp["arch"] != [args.arch]: + if "arch" in kdp and kdp["arch"] != _kdp_arch(args, name): failures.add("arch-field", f"{name} arch is {kdp['arch']!r}") for ukd in kdp.get("kernelDescriptors", []): - if _check_ukd(args, name, ukd, archive, failures): + if _check_ukd(args, name, ukd, archives, failures): matched += 1 expected = len(args.expected) if matched != expected or matched < 1: @@ -343,9 +366,11 @@ def _check_descriptors(args, arch_dir, archive, failures): ) -def _needs_archive(args): - """Whether any expected UKD has kpack output, so the archive must exist.""" - return any(_output_of(e["kind"]) == "kpack" for e in args.expected.values()) +def _archive_passes(args): + """The passes with an expected UKD of kpack output, whose archive must exist.""" + return sorted( + {e["arch"] for e in args.expected.values() if _output_of(e["kind"]) == "kpack"} + ) def run(args): @@ -357,19 +382,19 @@ def run(args): arch_dir = _check_layout(args, failures) if arch_dir is None: return failures - archive = None - if _needs_archive(args): - kpack_path = _check_kpack(args, arch_dir, failures) + archives = {} + for pass_arch in _archive_passes(args): + kpack_path = _check_kpack(arch_dir, pass_arch, failures) if kpack_path is not None: kpack = _load_kpack(args.kpack_python_dir) try: - archive = kpack.PackedKernelArchive.read(kpack_path) + archives[pass_arch] = kpack.PackedKernelArchive.read(kpack_path) except Exception as exc: failures.add( "kpack-toc", f"{kpack_path} is unreadable: {type(exc).__name__}: {exc}", ) - _check_descriptors(args, arch_dir, archive, failures) + _check_descriptors(args, arch_dir, archives, failures) return failures diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py index 622161bdfc35..7c15df06f2d0 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py @@ -3,12 +3,16 @@ Copies every file under `--from` to `--out`, except that each KDP shipping for `--arch` has its inline `kernelDescriptors` reduced to the UKDs the probe packs, -among the UKDs that themselves ship for `--arch`. +among the UKDs that themselves ship for that pass. -The root is read with the packer's own loader (`hkp_pack.descriptors.load_flat_input`), -so derive sees exactly the descriptors the packer sees and rejects what the packer -rejects. Which KDPs ship for `--arch` is the packer's `kdp_survives`, and which of their -UKDs ship is the packer's `arch_matches`: an empty or absent `arch` list is a wildcard. +The root is read with the packer's own loader (`hkp_pack.descriptors.load_flat_input`) +and the generic target table `--generic-targets-json`, so derive sees exactly the +descriptors the packer sees and rejects what the packer rejects. `--arch m` packs the +passes the packer runs for `--arches m` (`hkp_pack.pipeline.generic_passes`): `m` itself +and every generic `g` with `m` among its members that some KDP ships for. Which KDPs +ship for a pass `p` is the packer's `kdp_survives(kdp, flat, p)`, and which of their UKDs +ship is the packer's `arch_matches`: an empty or absent `arch` list is a wildcard. A KDP +that ships for the generic pass is trimmed like one that ships for `m`. Default mode (no `--ukd`): UKDs are partitioned into compile groups, where the group key is the kind plus the kernel_source fields hkp_probe_kinds.KINDS names for it: @@ -41,16 +45,28 @@ `expect.json` is written beside `--out` (`/expect.json`): a list of {"kdp": , "name": , "kind": }, one entry per kept UKD, for hkp_probe_assert.py. Entries of kinds -whose packed output is a pass-through also carry the authored "kernel_source". +kernel_source.kind>, "arch": }, +one entry per kept UKD, for hkp_probe_assert.py. Entries of kinds whose packed output is +a pass-through also carry the authored "kernel_source". Files are written only when their content differs and files absent from the derived set are removed, so deriving twice leaves every mtime of the derived root alone and the pack stamp stays fresh across a reconfigure. -`--list-arches --from ` prints, one per line and sorted, the union of the -explicit `arch` entries of every KDP and UKD (inline and standalone) under the root. -Descriptors with no `arch` (wildcards) contribute nothing. +`--list-arches --from --generic-targets-json [--exclude ]...` +prints, one per line and sorted by probe arch, the probe arches of the explicit `arch` +entries of every KDP and UKD (inline and standalone) under the root. Descriptors with no +`arch` (wildcards) contribute nothing. A line is the arch followed by the generics it +stands for, space separated: + + - a concrete or unlisted name prints as itself (`gfx942`; a TheRock family name prints + too, for the caller to skip; a `*-generic` absent from the table exits 2, as the + packer's loader rejects it); + - a generic in the table prints as its first member, in table order, followed by the + generic (`gfx1100 gfx11-generic`), unless any of its members is `--exclude`d (the + build's own targets, whose production pack already compiles the generic); + - one line per probe arch: a member that is also named explicitly, or chosen for two + generics, shares one line (`gfx1100 gfx11-generic`). Standalone UKDs (a `kernelDescriptors` entry that is a bare id) are out of probe scope by policy: a KDP that would be trimmed and references one is refused. @@ -87,6 +103,8 @@ load_flat_input, ) # noqa: E402 from hkp_pack.errors import HkpPackError # noqa: E402 +from hkp_pack.generic_targets import GenericTargets # noqa: E402 +from hkp_pack.pipeline import generic_passes # noqa: E402 from hkp_probe_kinds import KINDS # noqa: E402 _PREFIX = "hkp_probe_derive:" @@ -100,10 +118,10 @@ def _discard(*_args, **_kwargs): pass -def _load(src): +def _load(src, table): """The packer's view of `src`; DeriveError when the packer rejects it.""" try: - return load_flat_input(src, log=_discard) + return load_flat_input(src, table, log=_discard) except HkpPackError as exc: raise DeriveError(str(exc)) from exc @@ -230,7 +248,7 @@ def _derive_kdp_listed(kdp, rel, arch, wanted): return kept -def _plan(src, arch, ukds=None): +def _plan(src, arch, table, ukds=None): """Return (files, expect): files maps relative posix path -> source Path or bytes. `ukds` is the list of UKD names to keep, or None for the default selection. @@ -241,7 +259,8 @@ def _plan(src, arch, ukds=None): raise DeriveError(f"--ukd lists {dupes} more than once") wanted = set(ukds) - flat = _load(src) + flat = _load(src, table) + passes = [arch] + generic_passes(flat, [arch]) files = {} for p in sorted(src.rglob("*")): if p.is_file(): @@ -251,12 +270,15 @@ def _plan(src, arch, ukds=None): kdps = {d.path.relative_to(src).as_posix(): d.doc for d in flat.kdps()} for rel in sorted(kdps): kdp = kdps[rel] - if not kdp_survives(kdp, flat, arch): + # A KDP ships under one pass: the arch lists of the two never overlap, since + # the packer rejects a list holding a generic with one of its members. + packed_as = next((p for p in passes if kdp_survives(kdp, flat, p)), None) + if packed_as is None: continue if ukds is None: - kept = _derive_kdp(kdp, rel, arch) + kept = _derive_kdp(kdp, rel, packed_as) else: - kept = _derive_kdp_listed(kdp, rel, arch, wanted) + kept = _derive_kdp_listed(kdp, rel, packed_as, wanted) if not kept: # Only listed UKDs are packed, so this KDP must not ship in full. del files[rel] @@ -264,7 +286,12 @@ def _plan(src, arch, ukds=None): kdp = dict(kdp, kernelDescriptors=kept) files[rel] = (json.dumps(kdp, indent=2) + "\n").encode("utf-8") for u in kept: - entry = {"kdp": rel, "name": u["name"], "kind": u["kernel_source"]["kind"]} + entry = { + "kdp": rel, + "name": u["name"], + "kind": u["kernel_source"]["kind"], + "arch": packed_as, + } if KINDS[entry["kind"]].output == "passthrough": # The packer ships the block as authored; the assertion compares it. entry["kernel_source"] = u["kernel_source"] @@ -281,9 +308,8 @@ def _plan(src, arch, ukds=None): return files, expect -def list_arches(src: Path) -> list[str]: - """Sorted union of the explicit `arch` entries of every KDP and UKD under `src`.""" - flat = _load(src) +def _named_arches(flat): + """The union of the explicit `arch` entries of every KDP and UKD in `flat`.""" arches = set() for kdp in flat.kdps(): arches.update(kdp.doc.get("arch") or []) @@ -292,7 +318,21 @@ def list_arches(src: Path) -> list[str]: arches.update(entry.get("arch") or []) for ukd in flat.ukds(): arches.update(ukd.doc.get("arch") or []) - return sorted(arches) + return arches + + +def list_arches(src: Path, table, excluded) -> list[str]: + """The probe arch lines for `src`; see the module docstring for the format.""" + named = _named_arches(_load(src, table)) + probes = {} + for name in named: + if not table.has(name): + probes.setdefault(name, set()) + else: + members = table.members(name) + if members and not any(m in excluded for m in members): + probes.setdefault(members[0], set()).add(name) + return [" ".join([arch, *sorted(probes[arch])]) for arch in sorted(probes)] def _same(dest, content): @@ -334,7 +374,7 @@ def _sync(out, files): def derive_root( - src: Path, arch: str, out: Path, ukds: list[str] | None = None + src: Path, arch: str, out: Path, table, ukds: list[str] | None = None ) -> str | None: """Write the derived root to `out`; return an error message, or None on success. @@ -343,7 +383,7 @@ def derive_root( if not src.is_dir(): return f"{_PREFIX} {src} is not a directory" try: - files, expect = _plan(src, arch, ukds) + files, expect = _plan(src, arch, table, ukds) except DeriveError as exc: return f"{_PREFIX} {exc}" @@ -361,10 +401,25 @@ def derive_root( def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--from", dest="src", type=Path, required=True) + parser.add_argument( + "--generic-targets-json", + type=Path, + required=True, + help="the generic GPU target table (gpu_generic_targets.json)", + ) parser.add_argument( "--list-arches", action="store_true", - help="print the explicit arches of the root's KDPs and UKDs, one per line", + help="print the probe arches of the root's KDPs and UKDs, one line each", + ) + parser.add_argument( + "--exclude", + dest="excluded", + action="append", + default=[], + metavar="ARCH", + help="with --list-arches: a build target (repeatable); a generic with a " + "member among them gets no probe arch", ) parser.add_argument("--arch", help="architecture to probe, e.g. gfx950") parser.add_argument("--out", type=Path) @@ -378,14 +433,24 @@ def main(argv: list[str] | None = None) -> int: ) args = parser.parse_args(argv) + if args.excluded and not args.list_arches: + parser.error("--exclude requires --list-arches") + try: + table = GenericTargets.load(args.generic_targets_json) + except HkpPackError as exc: + print(f"{_PREFIX} {exc}", file=sys.stderr) + return 2 + if args.list_arches: if args.arch or args.out or args.ukds: - parser.error("--list-arches takes only --from") + parser.error( + "--list-arches takes only --from, --generic-targets-json and --exclude" + ) if not args.src.is_dir(): print(f"{_PREFIX} {args.src} is not a directory", file=sys.stderr) return 2 try: - arches = list_arches(args.src) + arches = list_arches(args.src, table, set(args.excluded)) except DeriveError as exc: print(f"{_PREFIX} {exc}", file=sys.stderr) return 2 @@ -395,7 +460,7 @@ def main(argv: list[str] | None = None) -> int: if not args.arch or args.out is None: parser.error("--arch and --out are required unless --list-arches is given") - error = derive_root(args.src, args.arch, args.out, args.ukds) + error = derive_root(args.src, args.arch, args.out, table, args.ukds) if error is not None: print(error, file=sys.stderr) return 2 From dfe2688a7be0858292abdaac0276e3878eaed5f9 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 23:42:48 +0000 Subject: [PATCH 45/52] hkp: skip packaging probes when rocKE is disabled --- .../cmake/HkpPackaging.cmake | 18 +++++++++--------- .../tools/ai/skills/hipdnn-superbuild/SKILL.md | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake index 351402c66969..13d75b6e3872 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake @@ -1724,16 +1724,16 @@ function(hkp_add_packaging) hkp_register_tests("${_rocm_kpack_dir}" "${HKP_HIPCC}" "${_rocke_comgr_lib}") if(HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES) - if(NOT HIPKERNELPROVIDER_ENABLE_ROCKE) - message(FATAL_ERROR - "hkp: HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON requires " - "HIPKERNELPROVIDER_ENABLE_ROCKE=ON: probes pack rocKE descriptors " - "with the resolved rocKE toolchain.") + if(HIPKERNELPROVIDER_ENABLE_ROCKE) + include("${HKP_PKG_DIR}/cmake/HkpPackagingProbes.cmake") + hkp_load_packaging_probes( + "${_rocm_kpack_dir}" "${HKP_HIPCC}" "${_rocke_comgr_lib}" + ENABLE_ROCKE ON ${_rocke_resolved_args}) + else() + message(STATUS + "hkp: packaging probes skipped (HIPKERNELPROVIDER_ENABLE_ROCKE=OFF); " + "probes pack rocKE descriptors with the rocKE toolchain") endif() - include("${HKP_PKG_DIR}/cmake/HkpPackagingProbes.cmake") - hkp_load_packaging_probes( - "${_rocm_kpack_dir}" "${HKP_HIPCC}" "${_rocke_comgr_lib}" - ENABLE_ROCKE ON ${_rocke_resolved_args}) endif() endfunction() diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md index 929b36b88f5e..620f6f305d3f 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md @@ -79,7 +79,7 @@ on Windows the wheel venv's `_rocm_sdk_devel/bin`. | `HIPKERNELPROVIDER_PRODUCTION_SOURCE_ROOT` | the in-tree `.../kernel_ingestor_engine/descriptors` | `CACHE PATH` naming the authored tree production packaging packages from. Every root, production or test, that is empty or has nothing to pack for the selected architectures under the build's filters is dormant: skipped at pack, its stale output tree removed, not an error. The in-tree root carries the rocKE `gfx950_attention_dense` bundle. Set but not a directory is fatal. | | `HIPKERNELPROVIDER_KPACK_PYTHON_DIR` | unset | Directory **containing** `rocm_kpack/`; this locates a package, not a compiler interpreter. | | `Python3_EXECUTABLE` | system | Explicit environment for packaging dependencies such as `msgpack` and `zstandard`; production compilation retains its selected hermetic wheel interpreter. | - | `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` | OFF | Packaging probes: one automatic probe per architecture explicitly named under the production descriptor root and absent from `GPU_TARGETS`, plus named probes in `descriptor-packaging/probes/probes.cmake` (`hkp_add_packaging_probe(ARCH NAME UKDS ...)`); superbuild-only, ON in the superbuild CI lanes. Each probe packs its descriptors for `ARCH` (any architecture) independent of `GPU_TARGETS`. When ON, the probe pack targets are part of `all`; `cmake --build --target hkp_packaging_probes` builds them alone. The ctest entries are `hkp-probe-` plus `hkp-probe-tools`. | + | `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` | OFF | Packaging probes: one automatic probe per architecture explicitly named under the production descriptor root and absent from `GPU_TARGETS`, plus named probes in `descriptor-packaging/probes/probes.cmake` (`hkp_add_packaging_probe(ARCH NAME UKDS ...)`); superbuild-only, ON in the superbuild CI lanes; skipped with a STATUS line when `HIPKERNELPROVIDER_ENABLE_ROCKE` is OFF. Each probe packs its descriptors for `ARCH` (any architecture) independent of `GPU_TARGETS`. When ON, the probe pack targets are part of `all`; `cmake --build --target hkp_packaging_probes` builds them alone. The ctest entries are `hkp-probe-` plus `hkp-probe-tools`. | These flags only do anything on a preset that actually builds hip-kernel-provider. The default `hipdnn-providers` preset does **not** include it; the presets that do are From 462f8eed1bc1289828592a6727adced8223674b5 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 23:53:22 +0000 Subject: [PATCH 46/52] hkp: probe the non-rocKE kinds when rocKE is disabled --- .../descriptor-packaging/README.md | 6 ++ .../cmake/HkpPackaging.cmake | 13 +--- .../cmake/HkpPackagingProbes.cmake | 35 ++++++++-- .../tests/test_hkp_probe_derive_root.py | 53 ++++++++++++++- .../tools/hkp_probe_derive_root.py | 64 +++++++++++++++---- .../ai/skills/hipdnn-superbuild/SKILL.md | 2 +- 6 files changed, 140 insertions(+), 33 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 72b32d6c420a..7831612d5e97 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -318,6 +318,12 @@ builds them alone. Probe output is written under `/hkp-probes//`, o shipped descriptor trees, and is never installed. With the option `OFF` the only difference is that cache entry; the probe module is never read. +Probes read and pack the root under the same filters the production pack gets. With +`HIPKERNELPROVIDER_ENABLE_ROCKE=OFF` the `rocKE/` family folder and every `rocke` UKD are +neither packed nor expected, so a probe covers whatever other kinds the arch carries, and +an architecture whose only content is rocKE gets no probe. Probes run for every producer +the build has; none is skipped wholesale. + **What a probe proves** - The descriptors it packs, taken from the real root (ids, the specialization contract, diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake index 13d75b6e3872..b38119585ec4 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake @@ -1724,16 +1724,9 @@ function(hkp_add_packaging) hkp_register_tests("${_rocm_kpack_dir}" "${HKP_HIPCC}" "${_rocke_comgr_lib}") if(HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES) - if(HIPKERNELPROVIDER_ENABLE_ROCKE) - include("${HKP_PKG_DIR}/cmake/HkpPackagingProbes.cmake") - hkp_load_packaging_probes( - "${_rocm_kpack_dir}" "${HKP_HIPCC}" "${_rocke_comgr_lib}" - ENABLE_ROCKE ON ${_rocke_resolved_args}) - else() - message(STATUS - "hkp: packaging probes skipped (HIPKERNELPROVIDER_ENABLE_ROCKE=OFF); " - "probes pack rocKE descriptors with the rocKE toolchain") - endif() + include("${HKP_PKG_DIR}/cmake/HkpPackagingProbes.cmake") + hkp_load_packaging_probes( + "${_rocm_kpack_dir}" "${HKP_HIPCC}" "${_rocke_comgr_lib}" ${_root_args}) endif() endfunction() diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake index f77545669e5d..667e0d9be133 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -51,6 +51,7 @@ function(_hkp_probe_derive_root name from_dir arch out_dir expect_var) PROPERTY CMAKE_CONFIGURE_DEPENDS ${_derive_inputs} "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" "${HKP_PKG_DIR}/tools/hkp_probe_kinds.py") + get_property(_filter_args GLOBAL PROPERTY HKP_PROBE_FILTER_ARGS) set(_ukd_args "") foreach(_ukd IN LISTS ARGN) list(APPEND _ukd_args --ukd "${_ukd}") @@ -61,6 +62,7 @@ function(_hkp_probe_derive_root name from_dir arch out_dir expect_var) --from "${from_dir}" --arch "${arch}" --out "${out_dir}" + ${_filter_args} ${_ukd_args} RESULT_VARIABLE _derive_rc OUTPUT_VARIABLE _derive_out @@ -213,7 +215,7 @@ function(hkp_add_packaging_probe) get_property(_kpack_dir GLOBAL PROPERTY HKP_PROBE_ROCM_KPACK_DIR) get_property(_hipcc GLOBAL PROPERTY HKP_PROBE_HIPCC) get_property(_comgr_lib GLOBAL PROPERTY HKP_PROBE_COMGR_LIB) - get_property(_rocke_args GLOBAL PROPERTY HKP_PROBE_ROCKE_ARGS) + get_property(_root_args GLOBAL PROPERTY HKP_PROBE_ROOT_ARGS) set(_probe_dir "${CMAKE_BINARY_DIR}/hkp-probes/${ARG_NAME}") set(_out_root "${_probe_dir}/out") @@ -232,7 +234,7 @@ function(hkp_add_packaging_probe) HIPCC "${_hipcc}" ROCM_KPACK_DIR "${_kpack_dir}" OUT_ROOT "${_out_root}" - ${_rocke_args} + ${_root_args} PACK_JOBS "${ARG_PACK_JOBS}") set(_assert_args @@ -266,6 +268,7 @@ endfunction() # re-runs when a file under the root or the derive tooling changes. # --------------------------------------------------------------------------- function(_hkp_probe_automatic_arches out_var) + get_property(_filter_args GLOBAL PROPERTY HKP_PROBE_FILTER_ARGS) set(_root "${HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT}") file(GLOB_RECURSE _root_inputs CONFIGURE_DEPENDS "${_root}/*") set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND @@ -275,7 +278,7 @@ function(_hkp_probe_automatic_arches out_var) execute_process( COMMAND "${Python3_EXECUTABLE}" "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" - --list-arches --from "${_root}" + --list-arches --from "${_root}" ${_filter_args} RESULT_VARIABLE _rc OUTPUT_VARIABLE _out ERROR_VARIABLE _err) @@ -317,9 +320,14 @@ function(_hkp_probe_automatic_arches out_var) endfunction() # --------------------------------------------------------------------------- -# hkp_load_packaging_probes( [...]) -# Store the toolchain hkp_add_packaging resolved, load the named probes from -# probes/probes.cmake, then declare one automatic probe per architecture the production +# hkp_load_packaging_probes( [...]) +# Store the toolchain hkp_add_packaging resolved ( are the keywords every +# production root is wired with: ENABLE_ROCKE, the ROCKE_* toolchain when ON, and +# EXCLUDE_FOLDERS). A build lacking a producer (rocKE OFF) still probes: derive and the +# pack read the root under the same excluded folders and disabled kinds the production +# pack gets, so rocKE content is neither packed nor expected and every other kind is +# probed as usual; an arch left with no content names no probe. Load the named +# probes from probes/probes.cmake, then declare one automatic probe per architecture the production # root names that the build does not target: the explicit `arch` entries of its KDPs # and UKDs (hkp_probe_derive_root.py --list-arches; wildcard descriptors name none) # minus hkp_selected_arches(), each as hkp_add_packaging_probe(ARCH ). Register @@ -333,7 +341,20 @@ function(hkp_load_packaging_probes rocm_kpack_dir hipcc rocke_comgr_lib) set_property(GLOBAL PROPERTY HKP_PROBE_ROCM_KPACK_DIR "${rocm_kpack_dir}") set_property(GLOBAL PROPERTY HKP_PROBE_HIPCC "${hipcc}") set_property(GLOBAL PROPERTY HKP_PROBE_COMGR_LIB "${rocke_comgr_lib}") - set_property(GLOBAL PROPERTY HKP_PROBE_ROCKE_ARGS "${ARGN}") + set_property(GLOBAL PROPERTY HKP_PROBE_ROOT_ARGS "${ARGN}") + # The filters the production pack runs under: derive and the arch listing read the + # root under them, so what a probe expects is what its pack ships. + _hkp_disabled_families(_exclude_folders) + _hkp_disabled_kinds(_disabled_pairs "${HIPKERNELPROVIDER_ENABLE_ROCKE}") + set(_filter_args "") + foreach(_folder IN LISTS _exclude_folders) + list(APPEND _filter_args --exclude-folder "${_folder}") + endforeach() + foreach(_pair IN LISTS _disabled_pairs) + string(REGEX REPLACE "=.*$" "" _kind "${_pair}") + list(APPEND _filter_args --disable-kind "${_kind}") + endforeach() + set_property(GLOBAL PROPERTY HKP_PROBE_FILTER_ARGS "${_filter_args}") set_property(GLOBAL PROPERTY HKP_PROBE_NAMES "") set_property(GLOBAL PROPERTY HKP_PROBE_TOOLCHAIN_SET TRUE) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py index 800845fbb84a..7bfe623dafbb 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py @@ -563,9 +563,16 @@ def test_rederive_follows_source_changes(tmp_path): # --- --list-arches ---------------------------------------------------------- -def _list_arches(src: Path): +def _list_arches(src: Path, *filters: str): r = subprocess.run( - [sys.executable, str(PROBE_DERIVE), "--list-arches", "--from", str(src)], + [ + sys.executable, + str(PROBE_DERIVE), + "--list-arches", + "--from", + str(src), + *filters, + ], capture_output=True, text=True, check=False, @@ -605,6 +612,48 @@ def test_list_arches_packer_rejection_exits_2(tmp_path): assert "is not a subset of KDP" in r.stderr +# --- disabled kinds and excluded folders ------------------------------------- +def test_disabled_kind_is_neither_kept_nor_expected(tmp_path): + src = tmp_path / "src" + _write_root(src, [_rocke("r", "b1"), _hip("h", "a.cpp")]) + out = tmp_path / "w" / "root" + r = subprocess.run( + [ + sys.executable, + str(PROBE_DERIVE), + "--from", + str(src), + "--arch", + "gfx950", + "--out", + str(out), + "--disable-kind", + "rocke", + ], + capture_output=True, + text=True, + check=False, + ) + assert r.returncode == 0, r.stderr + assert _kept(out) == ["h"] + expect = json.loads((out.parent / "expect.json").read_text()) + assert [(e["kind"], e["name"]) for e in expect] == [("hip", "h")] + + +def test_list_arches_ignores_content_of_disabled_kind_and_excluded_folder(tmp_path): + src = tmp_path / "src" + _write_root(src, [_hip("h", arch=["gfx942"])], kdp_arch=("gfx942",)) + _write_root(src, [_rocke("r", arch=["gfx950"])], kdp_file="rocKE/r.kdp.json") + _write_root( + src, + [_rocke("q", arch=["gfx1201"])], + kdp_arch=("gfx1201",), + kdp_file="q.kdp.json", + ) + assert _list_arches(src, "--exclude-folder", "rocKE") == ["gfx1201", "gfx942"] + assert _list_arches(src, "--disable-kind", "rocke") == ["gfx942"] + + # --- the kinds registry ----------------------------------------------------- def _tool_module(name): sys.path.insert(0, str(PROBE_DERIVE.parent)) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py index 622161bdfc35..b655b2a2b7c4 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py @@ -10,6 +10,11 @@ rejects. Which KDPs ship for `--arch` is the packer's `kdp_survives`, and which of their UKDs ship is the packer's `arch_matches`: an empty or absent `arch` list is a wildcard. +`--exclude-folder` and `--disable-kind` are the packer's `exclude_folders` and +`disabled_kinds`: the filters of a build that lacks a producer (rocKE OFF). Derive reads +the root under them, so a UKD of a disabled kind is neither kept nor expected, and a KDP +left with no UKD names no arch. The probe's pack step gets the same filters. + Default mode (no `--ukd`): UKDs are partitioned into compile groups, where the group key is the kind plus the kernel_source fields hkp_probe_kinds.KINDS names for it: @@ -100,10 +105,18 @@ def _discard(*_args, **_kwargs): pass -def _load(src): - """The packer's view of `src`; DeriveError when the packer rejects it.""" +def _load(src, filters): + """The packer's view of `src` under `filters` (a (exclude_folders, + disabled_kinds) pair, as the pack step gets them); DeriveError when the packer + rejects it.""" + exclude_folders, disabled_kinds = filters try: - return load_flat_input(src, log=_discard) + return load_flat_input( + src, + log=_discard, + exclude_folders=exclude_folders, + disabled_kinds=disabled_kinds, + ) except HkpPackError as exc: raise DeriveError(str(exc)) from exc @@ -230,7 +243,7 @@ def _derive_kdp_listed(kdp, rel, arch, wanted): return kept -def _plan(src, arch, ukds=None): +def _plan(src, arch, ukds=None, filters=((), ())): """Return (files, expect): files maps relative posix path -> source Path or bytes. `ukds` is the list of UKD names to keep, or None for the default selection. @@ -241,7 +254,7 @@ def _plan(src, arch, ukds=None): raise DeriveError(f"--ukd lists {dupes} more than once") wanted = set(ukds) - flat = _load(src) + flat = _load(src, filters) files = {} for p in sorted(src.rglob("*")): if p.is_file(): @@ -281,9 +294,11 @@ def _plan(src, arch, ukds=None): return files, expect -def list_arches(src: Path) -> list[str]: - """Sorted union of the explicit `arch` entries of every KDP and UKD under `src`.""" - flat = _load(src) +def list_arches(src: Path, filters=((), ())) -> list[str]: + """Sorted union of the explicit `arch` entries of every KDP and UKD under `src` + that survive `filters`: a KDP whose UKDs are all of a disabled kind, or that sits + in an excluded folder, names no arch.""" + flat = _load(src, filters) arches = set() for kdp in flat.kdps(): arches.update(kdp.doc.get("arch") or []) @@ -334,7 +349,11 @@ def _sync(out, files): def derive_root( - src: Path, arch: str, out: Path, ukds: list[str] | None = None + src: Path, + arch: str, + out: Path, + ukds: list[str] | None = None, + filters=((), ()), ) -> str | None: """Write the derived root to `out`; return an error message, or None on success. @@ -343,7 +362,7 @@ def derive_root( if not src.is_dir(): return f"{_PREFIX} {src} is not a directory" try: - files, expect = _plan(src, arch, ukds) + files, expect = _plan(src, arch, ukds, filters) except DeriveError as exc: return f"{_PREFIX} {exc}" @@ -366,6 +385,22 @@ def main(argv: list[str] | None = None) -> int: action="store_true", help="print the explicit arches of the root's KDPs and UKDs, one per line", ) + parser.add_argument( + "--exclude-folder", + action="append", + default=[], + metavar="NAME", + help="top-level folder of the root this build does not pack (repeatable); " + "the packer's own exclude_folders", + ) + parser.add_argument( + "--disable-kind", + action="append", + default=[], + metavar="KIND", + help="kernel_source kind this build has no producer for (repeatable); " + "the packer's own disabled_kinds", + ) parser.add_argument("--arch", help="architecture to probe, e.g. gfx950") parser.add_argument("--out", type=Path) parser.add_argument( @@ -377,15 +412,18 @@ def main(argv: list[str] | None = None) -> int: "one UKD or a value sweep (see the module docstring)", ) args = parser.parse_args(argv) + filters = (tuple(args.exclude_folder), tuple(args.disable_kind)) if args.list_arches: if args.arch or args.out or args.ukds: - parser.error("--list-arches takes only --from") + parser.error( + "--list-arches takes only --from, --exclude-folder and --disable-kind" + ) if not args.src.is_dir(): print(f"{_PREFIX} {args.src} is not a directory", file=sys.stderr) return 2 try: - arches = list_arches(args.src) + arches = list_arches(args.src, filters) except DeriveError as exc: print(f"{_PREFIX} {exc}", file=sys.stderr) return 2 @@ -395,7 +433,7 @@ def main(argv: list[str] | None = None) -> int: if not args.arch or args.out is None: parser.error("--arch and --out are required unless --list-arches is given") - error = derive_root(args.src, args.arch, args.out, args.ukds) + error = derive_root(args.src, args.arch, args.out, args.ukds, filters) if error is not None: print(error, file=sys.stderr) return 2 diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md index 620f6f305d3f..edce85685d2e 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-superbuild/SKILL.md @@ -79,7 +79,7 @@ on Windows the wheel venv's `_rocm_sdk_devel/bin`. | `HIPKERNELPROVIDER_PRODUCTION_SOURCE_ROOT` | the in-tree `.../kernel_ingestor_engine/descriptors` | `CACHE PATH` naming the authored tree production packaging packages from. Every root, production or test, that is empty or has nothing to pack for the selected architectures under the build's filters is dormant: skipped at pack, its stale output tree removed, not an error. The in-tree root carries the rocKE `gfx950_attention_dense` bundle. Set but not a directory is fatal. | | `HIPKERNELPROVIDER_KPACK_PYTHON_DIR` | unset | Directory **containing** `rocm_kpack/`; this locates a package, not a compiler interpreter. | | `Python3_EXECUTABLE` | system | Explicit environment for packaging dependencies such as `msgpack` and `zstandard`; production compilation retains its selected hermetic wheel interpreter. | - | `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` | OFF | Packaging probes: one automatic probe per architecture explicitly named under the production descriptor root and absent from `GPU_TARGETS`, plus named probes in `descriptor-packaging/probes/probes.cmake` (`hkp_add_packaging_probe(ARCH NAME UKDS ...)`); superbuild-only, ON in the superbuild CI lanes; skipped with a STATUS line when `HIPKERNELPROVIDER_ENABLE_ROCKE` is OFF. Each probe packs its descriptors for `ARCH` (any architecture) independent of `GPU_TARGETS`. When ON, the probe pack targets are part of `all`; `cmake --build --target hkp_packaging_probes` builds them alone. The ctest entries are `hkp-probe-` plus `hkp-probe-tools`. | + | `HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES` | OFF | Packaging probes: one automatic probe per architecture explicitly named under the production descriptor root and absent from `GPU_TARGETS`, plus named probes in `descriptor-packaging/probes/probes.cmake` (`hkp_add_packaging_probe(ARCH NAME UKDS ...)`); superbuild-only, ON in the superbuild CI lanes; with `HIPKERNELPROVIDER_ENABLE_ROCKE` OFF the rocKE content is neither packed nor expected and the other kinds are probed as usual. Each probe packs its descriptors for `ARCH` (any architecture) independent of `GPU_TARGETS`. When ON, the probe pack targets are part of `all`; `cmake --build --target hkp_packaging_probes` builds them alone. The ctest entries are `hkp-probe-` plus `hkp-probe-tools`. | These flags only do anything on a preset that actually builds hip-kernel-provider. The default `hipdnn-providers` preset does **not** include it; the presets that do are From fa58624a2a83168b8a164d5ba7d0f01fbf69a98e Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 7 Oct 2026 23:57:19 +0000 Subject: [PATCH 47/52] hkp: share one disabled-kind list across pack, wiring and probes --- .../cmake/HkpPackaging.cmake | 22 ++++++++++++------- .../cmake/HkpPackagingProbes.cmake | 14 ++++++------ 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake index b38119585ec4..6161eca2a5b0 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake @@ -162,6 +162,17 @@ function(_hkp_disabled_kinds out_var enable_rocke) set(${out_var} "${_pairs}" PARENT_SCOPE) endfunction() +# --------------------------------------------------------------------------- +# _hkp_disabled_kind_names( ) +# The bare kind names of _hkp_disabled_kinds(), the form the packer's --disable-kind +# and load_flat_input(disabled_kinds=) take. Every consumer of the list reads it here. +# --------------------------------------------------------------------------- +function(_hkp_disabled_kind_names out_var enable_rocke) + _hkp_disabled_kinds(_pairs "${enable_rocke}") + list(TRANSFORM _pairs REPLACE "=.*$" "") + set(${out_var} "${_pairs}" PARENT_SCOPE) +endfunction() + # --------------------------------------------------------------------------- # _hkp_pack_producer() # Resolve the producer half of one pack step into the caller's scope: @@ -205,10 +216,9 @@ function(_hkp_pack_producer) "${Python3_EXECUTABLE}" "${HKP_TOOL}" PARENT_SCOPE) # A kind whose producer this build never made prunes like an arch-pruned UKD # rather than reaching a producer that does not exist. - _hkp_disabled_kinds(_disabled_pairs "${ARG_ENABLE_ROCKE}") + _hkp_disabled_kind_names(_kinds "${ARG_ENABLE_ROCKE}") set(_disable_args "") - foreach(_pair IN LISTS _disabled_pairs) - string(REGEX REPLACE "=.*$" "" _kind "${_pair}") + foreach(_kind IN LISTS _kinds) list(APPEND _disable_args --disable-kind "${_kind}") endforeach() set(_producer_arg ${_disable_args} PARENT_SCOPE) @@ -1569,11 +1579,7 @@ function(_hkp_wire_root) if(NOT W_SOURCE_ROOT) set(_reason "empty-root") else() - set(_kinds "") - foreach(_pair IN LISTS _disabled_pairs) - string(REGEX REPLACE "=.*$" "" _kind "${_pair}") - list(APPEND _kinds "${_kind}") - endforeach() + _hkp_disabled_kind_names(_kinds "${W_ENABLE_ROCKE}") _hkp_root_probe(_shipped _offered _probe_ok "${W_SOURCE_ROOT}" "${W_ARCHES}" "${W_EXCLUDE_FOLDERS}" "${_kinds}") set_property(GLOBAL PROPERTY HKP_PACK_PROBE_OK_${W_NAME} "${_probe_ok}") diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake index 667e0d9be133..19f24ca4e033 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -342,16 +342,16 @@ function(hkp_load_packaging_probes rocm_kpack_dir hipcc rocke_comgr_lib) set_property(GLOBAL PROPERTY HKP_PROBE_HIPCC "${hipcc}") set_property(GLOBAL PROPERTY HKP_PROBE_COMGR_LIB "${rocke_comgr_lib}") set_property(GLOBAL PROPERTY HKP_PROBE_ROOT_ARGS "${ARGN}") - # The filters the production pack runs under: derive and the arch listing read the - # root under them, so what a probe expects is what its pack ships. - _hkp_disabled_families(_exclude_folders) - _hkp_disabled_kinds(_disabled_pairs "${HIPKERNELPROVIDER_ENABLE_ROCKE}") + # The filters the production roots were wired with, read from those very arguments: + # derive and the arch listing see the root as the pack step does, so what a probe + # expects is what its pack ships. + cmake_parse_arguments(_F "" "ENABLE_ROCKE" "EXCLUDE_FOLDERS" ${ARGN}) + _hkp_disabled_kind_names(_kinds "${_F_ENABLE_ROCKE}") set(_filter_args "") - foreach(_folder IN LISTS _exclude_folders) + foreach(_folder IN LISTS _F_EXCLUDE_FOLDERS) list(APPEND _filter_args --exclude-folder "${_folder}") endforeach() - foreach(_pair IN LISTS _disabled_pairs) - string(REGEX REPLACE "=.*$" "" _kind "${_pair}") + foreach(_kind IN LISTS _kinds) list(APPEND _filter_args --disable-kind "${_kind}") endforeach() set_property(GLOBAL PROPERTY HKP_PROBE_FILTER_ARGS "${_filter_args}") From 26e3722617b2c5083638c251132cc76632341a1c Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Thu, 8 Oct 2026 00:14:46 +0000 Subject: [PATCH 48/52] hkp: probe tests that pack rocKE live under probes/tests/rocke; skip filtered named probes --- .../descriptor-packaging/README.md | 11 +- .../cmake/HkpPackagingProbes.cmake | 51 ++++++-- .../probes/tests/probe_support.py | 116 +++++++++++------- .../probes/tests/rocke/conftest.py | 14 +++ .../test_hkp_probe_assert_rocke.py} | 0 .../tests/rocke/test_hkp_probe_mixed_rocke.py | 52 ++++++++ .../tests/test_hkp_probe_derive_root.py | 32 +++++ .../probes/tests/test_hkp_probe_end_to_end.py | 45 +++---- .../tools/hkp_probe_derive_root.py | 38 ++++-- 9 files changed, 270 insertions(+), 89 deletions(-) create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/conftest.py rename dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/{test_hkp_probe_assert.py => rocke/test_hkp_probe_assert_rocke.py} (100%) create mode 100644 dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/test_hkp_probe_mixed_rocke.py diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 7831612d5e97..5123c4df18b2 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -322,7 +322,9 @@ Probes read and pack the root under the same filters the production pack gets. W `HIPKERNELPROVIDER_ENABLE_ROCKE=OFF` the `rocKE/` family folder and every `rocke` UKD are neither packed nor expected, so a probe covers whatever other kinds the arch carries, and an architecture whose only content is rocKE gets no probe. Probes run for every producer -the build has; none is skipped wholesale. +the build has; none is skipped wholesale. A named probe whose listed `UKDS` all ship for its +`ARCH` but are removed by those filters is skipped with a STATUS line (derive exit 3); a +listed name that does not exist, or does not ship for `ARCH`, still fails configure. **What a probe proves** @@ -509,7 +511,9 @@ python3 .github/scripts/check_probe_junit.py \ The selector is `^hkp-probe-`, anchored: `-R` is an unanchored regex. It matches every probe (`hkp-probe-`) and `hkp-probe-tools`, the pytest suite of the probe tooling -(`probes/tests/`). `--no-tests=error` makes an empty selection a failure instead of a +(`probes/tests/`; the tests that pack rocKE content live in `probes/tests/rocke/` and +nowhere else, which a build with `HIPKERNELPROVIDER_ENABLE_ROCKE=OFF` never collects). +`--no-tests=error` makes an empty selection a failure instead of a green run that proved nothing. `manifest.txt` is written at configure and lists the ctest names that must run; the junit check fails unless every one ran and passed (a skipped, disabled or missing entry fails), and ignores testcases not in the manifest, so it also @@ -564,7 +568,8 @@ does not build it, with the default sample; nothing needs declaring. - **Another descriptor root:** one line with `ROOT `. - **New producer kind:** the packer must support the `kernel_source.kind` first. Then one `KINDS` entry in `tools/hkp_probe_kinds.py` (compile-group fields, provenance checks by - name, output type), one end-to-end test in `probes/tests/`, and the kinds list above. + name, output type), one end-to-end test in `probes/tests/` (in `probes/tests/rocke/` when it needs the rocKE + toolchain; `pack_root` packs without it unless `rocke=True`), and the kinds list above. - **New provenance check:** a function in `_PROVENANCE_CHECKS` (`tools/hkp_probe_assert.py`), its name in the kind's `provenance`, a row in the assertion-id table and a test showing the check fails on a mutated output. diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake index 19f24ca4e033..33b247789da0 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -67,6 +67,14 @@ function(_hkp_probe_derive_root name from_dir arch out_dir expect_var) RESULT_VARIABLE _derive_rc OUTPUT_VARIABLE _derive_out ERROR_VARIABLE _derive_err) + # Exit 3: every listed UKD ships for but this build's filters (rocKE OFF) + # remove it. The probe does not apply to this build; is set empty. + set(${expect_var} "" PARENT_SCOPE) + if(_derive_rc EQUAL 3) + string(STRIP "${_derive_err}" _derive_err) + message(STATUS "hkp: probe ${name} skipped: ${_derive_err}") + return() + endif() if(NOT _derive_rc EQUAL 0) message(FATAL_ERROR "hkp probe '${name}': deriving the probe root failed (exit " @@ -148,7 +156,9 @@ endfunction() # (combinations of values are not guaranteed). So the probe packs the real # descriptors but compiles each compile path, and each value, once. With UKDS, the probe packs exactly the listed UKDs (by # UKD name): KDPs keeping none of them are left out of the derived root, and -# configure fails when a listed name is kept by no KDP shipping for ARCH. The +# configure fails when a listed name is kept by no KDP shipping for ARCH, except that +# a probe whose listed UKDs all ship for ARCH but are removed by this build's filters +# (a rocke UKD with rocKE OFF) is skipped with a STATUS line, declaring nothing. The # assertion expects exactly the kept UKDs, each with the provenance of its producer # kind (see hkp_probe_assert.py). Adding a pack under an already probed ARCH needs no # new declaration unless the probe lists UKDS. @@ -225,6 +235,9 @@ function(hkp_add_packaging_probe) set(_root "${_probe_dir}/root") _hkp_probe_derive_root("${ARG_NAME}" "${ARG_ROOT}" "${ARG_ARCH}" "${_root}" _expect_file ${ARG_UKDS}) + if(NOT _expect_file) + return() + endif() # Empty PACK_JOBS passes nothing to the packer, which then uses its own default. hkp_wire_pack_target( @@ -319,6 +332,23 @@ function(_hkp_probe_automatic_arches out_var) set(${out_var} "${_auto}" PARENT_SCOPE) endfunction() +# --------------------------------------------------------------------------- +# _hkp_probe_filter_args( [...]) +# The derive tool's --exclude-folder / --disable-kind flags for the filters the +# production roots are wired with, the disabled kinds from _hkp_disabled_kind_names(). +# --------------------------------------------------------------------------- +function(_hkp_probe_filter_args out_var enable_rocke) + _hkp_disabled_kind_names(_kinds "${enable_rocke}") + set(_args "") + foreach(_folder IN LISTS ARGN) + list(APPEND _args --exclude-folder "${_folder}") + endforeach() + foreach(_kind IN LISTS _kinds) + list(APPEND _args --disable-kind "${_kind}") + endforeach() + set(${out_var} "${_args}" PARENT_SCOPE) +endfunction() + # --------------------------------------------------------------------------- # hkp_load_packaging_probes( [...]) # Store the toolchain hkp_add_packaging resolved ( are the keywords every @@ -346,14 +376,7 @@ function(hkp_load_packaging_probes rocm_kpack_dir hipcc rocke_comgr_lib) # derive and the arch listing see the root as the pack step does, so what a probe # expects is what its pack ships. cmake_parse_arguments(_F "" "ENABLE_ROCKE" "EXCLUDE_FOLDERS" ${ARGN}) - _hkp_disabled_kind_names(_kinds "${_F_ENABLE_ROCKE}") - set(_filter_args "") - foreach(_folder IN LISTS _F_EXCLUDE_FOLDERS) - list(APPEND _filter_args --exclude-folder "${_folder}") - endforeach() - foreach(_kind IN LISTS _kinds) - list(APPEND _filter_args --disable-kind "${_kind}") - endforeach() + _hkp_probe_filter_args(_filter_args "${_F_ENABLE_ROCKE}" ${_F_EXCLUDE_FOLDERS}) set_property(GLOBAL PROPERTY HKP_PROBE_FILTER_ARGS "${_filter_args}") set_property(GLOBAL PROPERTY HKP_PROBE_NAMES "") set_property(GLOBAL PROPERTY HKP_PROBE_TOOLCHAIN_SET TRUE) @@ -390,8 +413,16 @@ function(hkp_load_packaging_probes rocm_kpack_dir hipcc rocke_comgr_lib) if(rocke_comgr_lib) list(APPEND _pyenv "ROCKE_COMGR_LIB=${rocke_comgr_lib}") endif() + # Tests that pack rocKE content live under probes/tests/rocke/ and nowhere else; a + # build with rocKE OFF has no rocKE toolchain to run them, so it never collects them + # (as hkp_register_tests does for tests/rocke). + set(_rocke_ignore "") + if(NOT _F_ENABLE_ROCKE) + set(_rocke_ignore "--ignore=${HKP_PKG_DIR}/probes/tests/rocke") + endif() add_test(NAME hkp-probe-tools - COMMAND "${Python3_EXECUTABLE}" -m pytest "${HKP_PKG_DIR}/probes/tests" -v) + COMMAND "${Python3_EXECUTABLE}" -m pytest "${HKP_PKG_DIR}/probes/tests" -v + ${_rocke_ignore}) set_tests_properties(hkp-probe-tools PROPERTIES ENVIRONMENT "${_pyenv}") set(_test_names hkp-probe-tools) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py index 1d41cf97527a..c0e99bbed368 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py @@ -61,46 +61,55 @@ def comgr_lib(): return os.environ.get("ROCKE_COMGR_LIB") or None -def pack_root(src, work, rocm_kpack_dir, hipcc, comgr_lib): +def pack_root( + src, work, rocm_kpack_dir, hipcc, comgr_lib, *, rocke=False, filters=((), ()) +): """Pack `src` for ARCH with the real packer; return the output root. + `rocke` packs with the rocKE toolchain (wheel stamp, in-tree rocke on PYTHONPATH, + comgr), as a build with HIPKERNELPROVIDER_ENABLE_ROCKE=ON does; the default packs + as an OFF build does, with none of it, so a test that does not pack rocKE content + needs no rocKE toolchain. `filters` is (exclude_folders, disabled_kinds), the + packer's own flags. + The stamp file is written by CMake in production; it is created here so the stamp assertion has something to find. """ out = work / "out" - wheel_stamp = work / "rocke-wheel.stamp" - wheel_stamp.write_text(hashlib.sha256(b"probe-test-wheel").hexdigest() + "\n") env = dict(os.environ) - env["PYTHONPATH"] = os.pathsep.join( - [str(p) for p in _ROCKE_SOURCE_DIRS] + [env.get("PYTHONPATH", "")] - ).rstrip(os.pathsep) - if comgr_lib: - env["ROCKE_COMGR_LIB"] = comgr_lib - result = subprocess.run( - [ - sys.executable, - str(HKP_PACK), - "--source-root", - str(src), - "--out-root", - str(out), - "--arches", - ARCH, - "--hipcc", - hipcc, - "--inter-root", - str(work / "inter"), - "--kpack-python-dir", - rocm_kpack_dir, - "--source-label", - "probe_assert_test", - "--rocke-wheel-stamp", - str(wheel_stamp), - ], - env=env, - capture_output=True, - text=True, - ) + cmd = [ + sys.executable, + str(HKP_PACK), + "--source-root", + str(src), + "--out-root", + str(out), + "--arches", + ARCH, + "--hipcc", + hipcc, + "--inter-root", + str(work / "inter"), + "--kpack-python-dir", + rocm_kpack_dir, + "--source-label", + "probe_assert_test", + ] + if rocke: + wheel_stamp = work / "rocke-wheel.stamp" + wheel_stamp.write_text(hashlib.sha256(b"probe-test-wheel").hexdigest() + "\n") + env["PYTHONPATH"] = os.pathsep.join( + [str(p) for p in _ROCKE_SOURCE_DIRS] + [env.get("PYTHONPATH", "")] + ).rstrip(os.pathsep) + if comgr_lib: + env["ROCKE_COMGR_LIB"] = comgr_lib + cmd += ["--rocke-wheel-stamp", str(wheel_stamp)] + exclude_folders, disabled_kinds = filters + for folder in exclude_folders: + cmd += ["--exclude-folder", folder] + for kind in disabled_kinds: + cmd += ["--disable-kind", kind] + result = subprocess.run(cmd, env=env, capture_output=True, text=True) if result.returncode != 0: pytest.fail(f"hkp_pack failed ({result.returncode}):\n{result.stderr}") (out / STAMP_NAME).write_text("") @@ -109,9 +118,9 @@ def pack_root(src, work, rocm_kpack_dir, hipcc, comgr_lib): @pytest.fixture(scope="module") def packed_root(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): - """The rocke fixture packed once for ARCH.""" + """The rocke fixture packed once for ARCH, with the rocKE toolchain.""" work = tmp_path_factory.mktemp("probe_pack") - return pack_root(ROCKE_FIXTURE, work, rocm_kpack_dir, hipcc, comgr_lib) + return pack_root(ROCKE_FIXTURE, work, rocm_kpack_dir, hipcc, comgr_lib, rocke=True) def run_assert(out_root, expect_path, rocm_kpack_dir, expect_comgr=None): @@ -134,8 +143,9 @@ def run_assert(out_root, expect_path, rocm_kpack_dir, expect_comgr=None): return subprocess.run(cmd, capture_output=True, text=True) -def run_derive(src, arch, out, ukds=()): - """Run the derive tool; one --ukd per entry of `ukds`.""" +def run_derive(src, arch, out, ukds=(), filters=((), ())): + """Run the derive tool; one --ukd per entry of `ukds`, and the same + (exclude_folders, disabled_kinds) `filters` pack_root takes.""" cmd = [ sys.executable, str(PROBE_DERIVE), @@ -148,6 +158,11 @@ def run_derive(src, arch, out, ukds=()): ] for name in ukds: cmd += ["--ukd", name] + exclude_folders, disabled_kinds = filters + for folder in exclude_folders: + cmd += ["--exclude-folder", folder] + for kind in disabled_kinds: + cmd += ["--disable-kind", kind] return subprocess.run(cmd, capture_output=True, text=True, check=False) @@ -163,9 +178,28 @@ def hip_source(dst): return dst -def derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib, ukds=()): - """Derive `src` for ARCH, pack the derived root; return (out root, expect path).""" - r = run_derive(src, ARCH, work / "probe" / "root", ukds) +def derive_and_pack( + src, + work, + rocm_kpack_dir, + hipcc, + comgr_lib, + ukds=(), + *, + rocke=False, + filters=((), ()), +): + """Derive `src` for ARCH, pack the derived root under the same `filters`; return + (out root, expect path). `rocke` as in pack_root.""" + r = run_derive(src, ARCH, work / "probe" / "root", ukds, filters) assert r.returncode == 0, r.stderr - out = pack_root(work / "probe" / "root", work, rocm_kpack_dir, hipcc, comgr_lib) + out = pack_root( + work / "probe" / "root", + work, + rocm_kpack_dir, + hipcc, + comgr_lib, + rocke=rocke, + filters=filters, + ) return out, work / "probe" / "expect.json" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/conftest.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/conftest.py new file mode 100644 index 000000000000..4297b86eb8ab --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/conftest.py @@ -0,0 +1,14 @@ +"""Probe tests that pack rocKE content, which live in this directory and nowhere else. + +A build with HIPKERNELPROVIDER_ENABLE_ROCKE=OFF makes no rocKE toolchain, and its +`hkp-probe-tools` entry `--ignore`s this directory, so neither this conftest nor any +test beside it is collected there. Everything here may therefore assume the build +asked for rocKE, and packs with the rocKE toolchain (probe_support.pack_root(rocke=True)). +""" + +import sys +from pathlib import Path + +_TESTS_DIR = str(Path(__file__).resolve().parents[1]) +if _TESTS_DIR not in sys.path: + sys.path.insert(0, _TESTS_DIR) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/test_hkp_probe_assert_rocke.py similarity index 100% rename from dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_assert.py rename to dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/test_hkp_probe_assert_rocke.py diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/test_hkp_probe_mixed_rocke.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/test_hkp_probe_mixed_rocke.py new file mode 100644 index 000000000000..55f16ba0655a --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/test_hkp_probe_mixed_rocke.py @@ -0,0 +1,52 @@ +"""derive -> real pack -> assert, for a root mixing rocke and hip UKDs, packed with the +rocKE toolchain. A build with HIPKERNELPROVIDER_ENABLE_ROCKE=OFF never collects this +directory; the rocKE-free equivalent is in test_hkp_probe_end_to_end.py.""" + +import json +import shutil + +import pytest + +from probe_support import ( # noqa: F401 (pytest fixtures) + ROCKE_FIXTURE, + comgr_lib, + derive_and_pack, + hip_source, + hipcc, + rocm_kpack_dir, + run_assert, +) + + +@pytest.fixture(scope="module") +def mixed_probe(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): + work = tmp_path_factory.mktemp("mixed_probe") + src = work / "src" + hip_source(src / "hip") + shutil.copytree(ROCKE_FIXTURE, src / "rocke") + return derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib, rocke=True) + + +def test_mixed_root_passes_with_per_ukd_kinds(mixed_probe, rocm_kpack_dir, comgr_lib): + out, expect = mixed_probe + entries = json.loads(expect.read_text()) + assert {(e["kdp"], e["kind"]) for e in entries} == { + ("hip/pointwise.kdp.json", "hip"), + ("hip/pointwise_wild.kdp.json", "hip"), + ("rocke/attention.kdp.json", "rocke"), + } + result = run_assert(out, expect, rocm_kpack_dir, expect_comgr=comgr_lib) + assert result.returncode == 0, result.stderr + + +def test_mixed_root_origin_is_checked_per_ukd(mixed_probe, rocm_kpack_dir, tmp_path): + # Expecting every UKD to be hip (a single global kind) must fail on the rocke one. + out, expect = mixed_probe + entries = [dict(e, kind="hip") for e in json.loads(expect.read_text())] + wrong = tmp_path / "expect.json" + wrong.write_text(json.dumps(entries)) + result = run_assert(out, wrong, rocm_kpack_dir) + assert result.returncode == 1 + assert "FAIL provenance-origin:" in result.stderr + assert "rocke/attention.kdp.json" in result.stderr + assert "hip/pointwise" not in result.stderr diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py index 7bfe623dafbb..b3b4f9e8a8a6 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py @@ -654,6 +654,38 @@ def test_list_arches_ignores_content_of_disabled_kind_and_excluded_folder(tmp_pa assert _list_arches(src, "--disable-kind", "rocke") == ["gfx942"] +def _derive_filtered(src, out, ukds, *filters): + cmd = [sys.executable, str(PROBE_DERIVE), "--from", str(src), "--arch", "gfx950"] + cmd += ["--out", str(out), *filters] + for name in ukds: + cmd += ["--ukd", name] + return subprocess.run(cmd, capture_output=True, text=True, check=False) + + +def test_listed_ukd_of_disabled_kind_exits_3_not_an_error(tmp_path): + src = tmp_path / "src" + _write_root(src, [_rocke("r"), _hip("h")]) + out = tmp_path / "w" / "root" + # Listed UKD removed by the filter: the probe does not apply, exit 3. + r = _derive_filtered(src, out, ["r"], "--disable-kind", "rocke") + assert r.returncode == 3, r.stderr + assert "disabled kind" in r.stderr + # The same name without the filter derives normally; one that does not exist + # is still an authoring error, filter or not. + assert _derive_filtered(src, out, ["r"]).returncode == 0 + r = _derive_filtered(src, out, ["nope"], "--disable-kind", "rocke") + assert r.returncode == 2, r.stderr + # A listed UKD that survives the filter is packed as usual. + assert _derive_filtered(src, out, ["h"], "--disable-kind", "rocke").returncode == 0 + + +def test_listed_ukd_missing_for_the_arch_is_an_error_even_when_filtered(tmp_path): + src = tmp_path / "src" + _write_root(src, [_rocke("r", arch=["gfx942"]), _hip("h")], kdp_arch=("gfx950",)) + r = _derive_filtered(src, tmp_path / "w" / "root", ["r"], "--disable-kind", "rocke") + assert r.returncode == 2, r.stderr + + # --- the kinds registry ----------------------------------------------------- def _tool_module(name): sys.path.insert(0, str(PROBE_DERIVE.parent)) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py index 16470c04467d..8bc2058baaa0 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py @@ -61,15 +61,6 @@ def hip_probe(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): ) -@pytest.fixture(scope="module") -def mixed_probe(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): - work = tmp_path_factory.mktemp("mixed_probe") - src = work / "src" - hip_source(src / "hip") - shutil.copytree(ROCKE_FIXTURE, src / "rocke") - return derive_and_pack(src, work, rocm_kpack_dir, hipcc, comgr_lib) - - def test_hip_root_passes(hip_probe, rocm_kpack_dir): out, expect = hip_probe assert json.loads(expect.read_text()) == _HIP_EXPECT @@ -77,31 +68,29 @@ def test_hip_root_passes(hip_probe, rocm_kpack_dir): assert result.returncode == 0, result.stderr -def test_mixed_root_passes_with_per_ukd_kinds(mixed_probe, rocm_kpack_dir, comgr_lib): - out, expect = mixed_probe - entries = json.loads(expect.read_text()) - assert {(e["kdp"], e["kind"]) for e in entries} == { +def test_rocke_off_probe_expects_and_packs_only_the_other_kinds( + tmp_path, rocm_kpack_dir, hipcc, comgr_lib +): + # A root holding hip UKDs beside a rocke KDP, probed as a build with rocKE OFF + # does: the rocKE/ family folder excluded and the rocke kind disabled, with no + # rocKE toolchain. Derive must expect exactly what that pack ships, or the probe + # would fail on UKDs the build never packs. + src = tmp_path / "src" + hip_source(src / "hip") + shutil.copytree(ROCKE_FIXTURE, src / "rocKE") + shutil.copytree(ROCKE_FIXTURE, src / "other_rocke") + filters = (("rocKE",), ("rocke",)) + out, expect = derive_and_pack( + src, tmp_path, rocm_kpack_dir, hipcc, comgr_lib, filters=filters + ) + assert {(e["kdp"], e["kind"]) for e in json.loads(expect.read_text())} == { ("hip/pointwise.kdp.json", "hip"), ("hip/pointwise_wild.kdp.json", "hip"), - ("rocke/attention.kdp.json", "rocke"), } - result = run_assert(out, expect, rocm_kpack_dir, expect_comgr=comgr_lib) + result = run_assert(out, expect, rocm_kpack_dir) assert result.returncode == 0, result.stderr -def test_mixed_root_origin_is_checked_per_ukd(mixed_probe, rocm_kpack_dir, tmp_path): - # Expecting every UKD to be hip (a single global kind) must fail on the rocke one. - out, expect = mixed_probe - entries = [dict(e, kind="hip") for e in json.loads(expect.read_text())] - wrong = tmp_path / "expect.json" - wrong.write_text(json.dumps(entries)) - result = run_assert(out, wrong, rocm_kpack_dir) - assert result.returncode == 1 - assert "FAIL provenance-origin:" in result.stderr - assert "rocke/attention.kdp.json" in result.stderr - assert "hip/pointwise" not in result.stderr - - def test_hsaco_root_passes(tmp_path, rocm_kpack_dir, hipcc, comgr_lib): src = _hsaco_source(tmp_path / "src") out, expect = derive_and_pack(src, tmp_path, rocm_kpack_dir, hipcc, comgr_lib) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py index b655b2a2b7c4..2e44ba1575f7 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py @@ -60,7 +60,9 @@ Standalone UKDs (a `kernelDescriptors` entry that is a bare id) are out of probe scope by policy: a KDP that would be trimmed and references one is refused. -Exit code 0 on success. Exit code 2, with a `hkp_probe_derive: ...` message on +Exit code 0 on success. Exit code 3 (EXIT_FILTERED), with a message on stderr, when +`--ukd` names UKDs that ship for `--arch` but that `--exclude-folder` or `--disable-kind` +removes: the probe does not apply to this build. Exit code 2, with a `hkp_probe_derive: ...` message on stderr, when the root cannot be derived: the packer's loader rejects the root (malformed or unreadable descriptor, dangling reference, UKD arch outside its KDP's), a KDP that keeps UKDs (or, in default mode, any KDP shipping for `--arch`) references @@ -95,12 +97,21 @@ from hkp_probe_kinds import KINDS # noqa: E402 _PREFIX = "hkp_probe_derive:" +EXIT_ERROR = 2 +# Every listed UKD is removed by the build's filters: not an error, the probe is skipped. +EXIT_FILTERED = 3 class DeriveError(Exception): pass +class FilteredOut(DeriveError): + """Every listed UKD exists and would ship for the arch, but the build's filters + (an excluded folder, a disabled kind) remove it: the probe does not apply to this + build, which is not an authoring error.""" + + def _discard(*_args, **_kwargs): pass @@ -285,6 +296,16 @@ def _plan(src, arch, ukds=None, filters=((), ())): if ukds is not None: missing = sorted(wanted - {e["name"] for e in expect}) + if missing and filters != ((), ()): + try: + _, unfiltered = _plan(src, arch, ukds) + except DeriveError: + unfiltered = [] + if not set(missing) - {e["name"] for e in unfiltered}: + raise FilteredOut( + f"--ukd {missing} ship for {arch} but are of a disabled kind or in " + "an excluded folder in this build" + ) if missing: raise DeriveError( f"--ukd {missing} matched no inline UKD shipping for {arch} in a KDP " @@ -354,17 +375,19 @@ def derive_root( out: Path, ukds: list[str] | None = None, filters=((), ()), -) -> str | None: - """Write the derived root to `out`; return an error message, or None on success. +) -> tuple[int, str] | None: + """Write the derived root to `out`; return (exit code, message), or None on success. `ukds` lists the UKD names to keep (`--ukd`); None selects per compile group. """ if not src.is_dir(): - return f"{_PREFIX} {src} is not a directory" + return EXIT_ERROR, f"{_PREFIX} {src} is not a directory" try: files, expect = _plan(src, arch, ukds, filters) + except FilteredOut as exc: + return EXIT_FILTERED, f"{_PREFIX} {exc}" except DeriveError as exc: - return f"{_PREFIX} {exc}" + return EXIT_ERROR, f"{_PREFIX} {exc}" _sync(out, files) expect_text = json.dumps(expect, indent=2) + "\n" @@ -435,8 +458,9 @@ def main(argv: list[str] | None = None) -> int: parser.error("--arch and --out are required unless --list-arches is given") error = derive_root(args.src, args.arch, args.out, args.ukds, filters) if error is not None: - print(error, file=sys.stderr) - return 2 + code, message = error + print(message, file=sys.stderr) + return code return 0 From c6dcfa0c803fed701b237937f63096246c3a8fad Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Thu, 8 Oct 2026 00:35:54 +0000 Subject: [PATCH 49/52] hkp: probe arch listing follows what ships; document skip-whole; cover folder-only exclusion --- .../descriptor-packaging/README.md | 8 ++-- .../cmake/HkpPackagingProbes.cmake | 5 ++- .../tests/test_hkp_probe_derive_root.py | 44 +++++++++++++++++++ .../tools/hkp_probe_derive_root.py | 37 +++++++++++----- 4 files changed, 77 insertions(+), 17 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 5123c4df18b2..62178ed3c2ff 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -322,9 +322,11 @@ Probes read and pack the root under the same filters the production pack gets. W `HIPKERNELPROVIDER_ENABLE_ROCKE=OFF` the `rocKE/` family folder and every `rocke` UKD are neither packed nor expected, so a probe covers whatever other kinds the arch carries, and an architecture whose only content is rocKE gets no probe. Probes run for every producer -the build has; none is skipped wholesale. A named probe whose listed `UKDS` all ship for its -`ARCH` but are removed by those filters is skipped with a STATUS line (derive exit 3); a -listed name that does not exist, or does not ship for `ARCH`, still fails configure. +the build has; none is skipped wholesale. A probe that those filters leave with nothing to pack is skipped with a STATUS line +(derive exit 3): a named probe when any listed `UKDS` entry ships for its `ARCH` but is +removed (skipped whole, never trimmed), one without `UKDS` when every UKD for `ARCH` is +removed. A listed name that does not exist, or does not ship for `ARCH`, still fails +configure. **What a probe proves** diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake index 33b247789da0..7008852e83bc 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -157,8 +157,9 @@ endfunction() # descriptors but compiles each compile path, and each value, once. With UKDS, the probe packs exactly the listed UKDs (by # UKD name): KDPs keeping none of them are left out of the derived root, and # configure fails when a listed name is kept by no KDP shipping for ARCH, except that -# a probe whose listed UKDs all ship for ARCH but are removed by this build's filters -# (a rocke UKD with rocKE OFF) is skipped with a STATUS line, declaring nothing. The +# a probe for which this build's filters (rocKE OFF) remove any listed UKD, or every UKD +# shipping for ARCH when none are listed, is skipped whole with a STATUS line, +# declaring nothing. The # assertion expects exactly the kept UKDs, each with the provenance of its producer # kind (see hkp_probe_assert.py). Adding a pack under an already probed ARCH needs no # new declaration unless the probe lists UKDS. diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py index b3b4f9e8a8a6..638bf89def83 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py @@ -686,6 +686,50 @@ def test_listed_ukd_missing_for_the_arch_is_an_error_even_when_filtered(tmp_path assert r.returncode == 2, r.stderr +def test_arch_only_a_disabled_kind_ships_for_is_not_listed_or_derived_as_empty( + tmp_path, +): + src = tmp_path / "src" + _write_root( + src, + [_hip("h", arch=["gfx950"]), _rocke("r", arch=["gfx1201"])], + kdp_arch=("gfx950", "gfx1201"), + ) + assert _list_arches(src) == ["gfx1201", "gfx950"] + assert _list_arches(src, "--disable-kind", "rocke") == ["gfx950"] + # Asked anyway, derive reports the filters as the reason rather than writing an + # empty probe for configure to reject. + r = _derive_filtered(src, tmp_path / "w" / "root", [], "--disable-kind", "rocke") + assert r.returncode == 0 # gfx950 keeps h + cmd = [sys.executable, str(PROBE_DERIVE), "--from", str(src), "--arch", "gfx1201"] + cmd += ["--out", str(tmp_path / "w2" / "root"), "--disable-kind", "rocke"] + r = subprocess.run(cmd, capture_output=True, text=True, check=False) + assert r.returncode == 3, r.stderr + + +def test_listed_mixed_ukds_with_one_filtered_out_skip_the_probe_whole(tmp_path): + src = tmp_path / "src" + _write_root(src, [_rocke("r"), _hip("h")]) + r = _derive_filtered( + src, tmp_path / "w" / "root", ["h", "r"], "--disable-kind", "rocke" + ) + assert r.returncode == 3, r.stderr + + +def test_excluded_folder_alone_removes_its_ukds_from_the_expectation(tmp_path): + src = tmp_path / "src" + _write_root(src, [_hip("top")]) + _write_root(src, [_hip("inside")], kdp_file="rocKE/in.kdp.json") + out = tmp_path / "w" / "root" + r = _derive_filtered(src, out, [], "--exclude-folder", "rocKE") + assert r.returncode == 0, r.stderr + expect = json.loads((out.parent / "expect.json").read_text()) + assert [e["name"] for e in expect] == ["top"] + assert _derive_filtered(src, out, []).returncode == 0 + expect = json.loads((out.parent / "expect.json").read_text()) + assert sorted(e["name"] for e in expect) == ["inside", "top"] + + # --- the kinds registry ----------------------------------------------------- def _tool_module(name): sys.path.insert(0, str(PROBE_DERIVE.parent)) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py index 2e44ba1575f7..8c68b7292f5e 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py @@ -61,8 +61,10 @@ scope by policy: a KDP that would be trimmed and references one is refused. Exit code 0 on success. Exit code 3 (EXIT_FILTERED), with a message on stderr, when -`--ukd` names UKDs that ship for `--arch` but that `--exclude-folder` or `--disable-kind` -removes: the probe does not apply to this build. Exit code 2, with a `hkp_probe_derive: ...` message on +`--exclude-folder` or `--disable-kind` leaves nothing to probe that would otherwise ship +for `--arch`: in default mode, every UKD shipping for it is removed; with `--ukd`, any +listed UKD is removed (the probe is skipped whole, not trimmed, so it never claims +coverage it does not give). The probe does not apply to this build. Exit code 2, with a `hkp_probe_derive: ...` message on stderr, when the root cannot be derived: the packer's loader rejects the root (malformed or unreadable descriptor, dangling reference, UKD arch outside its KDP's), a KDP that keeps UKDs (or, in default mode, any KDP shipping for `--arch`) references @@ -294,18 +296,27 @@ def _plan(src, arch, ukds=None, filters=((), ())): entry["kernel_source"] = u["kernel_source"] expect.append(entry) - if ukds is not None: - missing = sorted(wanted - {e["name"] for e in expect}) - if missing and filters != ((), ()): + if filters != ((), ()): + # What the filters removed, if that is why there is nothing (listed mode: a + # listed UKD) to probe, is not an authoring error. + missing = sorted(wanted - {e["name"] for e in expect}) if ukds else () + if missing or (ukds is None and not expect): try: _, unfiltered = _plan(src, arch, ukds) except DeriveError: unfiltered = [] - if not set(missing) - {e["name"] for e in unfiltered}: + unfiltered_names = {e["name"] for e in unfiltered} + if (missing and not set(missing) - unfiltered_names) or ( + not missing and unfiltered + ): raise FilteredOut( - f"--ukd {missing} ship for {arch} but are of a disabled kind or in " - "an excluded folder in this build" + f"{'--ukd ' + str(missing) if missing else 'every UKD'} shipping " + f"for {arch} is of a disabled kind or in an excluded folder in " + "this build" ) + + if ukds is not None: + missing = sorted(wanted - {e["name"] for e in expect}) if missing: raise DeriveError( f"--ukd {missing} matched no inline UKD shipping for {arch} in a KDP " @@ -316,9 +327,10 @@ def _plan(src, arch, ukds=None, filters=((), ())): def list_arches(src: Path, filters=((), ())) -> list[str]: - """Sorted union of the explicit `arch` entries of every KDP and UKD under `src` - that survive `filters`: a KDP whose UKDs are all of a disabled kind, or that sits - in an excluded folder, names no arch.""" + """Sorted explicit `arch` entries of every KDP and UKD under `src` that survive + `filters` and that some KDP actually ships for (the packer's `kdp_survives`): an + arch only a disabled-kind UKD or an excluded folder carries is not listed, so a + probe of a listed arch always has content to pack.""" flat = _load(src, filters) arches = set() for kdp in flat.kdps(): @@ -328,7 +340,8 @@ def list_arches(src: Path, filters=((), ())) -> list[str]: arches.update(entry.get("arch") or []) for ukd in flat.ukds(): arches.update(ukd.doc.get("arch") or []) - return sorted(arches) + kdps = [kdp.doc for kdp in flat.kdps()] + return sorted(a for a in arches if any(kdp_survives(k, flat, a) for k in kdps)) def _same(dest, content): From 30e4f8e4d850a27de9ede1a98194ff849fa38b9c Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Fri, 9 Oct 2026 20:34:07 +0000 Subject: [PATCH 50/52] hkp: packaging probe CI requires at least one probe; omit empty PACK_JOBS check_probe_junit.py gains an opt-in --min-probes N so a lane that must always have a probe fails when arch discovery declares none. Both superbuild lanes pass --min-probes 1. hkp_add_packaging_probe passes PACK_JOBS to hkp_wire_pack_target only when set, avoiding the CMP0174 empty-value warning. --- .github/scripts/check_probe_junit.py | 38 ++++++++++++-- .../scripts/tests/test_check_probe_junit.py | 23 ++++++++- .github/workflows/hipdnn-superbuild-ci.yml | 12 ++++- .../descriptor-packaging/README.md | 9 ++-- .../cmake/HkpPackagingProbes.cmake | 49 ++++++++++++------- 5 files changed, 102 insertions(+), 29 deletions(-) diff --git a/.github/scripts/check_probe_junit.py b/.github/scripts/check_probe_junit.py index 2021e5d2d4d9..f8f4b379715f 100755 --- a/.github/scripts/check_probe_junit.py +++ b/.github/scripts/check_probe_junit.py @@ -14,19 +14,25 @@ descriptors name declares no probe, so "hkp-probe-tools" alone is a valid manifest; an empty one means the configure did not write it properly. +The manifest is written by the same configure that discovers the probes, so on its +own it cannot tell "this lane builds every architecture" from "discovery found +nothing". A lane that must always declare probes passes --min-probes N: the check +fails when the manifest names fewer than N tests besides "hkp-probe-tools". The +default is 0, which accepts a manifest with no probe. + For every manifest name, the junit must hold exactly one testcase with that name, and that testcase must have run (status "run" or "fail"; without a status attribute, no child) and must have no or child. Usage: check_probe_junit.py --junit build/ctest-junit.xml \ - --manifest build/hkp-probes/manifest.txt + --manifest build/hkp-probes/manifest.txt [--min-probes N] Exit codes: 0 every manifest name ran exactly once and passed 1 one or more checks failed; each prints "check_probe_junit: FAIL : ", where is one of - manifest-unreadable, manifest-too-small, junit-unreadable, + manifest-unreadable, manifest-too-small, probes-too-few, junit-unreadable, name-missing, name-duplicate, not-run, failure, error """ @@ -35,8 +41,9 @@ import xml.etree.ElementTree as ET from pathlib import Path -# The "hkp-probe-tools" test; probes are optional. +# The "hkp-probe-tools" test; probes are optional unless --min-probes asks for them. MIN_MANIFEST_ENTRIES = 1 +TOOLS_TEST = "hkp-probe-tools" def read_manifest(path: Path) -> list[str]: @@ -55,7 +62,9 @@ def ran(case: ET.Element) -> bool: return status in ("run", "fail") -def check(junit_path: Path, manifest_path: Path) -> list[tuple[str, str]]: +def check( + junit_path: Path, manifest_path: Path, min_probes: int = 0 +) -> list[tuple[str, str]]: """Return (id, detail) for every failed check; empty means green.""" try: names = read_manifest(manifest_path) @@ -72,6 +81,18 @@ def check(junit_path: Path, manifest_path: Path) -> list[tuple[str, str]]: ) ) + probes = [name for name in names if name != TOOLS_TEST] + if len(probes) < min_probes: + failures.append( + ( + "probes-too-few", + f"{manifest_path} declares {len(probes)} probe(s), expected at " + f"least {min_probes}; if this lane now builds every architecture " + f"the descriptors name, drop --min-probes for it, otherwise probe " + f"discovery found nothing", + ) + ) + try: root = ET.parse(junit_path).getroot() except (OSError, ET.ParseError) as exc: @@ -122,9 +143,16 @@ def main(argv: list[str] | None = None) -> int: type=Path, help="file listing one declared ctest test name per line", ) + parser.add_argument( + "--min-probes", + type=int, + default=0, + help="fail when the manifest names fewer probes than this, not counting " + f"{TOOLS_TEST} (default 0: no probe is acceptable)", + ) args = parser.parse_args(argv) - failures = check(args.junit, args.manifest) + failures = check(args.junit, args.manifest, args.min_probes) for check_id, detail in failures: print(f"check_probe_junit: FAIL {check_id}: {detail}") if failures: diff --git a/.github/scripts/tests/test_check_probe_junit.py b/.github/scripts/tests/test_check_probe_junit.py index 40ec0aa186ef..0cfa93080774 100644 --- a/.github/scripts/tests/test_check_probe_junit.py +++ b/.github/scripts/tests/test_check_probe_junit.py @@ -37,12 +37,16 @@ def setUp(self): self.addCleanup(tmp.cleanup) self.dir = Path(tmp.name) - def run_check(self, xml: str, manifest: list[str]) -> list[str]: + def run_check( + self, xml: str, manifest: list[str], min_probes: int = 0 + ) -> list[str]: junit_path = self.dir / "ctest-junit.xml" manifest_path = self.dir / "manifest.txt" junit_path.write_text(xml) manifest_path.write_text("".join(f"{n}\n" for n in manifest)) - return [check_id for check_id, _ in cpj.check(junit_path, manifest_path)] + return [ + check_id for check_id, _ in cpj.check(junit_path, manifest_path, min_probes) + ] def test_all_declared_tests_passed(self): xml = junit_xml([case_xml(n) for n in MANIFEST]) @@ -115,6 +119,17 @@ def test_error_fails(self): def test_unparseable_junit_fails(self): self.assertEqual(self.run_check("/hkp-probes/gfx950/root (17 UKD(s) for gf A new architecture, or a new pack under one, is therefore probed with no edit anywhere. A lane whose `GPU_TARGETS` cover every named architecture declares no probe; that is valid, -and the manifest then lists `hkp-probe-tools` alone. +and the manifest then lists `hkp-probe-tools` alone. A lane that must always have a probe +passes `--min-probes N` to `check_probe_junit.py`, which fails when the manifest names fewer +than N probes; without it, a tools-only manifest is accepted. **Wildcards are excluded.** A descriptor with no `arch` (absent or empty) names no architecture and gets no automatic probe. That is safe: a wildcard ships for whatever the @@ -525,8 +527,9 @@ build and ctest lines fails (unknown target, then "No tests were found"). In CI both superbuild lanes configure with the option `ON`. The probe packs run in the Build step as part of `all`; the probe ctest entries run once, inside the lane's normal "Run tests" ctest, which writes `build/ctest-junit.xml`. The "Check packaging probes ran" -step then runs only `check_probe_junit.py` on that junit against the manifest. It runs -whenever "Run tests" ran, pass or fail. +step then runs only `check_probe_junit.py` on that junit against the manifest, with +`--min-probes 1` because neither lane builds every architecture the production root names +(drop the flag from a lane that does). It runs whenever "Run tests" ran, pass or fail. Locally on Linux the build is quick (the wheel environment plus the probed UKDs) and each probe ctest entry takes a few seconds; `hkp-probe-tools` takes longer, since it really diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake index 7008852e83bc..68472cba0146 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -142,6 +142,30 @@ function(_hkp_probe_check_ukds name) endforeach() endfunction() +# --------------------------------------------------------------------------- +# _hkp_probe_add_assert_test( ) +# Register ctest entry hkp-probe-, which asserts the probe's packed output with +# the kpack dir and comgr library hkp_load_packaging_probes() stored. +# --------------------------------------------------------------------------- +function(_hkp_probe_add_assert_test name out_root arch expect_file) + get_property(_kpack_dir GLOBAL PROPERTY HKP_PROBE_ROCM_KPACK_DIR) + get_property(_comgr_lib GLOBAL PROPERTY HKP_PROBE_COMGR_LIB) + set(_assert_args + --out-root "${out_root}" + --arch "${arch}" + --expect "${expect_file}" + --kpack-python-dir "${_kpack_dir}" + --stamp-name "${HKP_PACK_STAMP_NAME}") + if(_comgr_lib) + list(APPEND _assert_args --expect-comgr "${_comgr_lib}") + endif() + add_test(NAME "hkp-probe-${name}" + COMMAND "${Python3_EXECUTABLE}" + "${HKP_PKG_DIR}/tools/hkp_probe_assert.py" ${_assert_args}) + set_tests_properties("hkp-probe-${name}" PROPERTIES + ENVIRONMENT "PYTHONPATH=${HKP_PYTHON_ROOT}") +endfunction() + # --------------------------------------------------------------------------- # hkp_add_packaging_probe(ARCH [ROOT ] [NAME ] [UKDS ...] # [PACK_JOBS ]) @@ -225,7 +249,6 @@ function(hkp_add_packaging_probe) get_property(_kpack_dir GLOBAL PROPERTY HKP_PROBE_ROCM_KPACK_DIR) get_property(_hipcc GLOBAL PROPERTY HKP_PROBE_HIPCC) - get_property(_comgr_lib GLOBAL PROPERTY HKP_PROBE_COMGR_LIB) get_property(_root_args GLOBAL PROPERTY HKP_PROBE_ROOT_ARGS) set(_probe_dir "${CMAKE_BINARY_DIR}/hkp-probes/${ARG_NAME}") @@ -240,7 +263,11 @@ function(hkp_add_packaging_probe) return() endif() - # Empty PACK_JOBS passes nothing to the packer, which then uses its own default. + # Omitted PACK_JOBS passes nothing to the packer, which then uses its own default. + set(_pack_jobs_args "") + if(ARG_PACK_JOBS) + list(APPEND _pack_jobs_args PACK_JOBS "${ARG_PACK_JOBS}") + endif() hkp_wire_pack_target( NAME "probe_${ARG_NAME}" SOURCE_ROOT "${_root}" @@ -249,22 +276,10 @@ function(hkp_add_packaging_probe) ROCM_KPACK_DIR "${_kpack_dir}" OUT_ROOT "${_out_root}" ${_root_args} - PACK_JOBS "${ARG_PACK_JOBS}") + ${_pack_jobs_args}) - set(_assert_args - --out-root "${_out_root}" - --arch "${ARG_ARCH}" - --expect "${_expect_file}" - --kpack-python-dir "${_kpack_dir}" - --stamp-name "${HKP_PACK_STAMP_NAME}") - if(_comgr_lib) - list(APPEND _assert_args --expect-comgr "${_comgr_lib}") - endif() - add_test(NAME "hkp-probe-${ARG_NAME}" - COMMAND "${Python3_EXECUTABLE}" - "${HKP_PKG_DIR}/tools/hkp_probe_assert.py" ${_assert_args}) - set_tests_properties("hkp-probe-${ARG_NAME}" PROPERTIES - ENVIRONMENT "PYTHONPATH=${HKP_PYTHON_ROOT}") + _hkp_probe_add_assert_test("${ARG_NAME}" "${_out_root}" "${ARG_ARCH}" + "${_expect_file}") set_property(GLOBAL APPEND PROPERTY HKP_PROBE_NAMES "${ARG_NAME}") endfunction() From ab19c31325405e9dc13af06cd1afdbf7ee2823a6 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Fri, 9 Oct 2026 20:38:50 +0000 Subject: [PATCH 51/52] hkp: probe assert reads packed UKD provenance from its sidecar Develop moved packed UKD provenance out of the descriptor into a per-descriptor sidecar. hkp_probe_assert.py now attaches it with hkp_pack.provenance_sidecar, reporting provenance-sidecar when it is missing or does not match, and the probe tests read and write packed trees through the sidecar. --- .../probes/tests/probe_support.py | 19 ++++++++++++ .../rocke/test_hkp_probe_assert_rocke.py | 29 ++++++++++++------- .../tests/test_hkp_probe_passthrough.py | 14 +++++---- .../tools/hkp_probe_assert.py | 17 +++++++++++ 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py index c0e99bbed368..3c466512cdd0 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py @@ -19,6 +19,9 @@ _PKG_DIR = _PROBES_TESTS_DIR.parent.parent _HKP_DIR = _PKG_DIR.parent +sys.path.insert(0, str(_PKG_DIR / "python")) +from hkp_pack import provenance_sidecar # noqa: E402 + ARCH = "gfx950" STAMP_NAME = ".hkp-packed.stamp" HKP_PACK = _PKG_DIR / "tools" / "hkp_pack.py" @@ -116,6 +119,22 @@ def pack_root( return out +def read_packed(path): + """A packed descriptor with each UKD's provenance attached from its sidecar.""" + doc = json.loads(Path(path).read_text(encoding="utf-8")) + provenance_sidecar.attach(Path(path), doc) + return doc + + +def write_packed(path, doc): + """Write `doc` (UKD provenance inline, as read_packed returns it) back as a + packed descriptor: provenance in a fresh sidecar, digests matching the UKDs.""" + path = Path(path) + sidecar_name, data = provenance_sidecar.detach(path.name, doc) + (path.parent / sidecar_name).write_bytes(data) + path.write_text(json.dumps(doc, indent=2), encoding="utf-8") + + @pytest.fixture(scope="module") def packed_root(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): """The rocke fixture packed once for ARCH, with the rocKE toolchain.""" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/test_hkp_probe_assert_rocke.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/test_hkp_probe_assert_rocke.py index 4046c0474a7d..eff1a46c95f1 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/test_hkp_probe_assert_rocke.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/test_hkp_probe_assert_rocke.py @@ -9,8 +9,10 @@ comgr_lib, hipcc, packed_root, + read_packed, rocm_kpack_dir, run_assert, + write_packed, ) KDP = f"{ARCH}/attention.kdp.json" @@ -39,17 +41,17 @@ def write_expect(self, entries): self.expect_path.write_text(json.dumps(entries), encoding="utf-8") def kdp(self): - return json.loads((self.root / KDP).read_text(encoding="utf-8")) + return read_packed(self.root / KDP) def mutate_ukd(self, edit): kdp = self.kdp() edit(kdp["kernelDescriptors"][0]) - (self.root / KDP).write_text(json.dumps(kdp, indent=2), encoding="utf-8") + write_packed(self.root / KDP, kdp) def mutate_kdp(self, edit): kdp = self.kdp() edit(kdp) - (self.root / KDP).write_text(json.dumps(kdp, indent=2), encoding="utf-8") + write_packed(self.root / KDP, kdp) def run(self, expect_comgr=None, root=None): return run_assert( @@ -66,9 +68,7 @@ def tree(packed_root, tmp_path, rocm_kpack_dir, comgr_lib): shutil.copytree(packed_root, copy) comgr = ( comgr_lib - or json.loads((copy / KDP).read_text(encoding="utf-8"))["kernelDescriptors"][0][ - "provenance" - ]["comgr_path"] + or read_packed(copy / KDP)["kernelDescriptors"][0]["provenance"]["comgr_path"] ) return _Tree(copy, tmp_path / "expect.json", rocm_kpack_dir, comgr) @@ -137,13 +137,22 @@ def test_ukd_count_mismatch(tree): def test_ukd_shipped_twice_fails_count(tree): - # Every shipped UKD is in the expect list, but one ships twice: matched > expected. - tree.mutate_kdp( - lambda kdp: kdp["kernelDescriptors"].append(kdp["kernelDescriptors"][0]) - ) + # Every shipped UKD is in the expect list, but one ships twice under another id + # (the sidecar keys UKDs by id): matched > expected. + def duplicate(kdp): + first = kdp["kernelDescriptors"][0] + kdp["kernelDescriptors"].append({**first, "id": first["id"] + "-copy"}) + + tree.mutate_kdp(duplicate) _assert_fails(tree.run(), "ukd-count") +def test_missing_provenance_sidecar_fails(tree): + for sidecar in tree.root.rglob("*.provenance.json.gz"): + sidecar.unlink() + _assert_fails(tree.run(), "provenance-sidecar") + + def test_ukd_count_zero_is_failure(tree): tree.mutate_kdp(lambda kdp: kdp.update(kernelDescriptors=[])) tree.write_expect([]) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_passthrough.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_passthrough.py index 215e313312f7..847851992fb6 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_passthrough.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_passthrough.py @@ -16,8 +16,10 @@ derive_and_pack, hip_source, hipcc, + read_packed, rocm_kpack_dir, run_assert, + write_packed, ) _EMBEDDED = { @@ -50,10 +52,10 @@ def _path(self): return self.root / ARCH / self.kdp_rel def mutate(self, name, edit): - kdp = json.loads(self._path().read_text()) + kdp = read_packed(self._path()) (ukd,) = [u for u in kdp["kernelDescriptors"] if u["name"] == name] edit(ukd) - self._path().write_text(json.dumps(kdp, indent=2)) + write_packed(self._path(), kdp) def mutate_expect(self, edit): entries = json.loads(self.expect_path.read_text()) @@ -140,9 +142,9 @@ def test_mixed_hip_and_embedded_source_root_passes(mixed, rocm_kpack_dir): def test_mixed_root_still_runs_archive_checks_for_the_kpack_ukds(mixed, rocm_kpack_dir): out, expect = mixed kdp = out / ARCH / "hip" / "pointwise.kdp.json" - doc = json.loads(kdp.read_text()) + doc = read_packed(kdp) doc["kernelDescriptors"][0]["kernel_source"]["sha256"] = "0" * 64 - kdp.write_text(json.dumps(doc)) + write_packed(kdp, doc) _fails(run_assert(out, expect, rocm_kpack_dir), "sha256") @@ -201,9 +203,9 @@ def test_passthrough_shipped_as_kpack_fails_ukd_kind(emb): def test_kpack_kind_shipped_as_passthrough_fails_ukd_kind(mixed, rocm_kpack_dir): out, expect = mixed kdp = out / ARCH / "hip" / "pointwise.kdp.json" - doc = json.loads(kdp.read_text()) + doc = read_packed(kdp) doc["kernelDescriptors"][0]["kernel_source"] = dict(_EMBEDDED) - kdp.write_text(json.dumps(doc)) + write_packed(kdp, doc) _fails(run_assert(out, expect, rocm_kpack_dir), "ukd-kind") diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py index b96d1b6cf65d..1743cb1fdc5a 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py @@ -29,6 +29,9 @@ .kpack is absent kpack-empty that archive has size 0 no-kdp no *.kdp.json under //, or one is not valid JSON + provenance-sidecar a KDP's provenance sidecar is missing, unreadable, or does not + match its UKDs (a packed UKD carries its provenance there, and + the checks below read it from it) ukd-count the number of shipped UKDs found in the --expect list differs from its length (or is 0), or a shipped UKD is not in the list ukd-kind a UKD's shipped kernel_source.kind is not what its expected @@ -61,6 +64,15 @@ from hkp_probe_kinds import KINDS +# tools/ holds hkp_pack.py, which must never resolve as the `hkp_pack` package. +_PKG_ROOT = str(Path(__file__).resolve().parent.parent / "python") +while _PKG_ROOT in sys.path: + sys.path.remove(_PKG_ROOT) +sys.path.insert(0, _PKG_ROOT) + +from hkp_pack import provenance_sidecar # noqa: E402 +from hkp_pack.errors import HkpPackError # noqa: E402 + _PREFIX = "hkp_probe_assert" @@ -329,6 +341,11 @@ def _check_descriptors(args, arch_dir, archive, failures): if not isinstance(kdp, dict): failures.add("no-kdp", f"{name} is not a JSON object") continue + try: + provenance_sidecar.attach(kdp_path, kdp) + except HkpPackError as exc: + failures.add("provenance-sidecar", f"{name}: {exc}") + continue if "arch" in kdp and kdp["arch"] != [args.arch]: failures.add("arch-field", f"{name} arch is {kdp['arch']!r}") for ukd in kdp.get("kernelDescriptors", []): From 0b81e24a7b36d4b905d35e3f2ee623871e9ecdcd Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 14 Oct 2026 09:54:35 -0400 Subject: [PATCH 52/52] docs(hipdnn): align comments and messages with tiered generic-target matching; add order-independent merge atomicity test --- .../python/hkp_pack/descriptors.py | 8 +++-- .../tests/test_hkp_pack_generic.py | 32 +++++++++++++++++++ .../tests/test_hkp_pack_hip.py | 13 +++----- .../src/compilation/KpackModuleCache.hpp | 4 +-- .../ingestor/DescriptorLoader.hpp | 14 ++++---- .../ingestor/Descriptors.hpp | 5 +-- .../ingestor/KernelIngestorStateManager.hpp | 3 +- .../codegen/config_loader.py | 2 +- .../IngestorGenerator/codegen/generator.py | 13 ++++---- .../tests/test_verify_variant_sets.py | 5 +-- 10 files changed, 67 insertions(+), 32 deletions(-) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py index 9f23307032d6..be34f0adc78b 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py @@ -168,7 +168,8 @@ def _arch_subset_ok(ukd_arch, kdp_arch, generic_targets): """A UKD's arch is admissible under a referencing KDP's arch. An empty list on either side is a wildcard: a wildcard KDP admits any UKD, - and a wildcard UKD is admissible under any KDP. Two explicit lists require + and a wildcard UKD is admissible under any KDP here (a standalone wildcard UKD + under a KDP that has an `arch` is rejected separately). Two explicit lists require the UKD's expanded device set to lie within the KDP's (a generic stands for its members). """ @@ -239,8 +240,9 @@ def _reject_nonbare_arch(archs, where): Mirrors the loader's isPlausibleArchBaseId (loader is authoritative): 'gfx' followed by one or more of [a-z0-9_-]. LLVM generic targets - ('gfx9-4-generic') are legal; a feature suffix ('gfx942:xnack-') is not, - since ':' is outside the set. + ('gfx9-4-generic') are well-formed here but must also be in the generic target + table (see `_reject_unknown_generics`); a feature suffix ('gfx942:xnack-') is + not, since ':' is outside the set. Fatal rather than advisory: a suffixed arch matches no shard, so the KDP prunes from every arch and the pack exits 0 having installed nothing -- diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py index d4bf0b2bcaf2..fd86eaee47b5 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py @@ -228,6 +228,38 @@ def test_merge_collision_with_differing_bytes_is_refused_and_leaves_the_member_u assert not staging.exists() +@pytest.mark.quick +def test_merge_copy_failure_midway_is_refused_and_leaves_the_member_untouched( + tmp_path, monkeypatch +): + generic_dir = tmp_path / "inter" / ".generic-out" / GENERIC + member_dir = tmp_path / "out" / MEMBER_A + staging = tmp_path / "out" / f".{MEMBER_A}.merge.staging" + _write(generic_dir / "first.kdp.json", {"id": "kdp-first"}) + _write(generic_dir / "second.kdp.json", {"id": "kdp-second"}) + _write(member_dir / "concrete_only.kdp.json", {"id": "kdp-c"}) + before = _files(member_dir) + + real_copyfile = shutil.copyfile + generic_copies = [] + + def failing_on_second_generic_copy(src, dst, *args, **kwargs): + if Path(src).is_relative_to(generic_dir): + generic_copies.append(src) + if len(generic_copies) == 2: + raise OSError("injected copy failure") + return real_copyfile(src, dst, *args, **kwargs) + + monkeypatch.setattr(shutil, "copyfile", failing_on_second_generic_copy) + + with pytest.raises(HkpPackError): + _merge_generic_into_member(generic_dir, member_dir, staging) + + assert len(generic_copies) == 2, "the merge no longer copies via shutil.copyfile" + assert _files(member_dir) == before + assert not staging.exists() + + @pytest.mark.quick @pytest.mark.parametrize( "step, dropped, with_generic", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py index 378bc7bc74dc..50ab9e616edb 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py @@ -910,9 +910,10 @@ def test_ukd_arch_not_subset_of_kdp_hard_errors( def test_ukd_arch_subset_wildcard_combinations_accepted( tmp_path, main_fixture, hipcc, rocm_kpack_dir ): - # (a) UKD explicit subset of KDP explicit; (b) UKD wildcard + KDP explicit; - # (c) KDP wildcard + UKD explicit. Each loads without error and emits the - # standalone into the expected shard(s). + # (a) UKD explicit subset of KDP explicit; (c) KDP wildcard + UKD explicit. + # Each loads without error and emits the standalone into the expected shard(s). + # The legal wildcard-UKD shape is the inline UKD inheriting a concrete KDP's arch, + # covered by _ACCEPTED["inline_inheriting_a_concrete_kdp"]. # (a) explicit subset: b128 -> [gfx942] under pointwise KDP [gfx942, gfx950]. src_a = _copy_fixture(tmp_path / "a", main_fixture) p = src_a / _STANDALONE_UKD_FILE @@ -923,12 +924,6 @@ def test_ukd_arch_subset_wildcard_combinations_accepted( assert (tmp_path / "a" / "out" / "gfx942" / _STANDALONE_UKD_FILE).exists() assert not (tmp_path / "a" / "out" / "gfx950" / _STANDALONE_UKD_FILE).exists() - # (b) UKD wildcard (unchanged fixture) under explicit KDP -> ships in both. - src_b = _copy_fixture(tmp_path / "b", main_fixture) - _run(src_b, tmp_path / "b", hipcc, rocm_kpack_dir, arches=["gfx942", "gfx950"]) - assert (tmp_path / "b" / "out" / "gfx942" / _STANDALONE_UKD_FILE).exists() - assert (tmp_path / "b" / "out" / "gfx950" / _STANDALONE_UKD_FILE).exists() - # (c) KDP wildcard + UKD explicit: reference b128 (arch [gfx942]) from the # wildcard KDP; it ships only in gfx942. src_c = _copy_fixture(tmp_path / "c", main_fixture) diff --git a/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp b/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp index df7b2de04744..5a4925d41a21 100644 --- a/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp +++ b/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp @@ -275,8 +275,8 @@ class KpackModuleCache : public ModuleCache& values, /// reports features too (`gfx942:sramecc+:xnack-`) and archMatches stops the candidate /// at ':', so a bare id matches such a device -- but an authored entry may not carry /// features. Matching compares target-id text, so a partial id like `gfx942:xnack-` -/// reads as reasonable and matches nothing. LLVM generic targets (`gfx9-4-generic`) stay -/// legal: the '-' is part of the base id. Catches an authoring typo, which would +/// reads as reasonable and matches nothing. LLVM generic targets (`gfx9-4-generic`) are +/// well-formed: the '-' is part of the base id. Catches an authoring typo, which would /// otherwise disable the pack everywhere with nothing louder than an INFO decline; not -/// an existence check, so an unheard-of but well-formed id still parses. +/// an existence check on its own, so an unheard-of but well-formed id still passes here. +/// A generic name absent from the generic target table is rejected separately, by +/// requireArchList. inline bool isPlausibleArchBaseId(std::string_view value) { constexpr std::string_view PREFIX = "gfx"; @@ -2254,9 +2256,9 @@ inline std::vector resolveDescriptorSets(DescriptorCatalog catalo { // Failure granularity is the pack: a pack whose cross-references dangle or // whose kernels contradict the KMD is dropped while the engine keeps its other - // packs. A duplicate kernel metadata tuple is the exception, for packs whose - // arch lists overlap -- the state manager's constructor throws on it, taking - // the whole engine. RFC 0017 §10 + // packs. A duplicate kernel metadata tuple is the exception, for kernels that + // compete at the same arch tier -- the state manager's constructor throws on it, + // taking the whole engine. RFC 0017 §10 // wants only the colliding kernel dropped; the upgrade is making that // constructor log and drop rather than throw, in one place, so hand-built packs // get the same behavior. diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/Descriptors.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/Descriptors.hpp index 55a78c7e594e..337da22ee861 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/Descriptors.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/Descriptors.hpp @@ -380,8 +380,9 @@ struct KernelDescriptorPack std::vector matcherIds; DescriptorId engineId; DescriptorId dispatchId; - /// GFX targets, e.g. `{"gfx942", "gfx950"}`; empty means arch-independent, matched - /// exactly against the device's base target id. Part of the pack's catalog identity + /// GFX targets, e.g. `{"gfx942", "gfx950"}`; empty means arch-independent. A concrete + /// entry is matched exactly against the device's base target id; a generic entry + /// matches by membership in the generic target table. Part of the pack's catalog identity /// -- packs are keyed by (id, arch), so per-arch shards may ship one pack id many /// times -- as well as a match-time filter. The filter itself is enforced at catalog /// build, not load time, so an excluded pack still builds and simply declines per diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp index 9e6582cf5c83..079a0fa1c581 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp @@ -486,7 +486,8 @@ class KernelIngestorStateManager // one that declared a narrower list claims only that. Claiming by the // kernel rather than the pack is what lets two kernels of ONE pack share a // tuple under disjoint arch -- one implementation per capability -- while - // still catching two that a single device would see together. + // still catching two that compete at the same arch tier on a device. + // Kernels at different tiers do not collide: the better tier wins. std::vector kernelArch = kernel.arch.empty() ? pack.arch : kernel.arch; MetadataTupleValues tuple; tuple.reserve(key.size()); diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py b/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py index db0fb066a996..5b266e59af72 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py @@ -1453,7 +1453,7 @@ def _check_generic_arch(config: IngestorConfig) -> None: def _check_kernel_arch_subset_of_pack(config: IngestorConfig) -> None: """Pre-mint check #4: a kernel's arch must be a subset of its pack's. - Mirrors ``DescriptorLoader.hpp``'s ``archCovers`` over expanded device sets. + Mirrors ``ingestor/DeviceProperties.hpp``'s ``archCovers`` over expanded device sets. """ table = _generic_table() for pack in config.packs: diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py b/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py index 610c30ac937b..8ded1ef6af24 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py @@ -482,19 +482,19 @@ def build_kdp( f"kernel {kernel.name!r} (pack {pack.name!r}) and kernel " f"{prior['name']!r} (pack {prior['pack']!r}) are the SAME " f"candidate and complete to the SAME catalog tuple " - f"{json.loads(key)}, but their architectures overlap without " - f"being equal ({arch or ['']} vs " + f"{json.loads(key)}, but they compete at the same arch tier " + f"without having equal arch lists ({arch or ['']} vs " f"{prior['arch'] or ['']}). On the shared architectures " f"the matcher would see one tuple twice, which drops the whole " f"engine at load; coalescing them would instead advertise one " f"of the two on devices its arch list never claimed. Give the " - f"two entries the SAME arch list so they de-duplicate, or make " - f"them disjoint." + f"two entries the SAME arch list so they de-duplicate, or put " + f"them at different arch tiers." ) raise ValueError( f"kernel {kernel.name!r} (pack {pack.name!r}) and kernel " f"{prior['name']!r} (pack {prior['pack']!r}) complete to the SAME " - f"catalog tuple {json.loads(key)} on overlapping architectures " + f"catalog tuple {json.loads(key)} at the same arch tier " f"({arch or ['']} vs {prior['arch'] or ['']}), but they " f"are not the same candidate: their kernel_source/priority differ, " f"so they name different binaries. The matcher compares the tuple " @@ -502,7 +502,8 @@ def build_kdp( f"whole engine at load, and dropping one of them here would " f"discard a binary that was deliberately built. Distinguish them " f"in metadata (a knob the spec pins belongs in the tuple), narrow " - f"one of the arch lists, or do not ship both." + f"one of the arch lists, move one to a different arch tier, or do " + f"not ship both." ) if already is not None: # Carry the prior kernel's own pack: a drop that empties this pack diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py index 820d85715f31..d765873a8889 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py @@ -416,8 +416,9 @@ def test_tuple_check_substitutes_kmd_defaults_like_the_loader(self, gate): class TestTheDeskCheckIdentityIsEngineWideAndArchAware: """The loader assembles ONE catalog per engine per device, so the identity is engine-wide and carries the effective architecture: an identical tuple in a gfx942 - and a gfx950 pack is legal, while an overlap -- including a wildcard over a concrete - arch -- drops the engine.""" + and a gfx950 pack is legal, while two packs that compete at the same arch tier drop the + engine. A wildcard over a concrete arch, or a generic over a member, sit at + different tiers and do not compete.""" def _two_packs(self, gate, tag, left_arch, right_arch): root = gate.tmp / tag