fix(hw-oracle): invalidate cached builds when inputs change
This commit is contained in:
parent
a11c8f7f70
commit
563b4738a5
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 <stdio.h>\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 <stdio.h>\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)]
|
||||
|
|
|
|||
Loading…
Reference in New Issue