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

363 lines
12 KiB
Python

# Copyright (c) Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
import inspect
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
from threading import Thread
from unittest.mock import Mock
import pandas as pd
import pytest
ROOT = os.path.dirname(os.path.dirname(__file__))
src_candidate = os.path.join(ROOT, "src")
SRC = src_candidate if os.path.isdir(src_candidate) else ROOT
if SRC not in sys.path:
sys.path.insert(0, SRC)
SUPPORTED_ARCHS = {
"gfx908": {"mi100": ["MI100"]},
"gfx90a": {"mi200": ["MI210", "MI250", "MI250X"]},
"gfx940": {"mi300": ["MI300A_A0"]},
"gfx941": {"mi300": ["MI300X_A0"]},
"gfx942": {"mi300": ["MI300A_A1", "MI300X_A1"]},
"gfx950": {"mi350": ["MI350"]},
"gfx1150": {"rdna35_point_1": ["RDNA35_POINT_1"]},
"gfx1151": {"rdna35_halo": ["RDNA35_HALO"]},
"gfx1152": {"rdna35_point_2": ["RDNA35_POINT_2"]},
"gfx1153": {"rdna35_gorgon_point": ["RDNA35_GORGON_POINT"]},
}
def check_resource_allocation():
"""Check if CTEST resource allocation is enabled for parallel testing and set
HIP_VISIBLE_DEVICES variable accordingly with assigned gpu index.
"""
if "CTEST_RESOURCE_GROUP_COUNT" not in os.environ:
return
if "CTEST_RESOURCE_GROUP_0_GPUS" in os.environ:
resource = os.environ["CTEST_RESOURCE_GROUP_0_GPUS"]
# extract assigned gpu id from env var: example format -> 'id:0,slots:1'
for item in resource.split(","):
key, value = item.split(":")
if key == "id":
os.environ["HIP_VISIBLE_DEVICES"] = value
return
return
def check_file_pattern(pattern, file_path):
"""Check if the given pattern exists in the file"""
content = ""
with open(file_path) as f:
content = f.read()
return len(re.findall(pattern, content)) != 0
def get_output_dir(suffix="_output", clean_existing=True, param_id=None):
"""
Provides a unique output directory based on the name of the calling test function
with a suffix applied. For parametrized tests, pass param_id to ensure unique
directory names and avoid NFS conflicts.
Args:
suffix (str, optional): suffix to append to output_dir.
Defaults to "_output".
clean_existing (bool, optional): Whether to remove existing directory if exists.
Defaults to True.
param_id (str, optional): Unique identifier for parametrized tests.
When provided, appended to the directory name to ensure uniqueness.
Defaults to None.
"""
func_name = inspect.stack()[1].function
param_suffix = ""
if param_id:
param_suffix = "_" + re.sub(r"[^\w\-]", "_", str(param_id))
output_dir = func_name + param_suffix + suffix
if clean_existing:
if Path(output_dir).exists():
shutil.rmtree(output_dir)
return output_dir
def setup_workload_dir(input_dir, suffix="_tmp", clean_existing=True, param_id=None):
"""Provides a unique input workload directory with contents of input_dir
based on the name of the calling test function. For parametrized tests,
pass param_id to ensure unique directory names and avoid NFS conflicts.
Creates a copy to avoid modifying source workload data.
Args:
input_dir (str): Source directory to copy from.
suffix (str, optional): suffix to append to output_dir.
Defaults to "_tmp".
clean_existing (bool, optional): Whether to remove existing directory if exists.
Defaults to True.
param_id (str, optional): Unique identifier for parametrized tests.
When provided, appended to the directory name to ensure uniqueness.
Defaults to None.
"""
func_name = inspect.stack()[1].function
# Include param_id in directory name if provided
param_suffix = ""
if param_id:
# Sanitize param_id: replace special chars that may not be valid in paths
param_suffix = "_" + re.sub(r"[^\w\-]", "_", str(param_id))
output_dir = func_name + param_suffix + suffix
if clean_existing:
if Path(output_dir).exists():
shutil.rmtree(output_dir)
shutil.copytree(input_dir, output_dir)
return output_dir
def clean_output_dir(cleanup, output_dir):
"""Remove output directory generated from rocprofiler-compute execution
Args:
cleanup (boolean): flag to enable/disable directory cleanup
output_dir (string): name of directory to remove
"""
if cleanup:
if Path(output_dir).exists():
try:
shutil.rmtree(output_dir)
except OSError:
print(
"WARNING: shutil.rmdir(output_dir): directory may not be empty..."
)
return
def check_csv_files(output_dir, num_devices, num_kernels):
"""Check profiling output csv files for expected
number of entries (based on kernel invocations)
Args:
output_dir (string): output directory containing csv files
num_kernels (int): number of kernels expected to have been profiled
Returns:
dict: dictionary housing file contents as pandas dataframe
(excludes PMC files - those are validated internally)
"""
files_in_workload = os.listdir(output_dir)
# Validate PMC data exists (profile creates pmc_perf_*.csv or results_*.csv)
has_separate = any(
f.startswith("pmc_perf_") and f.endswith(".csv") for f in files_in_workload
)
has_results = any(
f.startswith("results_") and f.endswith(".csv") for f in files_in_workload
)
assert has_separate or has_results, (
"Expected pmc_perf_*.csv or results_*.csv from profile mode"
)
# Validate row counts for PMC files (but don't add to return dict)
for file in files_in_workload:
is_pmc = file.startswith("pmc_perf_") or file.startswith("results_")
if is_pmc and file.endswith(".csv"):
df = pd.read_csv(output_dir + "/" + file)
err_msg = (
f"PMC file {file} has insufficient rows: "
f"{len(df.index)} < {num_kernels}"
)
assert len(df.index) >= num_kernels, err_msg
# Check and return non-PMC files
return check_non_pmc_files(output_dir, num_devices, num_kernels)
def check_non_pmc_files(output_dir, num_devices, num_kernels):
"""
Check profiling output non-PMC files and return them as a dictionary.
Args:
output_dir (string): output directory containing non-PMC files
num_devices (int): number of devices expected to have been profiled
num_kernels (int): number of kernels expected to have been profiled
Returns:
dict: dictionary housing file contents as pandas dataframe
"""
file_dict = {}
files_in_workload = os.listdir(output_dir)
# Load non-PMC files into return dict
for file in files_in_workload:
if file.endswith(".csv"):
# Skip PMC files (already validated above)
if file.startswith("pmc_perf_") or file.startswith("results_"):
continue
# Load other CSV files
file_dict[file] = pd.read_csv(output_dir + "/" + file)
if "roofline" in file:
assert len(file_dict[file].index) >= num_devices
elif "sysinfo" not in file and "ps_file" not in file:
assert len(file_dict[file].index) >= num_kernels
elif file.endswith(".html"):
file_dict[file] = "html"
elif file.endswith(".json"):
file_dict[file] = "json"
return file_dict
def get_num_pmc_file(output_dir):
"""
Returns:
int: number of pmc perf yaml files in perfmon dir
"""
perfmon_path = Path(output_dir) / "perfmon"
return len([
f
for f in perfmon_path.iterdir()
if f.is_file() and f.name.startswith("pmc_perf_") and f.suffix == ".yaml"
])
def strip_ansi(s: str) -> str:
ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]")
return ansi_escape.sub("", s)
def _tee(pipe, sink, out) -> None:
"""Echo each line from pipe to sink while accumulating it in out."""
with pipe:
for line in pipe:
print(line, end="", file=sink, flush=True)
out.append(line)
def run_subprocess(
command, capture_output=False, stream=False
) -> subprocess.CompletedProcess:
"""Run command in text mode and return a CompletedProcess.
capture_output: capture stdout and stderr onto the returned object.
stream: echo output line by line as the child produces it (requires
capture_output); otherwise captured output is printed once at the end.
"""
if not capture_output:
return subprocess.run(command, text=True)
if not stream:
# Capture everything, then echo it in one shot after the child exits.
process = subprocess.run(command, text=True, capture_output=True)
if process.stdout:
print(process.stdout, end="")
if process.stderr:
print(process.stderr, end="", file=sys.stderr)
return process
# Read each pipe on its own thread; reading serially can deadlock if one
# fills its buffer while we block on the other.
proc = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
)
out_buf, err_buf = [], []
# Tee to the real fds, not sys.stdout/stderr, which pytest's capsys swaps
# for in-memory buffers that never reach the terminal.
with os.fdopen(os.dup(1), "w", closefd=True) as real_out, os.fdopen(
os.dup(2), "w", closefd=True
) as real_err:
readers = [
Thread(target=_tee, args=(proc.stdout, real_out, out_buf)),
Thread(target=_tee, args=(proc.stderr, real_err, err_buf)),
]
for r in readers:
r.start()
for r in readers:
r.join()
proc.wait()
return subprocess.CompletedProcess(
command, proc.returncode, "".join(out_buf), "".join(err_buf)
)
def patch_console(monkeypatch, module, *names, **overrides):
"""Patch ``module.console_<name>`` with a Mock for each name; return {name: Mock}.
Pass ``name=callable`` to substitute a specific mock (e.g. a record-and-raise
stub for the console_error exit path).
"""
mocks = {}
for name in names:
mock = overrides.get(name, Mock())
monkeypatch.setattr(f"{module}.console_{name}", mock)
mocks[name] = mock
return mocks
def gpu_soc():
"""Return (arch, model) from rocminfo, e.g. ('gfx942', 'MI300').
Both are '' when no supported GPU is detected.
"""
# decode with utf-8 to account for rocm-smi changes in latest rocm
rocminfo = (
subprocess
.run(["rocminfo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
.stdout.decode("utf-8")
.split("\n")
)
soc_regex = re.compile(r"^\s*Name\s*:\s+ ([a-zA-Z0-9]+)\s*$", re.MULTILINE)
devices = list(filter(soc_regex.match, rocminfo))
if not devices:
return "", ""
arch = devices[0].split()[1]
if arch not in SUPPORTED_ARCHS:
return "", ""
model = list(SUPPORTED_ARCHS[arch].keys())[0].upper()
return arch, model
def skip_unsupported_pc_sampling_soc(is_stochastic=False):
"""Skip PC-sampling tests on SoCs that do not support the selected mode."""
_, soc = gpu_soc()
unsupported_socs = {
"MI100",
"RDNA35_POINT_1",
"RDNA35_HALO",
"RDNA35_POINT_2",
"RDNA35_GORGON_POINT",
}
if is_stochastic:
unsupported_socs.add("MI200")
if soc in unsupported_socs:
pytest.skip(f"PC sampling is not supported on {soc}")
def require_pc_sampling_gpu(is_stochastic=False):
"""Skip the test unless a GPU that supports the selected PC sampling mode is
present."""
_, soc = gpu_soc()
if not soc:
pytest.skip("GPU not supported")
skip_unsupported_pc_sampling_soc(is_stochastic=is_stochastic)