Files
TheRock/rocm-systems/projects/rocprofiler-compute/tests/test_torch_cpp_loader.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

1074 lines
39 KiB
Python

# Copyright (c) Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Unit tests for utils.inject_roctx._backends.torch_cpp_loader. No GPU.
CMake build is end-to-end in test_torch_trace_coverage; this file stubs the
loader to pin contracts: tier order, cache fingerprint, REBUILD env var,
no-ninja policy.
"""
import importlib
import sys
import common # noqa: F401
import pytest
from utils.inject_roctx._backends import torch_cpp_loader as inject_roctx_loader
_FAKE_TAG = "py3.12_torch2.9_src000000000000"
# ---------------------------------------------------------------------------
# compute_tag / fingerprint
# ---------------------------------------------------------------------------
def test_compute_tag_returns_well_formed_string():
"""The cache tag follows ``py<X>.<Y>_torch<ver>_src<12-hex>``."""
tag = inject_roctx_loader.compute_tag()
if tag is None:
pytest.skip("torch not importable")
parts = tag.split("_")
assert any(p.startswith("py") for p in parts)
assert any(p.startswith("torch") for p in parts)
src_components = [p for p in parts if p.startswith("src")]
assert len(src_components) == 1, (
f"expected exactly one '_src...' component in tag {tag!r}"
)
src_value = src_components[0][len("src") :]
assert src_value == "missing" or (
len(src_value) == 12 and all(c in "0123456789abcdef" for c in src_value)
), f"unexpected src component {src_value!r} in tag {tag!r}"
def test_compute_tag_is_stable_across_calls():
"""``compute_tag()`` returns the same value for repeated calls in one process."""
assert inject_roctx_loader.compute_tag() == inject_roctx_loader.compute_tag()
def test_source_fingerprint_changes_when_inputs_change(tmp_path, monkeypatch):
"""A single-byte edit to any input changes the fingerprint."""
cpp = tmp_path / "roctx_recordfn.cpp"
cmake = tmp_path / "CMakeLists.txt"
cpp.write_text("// fingerprint test source\n")
cmake.write_text("# fingerprint test buildfile\n")
monkeypatch.setattr(
inject_roctx_loader,
"_FINGERPRINT_INPUTS",
(cpp, cmake),
)
baseline = inject_roctx_loader._source_fingerprint()
assert len(baseline) == 12
for input_path in (cpp, cmake):
original = input_path.read_bytes()
input_path.write_bytes(original + b"\n# mutation\n")
mutated = inject_roctx_loader._source_fingerprint()
assert mutated != baseline, (
f"editing {input_path.name} did not change the fingerprint"
)
input_path.write_bytes(original)
assert inject_roctx_loader._source_fingerprint() == baseline
def test_source_fingerprint_excludes_tool_version_file():
"""The tool ``VERSION`` file is not a fingerprint input."""
for path in inject_roctx_loader._FINGERPRINT_INPUTS:
assert path.name != "VERSION", (
f"VERSION should not be in _FINGERPRINT_INPUTS; saw {path}"
)
def test_source_fingerprint_is_missing_sentinel_when_no_inputs_readable(
tmp_path,
monkeypatch,
):
"""When no inputs are readable, the fingerprint is the ``"missing"`` sentinel."""
monkeypatch.setattr(
inject_roctx_loader,
"_FINGERPRINT_INPUTS",
(tmp_path / "does_not_exist.cpp",),
)
assert inject_roctx_loader._source_fingerprint() == "missing"
def test_source_fingerprint_is_length_delimited(tmp_path, monkeypatch):
"""Reshuffling bytes across input boundaries changes the fingerprint."""
a = tmp_path / "a.cpp"
b = tmp_path / "b.txt"
monkeypatch.setattr(inject_roctx_loader, "_FINGERPRINT_INPUTS", (a, b))
a.write_bytes(b"AB")
b.write_bytes(b"C")
fp1 = inject_roctx_loader._source_fingerprint()
a.write_bytes(b"A")
b.write_bytes(b"BC")
fp2 = inject_roctx_loader._source_fingerprint()
assert fp1 != fp2, "fingerprint collided across an input boundary"
def test_cmake_and_runtime_compute_identical_fingerprint():
"""The loader and CMake compute the same source fingerprint."""
import subprocess
cmake_dir = inject_roctx_loader._SO_SOURCE_DIR
assert cmake_dir.is_dir(), f"missing cmake source dir at {cmake_dir}"
snippet = (
"import sys, pathlib; "
f"sys.path.insert(0, str(pathlib.Path('{cmake_dir}/../..').resolve())); "
"from utils.inject_roctx._backends.torch_cpp_loader "
"import _source_fingerprint; "
"print(_source_fingerprint())"
)
result = subprocess.run(
[sys.executable, "-c", snippet],
check=True,
capture_output=True,
text=True,
)
cmake_side = result.stdout.strip()
runtime_side = inject_roctx_loader._source_fingerprint()
assert cmake_side == runtime_side, (
f"install-time fingerprint {cmake_side!r} != runtime {runtime_side!r}"
)
# ---------------------------------------------------------------------------
# Source / buildfile hygiene
# ---------------------------------------------------------------------------
def test_roctx_recordfn_source_avoids_torch_umbrella_headers():
"""The source must not include ``<torch/{extension,all,torch}.h>``."""
cpp_path = inject_roctx_loader._SO_SOURCE
assert cpp_path.is_file(), f"missing C++ source at {cpp_path}"
active_lines = [
line
for line in cpp_path.read_text().splitlines()
if not line.lstrip().startswith("//")
]
active_src = "\n".join(active_lines)
forbidden = (
"<torch/extension.h>",
"<torch/all.h>",
"<torch/torch.h>",
)
for header in forbidden:
directive = f"#include {header}"
assert directive not in active_src, f"must not include {header}"
def test_roctx_recordfn_source_uses_narrow_includes():
"""The source includes only the required narrow PyTorch headers."""
cpp_path = inject_roctx_loader._SO_SOURCE
src = cpp_path.read_text()
required = (
"#include <ATen/record_function.h>",
"#include <c10/util/ThreadLocalDebugInfo.h>",
"#include <pybind11/pybind11.h>",
"#include <pybind11/stl.h>",
)
for directive in required:
assert directive in src, f"must include {directive}"
def test_cmake_buildfile_does_not_override_output_name():
"""``CMakeLists.txt`` must not set ``OUTPUT_NAME``."""
cmake_path = inject_roctx_loader._SO_BUILDFILE
assert cmake_path.is_file(), f"missing CMakeLists.txt at {cmake_path}"
active_lines = [
line
for line in cmake_path.read_text().splitlines()
if not line.lstrip().startswith("#")
]
active_src = "\n".join(active_lines)
assert "OUTPUT_NAME" not in active_src, "must not set OUTPUT_NAME"
def test_cmake_buildfile_strips_lib_prefix():
"""``CMakeLists.txt`` sets ``PREFIX ""`` so the artifact is not ``lib``-prefixed."""
import re
cmake_path = inject_roctx_loader._SO_BUILDFILE
active_lines = [
line
for line in cmake_path.read_text().splitlines()
if not line.lstrip().startswith("#")
]
active_src = "\n".join(active_lines)
assert re.search(r'PREFIX\s+""', active_src), 'must set PREFIX ""'
def test_loader_and_cmake_agree_on_artifact_filename_shape():
"""The loader and CMake produce identical artifact filenames."""
import inspect
src = inspect.getsource(inject_roctx_loader._try_jit)
assert 'f"roctx_recordfn-{tag}.so"' in src, (
"artifact filename has changed; update this test accordingly"
)
def test_loader_source_never_recommends_installing_ninja():
"""The loader source must not recommend installing ninja."""
import inspect as _stdlib_inspect
src = _stdlib_inspect.getsource(inject_roctx_loader).lower()
forbidden = (
"pip install ninja",
"apt install ninja",
"apt-get install ninja",
"install ninja",
"add ninja to requirements",
"ninja>=",
"ninja==",
)
for token in forbidden:
assert token not in src, f"loader source must not contain {token!r}"
# ---------------------------------------------------------------------------
# Tier-name surface
# ---------------------------------------------------------------------------
def test_c_tier_names_matches_the_tier_ladder():
"""``C_TIER_NAMES`` enumerates exactly the prebuilt and JIT tiers."""
assert inject_roctx_loader.C_TIER_NAMES == frozenset((
inject_roctx_loader.TIER_PREBUILT,
inject_roctx_loader.TIER_JIT,
))
# ---------------------------------------------------------------------------
# Misc helpers
# ---------------------------------------------------------------------------
def test_force_python_fallback_returns_none():
assert inject_roctx_loader.load(force_python_fallback=True).module is None
def test_install_cached_so_overwrites_stale_artifact(tmp_path):
"""``_install_cached_so`` overwrites an existing cached ``.so``."""
src = tmp_path / "build" / "roctx_recordfn.so"
src.parent.mkdir()
src.write_bytes(b"new content")
cached = tmp_path / "cache" / "roctx_recordfn-tag.so"
cached.parent.mkdir()
cached.write_bytes(b"STALE content -- must be overwritten")
inject_roctx_loader._install_cached_so(src, cached)
assert cached.read_bytes() == b"new content", (
"cache copy did not overwrite the existing cached .so"
)
def test_install_cached_so_is_a_noop_when_src_missing(tmp_path):
"""``_install_cached_so`` is a no-op when the source ``.so`` is missing."""
src = tmp_path / "build" / "does_not_exist.so"
cached = tmp_path / "cache" / "roctx_recordfn-tag.so"
cached.parent.mkdir()
cached.write_bytes(b"good content")
inject_roctx_loader._install_cached_so(src, cached)
assert cached.read_bytes() == b"good content", (
"missing src must leave cached .so untouched"
)
def test_jit_cache_dir_is_creatable(monkeypatch, tmp_path):
"""``_jit_cache_dir`` creates the directory on first call and is idempotent."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
d = inject_roctx_loader._jit_cache_dir()
assert d.exists() and d.is_dir()
assert inject_roctx_loader._jit_cache_dir() == d
def test_cmake_executable_honors_env_var_then_falls_back(monkeypatch):
"""``$CMAKE`` takes precedence over ``PATH``."""
seen = []
def fake_which(name):
seen.append(name)
return f"/fake/bin/{name}"
monkeypatch.setattr(inject_roctx_loader.shutil, "which", fake_which)
monkeypatch.setenv("CMAKE", "my-custom-cmake")
assert inject_roctx_loader._cmake_executable() == "/fake/bin/my-custom-cmake"
assert seen[-1] == "my-custom-cmake"
monkeypatch.delenv("CMAKE", raising=False)
assert inject_roctx_loader._cmake_executable() == "/fake/bin/cmake"
assert seen[-1] == "cmake"
def test_no_prebuilt_returns_none_for_unknown_tag(monkeypatch):
"""``_try_prebuilt`` returns ``None`` when no candidate matches the tag."""
monkeypatch.setattr(
inject_roctx_loader,
"_install_tree_prebuilt_candidates",
lambda _tag: [],
)
assert inject_roctx_loader._try_prebuilt("py3.10_torch2.9") is None
# ---------------------------------------------------------------------------
# JIT failure marker
# ---------------------------------------------------------------------------
def test_jit_failure_marker_round_trip(monkeypatch, tmp_path):
"""``_previous_jit_failure`` reads the reason from ``_record_jit_failure``."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
tag = _FAKE_TAG
assert inject_roctx_loader._previous_jit_failure(tag) is None
inject_roctx_loader._record_jit_failure(tag, RuntimeError("ninja missing"))
recorded = inject_roctx_loader._previous_jit_failure(tag)
assert recorded is not None
assert "ninja missing" in recorded
assert "RuntimeError" in recorded
def test_jit_failure_marker_cleared_on_demand(monkeypatch, tmp_path):
"""_clear_jit_failure() must remove the marker and be idempotent."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
tag = _FAKE_TAG
inject_roctx_loader._record_jit_failure(tag, RuntimeError("x"))
assert inject_roctx_loader._previous_jit_failure(tag) is not None
inject_roctx_loader._clear_jit_failure(tag)
assert inject_roctx_loader._previous_jit_failure(tag) is None
inject_roctx_loader._clear_jit_failure(tag)
# ---------------------------------------------------------------------------
# _try_jit: cache-hit path
# ---------------------------------------------------------------------------
def test_try_jit_returns_cache_hit_without_invoking_cmake(monkeypatch, tmp_path):
"""An on-disk cached ``.so`` is loaded directly, no cmake invocation."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
cache_dir = inject_roctx_loader._jit_cache_dir()
cached_so = cache_dir / f"roctx_recordfn-{_FAKE_TAG}.so"
cached_so.write_bytes(b"stub-so")
sentinel = object()
monkeypatch.setattr(
inject_roctx_loader,
"_import_module_from_path",
lambda _n, _p: sentinel,
)
def fail_subprocess(*_a, **_k):
pytest.fail("subprocess.run must not fire on cache hit")
monkeypatch.setattr(inject_roctx_loader.subprocess, "run", fail_subprocess)
assert inject_roctx_loader._try_jit(_FAKE_TAG) is sentinel
def test_try_jit_force_rebuild_bypasses_cache(monkeypatch, tmp_path):
"""``force_rebuild=True`` ignores the cache and reaches the build path."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
cache_dir = inject_roctx_loader._jit_cache_dir()
cached_so = cache_dir / f"roctx_recordfn-{_FAKE_TAG}.so"
cached_so.write_bytes(b"stale")
def must_not_import(_n, _p):
pytest.fail("cache hit must not be consulted under force_rebuild")
monkeypatch.setattr(
inject_roctx_loader, "_import_module_from_path", must_not_import
)
monkeypatch.setattr(inject_roctx_loader, "_SO_SOURCE", tmp_path / "missing.cpp")
assert inject_roctx_loader._try_jit(_FAKE_TAG, force_rebuild=True) is None
# ---------------------------------------------------------------------------
# _try_jit: cmake build path
# ---------------------------------------------------------------------------
class _StubCompleted:
"""Minimal ``subprocess.CompletedProcess`` stand-in."""
def __init__(self, returncode=0, stdout="", stderr=""):
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
def _set_so_inputs_present(monkeypatch, tmp_path):
"""Point the loader at synthetic source files."""
src_dir = tmp_path / "roctx_recordfn"
src_dir.mkdir(parents=True, exist_ok=True)
cpp = src_dir / "roctx_recordfn.cpp"
cml = src_dir / "CMakeLists.txt"
cpp.write_text("// stub\n")
cml.write_text("# stub\n")
monkeypatch.setattr(inject_roctx_loader, "_SO_SOURCE_DIR", src_dir)
monkeypatch.setattr(inject_roctx_loader, "_SO_SOURCE", cpp)
monkeypatch.setattr(inject_roctx_loader, "_SO_BUILDFILE", cml)
return src_dir
def test_try_jit_skips_when_sources_missing(monkeypatch, tmp_path):
"""``_try_jit`` returns ``None`` when sources are missing (no cache hit)."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
monkeypatch.setattr(inject_roctx_loader, "_SO_SOURCE", tmp_path / "nonexistent.cpp")
monkeypatch.setattr(
inject_roctx_loader, "_SO_BUILDFILE", tmp_path / "nonexistent.txt"
)
def fail_subprocess(*_a, **_k):
pytest.fail("subprocess.run must not be called when sources are missing")
monkeypatch.setattr(inject_roctx_loader.subprocess, "run", fail_subprocess)
assert inject_roctx_loader._try_jit(_FAKE_TAG) is None
def test_try_jit_skips_when_cmake_not_on_path(monkeypatch, tmp_path):
"""Absence of cmake on ``PATH`` does not write a failure marker."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
_set_so_inputs_present(monkeypatch, tmp_path)
monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: None)
def fail_subprocess(*_a, **_k):
pytest.fail("subprocess.run must not be called when cmake is absent")
monkeypatch.setattr(inject_roctx_loader.subprocess, "run", fail_subprocess)
assert inject_roctx_loader._try_jit(_FAKE_TAG) is None
assert inject_roctx_loader._previous_jit_failure(_FAKE_TAG) is None
def test_try_jit_short_circuits_on_prior_failure(monkeypatch, tmp_path):
"""A prior failure marker vetoes ``_try_jit``."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
_set_so_inputs_present(monkeypatch, tmp_path)
monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: "/fake/cmake")
inject_roctx_loader._record_jit_failure(
_FAKE_TAG,
RuntimeError("earlier failure"),
)
def fail_subprocess(*_a, **_k):
pytest.fail("subprocess.run must not fire when marker is present")
monkeypatch.setattr(inject_roctx_loader.subprocess, "run", fail_subprocess)
assert inject_roctx_loader._try_jit(_FAKE_TAG) is None
def test_try_jit_passes_runtime_python_to_cmake(monkeypatch, tmp_path):
"""``_try_jit`` passes ``-DTORCH_TRACE_PYTHON=sys.executable`` to cmake."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
_set_so_inputs_present(monkeypatch, tmp_path)
monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: "/fake/cmake")
invocations = []
def fake_run(argv, **_kw):
invocations.append(list(argv))
return _StubCompleted(returncode=0)
monkeypatch.setattr(inject_roctx_loader.subprocess, "run", fake_run)
cache_dir = inject_roctx_loader._jit_cache_dir()
build_dir = cache_dir / f"cmake-build-{_FAKE_TAG}"
produced = build_dir / f"roctx_recordfn-{_FAKE_TAG}.so"
produced.parent.mkdir(parents=True, exist_ok=True)
produced.write_bytes(b"stub-so")
monkeypatch.setattr(
inject_roctx_loader,
"_import_module_from_path",
lambda _n, _p: object(),
)
inject_roctx_loader._try_jit(_FAKE_TAG)
assert len(invocations) == 2, f"expected two cmake invocations, saw {invocations!r}"
configure_argv = invocations[0]
runtime_python_flag = f"-DTORCH_TRACE_PYTHON={sys.executable}"
assert runtime_python_flag in configure_argv, (
f"-DTORCH_TRACE_PYTHON must equal sys.executable; saw {configure_argv!r}"
)
build_argv = invocations[1]
assert build_argv[1] == "--build", (
f"second invocation must be `cmake --build`, saw {build_argv!r}"
)
def test_try_jit_records_failure_marker_on_configure_failure(monkeypatch, tmp_path):
"""A non-zero configure return code writes a JIT failure marker."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
_set_so_inputs_present(monkeypatch, tmp_path)
monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: "/fake/cmake")
def fake_run(argv, **_kw):
return _StubCompleted(
returncode=1,
stderr="CMake Error: Could not find Torch (missing: TORCH_DIR)\n",
)
monkeypatch.setattr(inject_roctx_loader.subprocess, "run", fake_run)
assert inject_roctx_loader._try_jit(_FAKE_TAG) is None
marker = inject_roctx_loader._previous_jit_failure(_FAKE_TAG)
assert marker is not None
assert inject_roctx_loader.TIER_JIT in marker
assert "Could not find Torch" in marker, (
f"marker must preserve stderr tail; saw {marker!r}"
)
def test_try_jit_records_failure_marker_on_build_failure(monkeypatch, tmp_path):
"""A non-zero build return code writes a JIT failure marker."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
_set_so_inputs_present(monkeypatch, tmp_path)
monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: "/fake/cmake")
call_count = [0]
def fake_run(argv, **_kw):
call_count[0] += 1
if call_count[0] == 1:
return _StubCompleted(returncode=0)
return _StubCompleted(
returncode=2,
stderr="error: undefined reference to `roctxRangePushA'\n",
)
monkeypatch.setattr(inject_roctx_loader.subprocess, "run", fake_run)
assert inject_roctx_loader._try_jit(_FAKE_TAG) is None
marker = inject_roctx_loader._previous_jit_failure(_FAKE_TAG)
assert marker is not None
assert inject_roctx_loader.TIER_JIT in marker
assert "undefined reference" in marker
def test_try_jit_records_failure_marker_when_so_missing(monkeypatch, tmp_path):
"""A successful build with no ``.so`` output writes a failure marker."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
_set_so_inputs_present(monkeypatch, tmp_path)
monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: "/fake/cmake")
monkeypatch.setattr(
inject_roctx_loader.subprocess,
"run",
lambda *a, **k: _StubCompleted(returncode=0),
)
assert inject_roctx_loader._try_jit(_FAKE_TAG) is None
marker = inject_roctx_loader._previous_jit_failure(_FAKE_TAG)
assert marker is not None
assert inject_roctx_loader.TIER_JIT in marker
assert "missing" in marker
def test_try_jit_cleans_build_dir_on_success(monkeypatch, tmp_path):
"""The build directory is removed after a successful build."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
_set_so_inputs_present(monkeypatch, tmp_path)
monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: "/fake/cmake")
monkeypatch.setattr(
inject_roctx_loader.subprocess,
"run",
lambda *a, **k: _StubCompleted(returncode=0),
)
cache_dir = inject_roctx_loader._jit_cache_dir()
build_dir = cache_dir / f"cmake-build-{_FAKE_TAG}"
produced = build_dir / f"roctx_recordfn-{_FAKE_TAG}.so"
produced.parent.mkdir(parents=True, exist_ok=True)
produced.write_bytes(b"stub-so")
leftover = build_dir / "CMakeFiles"
leftover.mkdir(exist_ok=True)
(leftover / "stale.o").write_bytes(b"x")
monkeypatch.setattr(
inject_roctx_loader,
"_import_module_from_path",
lambda _n, _p: object(),
)
result = inject_roctx_loader._try_jit(_FAKE_TAG)
assert result is not None
assert not build_dir.exists(), (
"build dir must be removed on success to bound cache disk usage"
)
cached_so = cache_dir / f"roctx_recordfn-{_FAKE_TAG}.so"
assert cached_so.exists() and cached_so.read_bytes() == b"stub-so", (
"produced .so must have been copied to the cache before cleanup"
)
def test_try_jit_keeps_build_dir_on_failure(monkeypatch, tmp_path):
"""Preserve the build dir on failure for post-mortem inspection."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
_set_so_inputs_present(monkeypatch, tmp_path)
monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: "/fake/cmake")
monkeypatch.setattr(
inject_roctx_loader.subprocess,
"run",
lambda *a, **k: _StubCompleted(returncode=1, stderr="boom\n"),
)
assert inject_roctx_loader._try_jit(_FAKE_TAG) is None
build_dir = inject_roctx_loader._jit_cache_dir() / f"cmake-build-{_FAKE_TAG}"
assert build_dir.exists(), "build dir must survive a failure"
# ---------------------------------------------------------------------------
# explain_cmake_failure classification
# ---------------------------------------------------------------------------
def test_explain_cmake_failure_classifies_torch_not_found():
reason, hint = inject_roctx_loader._explain_cmake_failure(
"configure",
RuntimeError("rc=1"),
"CMake Error: Could not find Torch (missing: TORCH_DIR)\n",
)
assert "libtorch" in reason.lower()
assert "torch" in hint.lower()
def test_explain_cmake_failure_classifies_roctx_not_found():
reason, hint = inject_roctx_loader._explain_cmake_failure(
"configure",
RuntimeError("rc=1"),
"find_library failed: rocprofiler-sdk-roctx not found\n",
)
assert "roctx" in reason.lower() or "rocprofiler-sdk-roctx" in reason.lower()
assert "rocm_path" in hint.lower() or "/opt/rocm" in hint.lower()
def test_explain_cmake_failure_classifies_missing_cxx_compiler():
reason, hint = inject_roctx_loader._explain_cmake_failure(
"configure",
RuntimeError("rc=1"),
"No CMAKE_CXX_COMPILER could be found.\n",
)
assert "compiler" in reason.lower()
assert "g++" in hint.lower() or "clang" in hint.lower()
def test_explain_cmake_failure_never_recommends_installing_ninja():
"""cmake-tier hints must not recommend installing ninja."""
samples = [
(RuntimeError("rc=1"), "CMake Error: ninja not found"),
(RuntimeError("rc=1"), "Could not find Torch"),
(RuntimeError("rc=1"), ""),
]
for err, stderr in samples:
reason, hint = inject_roctx_loader._explain_cmake_failure(
"configure",
err,
stderr,
)
forbidden = ("install ninja", "pip install ninja", "apt install ninja")
joined = (reason + " " + hint).lower()
for token in forbidden:
assert token not in joined, (
f"cmake-tier hint must not recommend installing ninja "
f"(found {token!r} in: {hint!r})"
)
# ---------------------------------------------------------------------------
# load(): tier ordering and REBUILD override
# ---------------------------------------------------------------------------
def test_rebuild_env_var_clears_failure_marker(monkeypatch, tmp_path):
"""``ROCPROFCOMPUTE_REBUILD_ROCTX=1`` clears any cached failure marker."""
monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG)
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
monkeypatch.setenv(inject_roctx_loader._REBUILD_ENV_VAR, "1")
inject_roctx_loader._record_jit_failure(_FAKE_TAG, RuntimeError("stale failure"))
assert inject_roctx_loader._previous_jit_failure(_FAKE_TAG) is not None
sentinel = object()
monkeypatch.setattr(
inject_roctx_loader,
"_try_prebuilt",
lambda _t, diagnostics=None: pytest.fail("prebuilt must be skipped"),
)
monkeypatch.setattr(
inject_roctx_loader,
"_try_jit",
lambda _t, force_rebuild=False, diagnostics=None: sentinel,
)
result = inject_roctx_loader.load()
assert result.module is sentinel
assert inject_roctx_loader._previous_jit_failure(_FAKE_TAG) is None, (
"REBUILD must clear the negative cache before the forced rebuild"
)
def test_rebuild_env_var_skips_prebuilt_and_forces_jit_rebuild(monkeypatch):
"""REBUILD=1 routes directly to the JIT build, skipping prebuilt and cache."""
monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG)
calls = []
monkeypatch.setattr(
inject_roctx_loader,
"_try_prebuilt",
lambda tag, diagnostics=None: calls.append(("prebuilt", tag)) or None,
)
sentinel = object()
def _jit(tag, force_rebuild=False, diagnostics=None):
calls.append(("jit", tag, force_rebuild))
return sentinel
monkeypatch.setattr(inject_roctx_loader, "_try_jit", _jit)
monkeypatch.setenv(inject_roctx_loader._REBUILD_ENV_VAR, "1")
result = inject_roctx_loader.load()
assert result.module is sentinel
assert [c[0] for c in calls] == ["jit"], (
f"expected only _try_jit to fire under REBUILD; saw {calls!r}"
)
assert calls[0][1] == _FAKE_TAG, "tag must propagate to the build step"
assert calls[0][2] is True, "REBUILD must pass force_rebuild=True"
def test_rebuild_env_var_returns_none_when_build_fails(monkeypatch):
"""Under REBUILD, JIT failing yields ``None`` with no fallback."""
monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG)
monkeypatch.setattr(
inject_roctx_loader,
"_try_prebuilt",
lambda _t, diagnostics=None: pytest.fail("_try_prebuilt called"),
)
monkeypatch.setattr(
inject_roctx_loader,
"_try_jit",
lambda _t, force_rebuild=False, diagnostics=None: None,
)
monkeypatch.setenv(inject_roctx_loader._REBUILD_ENV_VAR, "1")
assert inject_roctx_loader.load().module is None
def test_default_load_path_still_tries_prebuilt_first(monkeypatch):
"""The prebuilt tier short-circuits the default load path."""
monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG)
monkeypatch.delenv(inject_roctx_loader._REBUILD_ENV_VAR, raising=False)
calls = []
sentinel = object()
monkeypatch.setattr(
inject_roctx_loader,
"_try_prebuilt",
lambda tag, diagnostics=None: calls.append("prebuilt") or sentinel,
)
monkeypatch.setattr(
inject_roctx_loader,
"_try_jit",
lambda tag, force_rebuild=False, diagnostics=None: calls.append("jit") or None,
)
assert inject_roctx_loader.load().module is sentinel
assert calls == ["prebuilt"], (
f"expected prebuilt to short-circuit before jit; saw {calls!r}"
)
def test_default_load_path_walks_prebuilt_then_jit(monkeypatch):
"""Tiers are tried in order: prebuilt, then jit."""
monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG)
monkeypatch.delenv(inject_roctx_loader._REBUILD_ENV_VAR, raising=False)
calls = []
monkeypatch.setattr(
inject_roctx_loader,
"_try_prebuilt",
lambda tag, diagnostics=None: calls.append("prebuilt") or None,
)
sentinel = object()
monkeypatch.setattr(
inject_roctx_loader,
"_try_jit",
lambda tag, force_rebuild=False, diagnostics=None: (
calls.append("jit") or sentinel
),
)
assert inject_roctx_loader.load().module is sentinel
assert calls == ["prebuilt", "jit"], (
f"expected order prebuilt -> jit, saw {calls!r}"
)
def test_load_does_not_raise_when_torch_missing(monkeypatch):
"""``load()`` returns ``None`` when ``torch`` is not importable."""
monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: None)
assert inject_roctx_loader.load().module is None
def test_load_returns_module_or_none_no_raise():
"""``load()`` returns a module exposing the documented API, or ``None``."""
mod = inject_roctx_loader.load().module
if mod is not None:
for sym in (
"install",
"uninstall",
"is_installed",
"push_user_scope",
"pop_user_scope",
"dump_stats",
):
assert hasattr(mod, sym), f"loaded module is missing {sym}"
# ---------------------------------------------------------------------------
# LoadResult: tier / diagnostics / format_load_diagnostic_trail
# ---------------------------------------------------------------------------
def test_load_result_records_successful_tier(monkeypatch):
"""``LoadResult.tier`` reports the name of the tier that returned a module."""
monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG)
monkeypatch.delenv(inject_roctx_loader._REBUILD_ENV_VAR, raising=False)
sentinel = object()
monkeypatch.setattr(
inject_roctx_loader, "_try_prebuilt", lambda _t, diagnostics=None: None
)
monkeypatch.setattr(
inject_roctx_loader,
"_try_jit",
lambda _t, force_rebuild=False, diagnostics=None: sentinel,
)
result = inject_roctx_loader.load()
assert result.module is sentinel
assert result.tier == inject_roctx_loader.TIER_JIT
assert inject_roctx_loader.TIER_JIT in inject_roctx_loader.C_TIER_NAMES
def test_load_result_tier_is_none_when_all_tiers_miss(monkeypatch):
"""``LoadResult.tier`` is ``None`` when every tier returns ``None``."""
monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG)
monkeypatch.delenv(inject_roctx_loader._REBUILD_ENV_VAR, raising=False)
monkeypatch.setattr(
inject_roctx_loader, "_try_prebuilt", lambda _t, diagnostics=None: None
)
monkeypatch.setattr(
inject_roctx_loader,
"_try_jit",
lambda _t, force_rebuild=False, diagnostics=None: None,
)
result = inject_roctx_loader.load()
assert result.module is None
assert result.tier is None
def test_load_returns_independent_diagnostics(monkeypatch):
"""Each ``load()`` call returns its own diagnostics list."""
monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG)
monkeypatch.delenv(inject_roctx_loader._REBUILD_ENV_VAR, raising=False)
monkeypatch.setattr(
inject_roctx_loader, "_try_prebuilt", lambda _t, diagnostics=None: object()
)
monkeypatch.setattr(
inject_roctx_loader,
"_try_jit",
lambda _t, force_rebuild=False, diagnostics=None: None,
)
first = inject_roctx_loader.load().diagnostics
second = inject_roctx_loader.load().diagnostics
assert first is not second, "each load() must return its own diagnostics list"
def test_safe_log_appends_to_provided_diagnostics():
"""Each ``_safe_log`` call appends to the provided diagnostics list."""
diagnostics: list[tuple[str, str]] = []
inject_roctx_loader._safe_log("log", "tier A skipped", diagnostics)
inject_roctx_loader._safe_log("warning", "tier B failed", diagnostics)
inject_roctx_loader._safe_log("log", "final fallback engaged", diagnostics)
assert [lvl for lvl, _ in diagnostics] == ["log", "warning", "log"]
assert [msg for _, msg in diagnostics] == [
"tier A skipped",
"tier B failed",
"final fallback engaged",
]
def test_load_result_carries_tier_and_diagnostics(monkeypatch):
"""``load()`` returns the tier scalar and a diagnostics list."""
monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG)
monkeypatch.delenv(inject_roctx_loader._REBUILD_ENV_VAR, raising=False)
monkeypatch.setattr(
inject_roctx_loader, "_try_prebuilt", lambda _t, diagnostics=None: None
)
monkeypatch.setattr(
inject_roctx_loader,
"_try_jit",
lambda _t, force_rebuild=False, diagnostics=None: object(),
)
result = inject_roctx_loader.load()
assert result.tier == inject_roctx_loader.TIER_JIT
assert isinstance(result.diagnostics, list)
def test_load_result_returns_python_tier_failure_trail(monkeypatch):
"""When every tier misses the trail includes the terminal fallback line."""
monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG)
monkeypatch.delenv(inject_roctx_loader._REBUILD_ENV_VAR, raising=False)
monkeypatch.setattr(
inject_roctx_loader, "_try_prebuilt", lambda _t, diagnostics=None: None
)
monkeypatch.setattr(
inject_roctx_loader,
"_try_jit",
lambda _t, force_rebuild=False, diagnostics=None: None,
)
result = inject_roctx_loader.load()
assert result.tier is None
joined = " ".join(msg for _, msg in result.diagnostics).lower()
assert "python-only injector" in joined or "no roctx_recordfn" in joined, (
f"terminal-fallback line missing from trail: "
f"{[(lvl, msg) for lvl, msg in result.diagnostics]!r}"
)
def test_format_load_diagnostic_trail_handles_empty():
"""An empty trail renders to an empty string."""
assert inject_roctx_loader.format_load_diagnostic_trail([]) == ""
def test_format_load_diagnostic_trail_caps_lines():
"""``format_load_diagnostic_trail`` caps output and keeps the latest lines."""
trail = [("log", f"line {i}") for i in range(100)]
rendered = inject_roctx_loader.format_load_diagnostic_trail(
trail,
max_lines=12,
)
lines = rendered.splitlines()
assert len(lines) == 12, (
f"expected max_lines=12 to cap output, saw {len(lines)} lines"
)
assert "line 99" in rendered, "must keep the trailing (latest) lines"
assert "line 0" not in rendered, "must drop the leading (oldest) lines"
def test_format_load_diagnostic_trail_includes_level_per_line():
"""Each rendered line carries its level (INFO vs WARNING)."""
trail = [
("log", "skipped tier A"),
("warning", "tier B failed"),
]
rendered = inject_roctx_loader.format_load_diagnostic_trail(trail)
assert "[log]" in rendered
assert "[warning]" in rendered
assert "skipped tier A" in rendered
assert "tier B failed" in rendered
# ---------------------------------------------------------------------------
# Python fallback integration
# ---------------------------------------------------------------------------
def test_python_fallback_path_still_works_without_so(monkeypatch):
"""With the loader returning ``None``, the Python fallback still works."""
try:
import torch # noqa: F401
except ImportError:
pytest.skip("torch not importable")
monkeypatch.setattr(inject_roctx_loader, "load", lambda **kw: None)
if "utils.inject_roctx" in sys.modules:
del sys.modules["utils.inject_roctx"]
importlib.import_module("utils.inject_roctx")
try:
from utils.inject_roctx import core
from utils.inject_roctx._backends import torch as _torch_backend
assert _torch_backend.using_c_tier() is False
assert _torch_backend.dump_recordfn_stats() is None
pushed: list[str] = []
original_io = core.get_python_tier_io()
core.set_python_tier_io(push=pushed.append, pop=lambda: None)
try:
core._push_scope("py.tier.test", "#1@test:1")
core._pop_scope()
finally:
core.set_python_tier_io(*original_io)
assert pushed, "Python-tier rangePush was not invoked in fallback mode"
finally:
sys.modules.pop("utils.inject_roctx", None)
def test_python_fallback_uses_python_dispatch_sentinel(monkeypatch):
"""When no user frame is available the fallback emits ``python.dispatch:0``."""
try:
import torch # noqa: F401
except ImportError:
pytest.skip("torch not importable; utils.inject_roctx module-load exits")
monkeypatch.setattr(inject_roctx_loader, "load", lambda **kw: None)
if "utils.inject_roctx" in sys.modules:
del sys.modules["utils.inject_roctx"]
importlib.import_module("utils.inject_roctx")
try:
from utils.inject_roctx import core
monkeypatch.setattr("inspect.currentframe", lambda: None)
assert core.resolve_user_caller_location() == "python.dispatch:0"
import inspect as _stdlib_inspect
src = _stdlib_inspect.getsource(core)
assert "dispatcher:0" not in src, "legacy 'dispatcher:0' sentinel still present"
finally:
sys.modules.pop("utils.inject_roctx", None)
def test_import_does_not_apply_global_patches(monkeypatch):
"""Importing ``utils.inject_roctx`` does not patch PyTorch."""
monkeypatch.setattr(inject_roctx_loader, "load", lambda **kw: None)
if "utils.inject_roctx" in sys.modules:
del sys.modules["utils.inject_roctx"]
try:
import torch # noqa: F401
except Exception:
pytest.skip("torch not importable")
import torch as _torch
pre = {
"compile": getattr(_torch, "compile", None),
}
importlib.import_module("utils.inject_roctx")
try:
post = {
"compile": getattr(_torch, "compile", None),
}
from utils.inject_roctx import core
for sym in (
"install_global_wraps",
"_push_scope",
"_pop_scope",
"resolve_user_caller_location",
):
assert hasattr(core, sym), f"core symbol missing: {sym}"
from utils.inject_roctx._backends import torch as _torch_backend
for sym in (
"install_function_apply_wrappers",
"using_c_tier",
"dump_recordfn_stats",
):
assert hasattr(_torch_backend, sym), f"torch backend symbol missing: {sym}"
assert post["compile"] is pre["compile"], "torch.compile was replaced on import"
finally:
sys.modules.pop("utils.inject_roctx", None)