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
363 lines
12 KiB
Python
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)
|