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
470 lines
16 KiB
Python
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
|