Files
TheRock/rocm-systems/projects/rocprofiler-systems/tests/pytest/rocprofsys/config.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

577 lines
22 KiB
Python

# Copyright (c) Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
from __future__ import annotations
from dataclasses import dataclass, field
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
from typing import Optional
import re
from .cache import persistent_cache, persistent_cached_property, resolve_username
from .capabilities import SystemCapabilities
@dataclass
class RocprofsysConfig:
"""Configuration for rocprofiler-systems test execution
Contains necessary paths to configure tests for build or for install modes.
Attributes:
- rocprofsys_build_dir: Path to either the build or install directory
- rocprofsys_instrument: Path to rocprof-sys-instrument executable
- rocprofsys_run: Path to rocprof-sys-run executable
- rocprofsys_sample: Path to rocprof-sys-sample executable
- rocprofsys_causal: Path to rocprof-sys-causal executable
- rocprofsys_avail: Path to rocprof-sys-avail executable
- rocm_path: Path to ROCm installation directory
- rocprofsys_lib_dir: Path to rocprofsys library directory
- rocprofsys_bin_dir: Path to rocprofsys binary directory
- rocprofsys_examples_dir: Path
In build mode, this is the root of the build directory (most examples lie here).
In install mode, this is the examples/ directory.
- rocprofsys_tests_dir: Path to rocprofsys tests directory
- test_output_dir: Path to test output directory
- rocpd_validation_rules: Path to rocprofiler-systems rocpd validation rules directory
- rocm_version: Tuple of (major, minor, patch) of the installed ROCm version
- is_installed: Whether this is an installed configuration
- rocprofsys_python: Path to rocprof-sys-python executable
- rocprofsys_site_packages: Path to rocprofsys site-packages directory
- _python_versions_hint: List of python versions available (hint for pytest_generate_tests)
- _python_root_dirs_hint: List of python root directories available (hint for pytest_generate_tests)
- Capabilities: SystemCapabilities instance
"""
rocprofsys_build_dir: Path
rocprofsys_instrument: Path
rocprofsys_run: Path
rocprofsys_sample: Path
rocprofsys_causal: Path
rocprofsys_avail: Path
rocm_path: Optional[Path]
rocprofsys_lib_dir: Path
rocprofsys_bin_dir: Path
rocprofsys_examples_dir: Path
rocprofsys_tests_dir: Path
rocpd_validation_rules: Path
test_output_dir: Path
rocprofsys_python: Optional[Path] = None
rocprofsys_site_packages: Optional[Path] = None
is_installed: bool = False
rocm_version: Optional[tuple[int, int, int]] = None
_python_versions_hint: Optional[list[str]] = field(default=None, repr=False)
_python_root_dirs_hint: Optional[list[Path]] = field(default=None, repr=False)
_capabilities: Optional[SystemCapabilities] = field(
default=None, init=False, repr=False
)
@property
def capabilities(self) -> SystemCapabilities:
"""Lazy-initialized system capabilities"""
if self._capabilities is None:
self._capabilities = SystemCapabilities.from_config(self)
return self._capabilities
@persistent_cached_property
def llvm_lib_paths(self) -> list[Path]:
"""Get list of found ROCm LLVM lib paths.
Returns:
List of existing LLVM lib paths found, empty list if none found.
"""
found_paths = []
if self.rocm_path:
# Match discover_llvm_libdir_for_ompt() logic
candidates = [
self.rocm_path / "llvm" / "lib",
self.rocm_path / "lib" / "llvm" / "lib",
]
# Determine the per-target runtime subdir used when
# LLVM_ENABLE_PER_TARGET_RUNTIME_DIR=ON
clang_candidates = [
self.rocm_path / "bin" / "amdclang",
self.rocm_path / "llvm" / "bin" / "amdclang",
self.rocm_path / "lib" / "llvm" / "bin" / "amdclang",
]
clang_path = next((c for c in clang_candidates if c.exists()), None)
if clang_path is None:
which_clang = shutil.which("amdclang")
if which_clang:
clang_path = Path(which_clang)
if clang_path is not None:
try:
host_triple = subprocess.run(
[str(clang_path), "--print-target-triple"],
capture_output=True,
text=True,
timeout=10,
check=True,
).stdout.strip()
except (subprocess.SubprocessError, OSError) as exc:
raise RuntimeError(
f"'{clang_path} --print-target-triple' failed; "
"this suggests a broken toolchain."
) from exc
if host_triple:
candidates.append(
self.rocm_path / "lib" / "llvm" / "lib" / host_triple
)
for candidate in candidates:
if candidate.exists():
found_paths.append(candidate)
return found_paths
def get_library_path(self) -> str:
"""Get LD_LIBRARY_PATH including rocprofiler-systems libraries.
Returns:
LD_LIBRARY_PATH string with rocprofiler-systems libraries
"""
paths = [str(self.rocprofsys_lib_dir.resolve())]
# Where libraries for the examples live
if self.is_installed:
examples_lib = self.rocprofsys_examples_dir / "lib"
if examples_lib.is_dir():
paths.append(str(examples_lib.resolve()))
existing = os.environ.get("LD_LIBRARY_PATH", "")
if existing:
paths.append(existing)
# Add ROCm LLVM lib as fallback
for llvm_path in self.llvm_lib_paths:
paths.append(str(llvm_path))
return ":".join(paths)
def get_preload_path(self) -> Optional[str]:
"""Get LD_PRELOAD for sanitizer builds, or None when not applicable.
When $ASAN_LIBRARY is set (exported by the sanitizer CI) it is prepended
to the shell's LD_PRELOAD so the asan runtime stays first in the link
order once rocprof-sys-run appends librocprof-sys-dl.so.
Returns:
LD_PRELOAD string, or None when not building with the sanitizer.
"""
asan_library = os.environ.get("ASAN_LIBRARY")
if not asan_library:
return None
existing = os.environ.get("LD_PRELOAD", "")
return f"{asan_library}:{existing}" if existing else asan_library
def get_target_executable(self, name: str) -> Path:
"""Get path to a test target executable.
The executable is always resolved from the build/install tree; the
system PATH is never consulted, so the suite cannot silently pick up an
unrelated copy (e.g. one under /opt/rocm/bin).
When is_installed is True, searches in the following order:
1. rocprofsys_examples_dir/name
2. rocprofsys_bin_dir/name
When is_installed is False, searches in the following order:
1. rocprofsys_examples_dir/examples/python/name (python scripts)
2. rocprofsys_examples_dir/name
3. rocprofsys_examples_dir/examples/code-coverage/name (code-coverage.py only)
4. rocprofsys_bin_dir/name
Args:
name: Name of the target executable
Returns:
Path to the executable
Raises:
FileNotFoundError: If the executable is not found
"""
if self.is_installed:
# examples directory layout
exe = self.rocprofsys_examples_dir / name
if exe.exists() and exe.is_file():
return exe
# binary directory
exe = self.rocprofsys_bin_dir / name
if exe.exists() and exe.is_file():
return exe
raise FileNotFoundError(
f"Target executable '{name}' not found. Searched in:\n"
f" - {self.rocprofsys_examples_dir}/{name}\n"
f" - {self.rocprofsys_bin_dir}/{name}\n"
)
else:
# Python check
exe = self.rocprofsys_examples_dir / "examples" / "python" / name
if exe.exists() and exe.is_file():
return exe
# examples directory layout
exe = self.rocprofsys_examples_dir / name
if exe.exists() and exe.is_file():
return exe
# code-coverage.py lies in the code-coverage directory
if name == "code-coverage.py":
exe = self.rocprofsys_examples_dir / "examples" / "code-coverage" / name
if exe.exists() and exe.is_file():
return exe
# binary directory
exe = self.rocprofsys_bin_dir / name
if exe.exists() and exe.is_file():
return exe
raise FileNotFoundError(
f"Target executable '{name}' not found. Searched in:\n"
f" - {self.rocprofsys_examples_dir}/examples/python/{name}\n"
f" - {self.rocprofsys_examples_dir}/{name}\n"
f" - {self.rocprofsys_examples_dir}/examples/code-coverage/{name}\n"
f" - {self.rocprofsys_bin_dir}/{name}\n"
)
def _find_rocm_path(optional: bool = False) -> Optional[Path]:
"""Find ROCm installation path.
Args:
optional: If True, return None instead of raising when ROCm is not found.
"""
for candidate in [
os.environ.get("ROCM_PATH"),
"/opt/rocm",
"/usr/local/rocm",
]:
if candidate and Path(candidate).exists():
return Path(candidate).resolve()
if optional:
return None
raise FileNotFoundError(
"Could not find ROCm installation. Set ROCM_PATH environment variable."
)
def _get_rocm_version(rocm_optional: bool = False) -> Optional[tuple[int, int, int]]:
"""Get the installed ROCm version as a tuple (major, minor, patch).
Returns:
Tuple of (major, minor, patch) or None if ROCm not found or version undetectable.
"""
rocm_path = _find_rocm_path(optional=rocm_optional)
if rocm_path is None:
return None
# Check .info/version file
version_file = rocm_path / ".info" / "version"
if not version_file.exists():
# Try alternative location
version_file = rocm_path / "share" / "rocm" / "version"
if version_file.exists():
try:
version_str = version_file.read_text().strip()
match = re.match(r"(\d+)\.(\d+)\.(\d+)", version_str)
if match:
return (int(match.group(1)), int(match.group(2)), int(match.group(3)))
except (OSError, ValueError):
pass
return None
def _find_executable(name: str, search_paths: list[Path]) -> Optional[Path]:
"""Find a rocprof-sys executable within the given search paths.
Only the explicit search paths are consulted -- these are the build/install
``bin`` directory, which already honors the ROCPROFSYS_BUILD_DIR and
ROCPROFSYS_INSTALL_DIR overrides. PATH is intentionally NOT searched: the
suite must exercise the rocprof-sys binaries from the build/install under
test, never an unrelated copy that happens to be on PATH (e.g.
``/opt/rocm/bin``), which would silently test the wrong build.
"""
for search_dir in search_paths:
exe = search_dir / name
if exe.exists() and exe.is_file():
return exe.resolve()
return None
def _find_rocprofsys_core_executables(
search_paths: list[Path],
) -> dict[str, Optional[Path]]:
"""Return a dictionary of rocprofiler-systems executables and their paths.
Throws a FileNotFoundError if any of the executables are not found.
"""
rocprof_instrument = _find_executable("rocprof-sys-instrument", search_paths)
rocprof_sample = _find_executable("rocprof-sys-sample", search_paths)
rocprof_run = _find_executable("rocprof-sys-run", search_paths)
rocprof_causal = _find_executable("rocprof-sys-causal", search_paths)
rocprof_avail = _find_executable("rocprof-sys-avail", search_paths)
required_executables = {
"rocprof-sys-instrument": rocprof_instrument,
"rocprof-sys-sample": rocprof_sample,
"rocprof-sys-run": rocprof_run,
"rocprof-sys-causal": rocprof_causal,
"rocprof-sys-avail": rocprof_avail,
}
missing = [name for name, path in required_executables.items() if path is None]
if missing:
raise FileNotFoundError(
f"Required rocprof-sys executables not found: {', '.join(missing)}. "
f"Searched in: {search_paths}. Build the project first, or point the "
f"suite at the correct location with ROCPROFSYS_BUILD_DIR (build tree) "
f"or ROCPROFSYS_INSTALL_DIR (install prefix)."
)
return required_executables
def _find_rocprofsys_python(
search_paths: list[Path], rocprofsys_build_dir: Path
) -> tuple[Optional[Path], Optional[Path]]:
"""Return the Python executable and rocprofsys site-package path.
Both must be found, otherwise (None, None) is returned.
"""
rocprof_python = _find_executable("rocprof-sys-python", search_paths)
if not rocprof_python:
return None, None
# It is either in the agnostic path or in the versioned path
agnostic_path = rocprofsys_build_dir / "lib" / "python" / "site-packages"
if (agnostic_path / "rocprofsys").is_dir():
return rocprof_python, agnostic_path
# Only one versioned path will exist
# Otherwise, it will be in the agnostic path
for child in (rocprofsys_build_dir / "lib").iterdir():
if child.is_dir() and re.match(r"python\d+\.\d+", child.name):
site_packages = child / "site-packages"
if (site_packages / "rocprofsys").is_dir():
return rocprof_python, site_packages
return None, None
def _merge_python_root_dirs(
explicit_dirs: Optional[list[Path]],
) -> Optional[list[Path]]:
"""Merge explicit --python-root-dirs with ROCPROFSYS_PYTHON_HINTS env var.
ROCPROFSYS_PYTHON_HINTS entries may point to bin/ directories or their parents;
both are normalized to the parent directory for pythonX.Y lookup.
"""
result: list[Path] = list(explicit_dirs or [])
env_hints = os.environ.get("ROCPROFSYS_PYTHON_HINTS", "")
if env_hints:
for hint in env_hints.split(";"):
hint = hint.strip()
if hint:
hint_path = Path(hint)
parent = hint_path.parent if hint_path.name == "bin" else hint_path
if parent not in result:
result.append(parent)
return result or None
@persistent_cache("config.discover_install_config")
def discover_install_config(
python_versions: Optional[list[str]] = None,
python_root_dirs: Optional[list[Path]] = None,
rocm_optional: bool = False,
) -> RocprofsysConfig:
"""Discover rocprofiler-systems installation configuration.
Creates configuration for testing against installed binaries. The install
prefix is resolved from ROCPROFSYS_INSTALL_DIR or common locations.
Returns:
RocprofsysConfig configured for installed binaries
Raises:
FileNotFoundError: If build/installation dirs and executables are not found
"""
install_dir: Optional[Path] = None
env_install = os.environ.get("ROCPROFSYS_INSTALL_DIR")
if env_install:
install_dir = Path(env_install).resolve()
else:
_rocm_candidate = _find_rocm_path(optional=True)
_install_candidates = [
Path("/usr/local"),
Path("/usr"),
Path("/opt/rocprofiler-systems"), # Standard install location from README.md
]
if _rocm_candidate is not None:
_install_candidates.insert(0, _rocm_candidate)
for candidate in _install_candidates:
if (
candidate
and (candidate / "share" / "rocprofiler-systems" / "tests").is_dir()
and (candidate / "share" / "rocprofiler-systems" / "examples").is_dir()
):
install_dir = candidate
break
if install_dir is None:
raise FileNotFoundError(
"Could not find a suitable rocprofiler-systems installation. Set ROCPROFSYS_INSTALL_DIR "
"environment variable."
"A suitable installation is one that has the following directory: share/rocprofiler-systems/examples "
"and share/rocprofiler-systems/tests"
)
install_dir = install_dir.resolve()
# Determine directory layout
bin_dir = install_dir / "bin"
lib_dir = install_dir / "lib"
# For lib64 systems
if not lib_dir.exists() and (install_dir / "lib64").exists():
lib_dir = install_dir / "lib64"
examples_dir = install_dir / "share" / "rocprofiler-systems" / "examples"
tests_dir = install_dir / "share" / "rocprofiler-systems" / "tests"
rocpd_validation_rules = tests_dir / "rocpd-validation-rules"
# Create a temporary directory for test outputs
username = resolve_username()
output_dir = Path(tempfile.gettempdir()) / username / "rocprof-sys-pytest-output"
output_dir.mkdir(parents=True, exist_ok=True)
rocm_path = _find_rocm_path(optional=rocm_optional)
search_paths = [bin_dir]
sys_execs = _find_rocprofsys_core_executables(search_paths)
rocprofsys_python, rocprofsys_site_packages = _find_rocprofsys_python(
search_paths, install_dir
)
return RocprofsysConfig(
rocprofsys_build_dir=install_dir,
rocprofsys_instrument=sys_execs["rocprof-sys-instrument"],
rocprofsys_run=sys_execs["rocprof-sys-run"],
rocprofsys_sample=sys_execs["rocprof-sys-sample"],
rocprofsys_causal=sys_execs["rocprof-sys-causal"],
rocprofsys_avail=sys_execs["rocprof-sys-avail"],
rocm_path=rocm_path,
rocprofsys_lib_dir=lib_dir,
rocprofsys_bin_dir=bin_dir,
rocprofsys_examples_dir=examples_dir,
rocprofsys_tests_dir=tests_dir,
rocpd_validation_rules=rocpd_validation_rules,
test_output_dir=output_dir,
rocm_version=_get_rocm_version(rocm_optional=rocm_optional),
is_installed=True,
rocprofsys_python=rocprofsys_python,
rocprofsys_site_packages=rocprofsys_site_packages,
_python_versions_hint=python_versions,
_python_root_dirs_hint=_merge_python_root_dirs(python_root_dirs),
)
@persistent_cache("config.discover_build_config")
def discover_build_config(
python_versions: Optional[list[str]] = None,
python_root_dirs: Optional[list[Path]] = None,
rocm_optional: bool = False,
) -> RocprofsysConfig:
"""Discover rocprofiler-systems build configuration.
Finds the build directory automatically (from ROCPROFSYS_BUILD_DIR or by
walking up from this file). If no build directory is found but an
installation is available, falls back to discover_install_config().
Returns:
RocprofsysConfig with discovered paths
Raises:
FileNotFoundError: If build/installation dirs and executables are not found
"""
# Explicit install directory check
if os.environ.get("ROCPROFSYS_INSTALL_DIR"):
return discover_install_config(
python_versions=python_versions,
python_root_dirs=python_root_dirs,
rocm_optional=rocm_optional,
)
# When running from pyz package (extracted to /tmp), fall back to install config
# The pyz extracts to paths like /tmp/rocprofsys-tests-*/tests/rocprofsys/config.py
current_file = Path(__file__).resolve()
if str(current_file).startswith(tempfile.gettempdir()):
return discover_install_config(
python_versions=python_versions,
python_root_dirs=python_root_dirs,
rocm_optional=rocm_optional,
)
# All files should be in the build directory
env_build = os.environ.get("ROCPROFSYS_BUILD_DIR")
if env_build:
build_dir = Path(env_build).resolve()
else:
build_dir = Path(__file__).resolve().parent.parent.parent.parent.parent.parent
if not build_dir.exists():
raise FileNotFoundError(
"Could not find build directory or installation. Set one of:\n"
" - ROCPROFSYS_BUILD_DIR: Path to build directory\n"
" - ROCPROFSYS_INSTALL_DIR: Path to installation prefix"
)
rocm_path = _find_rocm_path(optional=rocm_optional)
bin_dir = build_dir / "bin"
lib_dir = build_dir / "lib"
search_paths = [bin_dir]
sys_execs = _find_rocprofsys_core_executables(search_paths)
rocprofsys_python, rocprofsys_site_packages = _find_rocprofsys_python(
search_paths, build_dir
)
share_path = build_dir / "share" / "rocprofiler-systems"
output_dir = build_dir / "rocprof-sys-pytest-output"
tests_dir = share_path / "tests"
return RocprofsysConfig(
rocprofsys_build_dir=build_dir,
rocprofsys_instrument=sys_execs["rocprof-sys-instrument"],
rocprofsys_run=sys_execs["rocprof-sys-run"],
rocprofsys_sample=sys_execs["rocprof-sys-sample"],
rocprofsys_causal=sys_execs["rocprof-sys-causal"],
rocprofsys_avail=sys_execs["rocprof-sys-avail"],
rocm_path=rocm_path,
rocprofsys_lib_dir=lib_dir,
rocprofsys_bin_dir=bin_dir,
rocprofsys_examples_dir=build_dir, # Example binaries are (almost always) in root of build directory
rocprofsys_tests_dir=tests_dir,
rocpd_validation_rules=tests_dir / "rocpd-validation-rules",
test_output_dir=output_dir,
rocm_version=_get_rocm_version(rocm_optional=rocm_optional),
is_installed=False,
rocprofsys_python=rocprofsys_python,
rocprofsys_site_packages=rocprofsys_site_packages,
_python_versions_hint=python_versions,
_python_root_dirs_hint=_merge_python_root_dirs(python_root_dirs),
)