From 563b4738a5e2ddfd97a03998035981966930615b Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:24:42 +0400 Subject: [PATCH 1/2] fix(hw-oracle): invalidate cached builds when inputs change --- tools/hw-oracle/hw_oracle.py | 33 ++++-- tools/hw-oracle/test_hw_oracle.py | 167 ++++++++++++++++++++++++++++-- 2 files changed, 185 insertions(+), 15 deletions(-) diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index 16cc7e87a..c0be67bf5 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -1,5 +1,7 @@ import argparse +import hashlib import os +import shlex import shutil import struct import subprocess @@ -33,15 +35,23 @@ def tool(name): def oracle(): - binary = CACHE / "oracle" source = HERE / "oracle.c" - if binary.exists() and binary.stat().st_mtime >= source.stat().st_mtime: + compiler = shlex.split(os.environ.get("CC", "cc")) + if not compiler: + raise ValueError("CC must name a compiler command") + root = rocm_root() + if root is not None: + root = root.resolve() + configuration = repr((compiler, str(root))).encode("utf-8") + key = hashlib.sha256(source.read_bytes() + configuration).hexdigest() + directory = CACHE / key + binary = directory / "oracle" + if binary.is_file(): return binary - CACHE.mkdir(parents=True, exist_ok=True) - with tempfile.TemporaryDirectory(dir=CACHE) as tmp: + directory.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(dir=directory) as tmp: output = Path(tmp) / "oracle" - command = [os.environ.get("CC", "cc"), "-O1", str(source), "-o", str(output)] - root = rocm_root() + command = [*compiler, "-O1", str(source), "-o", str(output)] if root: lib = root / "lib" command += [f"-I{root / 'include'}", f"-L{lib}", f"-Wl,-rpath,{lib}"] @@ -51,8 +61,12 @@ def oracle(): @cache +def gpu_target(): + return subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip() + + def target(): - return os.environ.get("HW_ORACLE_TARGET") or subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip() + return os.environ.get("HW_ORACLE_TARGET") or gpu_target() def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): @@ -78,6 +92,11 @@ def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round3 def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): rows = [tuple(r) for r in rows] + for index, row in enumerate(rows, 1): + try: + struct.pack("<4I", *row) + except struct.error as error: + raise ValueError(f"row {index} must contain four unsigned 32-bit integers") from error wave = 64 if wave64 else 32 padded = rows + [(0, 0, 0, 0)] * (-len(rows) % wave) env = dict(os.environ) diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 606517706..8cf1dd492 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -1,7 +1,10 @@ import contextlib import io import os +import shlex +import shutil import subprocess +import sys import tempfile import unittest from pathlib import Path @@ -38,24 +41,26 @@ class OracleCacheTests(unittest.TestCase): with self.assertRaises(subprocess.CalledProcessError): hw_oracle.oracle() self.assertEqual(binary.read_bytes() if binary.exists() else None, previous) - self.assertEqual(list(cache.iterdir()), [binary] if previous is not None else []) + self.assertEqual([file for file in cache.rglob("*") if file.is_file()], [binary] if previous is not None else []) compiler.side_effect = succeed - self.assertEqual(hw_oracle.oracle(), binary) - self.assertEqual(binary.read_bytes(), b"complete executable") - self.assertEqual(hw_oracle.oracle(), binary) + built = hw_oracle.oracle() + self.assertEqual(built.read_bytes(), b"complete executable") + self.assertEqual(hw_oracle.oracle(), built) self.assertEqual(compiler.call_count, 2) - self.assertEqual(list(cache.iterdir()), [binary]) + self.assertEqual(set(file for file in cache.rglob("*") if file.is_file()), {built, binary} if previous is not None else {built}) + if previous is not None: + self.assertEqual(binary.read_bytes(), previous) def test_overlapping_builds_do_not_expose_partial_output(self): with tempfile.TemporaryDirectory() as tmp: cache = Path(tmp) / "cache" - binary = cache / "oracle" outputs = [] def compile(command, *, check): self.assertTrue(check) output = Path(command[command.index("-o") + 1]) outputs.append(output) + binary = output.parent.parent / "oracle" output.write_bytes(b"partial executable") if len(outputs) == 1: self.assertFalse(binary.exists()) @@ -64,11 +69,11 @@ class OracleCacheTests(unittest.TestCase): with patch.object(hw_oracle, "CACHE", cache), patch.object(hw_oracle, "rocm_root", return_value=None), \ patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: - self.assertEqual(hw_oracle.oracle(), binary) + binary = hw_oracle.oracle() self.assertEqual(binary.read_bytes(), b"complete executable") self.assertEqual(compiler.call_count, 2) self.assertNotEqual(outputs[0], outputs[1]) - self.assertEqual(list(cache.iterdir()), [binary]) + self.assertEqual([file for file in cache.rglob("*") if file.is_file()], [binary]) class FloatModeTests(unittest.TestCase): @@ -122,6 +127,152 @@ class FloatModeTests(unittest.TestCase): self.assertEqual(assemble.call_args.kwargs, MODES | {"lds": 4096}) +class OracleConfigurationTests(unittest.TestCase): + @unittest.skipUnless(sys.platform.startswith("linux"), "Linux compiler and shared-library fixture") + def test_cache_and_compiler_command_with_host_toolchain(self): + compiler = shutil.which("cc") + if compiler is None: + self.skipTest("C compiler not found") + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + runtime = root / "runtime" + (runtime / "lib").mkdir(parents=True) + stub = root / "runtime.c" + stub.write_text("void runtime_stub(void) {}\n", encoding="utf-8") + subprocess.run([compiler, "-shared", "-fPIC", str(stub), "-o", str(runtime / "lib/libhsa-runtime64.so")], check=True) + source = root / "source" + source.mkdir() + source_file = source / "oracle.c" + source_file.write_text('#include \nint main(void) { printf("%d", VALUE); }\n', encoding="utf-8") + os.utime(source_file, (0, 0)) + wrapper = root / "compiler wrapper" + wrapper.write_text("#!/bin/sh\nexec " + shlex.quote(compiler) + ' "$@"\n', encoding="utf-8") + wrapper.chmod(0o755) + with patch.object(hw_oracle, "HERE", source), patch.object(hw_oracle, "CACHE", root / "cache"), \ + patch.object(hw_oracle, "rocm_root", return_value=runtime): + with patch.dict(os.environ, {"CC": shlex.quote(str(wrapper)) + " -DVALUE=7"}): + first = hw_oracle.oracle() + self.assertEqual(subprocess.check_output([first]), b"7") + self.assertEqual(hw_oracle.oracle(), first) + with patch.dict(os.environ, {"CC": shlex.quote(str(wrapper)) + " -DVALUE=8"}): + second = hw_oracle.oracle() + self.assertNotEqual(second, first) + self.assertEqual(subprocess.check_output([second]), b"8") + source_file.write_text('#include \nint main(void) { printf("%d", VALUE + 1); }\n', encoding="utf-8") + os.utime(source_file, (0, 0)) + third = hw_oracle.oracle() + self.assertNotEqual(third, second) + self.assertEqual(subprocess.check_output([third]), b"9") + + def test_compiler_command_supports_wrapper_and_flags(self): + with tempfile.TemporaryDirectory() as tmp: + def compile(command, *, check): + self.assertEqual(command[:3], ["ccache", "cc", "-fno-omit-frame-pointer"]) + Path(command[command.index("-o") + 1]).write_bytes(b"executable") + + with patch.object(hw_oracle, "CACHE", Path(tmp)), patch.object(hw_oracle, "rocm_root", return_value=None), \ + patch.dict(os.environ, {"CC": "ccache cc -fno-omit-frame-pointer"}), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile): + self.assertEqual(hw_oracle.oracle().read_bytes(), b"executable") + + def test_source_change_with_preserved_timestamp_invalidates_cache(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + source = root / "source" + source.mkdir() + source_file = source / "oracle.c" + source_file.write_text("first", encoding="utf-8") + os.utime(source_file, (0, 0)) + + def compile(command, *, check): + Path(command[command.index("-o") + 1]).write_bytes(source_file.read_bytes()) + + with patch.object(hw_oracle, "HERE", source), patch.object(hw_oracle, "CACHE", root / "cache"), \ + patch.object(hw_oracle, "rocm_root", return_value=None), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: + self.assertEqual(hw_oracle.oracle().read_bytes(), b"first") + source_file.write_text("second", encoding="utf-8") + os.utime(source_file, (0, 0)) + self.assertEqual(hw_oracle.oracle().read_bytes(), b"second") + self.assertEqual(compiler.call_count, 2) + + def test_compiler_change_invalidates_cache(self): + with tempfile.TemporaryDirectory() as tmp: + def compile(command, *, check): + Path(command[command.index("-o") + 1]).write_bytes(command[0].encode()) + + with patch.object(hw_oracle, "CACHE", Path(tmp)), patch.object(hw_oracle, "rocm_root", return_value=None), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: + with patch.dict(os.environ, {"CC": "cc"}): + self.assertEqual(hw_oracle.oracle().read_bytes(), b"cc") + with patch.dict(os.environ, {"CC": "clang"}): + self.assertEqual(hw_oracle.oracle().read_bytes(), b"clang") + self.assertEqual(compiler.call_count, 2) + + def test_runtime_path_change_invalidates_cache(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + + def compile(command, *, check): + library_path = next(option for option in command if option.startswith("-L")) + Path(command[command.index("-o") + 1]).write_bytes(library_path.encode()) + + with patch.object(hw_oracle, "CACHE", root / "cache"), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: + with patch.object(hw_oracle, "rocm_root", return_value=root / "one"): + first = hw_oracle.oracle() + self.assertEqual(first.read_bytes(), ("-L" + str(root / "one/lib")).encode()) + with patch.object(hw_oracle, "rocm_root", return_value=root / "two"): + second = hw_oracle.oracle() + self.assertNotEqual(second, first) + self.assertEqual(second.read_bytes(), ("-L" + str(root / "two/lib")).encode()) + self.assertEqual(compiler.call_count, 2) + + def test_target_override_is_read_for_each_call(self): + reset = getattr(hw_oracle.target, "cache_clear", lambda: None) + reset() + self.addCleanup(reset) + with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1036"}): + self.assertEqual(hw_oracle.target(), "gfx1036") + with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1100"}): + self.assertEqual(hw_oracle.target(), "gfx1100") + + def test_override_does_not_replace_cached_gpu_detection(self): + hw_oracle.gpu_target.cache_clear() + self.addCleanup(hw_oracle.gpu_target.cache_clear) + with patch.dict(os.environ), patch.object(hw_oracle, "oracle", return_value="oracle"), \ + patch.object(hw_oracle.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, "gfx1036\n")) as query: + os.environ.pop("HW_ORACLE_TARGET", None) + self.assertEqual(hw_oracle.target(), "gfx1036") + with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1100"}): + self.assertEqual(hw_oracle.target(), "gfx1100") + self.assertEqual(hw_oracle.target(), "gfx1036") + query.assert_called_once() + + def test_invalid_rows_fail_before_assembler_or_gpu_lookup(self): + rows = [[(1, 2, 3)], [(1, 2, 3, 4, 5)], [(-1, 0, 0, 0)], [(1 << 32, 0, 0, 0)], [(0.5, 0, 0, 0)]] + with patch.object(hw_oracle, "assemble") as assemble, patch.object(hw_oracle, "oracle") as oracle: + for invalid in rows: + with self.subTest(rows=invalid), self.assertRaisesRegex(ValueError, "row"): + hw_oracle.run("s_nop 0", invalid, **MODES) + assemble.assert_not_called() + oracle.assert_not_called() + + def test_integer_index_values_are_preserved(self): + class Dword: + def __index__(self): + return 0xffffffff + + def dispatch(command, *, check, env): + self.assertEqual(Path(command[3]).read_bytes()[:16], b"\xff" * 4 + bytes(12)) + Path(command[4]).write_bytes(bytes(int(command[2]) * 64)) + + with patch.object(hw_oracle, "assemble", return_value=Path("kernel.co")), \ + patch.object(hw_oracle, "oracle", return_value="oracle"), \ + patch.object(hw_oracle.subprocess, "run", side_effect=dispatch): + self.assertEqual(hw_oracle.run("s_nop 0", [(Dword(), 0, 0, 0)], **MODES), [(0,) * 16]) + + class GroupSegmentTests(unittest.TestCase): def test_wave64_dispatches_at_most_512_rows(self): rows = [(index, 0, 0, 0) for index in range(1025)] From cf1195ad20d6fc83a2d0ea6fe25c9effc7af53c9 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:31:39 +0400 Subject: [PATCH 2/2] fix(hw-oracle): narrow cache PR and normalize runtime paths --- tools/hw-oracle/hw_oracle.py | 13 ++------- tools/hw-oracle/test_hw_oracle.py | 48 ++----------------------------- 2 files changed, 5 insertions(+), 56 deletions(-) diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index c0be67bf5..46b59c202 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -60,13 +60,11 @@ def oracle(): return binary + + @cache -def gpu_target(): - return subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip() - - def target(): - return os.environ.get("HW_ORACLE_TARGET") or gpu_target() + return os.environ.get("HW_ORACLE_TARGET") or subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip() def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): @@ -92,11 +90,6 @@ def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round3 def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): rows = [tuple(r) for r in rows] - for index, row in enumerate(rows, 1): - try: - struct.pack("<4I", *row) - except struct.error as error: - raise ValueError(f"row {index} must contain four unsigned 32-bit integers") from error wave = 64 if wave64 else 32 padded = rows + [(0, 0, 0, 0)] * (-len(rows) % wave) env = dict(os.environ) diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 8cf1dd492..7de671b40 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -221,57 +221,13 @@ class OracleConfigurationTests(unittest.TestCase): patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: with patch.object(hw_oracle, "rocm_root", return_value=root / "one"): first = hw_oracle.oracle() - self.assertEqual(first.read_bytes(), ("-L" + str(root / "one/lib")).encode()) + self.assertEqual(first.read_bytes(), ("-L" + str((root / "one").resolve() / "lib")).encode()) with patch.object(hw_oracle, "rocm_root", return_value=root / "two"): second = hw_oracle.oracle() self.assertNotEqual(second, first) - self.assertEqual(second.read_bytes(), ("-L" + str(root / "two/lib")).encode()) + self.assertEqual(second.read_bytes(), ("-L" + str((root / "two").resolve() / "lib")).encode()) self.assertEqual(compiler.call_count, 2) - def test_target_override_is_read_for_each_call(self): - reset = getattr(hw_oracle.target, "cache_clear", lambda: None) - reset() - self.addCleanup(reset) - with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1036"}): - self.assertEqual(hw_oracle.target(), "gfx1036") - with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1100"}): - self.assertEqual(hw_oracle.target(), "gfx1100") - - def test_override_does_not_replace_cached_gpu_detection(self): - hw_oracle.gpu_target.cache_clear() - self.addCleanup(hw_oracle.gpu_target.cache_clear) - with patch.dict(os.environ), patch.object(hw_oracle, "oracle", return_value="oracle"), \ - patch.object(hw_oracle.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, "gfx1036\n")) as query: - os.environ.pop("HW_ORACLE_TARGET", None) - self.assertEqual(hw_oracle.target(), "gfx1036") - with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1100"}): - self.assertEqual(hw_oracle.target(), "gfx1100") - self.assertEqual(hw_oracle.target(), "gfx1036") - query.assert_called_once() - - def test_invalid_rows_fail_before_assembler_or_gpu_lookup(self): - rows = [[(1, 2, 3)], [(1, 2, 3, 4, 5)], [(-1, 0, 0, 0)], [(1 << 32, 0, 0, 0)], [(0.5, 0, 0, 0)]] - with patch.object(hw_oracle, "assemble") as assemble, patch.object(hw_oracle, "oracle") as oracle: - for invalid in rows: - with self.subTest(rows=invalid), self.assertRaisesRegex(ValueError, "row"): - hw_oracle.run("s_nop 0", invalid, **MODES) - assemble.assert_not_called() - oracle.assert_not_called() - - def test_integer_index_values_are_preserved(self): - class Dword: - def __index__(self): - return 0xffffffff - - def dispatch(command, *, check, env): - self.assertEqual(Path(command[3]).read_bytes()[:16], b"\xff" * 4 + bytes(12)) - Path(command[4]).write_bytes(bytes(int(command[2]) * 64)) - - with patch.object(hw_oracle, "assemble", return_value=Path("kernel.co")), \ - patch.object(hw_oracle, "oracle", return_value="oracle"), \ - patch.object(hw_oracle.subprocess, "run", side_effect=dispatch): - self.assertEqual(hw_oracle.run("s_nop 0", [(Dword(), 0, 0, 0)], **MODES), [(0,) * 16]) - class GroupSegmentTests(unittest.TestCase): def test_wave64_dispatches_at_most_512_rows(self):