Files
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

470 lines
16 KiB
Python

# Copyright (c) Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
import builtins
import importlib.util
import os
import random
import shutil
import subprocess
import sys
from importlib.machinery import SourceFileLoader
from pathlib import Path
from unittest.mock import patch
import common
import pytest
from common import ROOT
from utils.analysis_orm import Database
# Determine script path
rocprof_compute_script_path = Path(ROOT) / "src/rocprof-compute"
if not rocprof_compute_script_path.exists():
rocprof_compute_script_path = Path(ROOT) / "rocprof-compute"
if not rocprof_compute_script_path.exists():
raise FileNotFoundError("Cannot find rocprof-compute script")
rocprof_compute_script_path = str(rocprof_compute_script_path)
class ProfileModeImportGuard:
"""
Import guard enforcing stdlib-only imports in profile mode.
Full enforcement on Python 3.10+ (uses sys.stdlib_module_names); no-op with
a warning on 3.8-3.9.
"""
_real_import = None
# Project modules that are allowed (non-stdlib)
ALLOWED_PROJECT_MODULES = frozenset([
"rocprof_compute",
"rocprof_compute_profile",
"rocprof_compute_analyze",
"rocprof_compute_soc",
"rocprof_compute_tui",
"pc_sampling",
"utils",
"vendored",
"roofline",
"config",
"argparser", # src/argparser.py, not stdlib argparse
"rocprof_compute_base",
])
# ROCm system libraries (not pip packages)
ALLOWED_ROCM_MODULES = frozenset([
"amdsmi", # AMD System Management Interface
"hip", # HIP runtime Python bindings
"rocprofv3", # rocprofv3 python modules such as avail
"rocprofv3_avail_module", # Alternative avail module for
# backward compatibility
])
def __enter__(self):
"""Install both import hooks (Python 3.10+ only)."""
if sys.version_info >= (3, 10):
sys.meta_path.insert(0, self)
self._real_import = builtins.__import__
builtins.__import__ = self._guarded_import
else:
print(
"\n" + "=" * 70 + "\n"
"WARNING: ProfileModeImportGuard requires Python 3.10+\n"
"(sys.stdlib_module_names unavailable).\n"
"Import enforcement DISABLED for this test run.\n" + "=" * 70 + "\n",
file=sys.stderr,
)
return self
def __exit__(self, _exc_type, _exc_val, _exc_tb):
"""Restore builtins.__import__ and remove the meta_path finder."""
if sys.version_info >= (3, 10):
if self._real_import is not None:
builtins.__import__ = self._real_import
self._real_import = None
if self in sys.meta_path:
sys.meta_path.remove(self)
def _guarded_import(self, name, *args, **kwargs):
"""Hook 1: builtins.__import__ wrapper; catches cached imports."""
# Relative imports (level > 0) resolve within their already-checked
# parent; hook 2 catches the resolved submodule, so only check absolute.
level = args[3] if len(args) > 3 else kwargs.get("level", 0)
if level == 0:
self._raise_if_forbidden(name)
return self._real_import(name, *args, **kwargs)
def find_spec(self, fullname, path, target=None):
"""Hook 2: meta_path finder; catches dynamic uncached imports."""
self._raise_if_forbidden(fullname)
return None
def _raise_if_forbidden(self, fullname):
"""Raise ImportError if the top-level package is not allowed."""
top_level = fullname.split(".")[0]
# Check stdlib
if top_level in sys.stdlib_module_names:
return
# Check ROCm modules
if top_level in self.ALLOWED_ROCM_MODULES:
return
# Check project modules (validate origin to prevent third-party modules
# with same name, e.g., "utils" from site-packages)
if top_level in self.ALLOWED_PROJECT_MODULES:
if self._is_from_project(top_level):
return
# Forbidden module
raise ImportError(
f"\n{'=' * 70}\n"
"PROFILE MODE DEPENDENCY VIOLATION\n"
f"{'=' * 70}\n"
f"Forbidden package: {top_level}\n\n"
"Profile mode must use ONLY Python stdlib + ROCm libraries.\n"
"Fix: Move import to analyze mode or use stdlib alternative.\n"
"See CONTRIBUTING.md 'Profile Mode Dependency Policy'\n"
f"{'=' * 70}\n"
)
def _is_from_project(self, module_name):
"""Check if module exists in project directory, not site-packages."""
project_root = Path(__file__).parent.parent
for base in [project_root / "src", project_root]:
# Check for: module.py, module/__init__.py, or module/ (namespace pkg)
candidates = [
base / f"{module_name}.py",
base / module_name / "__init__.py",
base / module_name, # namespace package (dir without __init__.py)
]
for p in candidates:
if p.is_file() or (p.is_dir() and p.exists()):
return True
return False
def inject_mpirun(command, num_ranks):
"""
Wrap a command with mpirun for multi-rank execution.
"""
mpirun_cmd = ["mpirun"]
# Add --allow-run-as-root only when running as root
# (needed for OpenMPI in containers)
# This flag is OpenMPI-specific and would cause errors
# with other MPI implementations
if os.geteuid() == 0:
mpirun_cmd.append("--allow-run-as-root")
mpirun_cmd.extend(["-n", str(num_ranks)])
return mpirun_cmd + command
def pytest_addoption(parser):
parser.addoption(
"--call-binary",
action="store_true",
default=False,
help="Call standalone binary instead of main function during tests",
)
parser.addoption(
"--rocprofiler-sdk-tool-path",
type=str,
default=str(
Path(os.getenv("ROCM_PATH", "/opt/rocm"))
/ "lib/rocprofiler-sdk/librocprofiler-sdk-tool.so"
),
help="Path to the rocprofiler-sdk tool",
)
parser.addoption(
"--coverage-seed",
type=int,
default=random.randrange(2**32),
help="RNG seed for test_torch_trace_coverage sampling.",
)
parser.addoption(
"--coverage-n",
type=int,
default=100,
help="Random ATen sample budget (default 100).",
)
def require_torch(*, gpu: bool = False) -> None:
"""Skip when PyTorch (or, with gpu=True, GPU) is unavailable."""
if importlib.util.find_spec("torch") is None:
pytest.skip("PyTorch is not installed")
try:
import torch
except Exception as e:
pytest.skip(f"PyTorch import failed: {type(e).__name__}: {e}")
if gpu and not torch.cuda.is_available():
pytest.skip("torch.cuda.is_available() is False")
def require_triton(*, gpu: bool = False) -> None:
"""Skip when Triton, or the PyTorch/GPU stack it requires, is unavailable."""
require_torch(gpu=gpu)
if importlib.util.find_spec("triton") is None:
pytest.skip("Triton is not installed")
try:
import triton # noqa: F401
except Exception as e:
pytest.skip(f"Triton import failed: {type(e).__name__}: {e}")
@pytest.fixture
def db_session():
"""An initialized in-memory analysis database, torn down after the test."""
Database.init(":memory:")
yield Database.get_session()
Database._session.close()
Database._engine.dispose()
Database._session = None
Database._engine = None
@pytest.fixture(autouse=True)
def skip_monkeypatch_with_binary(request):
"""Skip monkeypatch tests under --call-binary (patches don't cross processes)."""
if (
request.config.getoption("--call-binary")
and "monkeypatch" in request.fixturenames
):
pytest.skip(
"Test uses monkeypatch which is incompatible with --call-binary mode"
)
@pytest.fixture
def binary_handler_profile_rocprof_compute(request):
"""
Fixture to run rocprof-compute profile command.
Args:
config: Test configuration dictionary containing app commands.
workload_dir: Directory to store profiling output.
options: Additional command-line options.
check_success: If True, assert that the command succeeds.
roof: If True, enable roofline.
app_name: Key in config dict for the application command.
attach_detach_para: Parameters for attach/detach mode.
skip_app_name: If True, skip adding --name option.
workload_dir_type: "output_directory" or "default".
num_ranks: Number of MPI ranks (1 = no MPI, >1 = use mpirun).
capture_output: If True, capture stdout/stderr and return
(returncode, stdout, stderr) tuple instead of just returncode.
stream: If True, echo child output line by line as it is produced
(requires capture_output).
Returns:
If capture_output is False: returncode (int)
If capture_output is True: (returncode, stdout, stderr) tuple
"""
def _handler(
config,
workload_dir,
options=[],
check_success=True,
roof=False,
app_name="app_1",
attach_detach_para=None,
skip_app_name=False,
workload_dir_type="output_directory",
num_ranks=1,
capture_output=False,
stream=False,
):
# Skip test if multiple ranks are requested but mpirun is not available
if num_ranks > 1 and shutil.which("mpirun") is None:
pytest.skip(f"mpirun not found, skipping {request.node.name}")
if request.config.getoption("--rocprofiler-sdk-tool-path"):
options.extend(
[
"--rocprofiler-sdk-tool-path",
request.config.getoption("--rocprofiler-sdk-tool-path"),
],
)
if request.config.getoption("--call-binary"):
baseline_opts = [
"./rocprof-compute.bin",
"profile",
"-VVV",
]
if not skip_app_name:
baseline_opts.extend(["-n", app_name])
if not roof:
baseline_opts.append("--no-roof")
command_rocprof_compute = baseline_opts + options
if workload_dir_type == "output_directory":
command_rocprof_compute = command_rocprof_compute + [
"--output-directory",
workload_dir,
]
if not attach_detach_para:
command_rocprof_compute = (
command_rocprof_compute + ["--"] + config[app_name]
)
else:
command_rocprof_compute = command_rocprof_compute + [
"--attach-pid",
str(attach_detach_para["attach_pid"]),
]
if attach_detach_para["attach-duration-msec"]:
command_rocprof_compute = command_rocprof_compute + [
"--attach-duration-msec",
str(attach_detach_para["attach-duration-msec"]),
]
# Wrap with mpirun if num_ranks > 1
if num_ranks > 1:
command_rocprof_compute = inject_mpirun(
command_rocprof_compute, num_ranks
)
process = common.run_subprocess(
command_rocprof_compute, capture_output=True, stream=stream
)
# verify run status
if check_success:
assert process.returncode == 0
# Return output tuple if capture_output is enabled
if capture_output:
return process.returncode, process.stdout, process.stderr
return process.returncode
else:
# Non-binary mode: use Python module directly or subprocess
baseline_opts = [
"rocprof-compute",
"profile",
"-VVV",
]
if not skip_app_name:
baseline_opts.extend(["-n", app_name])
if not roof:
baseline_opts.append("--no-roof")
command_rocprof_compute = baseline_opts + options
if workload_dir_type == "output_directory":
command_rocprof_compute = command_rocprof_compute + [
"--output-directory",
workload_dir,
]
if not attach_detach_para:
command_rocprof_compute = (
command_rocprof_compute + ["--"] + config[app_name]
)
else:
command_rocprof_compute = command_rocprof_compute + [
"--attach-pid",
str(attach_detach_para["attach_pid"]),
]
if attach_detach_para["attach-duration-msec"]:
command_rocprof_compute = command_rocprof_compute + [
"--attach-duration-msec",
str(attach_detach_para["attach-duration-msec"]),
]
# For multi-rank, use mpirun to run the command
if num_ranks > 1:
# Use rocprof_compute_script_path instead of rocprof-compute
command_rocprof_compute[0] = rocprof_compute_script_path
command_rocprof_compute = inject_mpirun(
command_rocprof_compute, num_ranks
)
# For capture_output or multi-rank, run the command with subprocess
if capture_output or num_ranks > 1:
# Use rocprof_compute_script_path instead of rocprof-compute
if num_ranks == 1:
command_rocprof_compute[0] = rocprof_compute_script_path
process = common.run_subprocess(
command_rocprof_compute,
capture_output=capture_output,
stream=stream,
)
# Verify run status
if check_success:
assert process.returncode == 0
# Return output tuple if capture_output is enabled
if capture_output:
return process.returncode, process.stdout, process.stderr
return process.returncode
# Default single-rank mode: patch sys.argv and call main() directly
# Guard imports during profile execution (test-time enforcement)
with pytest.raises(SystemExit) as e:
with patch(
"sys.argv",
command_rocprof_compute,
):
with ProfileModeImportGuard():
rocprof_compute = SourceFileLoader(
"rocprof-compute", rocprof_compute_script_path
).load_module()
rocprof_compute.main()
# verify run status
if check_success:
assert e.value.code == 0
return e.value.code
return _handler
@pytest.fixture
def binary_handler_analyze_rocprof_compute(request):
"""
Fixture to run rocprof-compute analyze command.
Args:
arguments: Command-line arguments for the analyze command.
Returns:
returncode (int): Exit code from the command.
"""
def _handler(arguments):
if request.config.getoption("--call-binary"):
process = subprocess.run(
["./rocprof-compute.bin", *arguments],
text=True,
capture_output=True,
)
# Print output so capsys can capture it
if process.stdout:
print(process.stdout, end="")
if process.stderr:
print(process.stderr, end="", file=sys.stderr)
return process.returncode
else:
with pytest.raises(SystemExit) as e:
with patch(
"sys.argv",
["rocprof-compute", *arguments],
):
# Load module (no guard needed for analyze mode)
rocprof_compute = SourceFileLoader(
"rocprof-compute", rocprof_compute_script_path
).load_module()
rocprof_compute.main()
return e.value.code
return _handler