Files
TheRock/rocm-systems/projects/rocprofiler-compute/tests/test_profile_general.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

3743 lines
124 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (c) Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
import csv
import inspect
import os
import re
import socket
import sqlite3
import subprocess
import sys
import time
from pathlib import Path
import common
import pandas as pd
import pytest
import yaml
from conftest import require_torch, require_triton
from utils.utils_common import canonical_config_arch
# Runtime config options
config = {}
config["kernel_name_1"] = "vecCopy"
config["app_1"] = ["./tests/vcopy", "-n", "1048576", "-b", "256", "-i", "3"]
config["app_occupancy"] = ["./tests/occupancy"]
config["app_mat_mul_max"] = ["./tests/mat_mul_max"]
config["app_hip_dynamic_shared"] = ["./tests/hip_dynamic_shared"]
config["app_laplace_eqn"] = ["./tests/laplace_eqn", "-i", "5000"]
config["app_laplace_eqn_iter"] = ["./tests/laplace_eqn", "-i", "15000"]
config["app_laplace_eqn_insufficient"] = ["./tests/laplace_eqn", "-i", "3"]
config["app_vcopy_multikernel_iter"] = [
"./tests/vcopy",
"-n",
"1048576",
"-b",
"256",
"-i",
"500",
"--multikernel",
]
config["app_mpi_aware_laplace_eqn"] = ["./tests/mpi_aware_laplace_eqn", "-i", "5"]
config["rocflop"] = ["./tests/rocflop", "--device", "0", "--fp16"]
config["torch_test_app"] = ["python3", "./tests/simple_net.py"]
config["triton_test_app"] = ["python3", "./tests/triton_ffn.py"]
config["torch_compile_test_app"] = ["python3", "./tests/torch_compile_triton.py"]
config["cleanup"] = True
config["METRIC_COMPARE"] = False
config["METRIC_LOGGING"] = False
arch_config = {}
num_kernels = 3
num_devices = 1
attach_detach_interval_msec_no_delay = 1000
DEFAULT_ABS_DIFF = 15
DEFAULT_REL_DIFF = 50
MAX_REOCCURING_COUNT = 28
CSVS = sorted([
"sysinfo.csv",
])
ROOF_ONLY_FILES = sorted([
"roofline.csv",
"sysinfo.csv",
])
METRIC_THRESHOLDS = {
"2.1.11": {"absolute": 0, "relative": 8},
"3.1.1": {"absolute": 0, "relative": 10},
"3.1.10": {"absolute": 0, "relative": 10},
"3.1.11": {"absolute": 0, "relative": 1},
"3.1.12": {"absolute": 0, "relative": 1},
"3.1.13": {"absolute": 0, "relative": 1},
"5.1.0": {"absolute": 0, "relative": 15},
"5.2.0": {"absolute": 0, "relative": 15},
"6.1.4": {"absolute": 4, "relative": 0},
"6.1.5": {"absolute": 0, "relative": 1},
"6.1.0": {"absolute": 0, "relative": 15},
"6.1.3": {"absolute": 0, "relative": 11},
"6.2.12": {"absolute": 0, "relative": 1},
"6.2.13": {"absolute": 0, "relative": 1},
"7.1.0": {"absolute": 0, "relative": 1},
"7.1.1": {"absolute": 0, "relative": 1},
"7.1.2": {"absolute": 0, "relative": 1},
"7.1.5": {"absolute": 0, "relative": 1},
"7.1.6": {"absolute": 0, "relative": 1},
"7.1.7": {"absolute": 0, "relative": 1},
"7.2.1": {"absolute": 0, "relative": 10},
"7.2.3": {"absolute": 0, "relative": 12},
"7.2.6": {"absolute": 0, "relative": 1},
"10.1.4": {"absolute": 0, "relative": 1},
"10.1.5": {"absolute": 0, "relative": 1},
"10.1.6": {"absolute": 0, "relative": 1},
"10.1.7": {"absolute": 0, "relative": 1},
"10.3.4": {"absolute": 0, "relative": 1},
"10.3.5": {"absolute": 0, "relative": 1},
"10.3.6": {"absolute": 0, "relative": 1},
"11.2.1": {"absolute": 0, "relative": 1},
"11.2.4": {"absolute": 0, "relative": 5},
"13.2.0": {"absolute": 0, "relative": 1},
"13.2.2": {"absolute": 0, "relative": 1},
"14.2.0": {"absolute": 0, "relative": 1},
"14.2.5": {"absolute": 0, "relative": 1},
"14.2.7": {"absolute": 0, "relative": 1},
"14.2.8": {"absolute": 0, "relative": 1},
"15.1.4": {"absolute": 0, "relative": 1},
"15.1.5": {"absolute": 0, "relative": 1},
"15.1.6": {"absolute": 0, "relative": 1},
"15.1.7": {"absolute": 0, "relative": 1},
"15.2.4": {"absolute": 0, "relative": 1},
"15.2.5": {"absolute": 0, "relative": 1},
"16.1.0": {"absolute": 0, "relative": 1},
"16.1.3": {"absolute": 0, "relative": 1},
"16.3.0": {"absolute": 0, "relative": 1},
"16.3.1": {"absolute": 0, "relative": 1},
"16.3.2": {"absolute": 0, "relative": 1},
"16.3.5": {"absolute": 0, "relative": 1},
"16.3.6": {"absolute": 0, "relative": 1},
"16.3.7": {"absolute": 0, "relative": 1},
"16.3.9": {"absolute": 0, "relative": 1},
"16.3.10": {"absolute": 0, "relative": 1},
"16.3.11": {"absolute": 0, "relative": 1},
"16.4.3": {"absolute": 0, "relative": 1},
"16.4.4": {"absolute": 0, "relative": 1},
"16.5.0": {"absolute": 0, "relative": 1},
"17.3.3": {"absolute": 0, "relative": 1},
"17.3.6": {"absolute": 0, "relative": 1},
"18.1.0": {"absolute": 0, "relative": 1},
"18.1.1": {"absolute": 0, "relative": 1},
"18.1.2": {"absolute": 0, "relative": 1},
"18.1.3": {"absolute": 0, "relative": 1},
"18.1.5": {"absolute": 0, "relative": 1},
"18.1.6": {"absolute": 1, "relative": 0},
}
# Shared constants for output directory tests.
GPU_MODEL = "MIXXX"
GPU_ARCH = "gfx000"
# SLURM rank/size env var pair used in rank-related tests
SLURM_RANK_VAR, SLURM_SIZE_VAR = "SLURM_PROCID", "SLURM_NTASKS"
# check for parallel resource allocation
common.check_resource_allocation()
# Get soc info
gpu_arch, soc = common.gpu_soc()
def get_available_sets_for_arch(gpu_arch):
"""Return available set options for the given GPU arch,
or [] if gpu_arch is falsy."""
if not gpu_arch:
return []
config_arch = canonical_config_arch(gpu_arch) or gpu_arch
sets_file = (
Path(common.SRC)
/ "rocprof_compute_soc"
/ "profile_configs"
/ "sets"
/ f"{config_arch}_sets.yaml"
)
if not sets_file.exists():
return []
data = yaml.safe_load(sets_file.read_text())
return [s["set_option"] for s in data.get("sets", []) if s.get("set_option")]
AVAILABLE_SETS = get_available_sets_for_arch(gpu_arch)
# Set default profiler
os.environ["ROCPROF"] = "rocprofiler-sdk"
def counter_compare(test_name, errors_pd, baseline_df, run_df, threshold=5):
# iterate data one row at a time
for idx_1 in run_df.index:
run_row = run_df.iloc[idx_1]
baseline_row = baseline_df.iloc[idx_1]
if not run_row["KernelName"] == baseline_row["KernelName"]:
print("Kernel/dispatch mismatch")
assert 0
kernel_name = run_row["KernelName"]
gpu_id = run_row["gpu-id"]
differences = {}
for pmc_counter in run_row.index:
if "Ns" in pmc_counter or "id" in pmc_counter or "[" in pmc_counter:
# print("skipping "+pmc_counter)
continue
# assert 0
if not pmc_counter in list(baseline_df.columns):
print("error: pmc mismatch! " + pmc_counter + " is not in baseline_df")
continue
run_data = run_row[pmc_counter]
baseline_data = baseline_row[pmc_counter]
if isinstance(run_data, str) and isinstance(baseline_data, str):
if run_data not in baseline_data:
print(baseline_data)
else:
# relative difference
if not run_data == 0:
diff = round(100 * abs(baseline_data - run_data) / run_data, 2)
if diff > threshold:
print("[" + pmc_counter + "] diff is :" + str(diff) + "%")
if pmc_counter not in differences.keys():
print(
"[" + pmc_counter + "] not found in ",
list(differences.keys()),
)
differences[pmc_counter] = [diff]
else:
# Why are we here?
print(
"Why did we get here?!?!? errors_pd[idx_1]:",
list(differences.keys()),
)
differences[pmc_counter].append(diff)
else:
# if 0 show absolute difference
diff = round(baseline_data - run_data, 2)
if diff > threshold:
print(
str(idx_1) + "[" + pmc_counter + "] diff is :" + str(diff)
)
differences["kernel_name"] = [kernel_name]
differences["test_name"] = [test_name]
differences["gpu-id"] = [gpu_id]
errors_pd = pd.concat([errors_pd, pd.DataFrame.from_dict(differences)])
return errors_pd
def baseline_compare_metric(test_name, workload_dir, args=[]):
baseline_dir = (Path("tests/workloads/vcopy") / soc).resolve()
if not baseline_dir.exists():
pytest.skip(f"Skipping test since {baseline_dir} does not exist")
baseline_dir = str(baseline_dir)
t = subprocess.Popen(
[
sys.executable,
"src/rocprof_compute",
"analyze",
"--path",
baseline_dir,
]
+ args
+ ["--path", workload_dir, "--report-diff", "-1"],
stdout=subprocess.PIPE,
)
captured_output = t.communicate(timeout=1300)[0].decode("utf-8")
print(captured_output)
assert t.returncode == 0
if "DEBUG ERROR" in captured_output:
error_df = pd.DataFrame()
if Path(baseline_dir + "/metric_error_log.csv").exists():
error_df = pd.read_csv(
baseline_dir + "/metric_error_log.csv",
index_col=0,
)
output_metric_errors = re.findall(r"(\')([0-9.]*)(\')", captured_output)
high_diff_metrics = [x[1] for x in output_metric_errors]
for metric in high_diff_metrics:
metric_info = re.findall(
r"(^"
+ metric
+ (
r")(?: *)([()0-9A-Za-z- ]+ )"
r"(?: *)([0-9.-]*)"
r"(?: *)([0-9.-]*)"
r"(?: *)\(([-0-9.]*)%\)"
r"(?: *)([-0-9.e]*)"
),
captured_output,
flags=re.MULTILINE,
)
if len(metric_info):
metric_info = metric_info[0]
metric_idx = metric_info[0]
metric_name = metric_info[1].strip()
baseline_val = metric_info[-3]
current_val = metric_info[-4]
relative_diff = float(metric_info[-2])
absolute_diff = float(metric_info[-1])
if relative_diff > -99:
if metric_idx in METRIC_THRESHOLDS.keys():
# print(metric_idx+" is in FIXED_METRICS")
threshold_type = (
"absolute"
if METRIC_THRESHOLDS[metric_idx]["absolute"]
> METRIC_THRESHOLDS[metric_idx]["relative"]
else "relative"
)
isValid = (
(
abs(absolute_diff)
<= METRIC_THRESHOLDS[metric_idx]["absolute"]
)
if (threshold_type == "absolute")
else (
abs(relative_diff)
<= METRIC_THRESHOLDS[metric_idx]["relative"]
)
)
if not isValid:
print(
"index "
+ metric_idx
+ " "
+ threshold_type
+ " difference is supposed to be "
+ str(METRIC_THRESHOLDS[metric_idx][threshold_type])
+ ", absolute diff:",
absolute_diff,
"relative diff: ",
relative_diff,
)
assert 0
continue
# Used for debugging metric lists
if config["METRIC_LOGGING"] and (
(
abs(relative_diff) <= abs(DEFAULT_REL_DIFF)
or (abs(absolute_diff) <= abs(DEFAULT_ABS_DIFF))
)
and (False if baseline_val == "" else float(baseline_val) > 0)
):
# print("logging...")
# print(metric_info)
new_error = pd.DataFrame.from_dict({
"Index": [metric_idx],
"Metric": [metric_name],
"Percent Difference": [relative_diff],
"Absolute Difference": [absolute_diff],
"Baseline": [baseline_val],
"Current": [current_val],
"Test Name": [test_name],
})
error_df = pd.concat([error_df, new_error])
counts = error_df.groupby(["Index"]).cumcount()
reoccurring_metrics = error_df.loc[
counts > MAX_REOCCURING_COUNT
]
reoccurring_metrics["counts"] = counts[
counts > MAX_REOCCURING_COUNT
]
if reoccurring_metrics.any(axis=None):
with pd.option_context(
"display.max_rows",
None,
"display.max_columns",
None,
# 'display.precision', 3,
):
print(
"These metrics appear alot\n",
reoccurring_metrics,
)
# print(list(reoccurring_metrics["Index"]))
# log into csv
if not error_df.empty:
error_df.to_csv(baseline_dir + "/metric_error_log.csv")
def validate(test_name, workload_dir, file_dict, args=[]):
if config["METRIC_COMPARE"]:
baseline_compare_metric(test_name, workload_dir, args)
def are_stochastic_counters_similar(test_dfs, baseline_df):
"""
Compares multiple test dataframes against a baseline dataframe to check
if the stochastic counter values are similar. Returns True if all test dataframes
have similar counter values to the baseline, otherwise returns False.
"""
group_labels = [
"Kernel_Name",
"Grid_Size",
"Workgroup_Size",
"LDS_Per_Workgroup",
"Counter_Name",
]
baseline_grouped = baseline_df.groupby(group_labels)
tests_grouped = [df.groupby(group_labels) for df in test_dfs]
baseline_group_keys = set(baseline_grouped.groups.keys())
tests_group_keys = [set(group.groups.keys()) for group in tests_grouped]
# Check if all test dataframes have the same group keys as the baseline
if not all(baseline_group_keys == keys for keys in tests_group_keys):
return False
stochastic_counter_patterns = list(
map(
re.compile,
[
".*REQ_sum$",
".*REQ_.*_sum$",
".*READ_sum$",
".*WRITE_sum$",
],
)
)
for group_key, baseline_group in baseline_grouped:
test_groups = [
test_grouped.get_group(group_key) for test_grouped in tests_grouped
]
baseline_counters = baseline_group["Counter_Value"]
test_counters_list = [test_group["Counter_Value"] for test_group in test_groups]
counter_name = group_key[4]
# Warmup values aren't ignored as they do not significantly impact
# the analysis for stochastic counters and leaves too few data points
# for baseline.
if any(
re.match(pattern, counter_name) for pattern in stochastic_counter_patterns
):
# Remove outliers using Z-score method
z_score_threshold = 2.0
test_z_scores_list = [
(
(test_counters - test_counters.mean()) / test_counters.std(ddof=0)
).abs()
for test_counters in test_counters_list
]
test_counters_list_trimmed = [
test_counters[test_z_scores < z_score_threshold]
for test_counters, test_z_scores in zip(
test_counters_list, test_z_scores_list
)
]
baseline_mean = baseline_counters.mean()
baseline_std = baseline_counters.std()
upper_bound = baseline_mean + 3 * baseline_std
lower_bound = baseline_mean - 3 * baseline_std
for test_counters in test_counters_list_trimmed:
if test_counters.between(lower_bound, upper_bound).all() is False:
return False
return True
def are_deterministic_counters_equal(test_dfs, baseline_df):
"""
Compares multiple test dataframes against a baseline dataframe to check
if the deterministic counter values are equal. Returns True if all test dataframes
have equal counter values to the baseline, otherwise returns False.
"""
group_labels = [
"Kernel_Name",
"Grid_Size",
"Workgroup_Size",
"LDS_Per_Workgroup",
"Counter_Name",
]
baseline_grouped = baseline_df.groupby(group_labels)
tests_grouped = [df.groupby(group_labels) for df in test_dfs]
baseline_group_keys = set(baseline_grouped.groups.keys())
tests_group_keys = [set(group.groups.keys()) for group in tests_grouped]
# Check if all test dataframes have the same group keys as the baseline
if not all(baseline_group_keys == keys for keys in tests_group_keys):
return False, "Group keys do not match between baseline and test dataframes"
# series prior to MI350 use CSN, MI350 uses CS{0,1,2,3}
deterministic_counter_patterns = list(
map(
re.compile,
[
"SQ_INSTS_.*",
"SPI_CS\\d_NUM_THREADGROUPS",
"SPI_CSN_NUM_THREADGROUPS",
"SPI_CS\\d_WAVE",
"SPI_CSN_WAVE",
"SQ_WAVES",
],
)
)
for group_key, baseline_group in baseline_grouped:
test_groups = [
test_grouped.get_group(group_key) for test_grouped in tests_grouped
]
baseline_counters = baseline_group["Counter_Value"]
test_counters_list = [test_group["Counter_Value"] for test_group in test_groups]
counter_name = group_key[4]
if any(
re.match(pattern, counter_name)
for pattern in deterministic_counter_patterns
):
if (
all([
test_counters.unique().size == 1
for test_counters in test_counters_list
])
and baseline_counters.unique().size == 1
and all([
test_counters.values[0] == baseline_counters.values[0]
for test_counters in test_counters_list
])
):
continue
return (
False,
f"{counter_name} is not equal between baseline and test dataframes",
)
return True, "All deterministic counters are equal"
# --
# Shared mocks and helpers for output directory tests
# --
class MockProfiler:
"""Mock profiler used by output directory tests."""
def __init__(self, *args, **kwargs):
pass
def run_profiling(self, *args, **kwargs):
pass
def sanitize(self, *args, **kwargs):
pass
def pre_processing(self, *args, **kwargs):
pass
def post_processing(self, *args, **kwargs):
pass
class MockMachineSpecs:
def __init__(self, model, arch):
self.gpu_model = model
self.gpu_arch = arch
class MockSoc:
def post_profiling(self, *args, **kwargs):
pass
def mock_generate_machine_specs(self):
"""Set mock machine specs so %gpumodel% resolves before load_soc_specs runs."""
self._RocProfCompute__mspec = MockMachineSpecs(GPU_MODEL, GPU_ARCH)
def mock_load_soc_specs(self, sysinfo=None):
self._RocProfCompute__mspec = MockMachineSpecs(GPU_MODEL, GPU_ARCH)
self._RocProfCompute__soc[GPU_ARCH] = MockSoc()
def clear_rank_env(monkeypatch, *env_vars):
"""Remove the specified environment variables."""
for key in env_vars:
monkeypatch.delenv(key, raising=False)
def skip_unsupported_roofline_soc():
if soc in {"MI100", "RDNA35_GORGON_POINT"}:
pytest.skip(f"Roofline is not supported on {soc}")
def is_gfx115x_soc():
return soc in {
"RDNA35_POINT_1",
"RDNA35_HALO",
"RDNA35_POINT_2",
"RDNA35_GORGON_POINT",
}
# --
# Start of profiling tests
# --
@pytest.mark.path
def test_path(binary_handler_profile_rocprof_compute):
workload_dir = common.get_output_dir()
binary_handler_profile_rocprof_compute(config, workload_dir)
file_dict = common.check_csv_files(workload_dir, num_devices, num_kernels)
assert sorted(list(file_dict.keys())) == CSVS
validate(inspect.stack()[0][3], workload_dir, file_dict)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.path
def test_path_rocflop(binary_handler_profile_rocprof_compute):
# Test whether multiprocess workloads like rocflop are handled correctly
workload_dir = common.get_output_dir()
options = ["--block", "2.1.0"]
_ = binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=True,
roof=False,
app_name="rocflop",
)
common.check_csv_files(workload_dir, num_devices, num_kernels)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.path
def test_path_no_native(binary_handler_profile_rocprof_compute):
workload_dir = common.get_output_dir()
options = ["--no-native-tool"]
binary_handler_profile_rocprof_compute(config, workload_dir, options)
file_dict = common.check_csv_files(workload_dir, num_devices, num_kernels)
assert sorted(list(file_dict.keys())) == CSVS
validate(inspect.stack()[0][3], workload_dir, file_dict)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.path
def test_path_rocpd(
binary_handler_profile_rocprof_compute, binary_handler_analyze_rocprof_compute
):
workload_dir = common.get_output_dir()
options = ["--format-rocprof-output", "rocpd"]
binary_handler_profile_rocprof_compute(config, workload_dir, options)
# Validate profile outputs (results_*.csv for rocpd format)
common.check_csv_files(workload_dir, num_devices, num_kernels)
assert common.check_file_pattern(
"format_rocprof_output: rocpd", f"{workload_dir}/profiling_config.yaml"
)
# Run analyze to create merged pmc_perf.csv
code = binary_handler_analyze_rocprof_compute(["analyze", "--path", workload_dir])
assert code == 0
# Validate merged pmc_perf.csv content
assert common.check_file_pattern("Counter_Name", f"{workload_dir}/pmc_perf.csv")
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.path
def test_path_csv(
binary_handler_profile_rocprof_compute, binary_handler_analyze_rocprof_compute
):
workload_dir = common.get_output_dir()
options = ["--format-rocprof-output", "csv"]
binary_handler_profile_rocprof_compute(config, workload_dir, options)
file_dict = common.check_csv_files(workload_dir, num_devices, num_kernels)
assert sorted(list(file_dict.keys())) == sorted(["sysinfo.csv"])
validate(inspect.stack()[0][3], workload_dir, file_dict)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.path
def test_output_directory_hostname(binary_handler_profile_rocprof_compute, monkeypatch):
"""Test that %hostname% placeholder is replaced with the actual hostname."""
from rocprof_compute_base import RocProfCompute
hostname = "test_node"
monkeypatch.setattr(RocProfCompute, "create_profiler", lambda self: MockProfiler())
monkeypatch.setattr(socket, "gethostname", lambda: hostname)
workload_base_dir = common.get_output_dir(param_id="hostname")
workload_dir = os.path.join(workload_base_dir, "%hostname%")
binary_handler_profile_rocprof_compute(config, workload_dir)
workload_dir = workload_dir.replace("%hostname%", hostname)
assert os.path.exists(workload_dir)
common.clean_output_dir(config["cleanup"], workload_base_dir)
@pytest.mark.path
def test_output_directory_gpumodel(binary_handler_profile_rocprof_compute, monkeypatch):
"""Test that %gpumodel% placeholder is replaced with the GPU model name."""
from rocprof_compute_base import RocProfCompute
monkeypatch.setattr(RocProfCompute, "create_profiler", lambda self: MockProfiler())
monkeypatch.setattr(
RocProfCompute, "generate_machine_specs", mock_generate_machine_specs
)
monkeypatch.setattr(RocProfCompute, "load_soc_specs", mock_load_soc_specs)
workload_base_dir = common.get_output_dir(param_id="gpumodel")
workload_dir = os.path.join(workload_base_dir, "%gpumodel%_output")
binary_handler_profile_rocprof_compute(config, workload_dir)
workload_dir = workload_dir.replace("%gpumodel%", GPU_MODEL)
assert os.path.exists(workload_dir)
common.clean_output_dir(config["cleanup"], workload_base_dir)
@pytest.mark.path
def test_output_directory_rank_ignored_without_mpi(
binary_handler_profile_rocprof_compute, monkeypatch
):
"""Test that %rank% is ignored when no MPI rank env var is set."""
from rocprof_compute_base import RocProfCompute
clear_rank_env(monkeypatch, SLURM_RANK_VAR, SLURM_SIZE_VAR)
monkeypatch.setattr(RocProfCompute, "create_profiler", lambda self: MockProfiler())
workload_base_dir = common.get_output_dir(param_id="no_rank")
workload_dir = os.path.join(workload_base_dir, "%rank%_output")
binary_handler_profile_rocprof_compute(config, workload_dir)
workload_dir = workload_dir.replace("%rank%", "")
assert os.path.exists(workload_dir)
common.clean_output_dir(config["cleanup"], workload_base_dir)
@pytest.mark.path
def test_output_directory_rank_replaced_with_mpi(
binary_handler_profile_rocprof_compute, monkeypatch
):
"""Test that %rank% is replaced with the rank value when SLURM env vars are set."""
from rocprof_compute_base import RocProfCompute
rank = "3"
monkeypatch.setattr(RocProfCompute, "create_profiler", lambda self: MockProfiler())
monkeypatch.setenv(SLURM_RANK_VAR, rank)
monkeypatch.setenv(SLURM_SIZE_VAR, "4")
workload_base_dir = common.get_output_dir(param_id="rank_env_SLURM")
workload_dir = os.path.join(workload_base_dir, "%rank%_output")
binary_handler_profile_rocprof_compute(config, workload_dir)
workload_dir = workload_dir.replace("%rank%", rank)
assert os.path.exists(workload_dir)
common.clean_output_dir(config["cleanup"], workload_base_dir)
clear_rank_env(monkeypatch, SLURM_RANK_VAR, SLURM_SIZE_VAR)
@pytest.mark.path
def test_output_directory_env_variable(
binary_handler_profile_rocprof_compute, monkeypatch
):
"""Test that %env{VAR}% is replaced with the environment variable value."""
from rocprof_compute_base import RocProfCompute
monkeypatch.setenv("ENV_1", "custom_env")
monkeypatch.setattr(RocProfCompute, "create_profiler", lambda self: MockProfiler())
workload_base_dir = common.get_output_dir(param_id="env")
workload_dir = os.path.join(workload_base_dir, "%env{ENV_1}%")
binary_handler_profile_rocprof_compute(config, workload_dir)
workload_dir = workload_dir.replace("%env{ENV_1}%", "custom_env")
assert os.path.exists(workload_dir)
common.clean_output_dir(config["cleanup"], workload_base_dir)
monkeypatch.delenv("ENV_1", raising=False)
@pytest.mark.path
def test_output_directory_env_variable_unset(
binary_handler_profile_rocprof_compute, monkeypatch
):
"""Test that %env{VAR}% resolves to empty string when the var is unset."""
from rocprof_compute_base import RocProfCompute
monkeypatch.delenv("ENV_2", raising=False)
monkeypatch.setattr(RocProfCompute, "create_profiler", lambda self: MockProfiler())
workload_base_dir = common.get_output_dir(param_id="no_env")
workload_dir = os.path.join(workload_base_dir, "%env{ENV_2}%")
binary_handler_profile_rocprof_compute(config, workload_dir)
workload_dir = workload_dir.replace("%env{ENV_2}%", "")
assert os.path.exists(workload_dir)
common.clean_output_dir(config["cleanup"], workload_base_dir)
@pytest.mark.path
def test_output_directory_all_placeholders_combined(
binary_handler_profile_rocprof_compute, monkeypatch
):
"""Test that all placeholders work together in a single path."""
from rocprof_compute_base import RocProfCompute
hostname = "test_node"
rank = "3"
monkeypatch.setattr(RocProfCompute, "create_profiler", lambda self: MockProfiler())
monkeypatch.setattr(socket, "gethostname", lambda: hostname)
monkeypatch.setattr(
RocProfCompute, "generate_machine_specs", mock_generate_machine_specs
)
monkeypatch.setattr(RocProfCompute, "load_soc_specs", mock_load_soc_specs)
monkeypatch.setenv("ENV_1", "custom_env")
monkeypatch.setenv("OMPI_COMM_WORLD_RANK", rank)
monkeypatch.setenv("OMPI_COMM_WORLD_SIZE", "4")
workload_base_dir = common.get_output_dir(param_id="host_gpu_env_rank")
workload_dir = os.path.join(
workload_base_dir,
"%hostname%_%gpumodel%_%env{ENV_1}%_%rank%_output",
)
binary_handler_profile_rocprof_compute(config, workload_dir)
workload_dir = (
workload_dir
.replace("%hostname%", hostname)
.replace("%gpumodel%", GPU_MODEL)
.replace("%env{ENV_1}%", "custom_env")
.replace("%rank%", rank)
)
assert os.path.exists(workload_dir)
common.clean_output_dir(config["cleanup"], workload_base_dir)
monkeypatch.delenv("OMPI_COMM_WORLD_RANK", raising=False)
monkeypatch.delenv("ENV_1", raising=False)
@pytest.mark.path
def test_output_directory_default_with_rank(
binary_handler_profile_rocprof_compute, monkeypatch
):
"""
Test that rank is appended to the default output
directory when MPI rank is set.
"""
from rocprof_compute_base import RocProfCompute
rank = "3"
original_cwd = os.getcwd()
monkeypatch.setattr(RocProfCompute, "create_profiler", lambda self: MockProfiler())
monkeypatch.setattr(
RocProfCompute, "generate_machine_specs", mock_generate_machine_specs
)
monkeypatch.setattr(RocProfCompute, "load_soc_specs", mock_load_soc_specs)
monkeypatch.setenv("PMI_RANK", rank)
monkeypatch.setenv("PMI_SIZE", "4")
workload_base_dir = common.get_output_dir(param_id="rank_def_dir")
p = Path(workload_base_dir)
if not p.exists():
p.mkdir(parents=True, exist_ok=True)
os.chdir(workload_base_dir)
binary_handler_profile_rocprof_compute(
config, workload_dir=workload_base_dir, workload_dir_type="default"
)
workload_dir = os.path.join(
workload_base_dir,
"workloads",
"app_1",
rank,
)
os.chdir(original_cwd)
assert os.path.exists(workload_dir)
common.clean_output_dir(config["cleanup"], workload_base_dir)
monkeypatch.delenv("PMI_RANK", raising=False)
@pytest.mark.path
def test_output_directory_default_without_rank(
binary_handler_profile_rocprof_compute, monkeypatch
):
"""Test default output directory layout when no MPI rank is set."""
from rocprof_compute_base import RocProfCompute
clear_rank_env(monkeypatch, SLURM_RANK_VAR, SLURM_SIZE_VAR)
original_cwd = os.getcwd()
monkeypatch.setattr(RocProfCompute, "create_profiler", lambda self: MockProfiler())
monkeypatch.setattr(
RocProfCompute, "generate_machine_specs", mock_generate_machine_specs
)
monkeypatch.setattr(RocProfCompute, "load_soc_specs", mock_load_soc_specs)
workload_base_dir = common.get_output_dir(param_id="no_rank_def_dir")
p = Path(workload_base_dir)
if not p.exists():
p.mkdir(parents=True, exist_ok=True)
os.chdir(workload_base_dir)
binary_handler_profile_rocprof_compute(
config, workload_dir=workload_base_dir, workload_dir_type="default"
)
os.chdir(original_cwd)
workload_dir = os.path.join(
workload_base_dir,
"workloads",
"app_1",
GPU_MODEL,
)
assert os.path.exists(workload_dir)
common.clean_output_dir(config["cleanup"], workload_base_dir)
@pytest.mark.path
def test_output_directory_no_name_with_output_dir(
binary_handler_profile_rocprof_compute, monkeypatch
):
"""Test that --output-directory works without --name."""
from rocprof_compute_base import RocProfCompute
monkeypatch.setattr(RocProfCompute, "create_profiler", lambda self: MockProfiler())
monkeypatch.setattr(
RocProfCompute, "generate_machine_specs", mock_generate_machine_specs
)
monkeypatch.setattr(RocProfCompute, "load_soc_specs", mock_load_soc_specs)
workload_dir = common.get_output_dir(param_id="dir_no_name")
binary_handler_profile_rocprof_compute(
config, workload_dir=workload_dir, skip_app_name=True
)
assert os.path.exists(workload_dir)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.path
def test_output_directory_no_name_no_output_dir(
binary_handler_profile_rocprof_compute, monkeypatch
):
"""Test that profiling fails when neither --name nor --output-directory is given."""
from rocprof_compute_base import RocProfCompute
monkeypatch.setattr(RocProfCompute, "create_profiler", lambda self: MockProfiler())
monkeypatch.setattr(
RocProfCompute, "generate_machine_specs", mock_generate_machine_specs
)
monkeypatch.setattr(RocProfCompute, "load_soc_specs", mock_load_soc_specs)
workload_dir = common.get_output_dir(param_id="no_name_no_dir")
error_code = binary_handler_profile_rocprof_compute(
config,
skip_app_name=True,
workload_dir=workload_dir,
check_success=False,
workload_dir_type="default",
)
assert error_code == 1
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.roofline_1
def test_roof_basic_validation(binary_handler_profile_rocprof_compute):
"""
Test basic roofline CSV generation in profile mode.
Validates that roofline.csv is generated via microbenchmarks.
"""
skip_unsupported_roofline_soc()
options = ["--device", "0", "--roof-only"]
workload_dir = common.get_output_dir()
returncode = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=False, roof=True
)
assert returncode == 0
file_dict = common.check_csv_files(workload_dir, 1, num_kernels)
assert sorted(list(file_dict.keys())) == ROOF_ONLY_FILES
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.roofline_1
def test_roof_file_validation(binary_handler_profile_rocprof_compute):
"""Test file validation paths in roofline"""
skip_unsupported_roofline_soc()
options = ["--device", "0", "--roof-only"]
workload_dir = common.get_output_dir()
try:
returncode = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=False, roof=True
)
if returncode == 0:
roofline_csv = f"{workload_dir}/roofline.csv"
if os.path.exists(roofline_csv):
import pandas as pd
df = pd.read_csv(roofline_csv)
assert len(df) >= 0
finally:
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.roofline_1
def test_roof_rocpd(
binary_handler_profile_rocprof_compute,
binary_handler_analyze_rocprof_compute,
):
skip_unsupported_roofline_soc()
workload_dir = common.get_output_dir()
options = ["--device", "0", "--roof-only", "--format-rocprof-output", "rocpd"]
binary_handler_profile_rocprof_compute(config, workload_dir, options, roof=True)
# Validate profile outputs
common.check_csv_files(workload_dir, num_devices, num_kernels)
assert (Path(workload_dir) / "roofline.csv").exists()
assert common.check_file_pattern(
"format_rocprof_output: rocpd", f"{workload_dir}/profiling_config.yaml"
)
# Run analyze to create merged pmc_perf.csv
code = binary_handler_analyze_rocprof_compute(["analyze", "--path", workload_dir])
assert code == 0
# Validate merged pmc_perf.csv content
assert common.check_file_pattern("Counter_Name", f"{workload_dir}/pmc_perf.csv")
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.misc
def test_analyze_rocpd(
binary_handler_profile_rocprof_compute, binary_handler_analyze_rocprof_compute
):
skip_unsupported_roofline_soc()
workload_dir = common.get_output_dir()
options = ["--device", "0", "--format-rocprof-output", "rocpd"]
binary_handler_profile_rocprof_compute(config, workload_dir, options, roof=True)
db_name = "test"
code = binary_handler_analyze_rocprof_compute([
"analyze",
"--output-format",
"db",
"--output-name",
f"{db_name}",
"--path",
workload_dir,
])
assert code == 0
assert os.path.isfile(f"{db_name}.db")
# Open the sqlite database and assert the schema
# Import Kernel from analysis_orm.py
from utils.analysis_orm import (
Dispatch,
Kernel,
KernelMetricValue,
KernelRooflineData,
Metadata,
MetricDefinition,
Workload,
WorkloadMetricValue,
WorkloadRooflineData,
)
table_name_map = {
"compute_workload": Workload,
"compute_metric_definition": MetricDefinition,
"compute_kernel_roofline_data": KernelRooflineData,
"compute_workload_roofline_data": WorkloadRooflineData,
"compute_dispatch": Dispatch,
"compute_kernel": Kernel,
"compute_kernel_metric_value": KernelMetricValue,
"compute_workload_metric_value": WorkloadMetricValue,
"compute_metadata": Metadata,
}
def check_cols(table_name, orm_obj):
conn = sqlite3.connect(f"{db_name}.db")
cursor = conn.cursor()
cursor.execute(f"PRAGMA table_info('{table_name}');")
columns = cursor.fetchall()
column_names = [column[1] for column in columns]
expected_columns = [col.name for col in orm_obj.__table__.columns]
assert column_names == expected_columns
conn.close()
for table_name, orm_obj in table_name_map.items():
check_cols(table_name, orm_obj)
os.remove(f"{db_name}.db")
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.misc
def test_save_csv(
binary_handler_profile_rocprof_compute, binary_handler_analyze_rocprof_compute
):
workload_dir = common.get_output_dir(param_id="profile")
analysis_workload_dir = common.get_output_dir(param_id="analysis")
options = ["--format-rocprof-output", "rocpd"]
binary_handler_profile_rocprof_compute(config, workload_dir, options)
code = binary_handler_analyze_rocprof_compute([
"analyze",
"--output-format",
"csv",
"--output-name",
analysis_workload_dir,
"--path",
workload_dir,
])
assert code == 0
csv_dir = Path(analysis_workload_dir)
assert csv_dir.is_dir()
expected_view_csvs = ["kernel.csv", "kernel_metric.csv", "workload_metric.csv"]
for csv_name in expected_view_csvs:
csv_path = csv_dir / csv_name
assert csv_path.is_file(), f"Missing per-view CSV: {csv_path}"
df = pd.read_csv(csv_path)
assert len(df.index) >= 1, f"Per-view CSV is empty: {csv_path}"
assert not Path(f"{analysis_workload_dir}.db").exists()
common.clean_output_dir(config["cleanup"], analysis_workload_dir)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.roofline_1
def test_roofline_workload_dir_not_set_error():
"""
Test roof_setup() error: "Workload directory is not set. Cannot perform setup."
This covers lines 113-117
"""
skip_unsupported_roofline_soc()
try:
from roofline.roofline_main import Roofline
from utils.specs import generate_machine_specs
class MockArgs:
def __init__(self):
self.roof_only = True
self.mem_level = "ALL"
self.sort = "ALL"
self.roofline_data_type = ["FP32"]
args = MockArgs()
mspec = generate_machine_specs(None, None)
run_parameters = {
"workload_dir": None,
"device_id": 0,
"sort_type": "kernels",
"mem_level": "ALL",
"roofline_data_type": ["FP32"],
}
roofline_instance = Roofline(args, mspec, run_parameters)
import contextlib
from io import StringIO
captured_output = StringIO()
with contextlib.redirect_stderr(captured_output):
try:
roofline_instance.roof_setup()
except SystemExit:
pass
assert True
except ImportError:
pytest.skip("Could not import roofline module for direct testing")
@pytest.mark.roofline_1
def test_roof_workload_dir_validation(binary_handler_profile_rocprof_compute):
skip_unsupported_roofline_soc()
options = ["--device", "0", "--roof-only"]
workload_dir = common.get_output_dir()
returncode = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=False, roof=True
)
assert returncode == 0
nested_dir = os.path.join(workload_dir, "nested", "structure")
os.makedirs(nested_dir, exist_ok=True)
returncode = binary_handler_profile_rocprof_compute(
config, nested_dir, options, check_success=False, roof=True
)
assert returncode == 0
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.roofline_1
def test_roofline_kernel_filter(binary_handler_profile_rocprof_compute):
"""
Test roofline multi-attempt profiling with `--kernel`
Expect to be able to re-profile into the same workload directory (with
--overwrite) if kernels are valid.
Roofline now takes in a dataframe that should already have filtering applied.
Any invald kernels should be handled prior to roof activity.
Check the following cases:
- no valid kernels
- one valid kernel
- 2 kernels, one valid and one invalid
"""
skip_unsupported_roofline_soc()
options = [
"--device",
"0",
"--roof-only",
"--overwrite",
]
workload_dir = common.get_output_dir()
returncode = binary_handler_profile_rocprof_compute( # noqa: F841
config, workload_dir, options, check_success=True, roof=True
)
# Wipe the directory where applicable with --overwrite, then
# Re-profile into the same workload directory
# Test only non-existent kernel: result should be passing
# Dataframe given to roofline should just be all available kernels with no filtering
options_bad = options.copy()
options_bad.extend([
"--kernel",
"nonexistent_kernel_name_that_should_not_match_anything",
])
returncode = binary_handler_profile_rocprof_compute( # noqa: F841
config,
workload_dir,
options_bad,
check_success=True,
roof=True,
)
assert returncode == 0
# Test one good kernel, re-profiling the same directory with --overwrite
# Result should be passing as usual
options_good = options.copy()
options_good.extend(["--kernel", config["kernel_name_1"]])
returncode = binary_handler_profile_rocprof_compute( # noqa: F841
config, workload_dir, options_good, check_success=True, roof=True
)
assert returncode == 0
# Test one good and one nonexistent kernel, re-profiling
# Result should be passing as usual
options_both = options.copy()
options_both.extend([
"--kernel",
config["kernel_name_1"],
"nonexistent_kernel_name_that_should_not_match_anything",
])
returncode = binary_handler_profile_rocprof_compute( # noqa: F841
config, workload_dir, options_both, check_success=False, roof=True
)
assert returncode == 0
# Verify CSV
assert (Path(workload_dir) / "roofline.csv").exists()
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.roofline_2
def test_roof_cli_plot_generation(binary_handler_profile_rocprof_compute):
skip_unsupported_roofline_soc()
try:
import plotext as plt # noqa: F401
cli_available = True
except ImportError:
cli_available = False
if cli_available:
options = ["--device", "0", "--roof-only"]
workload_dir = common.get_output_dir()
returncode = binary_handler_profile_rocprof_compute( # noqa: F841
config, workload_dir, options, check_success=False, roof=True
)
common.clean_output_dir(config["cleanup"], workload_dir)
else:
pytest.skip("plotext not available for CLI testing")
@pytest.mark.roofline_2
def test_roof_error_handling(binary_handler_profile_rocprof_compute):
skip_unsupported_roofline_soc()
options = ["--device", "0", "--roof-only"]
workload_dir = common.get_output_dir()
returncode = binary_handler_profile_rocprof_compute( # noqa: F841
config, workload_dir, options, check_success=False, roof=True
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.roofline_1
def test_bench_only_basic(binary_handler_profile_rocprof_compute):
"""
Test that --bench-only generates roofline.csv standalone (no application
profiling and no performance counter collection).
"""
skip_unsupported_roofline_soc()
options = ["--device", "0", "--bench-only"]
workload_dir = common.get_output_dir()
returncode = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=True, roof=True
)
assert returncode == 0
workload_path = Path(workload_dir)
roofline_csv = workload_path / "roofline.csv"
assert roofline_csv.exists(), f"Expected {roofline_csv} to be created"
# Bench-only must not produce profiling artifacts
assert not (workload_path / "perfmon").exists()
assert not (workload_path / "sysinfo.csv").exists()
assert not (workload_path / "profiling_config.yaml").exists()
assert not list(workload_path.glob("results_*.csv"))
assert not list(workload_path.glob("pmc_perf_*.csv"))
@pytest.mark.roofline_1
@pytest.mark.parametrize(
"conflicting_options",
[
pytest.param(["--set", "compute_thruput_util"], id="set"),
pytest.param(["--block", "2"], id="block"),
pytest.param(["--roof-only"], id="roof_only"),
],
)
def test_bench_only_mutual_exclusion(
binary_handler_profile_rocprof_compute, conflicting_options
):
"""
--bench-only must be rejected when paired with --set, --block, or --roof-only.
These options are profiling-oriented and meaningless for a standalone benchmark.
"""
skip_unsupported_roofline_soc()
options = ["--device", "0", "--bench-only"] + conflicting_options
workload_dir = common.get_output_dir()
returncode = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=False, roof=True
)
assert returncode == 1, (
f"Expected --bench-only with {conflicting_options} to fail, "
f"but command exited with {returncode}"
)
common.clean_output_dir(config["cleanup"], workload_dir)
def test_pc_sampling_requires_experimental(binary_handler_profile_rocprof_compute):
"""
--pc-sampling must be rejected at argparse time when --experimental is
not also passed (ExperimentalAction gating). This fires before hardware
detection, so it is intentionally not gated on SoC support.
"""
options = ["--pc-sampling"]
workload_dir = common.get_output_dir()
returncode = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=False, roof=False
)
assert returncode != 0, (
"Expected --pc-sampling without --experimental to fail, "
f"but command exited with {returncode}"
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.roofline_1
def test_bench_only_no_roof_mutual_exclusion(binary_handler_profile_rocprof_compute):
"""
--bench-only must be rejected when combined with --no-roof, since the option
explicitly disables the roofline microbenchmark we are trying to run.
"""
skip_unsupported_roofline_soc()
options = ["--device", "0", "--bench-only"]
workload_dir = common.get_output_dir()
# roof=False makes the fixture inject --no-roof automatically
returncode = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=False, roof=False
)
assert returncode == 1, (
"Expected --bench-only combined with --no-roof to fail, "
f"but command exited with {returncode}"
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.roofline_2
def test_roofline_plot_points_data_generation():
"""
Test that plot points data structure is correctly generated with:
- Symbol assignments
- AI values (FLOPs/Byte)
- Performance values (GFLOPs/s)
- Memory/Compute bound status
- Cache level information
Simulates a CDNA4 roofline run- has HBM, L1, and L2 cache levels.
"""
skip_unsupported_roofline_soc()
try:
from roofline.roofline_main import Roofline
from utils.specs import generate_machine_specs
class MockArgs:
def __init__(self):
self.roof_only = True
self.mem_level = "ALL"
self.sort = "ALL"
self.roofline_data_type = ["FP32"]
args = MockArgs()
mspec = generate_machine_specs(None, None)
mock_ai_data = {
"ai_l1": [[0.5, 1.2], [100.0, 150.0]],
"ai_l2": [[0.3, 0.8], [80.0, 120.0]],
"ai_hbm": [[0.1, 0.4], [50.0, 90.0]],
"ai_lds": [[0.7, 1.5], [100.0, 150.0]],
"kernelNames": ["kernel_A", "kernel_B"],
}
mock_ceiling_data = {
"l1": [[0.01, 10], [10, 1000], 100],
"l2": [[0.01, 10], [10, 800], 80],
"hbm": [[0.01, 10], [10, 500], 50],
"lds": [[0.01, 10], [10, 1200], 120],
"valu": [[1, 100], [200, 200], 200],
"matrix_ops": [[1, 100], [500, 500], 500],
}
plot_points_data = []
cache_colors = {
"ai_l1": "blue",
"ai_l2": "green",
"ai_hbm": "red",
"ai_lds": "orange",
}
run_parameters = {
"workload_dir": None,
"device_id": 0,
"sort_type": "kernels",
"mem_level": "ALL",
"roofline_data_type": ["FP32"],
}
roofline_instance = Roofline(args, mspec, run_parameters)
for cache_level in ["ai_l1", "ai_l2", "ai_hbm", "ai_lds"]:
if cache_level in mock_ai_data:
x_vals = mock_ai_data[cache_level][0]
y_vals = mock_ai_data[cache_level][1]
num_kernels = len(mock_ai_data["kernelNames"])
for i in range(min(len(x_vals), num_kernels)):
if x_vals[i] > 0 and y_vals[i] > 0:
status = roofline_instance._determine_kernel_bound_status(
ai_value=x_vals[i],
performance=y_vals[i],
cache_level=cache_level,
ceiling_data=mock_ceiling_data,
)
plot_points_data.append({
"symbol": None,
"color": cache_colors.get(cache_level, "gray"),
"cache_level": cache_level.replace("ai_", "", 1).upper(),
"ai": f"{x_vals[i]:.2f}",
"performance": f"{y_vals[i]:.2f}",
"status": status,
"kernel_idx": i,
})
assert len(plot_points_data) > 0, "Plot points data should not be empty"
for point in plot_points_data:
assert "cache_level" in point
assert "ai" in point
assert "performance" in point
assert "status" in point
assert "kernel_idx" in point
assert "color" in point
assert point["cache_level"] in ["L1", "L2", "HBM", "LDS"]
assert point["status"] in ["Memory Bound", "Compute Bound", "Unknown"]
assert isinstance(point["ai"], str)
assert isinstance(point["performance"], str)
except ImportError:
pytest.skip("Could not import roofline module for direct testing")
@pytest.mark.roofline_2
def test_roofline_bound_status_calculation():
"""
Test _determine_kernel_bound_status() correctly classifies kernels as
Memory Bound or Compute Bound based on their AI and performance vs ceilings.
Simulates a CDNA4 roofline run- has HBM, valu, and matrix ops.
"""
skip_unsupported_roofline_soc()
try:
from roofline.roofline_main import Roofline
from utils.specs import generate_machine_specs
class MockArgs:
def __init__(self):
self.roof_only = True
self.mem_level = "ALL"
self.sort = "ALL"
self.roofline_data_type = ["FP32"]
args = MockArgs()
mspec = generate_machine_specs(None, None)
run_parameters = {
"workload_dir": None,
"device_id": 0,
"sort_type": "kernels",
"mem_level": "ALL",
"roofline_data_type": ["FP32"],
}
roofline_instance = Roofline(args, mspec, run_parameters)
ceiling_data = {
"hbm": [[0.01, 10], [10, 1000], 100],
"lds": [[0.01, 10], [10, 1200], 120],
"valu": [[1, 100], [200, 200], 200],
"matrix_ops": [[1, 100], [500, 500], 500],
}
status1 = roofline_instance._determine_kernel_bound_status(
ai_value=1.0,
performance=100.0,
cache_level="ai_hbm",
ceiling_data=ceiling_data,
)
assert status1 == "Memory Bound", f"Expected Memory Bound, got {status1}"
status2 = roofline_instance._determine_kernel_bound_status(
ai_value=5.0,
performance=150.0,
cache_level="ai_hbm",
ceiling_data=ceiling_data,
)
assert status2 == "Compute Bound", f"Expected Compute Bound, got {status2}"
status_lds = roofline_instance._determine_kernel_bound_status(
ai_value=1.0,
performance=100.0,
cache_level="ai_lds",
ceiling_data=ceiling_data,
)
assert status_lds == "Memory Bound", (
f"Expected LDS Memory Bound, got {status_lds}"
)
status3 = roofline_instance._determine_kernel_bound_status(
ai_value=1.0,
performance=100.0,
cache_level="ai_l1",
ceiling_data=ceiling_data,
)
assert status3 == "Unknown", f"Expected Unknown, got {status3}"
bad_ceiling_data = {
"hbm": [100],
}
status4 = roofline_instance._determine_kernel_bound_status(
ai_value=1.0,
performance=100.0,
cache_level="ai_hbm",
ceiling_data=bad_ceiling_data,
)
assert status4 == "Unknown", f"Expected Unknown for bad data, got {status4}"
except ImportError:
pytest.skip("Could not import roofline module for direct testing")
@pytest.mark.roofline_2
def test_roofline_many_kernels_dynamic_height(binary_handler_profile_rocprof_compute):
"""
Test roofline CSV generation with many kernels.
"""
skip_unsupported_roofline_soc()
options = ["--device", "0", "--roof-only"]
workload_dir = common.get_output_dir()
returncode = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=False, roof=True
)
assert returncode == 0, "Roofline profiling should succeed"
assert (Path(workload_dir) / "roofline.csv").exists()
file_dict = common.check_csv_files(workload_dir, 1, num_kernels)
assert sorted(list(file_dict.keys())) == ROOF_ONLY_FILES
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.misc
def test_device_filter(binary_handler_profile_rocprof_compute):
options = ["--device", "0"]
workload_dir = common.get_output_dir()
binary_handler_profile_rocprof_compute(config, workload_dir, options)
file_dict = common.check_csv_files(workload_dir, 1, num_kernels)
assert sorted(list(file_dict.keys())) == CSVS
# TODO - verify expected device id in results
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.kernel_execution
def test_kernel(binary_handler_profile_rocprof_compute):
options = ["--kernel", config["kernel_name_1"]]
workload_dir = common.get_output_dir()
binary_handler_profile_rocprof_compute(config, workload_dir, options)
file_dict = common.check_csv_files(workload_dir, num_devices, num_kernels)
assert sorted(list(file_dict.keys())) == CSVS
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.dispatch
def test_dispatch_0(binary_handler_profile_rocprof_compute):
options = ["--dispatch", "1"]
workload_dir = common.get_output_dir()
binary_handler_profile_rocprof_compute(config, workload_dir, options)
file_dict = common.check_csv_files(workload_dir, num_devices, 1)
assert sorted(list(file_dict.keys())) == CSVS
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
[
"--dispatch",
"1",
],
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.dispatch
def test_dispatch_0_1(binary_handler_profile_rocprof_compute):
options = ["--dispatch", "1:2"]
workload_dir = common.get_output_dir()
binary_handler_profile_rocprof_compute(config, workload_dir, options)
file_dict = common.check_csv_files(workload_dir, num_devices, 2)
assert sorted(list(file_dict.keys())) == CSVS
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
["--dispatch", "1", "2"],
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.dispatch
def test_dispatch_2(binary_handler_profile_rocprof_compute):
options = ["--dispatch", "1"]
workload_dir = common.get_output_dir()
binary_handler_profile_rocprof_compute(config, workload_dir, options)
file_dict = common.check_csv_files(workload_dir, num_devices, 1)
assert sorted(list(file_dict.keys())) == CSVS
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
[
"--dispatch",
"1",
],
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.dispatch
@pytest.mark.parametrize(
"bad_value",
["0", "-1", "abc", "1:0", "5:3", "1:", ":3", "1:2:3"],
)
def test_dispatch_invalid_rejected(binary_handler_profile_rocprof_compute, bad_value):
workload_dir = common.get_output_dir(
param_id=f"dispatch_{bad_value}".replace(":", "_")
)
returncode, stdout, stderr = binary_handler_profile_rocprof_compute(
config,
workload_dir,
["--dispatch", bad_value],
check_success=False,
capture_output=True,
)
assert returncode == 1
output = stdout + stderr
assert f"Invalid --dispatch value '{bad_value}'" in output
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.join
def test_join_type_grid(binary_handler_profile_rocprof_compute):
options = ["--join-type", "grid"]
workload_dir = common.get_output_dir()
binary_handler_profile_rocprof_compute(config, workload_dir, options)
file_dict = common.check_csv_files(workload_dir, num_devices, num_kernels)
assert sorted(list(file_dict.keys())) == CSVS
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.join
def test_join_type_kernel(binary_handler_profile_rocprof_compute):
options = ["--join-type", "kernel"]
workload_dir = common.get_output_dir()
binary_handler_profile_rocprof_compute(config, workload_dir, options)
file_dict = common.check_csv_files(workload_dir, num_devices, num_kernels)
assert sorted(list(file_dict.keys())) == CSVS
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.sort
def test_roof_sort_dispatches(
binary_handler_profile_rocprof_compute,
binary_handler_analyze_rocprof_compute,
):
"""Profile creates CSV; analyze with --sort dispatches generates output."""
skip_unsupported_roofline_soc()
profile_options = ["--device", "0", "--roof-only"]
workload_dir = common.get_output_dir()
returncode = binary_handler_profile_rocprof_compute(
config, workload_dir, profile_options, check_success=False, roof=True
)
assert returncode == 0
file_dict = common.check_csv_files(workload_dir, 1, num_kernels)
assert sorted(list(file_dict.keys())) == ROOF_ONLY_FILES
code = binary_handler_analyze_rocprof_compute([
"analyze",
"--path",
workload_dir,
"--sort",
"dispatches",
])
assert code == 0
html_files = list(Path(workload_dir).glob("empirRoof_*.html"))
assert len(html_files) > 0, "Analyze should generate roofline HTML files"
validate(inspect.stack()[0][3], workload_dir, file_dict)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.sort
def test_roof_sort_kernels(
binary_handler_profile_rocprof_compute,
binary_handler_analyze_rocprof_compute,
):
"""Profile creates CSV; analyze with --sort kernels generates output."""
skip_unsupported_roofline_soc()
profile_options = ["--device", "0", "--roof-only"]
workload_dir = common.get_output_dir()
returncode = binary_handler_profile_rocprof_compute(
config, workload_dir, profile_options, check_success=False, roof=True
)
assert returncode == 0
file_dict = common.check_csv_files(workload_dir, 1, num_kernels)
assert sorted(list(file_dict.keys())) == ROOF_ONLY_FILES
code = binary_handler_analyze_rocprof_compute([
"analyze",
"--path",
workload_dir,
"--sort",
"kernels",
])
assert code == 0
html_files = list(Path(workload_dir).glob("empirRoof_*.html"))
assert len(html_files) > 0, "Analyze should generate roofline HTML files"
validate(inspect.stack()[0][3], workload_dir, file_dict)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.section
def test_lds_section(binary_handler_profile_rocprof_compute):
lds_block = "3" if is_gfx115x_soc() else "12"
options = ["--block", lds_block]
workload_dir = common.get_output_dir()
_ = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=True, roof=False
)
file_dict = common.check_csv_files(workload_dir, 1, num_kernels)
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
)
assert common.check_file_pattern(
f"- '{lds_block}'", f"{workload_dir}/profiling_config.yaml"
)
results_files = Path(workload_dir).glob("results_*.csv")
assert any(common.check_file_pattern("SQ_INSTS_LDS", str(f)) for f in results_files)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.section
def test_instmix_memchart_section(binary_handler_profile_rocprof_compute):
instmix_block = "7" if is_gfx115x_soc() else "10"
options = ["--block", instmix_block, "3"]
workload_dir = common.get_output_dir()
_ = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=True, roof=False
)
file_dict = common.check_csv_files(workload_dir, 1, num_kernels)
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
)
assert common.check_file_pattern(
f"- '{instmix_block}'", f"{workload_dir}/profiling_config.yaml"
)
assert common.check_file_pattern("- '3'", f"{workload_dir}/profiling_config.yaml")
instmix_counter = "SQ_INSTS_FLAT" if is_gfx115x_soc() else "TA_FLAT_WAVEFRONTS"
results_files = Path(workload_dir).glob("results_*.csv")
assert any(
common.check_file_pattern(instmix_counter, str(f)) for f in results_files
)
results_files = Path(workload_dir).glob("results_*.csv")
assert any(
common.check_file_pattern("SQC_TC_DATA_READ_REQ", str(f)) for f in results_files
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.section
def test_lds_sol_section(binary_handler_profile_rocprof_compute):
lds_sol_block = "3" if is_gfx115x_soc() else "12.1"
options = ["--block", lds_sol_block]
workload_dir = common.get_output_dir()
_ = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=True, roof=False
)
file_dict = common.check_csv_files(workload_dir, 1, num_kernels)
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
)
assert common.check_file_pattern(
f"- '{lds_sol_block}'", f"{workload_dir}/profiling_config.yaml"
)
lds_sol_counter = "SQC_LDS_IDX_ACTIVE" if is_gfx115x_soc() else "SQ_ACTIVE_INST_LDS"
results_files = Path(workload_dir).glob("results_*.csv")
assert any(
common.check_file_pattern(lds_sol_counter, str(f)) for f in results_files
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.section
def test_instmix_section_global_write_kernel(binary_handler_profile_rocprof_compute):
instmix_block = "7" if is_gfx115x_soc() else "10"
options = ["-k", "global_write", "--block", instmix_block]
custom_config = dict(config)
custom_config["kernel_name_1"] = "global_write"
custom_config["app_1"] = ["./tests/vmem"]
num_kernels = 1
workload_dir = common.get_output_dir()
_ = binary_handler_profile_rocprof_compute(
custom_config, workload_dir, options, check_success=True, roof=False
)
file_dict = common.check_csv_files(workload_dir, 1, num_kernels)
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
)
assert common.check_file_pattern(
f"- '{instmix_block}'", f"{workload_dir}/profiling_config.yaml"
)
assert common.check_file_pattern(
"- global_write", f"{workload_dir}/profiling_config.yaml"
)
kernel_counter = "SQ_INSTS_FLAT_STORE" if is_gfx115x_soc() else "TA_FLAT_WAVEFRONTS"
results_files = Path(workload_dir).glob("results_*.csv")
assert any(common.check_file_pattern(kernel_counter, str(f)) for f in results_files)
results_files = Path(workload_dir).glob("results_*.csv")
assert any(common.check_file_pattern("global_write", str(f)) for f in results_files)
results_files = Path(workload_dir).glob("results_*.csv")
assert not any(
common.check_file_pattern("global_read", str(f)) for f in results_files
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.section
def test_list_metrics(binary_handler_profile_rocprof_compute):
options = ["--list-metrics", "gfx90a"]
workload_dir = common.get_output_dir()
_ = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=True, roof=False
)
# workload dir should not exist
assert not Path(workload_dir).exists()
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.section
def test_list_metrics_with_block(binary_handler_profile_rocprof_compute):
options = ["--list-metrics", "gfx90a", "--block", "10"]
workload_dir = common.get_output_dir()
code = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=False, roof=False
)
# Should return code 1 since --block cannot be used with --list-metrics
assert code == 1
# workload dir should not exist
assert not Path(workload_dir).exists()
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.section
def test_list_available_metrics(binary_handler_profile_rocprof_compute, capsys):
options = ["--list-available-metrics"]
workload_dir = common.get_output_dir()
_ = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=True, roof=False
)
# workload dir should not exist
assert not Path(workload_dir).exists()
common.clean_output_dir(config["cleanup"], workload_dir)
# Test output
output = capsys.readouterr().out
assert "0 -> Top Stats" in output
assert "1 -> System Info" in output
@pytest.mark.section
def test_list_available_metrics_with_block(
binary_handler_profile_rocprof_compute, capsys
):
options = ["--list-available-metrics", "--block", "10"]
workload_dir = common.get_output_dir()
code = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=False, roof=False
)
# Should return code 1 since --block cannot be used with --list-available-metrics
assert code == 1
# workload dir should not exist
assert not Path(workload_dir).exists()
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.path
def test_comprehensive_error_paths():
"""Simplified test for error path coverage"""
from utils.parser import build_comparable_columns
columns = build_comparable_columns("ms")
expected = [
"Count(ms)",
"Sum(ms)",
"Mean(ms)",
"Median(ms)",
"Standard Deviation(ms)",
]
for expected_col in expected:
assert expected_col in columns
@pytest.mark.live_attach_detach
@pytest.mark.parametrize("profile_format", ["rocpd", "csv"])
def test_live_attach_detach_block(
binary_handler_profile_rocprof_compute, profile_format
):
options = [
"--block",
"3.1.1",
"4.1.1",
"5.1.1",
"--format-rocprof-output",
profile_format,
]
workload_dir = common.get_output_dir()
# TODO: temp fix for sdk defautly disable attach/detach,
# remove after it sets default to enable
env = os.environ.copy()
env["ROCP_TOOL_ATTACH"] = "1"
process_workload = None
try:
# Start workload
process_workload = subprocess.Popen(config["app_hip_dynamic_shared"], env=env)
time.sleep(5) # Give workload time to start
attach_detach = {
"attach_pid": process_workload.pid,
"attach-duration-msec": attach_detach_interval_msec_no_delay,
}
# Run profiler (might fail / timeout / throw)
binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=True,
roof=False,
app_name="app_hip_dynamic_shared",
attach_detach_para=attach_detach,
)
finally:
if process_workload and process_workload.poll() is None:
print(f"[finally] killing workload pid={process_workload.pid}")
process_workload.kill()
process_workload.wait()
# Clean up any stale rocprof-attach processes to prevent interference
# with subsequent tests.
subprocess.run(
["pkill", "-9", "-f", "rocprof-attach"],
capture_output=True,
)
# Validate results
file_dict = common.check_csv_files(workload_dir, 1, num_kernels)
validate(inspect.stack()[0][3], workload_dir, file_dict)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.live_attach_detach
def test_live_attach_detach_pc_sampling(
binary_handler_profile_rocprof_compute,
):
common.skip_unsupported_pc_sampling_soc(is_stochastic=True)
options = ["--experimental", "--pc-sampling"]
workload_dir = common.get_output_dir()
# TODO: temp fix for sdk defautly disable attach/detach,
# remove after it sets default to enable
env = os.environ.copy()
env["ROCP_TOOL_ATTACH"] = "1"
process_workload = None
try:
# Start workload
process_workload = subprocess.Popen(config["app_hip_dynamic_shared"], env=env)
time.sleep(15) # Give workload time to start
attach_detach = {
"attach_pid": process_workload.pid,
"attach-duration-msec": attach_detach_interval_msec_no_delay,
}
# Profiling step (may fail)
binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=True,
roof=False,
app_name="app_hip_dynamic_shared",
attach_detach_para=attach_detach,
)
finally:
if process_workload and process_workload.poll() is None:
print(f"[finally] killing workload pid={process_workload.pid}")
process_workload.kill()
process_workload.wait()
# Clean up any stale rocprof-attach processes to prevent interference
# with subsequent tests.
subprocess.run(
["pkill", "-9", "-f", "rocprof-attach"],
capture_output=True,
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.sets_func
class TestSetsIntegration:
# Ensure single pass for auto-discovered sets from YAML for the current GPU arch.
@pytest.mark.parametrize("set_name", AVAILABLE_SETS, ids=lambda s: s)
def test_set_profiling(
self, binary_handler_profile_rocprof_compute, set_name, request
):
"""Each set_option runs successfully and produces a single PMC file."""
options = ["--set", set_name]
workload_dir = common.get_output_dir(param_id=set_name)
returncode = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=True, roof=False
)
assert returncode == 0
assert common.get_num_pmc_file(workload_dir) == 1
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.parametrize(
"set_name",
[
pytest.param("nonexistent_set", id="nonexistent"),
pytest.param("x" * 1024, id="very_long_name"),
pytest.param("mem_thruput; rm -rf /", id="shell_metachar"),
],
)
def test_invalid_set_rejected(
self, binary_handler_profile_rocprof_compute, set_name, request
):
"""Invalid or adversarial set names are rejected with exit code 1."""
options = ["--set", set_name]
workload_dir = common.get_output_dir(
param_id=f"invalid_{request.node.callspec.id}"
)
returncode = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=False, roof=False
)
assert returncode == 1
common.clean_output_dir(config["cleanup"], workload_dir)
def test_set_and_block_mutual_exclusion(
self, binary_handler_profile_rocprof_compute
):
options = ["--set", "compute_thruput_util", "--block", "12"]
workload_dir = common.get_output_dir()
returncode = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=False, roof=False
)
assert returncode == 1
common.clean_output_dir(config["cleanup"], workload_dir)
def test_list_sets_functionality(self, binary_handler_profile_rocprof_compute):
options = ["--list-sets"]
workload_dir = common.get_output_dir()
binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=False,
roof=False,
)
# workload dir should not exist
assert not Path(workload_dir).exists()
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.iteration_multiplexing_1
def test_profiler_options(binary_handler_profile_rocprof_compute):
options = ["--no-native-tool", "--iteration-multiplexing"]
workload_dir = common.get_output_dir()
code = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=False, roof=False
)
assert code == 1
@pytest.mark.iteration_multiplexing_1
def test_iteration_multiplexing(binary_handler_profile_rocprof_compute):
options = ["--iteration-multiplexing"]
workload_dir = common.get_output_dir()
_ = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=True, roof=False
)
file_dict = common.check_csv_files(workload_dir, num_devices, num_kernels)
assert sorted(list(file_dict.keys())) == CSVS
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.iteration_multiplexing_1
def test_iteration_multiplexing_kernel(binary_handler_profile_rocprof_compute):
options = ["--iteration-multiplexing", "kernel"]
workload_dir = common.get_output_dir()
_ = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=True, roof=False
)
file_dict = common.check_csv_files(workload_dir, num_devices, num_kernels)
assert sorted(list(file_dict.keys())) == CSVS
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.iteration_multiplexing_1
def test_iteration_multiplexing_kernel_launch_params(
binary_handler_profile_rocprof_compute,
):
options = ["--iteration-multiplexing", "kernel_launch_params"]
workload_dir = common.get_output_dir()
_ = binary_handler_profile_rocprof_compute(
config, workload_dir, options, check_success=True, roof=False
)
file_dict = common.check_csv_files(workload_dir, num_devices, num_kernels)
assert sorted(list(file_dict.keys())) == CSVS
validate(
inspect.stack()[0][3],
workload_dir,
file_dict,
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.iteration_multiplexing_2
@pytest.mark.xfail(
reason="Multiple profiling workloads mapped to the same GPU corrupts the counters"
)
def test_iteration_multiplexing_deterministic_counter_accuracy(
binary_handler_profile_rocprof_compute,
binary_handler_analyze_rocprof_compute,
):
skip_unsupported_roofline_soc()
# These metrics should cover the deterministic counters being checked
# Block 4 (roofline) included to verify roofline counters under multiplexing
options = ["--block", "4", "6.1.5", "6.1.6", "7.2.2", "10.1"]
workload_dir = common.get_output_dir(param_id="no_iter_mplx")
_ = binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=True,
roof=False,
app_name="app_laplace_eqn",
)
common.check_csv_files(workload_dir, num_devices, num_kernels)
binary_handler_analyze_rocprof_compute(["analyze", "--path", workload_dir])
counters_no_multiplexing = pd.read_csv(Path(workload_dir) / "pmc_perf.csv")
common.clean_output_dir(config["cleanup"], workload_dir)
options = [
"--block",
"4",
"6.1.5",
"6.1.6",
"7.2.2",
"10.1",
"--iteration-multiplexing",
"kernel",
]
workload_dir = common.get_output_dir(param_id="iter_mplx_kernel")
_ = binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=True,
roof=False,
app_name="app_laplace_eqn_iter",
)
common.check_csv_files(workload_dir, num_devices, num_kernels)
binary_handler_analyze_rocprof_compute(["analyze", "--path", workload_dir])
counters_kernel = pd.read_csv(Path(workload_dir) / "pmc_perf.csv")
common.clean_output_dir(config["cleanup"], workload_dir)
options = [
"--block",
"4",
"6.1.5",
"6.1.6",
"7.2.2",
"10.1",
"--iteration-multiplexing",
"kernel_launch_params",
]
workload_dir_klp = common.get_output_dir(param_id="iter_mplx_params")
_ = binary_handler_profile_rocprof_compute(
config,
workload_dir_klp,
options,
check_success=True,
roof=True,
app_name="app_laplace_eqn_iter",
)
common.check_csv_files(workload_dir_klp, num_devices, num_kernels)
binary_handler_analyze_rocprof_compute(["analyze", "--path", workload_dir_klp])
counters_kernel_launch_params = pd.read_csv(Path(workload_dir_klp) / "pmc_perf.csv")
assert are_deterministic_counters_equal(
[counters_kernel, counters_kernel_launch_params], counters_no_multiplexing
)
assert os.path.exists(f"{workload_dir_klp}/roofline.csv")
roofline_df = pd.read_csv(f"{workload_dir_klp}/roofline.csv")
assert len(roofline_df) >= num_devices
common.clean_output_dir(config["cleanup"], workload_dir_klp)
@pytest.mark.iteration_multiplexing_stochastic
def test_iteration_multiplexing_stochastic_counter_accuracy(
binary_handler_profile_rocprof_compute,
binary_handler_analyze_rocprof_compute,
):
skip_unsupported_roofline_soc()
workload_dir = common.get_output_dir(param_id="no_iter_mplx")
# These metrics should cover the L1 cache stochastic counters
# Block 4 (roofline) included to verify roofline counters under multiplexing
options = ["--block", "4", "16.1", "16.3"]
_ = binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=True,
roof=False,
app_name="app_laplace_eqn",
)
common.check_csv_files(workload_dir, num_devices, num_kernels)
binary_handler_analyze_rocprof_compute(["analyze", "--path", workload_dir])
counters_no_multiplexing = pd.read_csv(Path(workload_dir) / "pmc_perf.csv")
common.clean_output_dir(config["cleanup"], workload_dir)
options = [
"--block",
"4",
"16.1",
"16.3",
"--iteration-multiplexing",
"kernel",
]
workload_dir = common.get_output_dir(param_id="iter_mplx_kernel")
_ = binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=True,
roof=False,
app_name="app_laplace_eqn_iter",
)
common.check_csv_files(workload_dir, num_devices, num_kernels)
binary_handler_analyze_rocprof_compute(["analyze", "--path", workload_dir])
counters_kernel = pd.read_csv(Path(workload_dir) / "pmc_perf.csv")
common.clean_output_dir(config["cleanup"], workload_dir)
options = [
"--block",
"4",
"16.1",
"16.3",
"--iteration-multiplexing",
"kernel_launch_params",
]
workload_dir_klp = common.get_output_dir(param_id="iter_mplx_params")
_ = binary_handler_profile_rocprof_compute(
config,
workload_dir_klp,
options,
check_success=True,
roof=True,
app_name="app_laplace_eqn_iter",
)
common.check_csv_files(workload_dir_klp, num_devices, num_kernels)
binary_handler_analyze_rocprof_compute(["analyze", "--path", workload_dir_klp])
counters_kernel_launch_params = pd.read_csv(Path(workload_dir_klp) / "pmc_perf.csv")
assert are_stochastic_counters_similar(
[counters_kernel, counters_kernel_launch_params], counters_no_multiplexing
)
assert os.path.exists(f"{workload_dir_klp}/roofline.csv")
roofline_df = pd.read_csv(f"{workload_dir_klp}/roofline.csv")
assert len(roofline_df) >= num_devices
common.clean_output_dir(config["cleanup"], workload_dir_klp)
# Not part of automated test runs since testing all counters is expensive
def test_iteration_multiplexing_all_counter_accuracy(
binary_handler_profile_rocprof_compute,
binary_handler_analyze_rocprof_compute,
):
workload_dir = common.get_output_dir(param_id="no_iter_mplx")
_ = binary_handler_profile_rocprof_compute(
config,
workload_dir,
check_success=True,
roof=False,
app_name="app_laplace_eqn",
)
common.check_csv_files(workload_dir, num_devices, num_kernels)
binary_handler_analyze_rocprof_compute(["analyze", "--path", workload_dir])
counters_no_multiplexing = pd.read_csv(Path(workload_dir) / "pmc_perf.csv")
common.clean_output_dir(config["cleanup"], workload_dir)
options = ["--iteration-multiplexing", "kernel"]
workload_dir = common.get_output_dir(param_id="iter_mplx_kernel")
_ = binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=True,
roof=False,
app_name="app_laplace_eqn_iter",
)
common.check_csv_files(workload_dir, num_devices, num_kernels)
binary_handler_analyze_rocprof_compute(["analyze", "--path", workload_dir])
counters_kernel = pd.read_csv(Path(workload_dir) / "pmc_perf.csv")
common.clean_output_dir(config["cleanup"], workload_dir)
options = ["--iteration-multiplexing", "kernel_launch_params"]
workload_dir = common.get_output_dir(param_id="iter_mplx_params")
_ = binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=True,
roof=False,
app_name="app_laplace_eqn_iter",
)
common.check_csv_files(workload_dir, num_devices, num_kernels)
binary_handler_analyze_rocprof_compute(["analyze", "--path", workload_dir])
counters_kernel_launch_params = pd.read_csv(Path(workload_dir) / "pmc_perf.csv")
common.clean_output_dir(config["cleanup"], workload_dir)
assert are_deterministic_counters_equal(
[counters_kernel, counters_kernel_launch_params], counters_no_multiplexing
)
assert are_stochastic_counters_similar(
[counters_kernel, counters_kernel_launch_params], counters_no_multiplexing
)
@pytest.mark.iteration_multiplexing_2
def test_iteration_multiplexing_insufficient_dispatches(
binary_handler_profile_rocprof_compute,
binary_handler_analyze_rocprof_compute,
capsys,
):
"""Verify graceful degradation when dispatches are too few for full
counter coverage under iteration multiplexing.
"""
options = [
"--iteration-multiplexing",
"kernel_launch_params",
]
workload_dir = common.get_output_dir(param_id="iter_mplx_insufficient")
binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=True,
roof=False,
app_name="app_laplace_eqn_insufficient",
)
code = binary_handler_analyze_rocprof_compute([
"analyze",
"--path",
workload_dir,
])
assert code == 0
captured = capsys.readouterr()
assert "missing counter data" in captured.out
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.iteration_multiplexing_2
def test_iteration_multiplexing_data_types(
binary_handler_profile_rocprof_compute,
binary_handler_analyze_rocprof_compute,
):
"""Verify roofline analysis with different data types (FP32 and FP16)
on iteration-multiplexed profiling data.
"""
skip_unsupported_roofline_soc()
options = [
"--block",
"4",
"--iteration-multiplexing",
"kernel_launch_params",
]
workload_dir = common.get_output_dir(param_id="iter_mplx_dtypes")
binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=True,
roof=True,
app_name="app_laplace_eqn",
)
assert os.path.exists(f"{workload_dir}/roofline.csv")
roofline_df = pd.read_csv(f"{workload_dir}/roofline.csv")
assert len(roofline_df) >= num_devices
code_fp32 = binary_handler_analyze_rocprof_compute([
"analyze",
"--path",
workload_dir,
"--roofline-data-type",
"FP32",
])
assert code_fp32 == 0
code_fp16 = binary_handler_analyze_rocprof_compute([
"analyze",
"--path",
workload_dir,
"--roofline-data-type",
"FP16",
])
assert code_fp16 == 0
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.torch_trace
def test_torch_trace_profile(
binary_handler_profile_rocprof_compute,
binary_handler_analyze_rocprof_compute,
capsys,
):
"""
Test profile and analyze flow for PyTorch torch-trace.
Runs profiling with --torch-trace, verifies profile outputs (pmc_perf, marker
and counter CSVs), then runs analyze with --list-torch-operators and
--torch-operator (shell-style fnmatch glob patterns like *relu, all), and verifies
ml_api_trace directory, consolidated CSV contents (hierarchy, kernel, counters),
and CLI output format (call tree grouped by source location, aggregated stats,
kernel IDs, sort order).
Requires PyTorch and GPU; not included in default suite.
"""
require_torch(gpu=True)
workload_dir = common.get_output_dir(param_id="torch_trace")
# --torch-trace needs --experimental for profiling
options = [
"--experimental",
"--torch-trace",
"--iteration-multiplexing",
]
returncode = binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=True,
app_name="torch_test_app",
)
# ---- Verify profiling output (checks 1–5) ----
# 1. Profiling completed successfully
assert returncode == 0, "Profiling the torch application failed"
# 2. Validate profile outputs (PMC data validated by check_csv_files)
num_devices = config.get("num_devices", 1)
common.check_csv_files(workload_dir, num_devices, 1)
# 3. Marker/counter CSV pairs exist and counts match
marker_api_trace_files = list(Path(workload_dir).glob("**/*marker_api_trace.csv"))
counter_collection_files = list(
Path(workload_dir).glob("**/*counter_collection.csv")
)
assert len(marker_api_trace_files) == len(counter_collection_files), (
"Mismatch in number of marker_api_trace.csv and counter_collection.csv files"
)
for marker_file in marker_api_trace_files:
corresponding_counter_file = marker_file.parent / marker_file.name.replace(
"marker_api_trace", "counter_collection"
)
assert corresponding_counter_file.exists(), (
f"counter_collection.csv not found for {marker_file}"
)
# 4. marker_api_trace CSVs: required columns and non-empty rows
expected_marker_columns = {
"Domain",
"Function",
"Process_Id",
"Thread_Id",
"Correlation_Id",
"Start_Timestamp",
"End_Timestamp",
}
with open(marker_file, newline="") as f:
reader = csv.DictReader(f)
fieldnames = reader.fieldnames
assert fieldnames is not None, f"No columns in {marker_file}"
for column in expected_marker_columns:
assert column in fieldnames, (
f"Column '{column}' missing in {marker_file}"
)
found_row = False
for row in reader:
found_row = True
assert row["Function"], f"Empty Function in {marker_file}"
assert row["Correlation_Id"], f"Empty Correlation ID in {marker_file}"
assert row["Start_Timestamp"], f"Empty Start_Timestamp in {marker_file}"
assert row["End_Timestamp"], f"Empty End_Timestamp in {marker_file}"
assert found_row, f"{marker_file} is empty"
# 5. counter_collection CSVs: required columns and non-empty rows
expected_counter_columns = {
"Correlation_Id",
"Kernel_Name",
"Counter_Name",
"Counter_Value",
"Start_Timestamp",
"End_Timestamp",
}
with open(corresponding_counter_file, newline="") as f:
reader = csv.DictReader(f)
fieldnames = reader.fieldnames
assert fieldnames is not None, f"No columns in {corresponding_counter_file}"
for column in expected_counter_columns:
assert column in fieldnames, (
f"Column '{column}' missing in {corresponding_counter_file}"
)
found_row = False
for row in reader:
found_row = True
assert row["Correlation_Id"], (
f"Empty Correlation_Id in {corresponding_counter_file}"
)
assert row["Kernel_Name"], (
f"Empty Kernel_Name in {corresponding_counter_file}"
)
assert row["Counter_Name"], (
f"Empty Counter_Name in {corresponding_counter_file}"
)
assert row["Start_Timestamp"], (
f"Empty Start_Timestamp in {corresponding_counter_file}"
)
assert row["End_Timestamp"], (
f"Empty End_Timestamp in {corresponding_counter_file}"
)
assert found_row, f"{corresponding_counter_file} is empty"
# Flush any profiling output so capsys captures only the analyze output
capsys.readouterr()
# ---- Verify analysis output from --list-torch-operators (checks 6–8) ----
# 6. Analyze with --list-torch-operators succeeds
returncode_analyze = binary_handler_analyze_rocprof_compute([
"--experimental",
"analyze",
"--path",
workload_dir,
"--list-torch-operators",
])
assert returncode_analyze == 0, "Analyze with --list-torch-operators failed"
list_output = capsys.readouterr().out
# 7. ml_api_trace directory created with consolidated.csv
ml_api_trace_dir = Path(workload_dir) / "ml_api_trace"
assert ml_api_trace_dir.exists(), "ml_api_trace directory not created"
consolidated_csv = ml_api_trace_dir / "consolidated.csv"
assert consolidated_csv.exists(), "consolidated.csv not found in ml_api_trace"
# 8. Consolidated CSV contains hierarchy, kernel names, and counter values
df = pd.read_csv(consolidated_csv)
assert not df.empty, "consolidated.csv is empty"
assert "Operator_Name" in df.columns, "Operator_Name column missing"
hierarchy_present = (
df["Operator_Name"].apply(lambda x: "/" in str(x) or "::" in str(x)).any()
)
assert hierarchy_present, "No hierarchy information in consolidated.csv"
assert "Kernel_Name" in df.columns, "Kernel_Name missing"
assert df["Kernel_Name"].notnull().all() and (df["Kernel_Name"] != "").all(), (
"Empty Kernel_Name in consolidated.csv"
)
assert "Counter_Value" in df.columns, "Counter_Value column missing"
assert df["Counter_Value"].notnull().all()
assert (df["Counter_Value"] != "").all(), "Empty Counter_Value in consolidated.csv"
# ---- Verify --list-torch-operators CLI output format (checks 9–14) ----
# 9. Banner
assert "PyTorch Operator Call Tree:" in list_output, "Missing banner line"
# 10. Source-location grouping (file:line headers)
location_headers = re.findall(
r"^(\S+:\d+)\s+\(dispatches:", list_output, re.MULTILINE
)
assert location_headers, "No source-location headers found in output"
# 11. Aggregated stats on tree nodes
assert re.search(r"\(dispatches:\s+\d+,\s+total:", list_output), (
"No aggregated stats found in output"
)
# 12. Kernel IDs
kernel_ids = re.findall(r"\(id (\d+)\)", list_output)
assert kernel_ids, "No kernel IDs found in output"
# 13. Kernel launch durations
assert re.search(r"dispatches:\s+\d+,\s+total:", list_output), (
"No kernel duration info in output"
)
# 14. Source locations sorted by descending total duration
location_durations = re.findall(
r"^(\S+:\d+)\s+\(dispatches:\s+\d+,\s+total:\s+([\d.]+)\s+(ms|us)",
list_output,
re.MULTILINE,
)
assert location_durations, "No location durations found for sort-order check"
durations_ms = [
float(val) if unit == "ms" else float(val) / 1000.0
for _, val, unit in location_durations
]
assert durations_ms == sorted(durations_ms, reverse=True), (
f"Source locations not sorted by descending duration: {location_durations}"
)
# 15. --list-torch-operators succeeds at every --kernel-verbose level 0-4
# (level 5 is the baseline run above)
for verbose_level in range(5):
capsys.readouterr()
rc = binary_handler_analyze_rocprof_compute([
"--experimental",
"analyze",
"--path",
workload_dir,
"--list-torch-operators",
"--kernel-verbose",
str(verbose_level),
])
assert rc == 0, (
f"--list-torch-operators failed with --kernel-verbose {verbose_level}"
)
# ---- Verify analysis output from --torch-operator (check 16) ----
# Analyze with --torch-operator needs --experimental flag
returncode_analyze_relu = binary_handler_analyze_rocprof_compute([
"--experimental",
"analyze",
"--path",
workload_dir,
"--torch-operator",
"*relu*",
])
# 16. Analyze with --torch-operator *relu* succeeds and matches the relu subtree
assert returncode_analyze_relu == 0, "Analyze with --torch-operator *relu* failed"
out_relu = capsys.readouterr().out
assert "Matched PyTorch Operators" in out_relu, (
"Expected 'Matched PyTorch Operators' header from --torch-operator *relu*"
)
# --- Verify torch-operator cli output ---
# 17. Substring wildcard pattern matches torch.nn.functional.relu at any
# position in the hierarchy.
capsys.readouterr()
rc_exact = binary_handler_analyze_rocprof_compute([
"--experimental",
"analyze",
"--path",
workload_dir,
"--torch-operator",
"*torch.nn.functional.relu*",
])
assert rc_exact == 0, (
"Analyze with --torch-operator *torch.nn.functional.relu* failed"
)
out_exact = capsys.readouterr().out
assert "Matched PyTorch Operators" in out_exact, (
"Expected 'Matched PyTorch Operators' header in --torch-operator output"
)
assert "dispatches" in out_exact, (
"Expected call tree with dispatches stats in --torch-operator output"
)
# 18. Glob wildcard pattern (*relu*) matches the relu operator subtree
capsys.readouterr()
rc_glob = binary_handler_analyze_rocprof_compute([
"--experimental",
"analyze",
"--path",
workload_dir,
"--torch-operator",
"*relu*",
])
assert rc_glob == 0, "Analyze with --torch-operator *relu* failed"
out_glob = capsys.readouterr().out
assert "dispatches" in out_glob, (
"Glob pattern *relu* should match relu operator and render call tree"
)
# 19. 'all' keyword matches every operator
capsys.readouterr()
rc_all = binary_handler_analyze_rocprof_compute([
"--experimental",
"analyze",
"--path",
workload_dir,
"--torch-operator",
"all",
])
assert rc_all == 0, "Analyze with --torch-operator all failed"
out_all = capsys.readouterr().out
assert "dispatches" in out_all, "'all' keyword should match operators"
# 20. --torch-operator + -k intersection succeeds and renders call tree
capsys.readouterr()
rc_intersect = binary_handler_analyze_rocprof_compute([
"--experimental",
"analyze",
"--path",
workload_dir,
"--torch-operator",
"all",
"-k",
"0",
])
assert rc_intersect == 0, "Analyze with --torch-operator all -k 0 failed"
out_intersect = capsys.readouterr().out
assert "Matched PyTorch Operators" in out_intersect, (
"Expected call tree output with --torch-operator all -k 0"
)
assert "Torch operator filter selected" in out_intersect, (
"Expected filter-selection log confirming -k intersection"
)
# 21. Non-matching pattern degrades gracefully with a warning
capsys.readouterr()
rc_nomatch = binary_handler_analyze_rocprof_compute([
"--experimental",
"analyze",
"--path",
workload_dir,
"--torch-operator",
"nonexistent_operator_xyz",
])
assert rc_nomatch == 0, (
"Analyze with non-matching --torch-operator should not crash"
)
out_nomatch = capsys.readouterr().out
assert "No PyTorch operators matched" in out_nomatch, (
"Expected warning about no operators matched"
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.triton_trace
def test_triton_trace_profile(
binary_handler_profile_rocprof_compute,
binary_handler_analyze_rocprof_compute,
capsys,
):
"""
Profile and analyze flow for the Triton backend.
Profiles a Triton FFN workload with --triton-trace, verifies the marker and
counter CSV outputs contain Triton markers, then runs analyze with
--list-triton-operators and --triton-operator and checks the call-tree
banner, the consolidated ml_api_trace CSV, and the matched and no-match output.
Requires PyTorch, Triton, and a GPU.
"""
require_triton(gpu=True)
workload_dir = common.get_output_dir(param_id="triton_trace")
options = [
"--experimental",
"--triton-trace",
"--iteration-multiplexing",
]
returncode = binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=True,
app_name="triton_test_app",
)
# ---- Profiling output ----
assert returncode == 0, "Profiling the Triton application failed"
marker_api_trace_files = list(Path(workload_dir).glob("**/*marker_api_trace.csv"))
counter_collection_files = list(
Path(workload_dir).glob("**/*counter_collection.csv")
)
assert marker_api_trace_files, "No marker_api_trace.csv produced"
assert len(marker_api_trace_files) == len(counter_collection_files), (
"marker_api_trace.csv and counter_collection.csv counts differ"
)
found_triton_marker = False
for marker_file in marker_api_trace_files:
with open(marker_file, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
assert reader.fieldnames is not None, f"No columns in {marker_file}"
assert "Function" in reader.fieldnames, (
f"'Function' column missing in {marker_file}"
)
for row in reader:
if "triton" in str(row["Function"]).lower():
found_triton_marker = True
break
if found_triton_marker:
break
assert found_triton_marker, "No Triton markers in marker_api_trace output"
# Flush profiling output so capsys captures only the analyze output.
capsys.readouterr()
# ---- analyze --list-triton-operators ----
returncode_list = binary_handler_analyze_rocprof_compute([
"--experimental",
"analyze",
"--path",
workload_dir,
"--list-triton-operators",
])
assert returncode_list == 0, "Analyze with --list-triton-operators failed"
list_output = capsys.readouterr().out
assert "Triton Operator Call Tree:" in list_output, "Missing call-tree banner"
# The workload launches a Triton matmul kernel.
assert "matmul" in list_output, "matmul kernel missing from operator list"
consolidated_csv = Path(workload_dir) / "ml_api_trace" / "consolidated.csv"
assert consolidated_csv.exists(), "consolidated.csv not found in ml_api_trace"
df = pd.read_csv(consolidated_csv)
assert not df.empty, "consolidated.csv is empty"
assert "Operator_Name" in df.columns, "Operator_Name column missing"
assert df["Operator_Name"].astype(str).str.contains("triton").any(), (
"No Triton operators in consolidated.csv"
)
# ---- analyze --triton-operator ----
capsys.readouterr()
returncode_match = binary_handler_analyze_rocprof_compute([
"--experimental",
"analyze",
"--path",
workload_dir,
"--triton-operator",
"*matmul*",
])
assert returncode_match == 0, "Analyze with --triton-operator *matmul* failed"
out_match = capsys.readouterr().out
assert "Matched Triton Operators" in out_match, "Missing matched-operators header"
assert "matmul" in out_match, "matmul kernel missing from matched output"
capsys.readouterr()
returncode_nomatch = binary_handler_analyze_rocprof_compute([
"--experimental",
"analyze",
"--path",
workload_dir,
"--triton-operator",
"nonexistent_kernel_xyz",
])
assert returncode_nomatch == 0, (
"Analyze with a non-matching --triton-operator failed"
)
out_nomatch = capsys.readouterr().out
assert "No Triton operators matched" in out_nomatch, "Missing no-match warning"
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.triton_trace
def test_ml_api_trace_torch_compile_triton(
binary_handler_profile_rocprof_compute,
binary_handler_analyze_rocprof_compute,
capsys,
):
"""
Validate the ML API trace flow for a torch.compile Triton workload.
Profiles the workload with --ml-api-trace and runs analyze with
--list-triton-operators, --triton-operator, and --torch-operator. Verifies
that a Triton kernel is attributed to an operator in the consolidated
trace. Requires PyTorch, Triton, and a GPU.
"""
require_triton(gpu=True)
workload_dir = common.get_output_dir(param_id="ml_api_trace")
options = [
"--experimental",
"--ml-api-trace",
"--iteration-multiplexing",
]
returncode = binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
check_success=True,
app_name="torch_compile_test_app",
)
# ---- Profiling output ----
assert returncode == 0, "Profiling the torch.compile/Triton workload failed"
marker_api_trace_files = list(Path(workload_dir).glob("**/*marker_api_trace.csv"))
counter_collection_files = list(
Path(workload_dir).glob("**/*counter_collection.csv")
)
assert marker_api_trace_files, "No marker_api_trace.csv produced"
assert len(marker_api_trace_files) == len(counter_collection_files), (
"marker_api_trace.csv and counter_collection.csv counts differ"
)
# Discard captured profiling output.
capsys.readouterr()
# ---- Consolidated ml_api_trace ----
returncode_list = binary_handler_analyze_rocprof_compute([
"--experimental",
"analyze",
"--path",
workload_dir,
"--list-triton-operators",
])
assert returncode_list == 0, "Analyze with --list-triton-operators failed"
capsys.readouterr()
consolidated_csv = Path(workload_dir) / "ml_api_trace" / "consolidated.csv"
assert consolidated_csv.exists(), "consolidated.csv not found in ml_api_trace"
df = pd.read_csv(consolidated_csv)
assert not df.empty, "consolidated.csv is empty"
for column in ("Operator_Name", "Backend", "Kernel_Name"):
assert column in df.columns, f"{column} column missing in consolidated.csv"
# A Triton kernel is attributed to an operator marker from the torch or
# triton backend.
attributed_triton = df[
df["Kernel_Name"].astype(str).str.contains("triton_", case=False, na=False)
& df["Operator_Name"].notna()
& df["Backend"].isin(["torch", "triton"])
]
assert not attributed_triton.empty, (
"No torch.compile Triton kernel (triton_*) was attributed to an "
"operator marker in consolidated.csv"
)
# ---- analyze --triton-operator ----
capsys.readouterr()
returncode_triton = binary_handler_analyze_rocprof_compute([
"--experimental",
"analyze",
"--path",
workload_dir,
"--triton-operator",
"all",
])
assert returncode_triton == 0, "Analyze with --triton-operator all failed"
capsys.readouterr()
returncode_torch = binary_handler_analyze_rocprof_compute([
"--experimental",
"analyze",
"--path",
workload_dir,
"--torch-operator",
"all",
])
assert returncode_torch == 0, "Analyze with --torch-operator all failed"
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.torch_trace
def test_torch_trace_overhead(binary_handler_profile_rocprof_compute):
"""
Measure overhead introduced by --torch-trace flag.
Compares execution time with and without the flag to ensure overhead is acceptable.
NOTE: Not included in the test suite since this requires PyTorch and GPU.
"""
require_torch(gpu=True)
# Run WITHOUT --torch-trace (baseline)
workload_dir_baseline = common.get_output_dir(param_id="torch_trace_baseline")
start_baseline = time.time()
returncode_baseline = binary_handler_profile_rocprof_compute(
config,
workload_dir_baseline,
["--iteration-multiplexing"], # Baseline without --torch-trace
check_success=True,
roof=False,
app_name="torch_test_app",
)
baseline_time = time.time() - start_baseline
assert returncode_baseline == 0, "Baseline profiling failed"
# Read baseline timestamps
baseline_results_files = list(Path(workload_dir_baseline).glob("results_*.csv"))
baseline_df = pd.concat(
[pd.read_csv(f) for f in baseline_results_files], ignore_index=True
)
baseline_kernel_duration_total = (
baseline_df["End_Timestamp"].max() - baseline_df["Start_Timestamp"].min()
)
common.clean_output_dir(config["cleanup"], workload_dir_baseline)
# Run WITH --torch-trace (requires --experimental)
workload_dir_with_flag = common.get_output_dir(param_id="torch_trace_with_flag")
start_with_flag = time.time()
returncode_with_flag = binary_handler_profile_rocprof_compute(
config,
workload_dir_with_flag,
["--experimental", "--torch-trace", "--iteration-multiplexing"],
check_success=True,
roof=False,
app_name="torch_test_app",
)
with_flag_time = time.time() - start_with_flag
assert returncode_with_flag == 0, "Profiling with torch-trace failed"
# Read with-flag timestamps
with_flag_results_files = list(Path(workload_dir_with_flag).glob("results_*.csv"))
with_flag_df = pd.concat(
[pd.read_csv(f) for f in with_flag_results_files], ignore_index=True
)
with_flag_kernel_duration_total = (
with_flag_df["End_Timestamp"].max() - with_flag_df["Start_Timestamp"].min()
)
longest_running_kernel_baseline = (
baseline_df["End_Timestamp"] - baseline_df["Start_Timestamp"]
).max()
longest_running_kernel_with_flag = (
with_flag_df["End_Timestamp"] - with_flag_df["Start_Timestamp"]
).max()
# Calculate overheads
longest_running_kernel_overhead = (
(longest_running_kernel_with_flag - longest_running_kernel_baseline)
/ longest_running_kernel_baseline
) * 100
wall_clock_overhead = ((with_flag_time - baseline_time) / baseline_time) * 100
kernel_overhead = (
(with_flag_kernel_duration_total - baseline_kernel_duration_total)
/ baseline_kernel_duration_total
) * 100
print(f"\n{'=' * 70}")
print("Performance Overhead Analysis:")
print(f" Longest running kernel overhead: {longest_running_kernel_overhead:.1f}%")
print(f" Baseline wall-clock time: {baseline_time:.2f}s")
print(f" With --torch-trace time: {with_flag_time:.2f}s")
print(f" Wall-clock overhead: {wall_clock_overhead:.1f}%")
print(f" Baseline kernel duration: {baseline_kernel_duration_total:.0f} ns")
print(f" With flag kernel duration: {with_flag_kernel_duration_total:.0f} ns")
print(f" Kernel execution overhead: {kernel_overhead:.1f}%")
print(f"{'=' * 70}\n")
common.clean_output_dir(config["cleanup"], workload_dir_with_flag)
# Assert overhead is reasonable (< 100% wall-clock, < 50% kernel)
assert wall_clock_overhead < 100, (
f"Wall-clock overhead too high: {wall_clock_overhead:.1f}%"
)
assert kernel_overhead < 50, (
f"Kernel execution overhead too high: {kernel_overhead:.1f}%"
)
assert longest_running_kernel_overhead < 50, (
f"longest running kernel increase too high: "
f"{longest_running_kernel_overhead:.1f}%"
)
@pytest.mark.multi_rank
def test_multi_rank_profiling_no_mpi_comm(binary_handler_profile_rocprof_compute):
"""
Test multi-rank profiling of a non-MPI application.
The fixture launches the profiling command with mpirun.
"""
num_ranks = 2
workload_dir = common.get_output_dir()
binary_handler_profile_rocprof_compute(config, workload_dir, num_ranks=num_ranks)
# Check output for each rank
for rank in range(num_ranks):
rank_dir = Path(workload_dir) / str(rank)
assert rank_dir.exists(), f"Rank directory {rank_dir} does not exist"
file_dict = common.check_csv_files(str(rank_dir), num_devices, num_kernels)
if soc == "MI100":
assert sorted(list(file_dict.keys())) == CSVS
elif soc == "MI200":
assert sorted(list(file_dict.keys())) == CSVS
elif "MI300" in soc:
assert sorted(list(file_dict.keys())) == CSVS
elif "MI350" in soc:
assert sorted(list(file_dict.keys())) == CSVS
elif is_gfx115x_soc():
assert sorted(list(file_dict.keys())) == CSVS
else:
print(f"Testing isn't supported yet for {soc}")
assert 0
validate(
inspect.stack()[0][3],
str(rank_dir),
file_dict,
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.multi_rank
def test_multi_rank_profiling_mpi_comm(
binary_handler_profile_rocprof_compute,
):
"""
Test multi-rank profiling of an MPI application.
The fixture launches the profiling command with mpirun.
"""
# Skip test if mpi_aware_laplace_eqn is not available
app_path = config.get("app_mpi_aware_laplace_eqn", [None])[0]
if not (app_path and Path(app_path).exists()):
pytest.skip(
f"mpi_aware_laplace_eqn not found, skipping {inspect.stack()[0][3]}"
)
num_ranks = 2
workload_dir = common.get_output_dir()
options = ["--iteration-multiplexing"]
binary_handler_profile_rocprof_compute(
config, workload_dir, options, app_name="app_mpi_aware_laplace_eqn", num_ranks=2
)
# Check output for each rank
for rank in range(num_ranks):
rank_dir = Path(workload_dir) / str(rank)
assert rank_dir.exists(), f"Rank directory {rank_dir} does not exist"
file_dict = common.check_csv_files(str(rank_dir), num_devices, num_kernels)
if soc == "MI100":
assert sorted(list(file_dict.keys())) == CSVS
elif soc == "MI200":
assert sorted(list(file_dict.keys())) == CSVS
elif "MI300" in soc:
assert sorted(list(file_dict.keys())) == CSVS
elif "MI350" in soc:
assert sorted(list(file_dict.keys())) == CSVS
elif is_gfx115x_soc():
assert sorted(list(file_dict.keys())) == CSVS
else:
print(f"Testing isn't supported yet for {soc}")
assert 0
validate(
inspect.stack()[0][3],
str(rank_dir),
file_dict,
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.multi_rank
def test_wrapped_mpi(binary_handler_profile_rocprof_compute):
"""
Test that using MPI launchers (mpirun, mpiexec, srun, orterun) after '--'
raises an error.
"""
config["wrapped_mpi"] = ["mpirun", "-n", "2", "./tests/occupancy"]
workload_dir = common.get_output_dir()
returncode = binary_handler_profile_rocprof_compute(
config,
workload_dir,
options=[],
check_success=False,
app_name="wrapped_mpi",
)
# Should fail with exit code 1
assert returncode == 1
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.multi_rank
def test_multi_rank_warning_application_replay(
binary_handler_profile_rocprof_compute, monkeypatch
):
"""
Test that a warning is printed when running a multi-rank application
in application replay mode.
"""
# Set MPI environment variables to simulate multi-rank
monkeypatch.setenv("OMPI_COMM_WORLD_RANK", "0")
monkeypatch.setenv("OMPI_COMM_WORLD_SIZE", "2")
workload_dir = common.get_output_dir()
_, stdout, stderr = binary_handler_profile_rocprof_compute(
config,
workload_dir,
app_name="app_1",
capture_output=True,
check_success=False,
)
# Check that warning message is in output
output = stdout + stderr
assert "Multi-rank application detected" in output
assert "Application replay mode" in output
assert "--iteration-multiplexing" in output
assert "--block" not in output
assert "--set" in output
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.multi_rank
def test_multi_rank_no_warning_with_iteration_multiplexing(
binary_handler_profile_rocprof_compute, monkeypatch
):
"""
Test that no application replay warning is printed when running a
multi-rank application with iteration multiplexing enabled.
"""
monkeypatch.setenv("OMPI_COMM_WORLD_RANK", "0")
monkeypatch.setenv("OMPI_COMM_WORLD_SIZE", "2")
workload_dir = common.get_output_dir()
options = ["--iteration-multiplexing"]
_, stdout, stderr = binary_handler_profile_rocprof_compute(
config,
workload_dir,
options,
app_name="app_1",
capture_output=True,
check_success=False,
)
output = stdout + stderr
assert "Multi-rank application detected" not in output
assert "Application replay mode" not in output
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.torch_trace
@pytest.mark.parametrize(
"workload_cmd, expected_exit",
[
pytest.param(
["python3", "nonexistent_script_abc.py"],
1,
id="missing_script",
),
pytest.param(
["python3"],
1,
id="bare_interpreter",
),
pytest.param(
["python3", "-u", "-v"],
1,
id="flags_only",
),
pytest.param(
["python3", "-u", "nonexistent_script_abc.py"],
1,
id="missing_script_after_flags",
),
pytest.param(
["nonexistentpython3", "script.py"],
1,
id="nonexistent_executable",
),
pytest.param(
["./no_such_binary"],
1,
id="nonexistent_binary",
),
],
)
def test_profile_invalid_workloads_torch_trace(
binary_handler_profile_rocprof_compute,
workload_cmd,
expected_exit,
request,
):
"""Integration test: workload validation exit codes with --torch-trace."""
require_torch(gpu=True)
app_name = "test_invalid_workload"
test_config = {**config, app_name: workload_cmd}
workload_dir = common.get_output_dir(
param_id=f"invalid_wl_{request.node.callspec.id}"
)
returncode, stdout, stderr = binary_handler_profile_rocprof_compute(
test_config,
workload_dir,
options=["--experimental", "--torch-trace", "--iteration-multiplexing"],
check_success=False,
app_name=app_name,
capture_output=True,
)
assert returncode == expected_exit, (
f"Expected exit code {expected_exit} for {workload_cmd}, "
f"got {returncode}.\nstdout: {stdout}\nstderr: {stderr}"
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.parametrize(
"workload_cmd, expected_exit",
[
pytest.param(
["python3", "nonexistent_script_abc.py"],
1,
id="missing_script",
),
pytest.param(
["python3"],
1,
id="bare_interpreter",
),
pytest.param(
["python3", "-u", "-v"],
1,
id="flags_only",
),
pytest.param(
["python3", "-u", "nonexistent_script_abc.py"],
1,
id="missing_script_after_flags",
),
pytest.param(
["nonexistentpython3", "script.py"],
1,
id="nonexistent_executable",
),
pytest.param(
["./no_such_binary"],
1,
id="nonexistent_binary",
),
pytest.param(
["python3", "-c", "print('hello')"],
0,
id="non_gpu_workload",
),
],
)
def test_profile_invalid_workloads_no_torch_trace(
binary_handler_profile_rocprof_compute,
workload_cmd,
expected_exit,
request,
):
"""Integration test: workload validation exit codes without --torch-trace."""
app_name = "test_invalid_workload"
test_config = {**config, app_name: workload_cmd}
workload_dir = common.get_output_dir(
param_id=f"invalid_wl_{request.node.callspec.id}"
)
returncode, stdout, stderr = binary_handler_profile_rocprof_compute(
test_config,
workload_dir,
options=[],
check_success=False,
app_name=app_name,
capture_output=True,
)
assert returncode == expected_exit, (
f"Expected exit code {expected_exit} for {workload_cmd}, "
f"got {returncode}.\nstdout: {stdout}\nstderr: {stderr}"
)
common.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.torch_trace
def test_torch_trace_deep_tensor_wraps_overhead(
binary_handler_profile_rocprof_compute,
):
"""Manual benchmark for the deep tensor wrap overhead.
Skipped unless ``ROCPROFCOMPUTE_RUN_DEEP_TENSOR_WRAP_BENCH=1``.
"""
require_torch(gpu=True)
run_bench = os.environ.get("ROCPROFCOMPUTE_RUN_DEEP_TENSOR_WRAP_BENCH", "")
if run_bench.strip().lower() not in ("1", "true", "yes", "on"):
pytest.skip(
"set ROCPROFCOMPUTE_RUN_DEEP_TENSOR_WRAP_BENCH=1 to run this benchmark"
)
def _run_once(*, deep_wraps: bool, param_id: str) -> tuple[float, float]:
workload_dir = common.get_output_dir(param_id=param_id)
prev = os.environ.get("ROCPROFCOMPUTE_ROCTX_DEEP_TENSOR_WRAPS")
os.environ["ROCPROFCOMPUTE_ROCTX_DEEP_TENSOR_WRAPS"] = (
"1" if deep_wraps else "0"
)
try:
start = time.time()
returncode = binary_handler_profile_rocprof_compute(
config,
workload_dir,
["--experimental", "--torch-trace", "--iteration-multiplexing"],
check_success=True,
roof=False,
app_name="torch_test_app",
)
elapsed = time.time() - start
assert returncode == 0, "torch-trace profiling run failed"
results_files = list(Path(workload_dir).glob("results_*.csv"))
df = pd.concat([pd.read_csv(f) for f in results_files], ignore_index=True)
kernel_duration_total = (
df["End_Timestamp"].max() - df["Start_Timestamp"].min()
)
return elapsed, kernel_duration_total
finally:
common.clean_output_dir(config["cleanup"], workload_dir)
if prev is None:
os.environ.pop("ROCPROFCOMPUTE_ROCTX_DEEP_TENSOR_WRAPS", None)
else:
os.environ["ROCPROFCOMPUTE_ROCTX_DEEP_TENSOR_WRAPS"] = prev
baseline_wall, baseline_kernel = _run_once(
deep_wraps=False,
param_id="torch_trace_deep_wraps_off",
)
deep_wall, deep_kernel = _run_once(
deep_wraps=True,
param_id="torch_trace_deep_wraps_on",
)
wall_overhead = (
((deep_wall - baseline_wall) / baseline_wall) * 100
if baseline_wall > 0
else 0.0
)
kernel_overhead = (
((deep_kernel - baseline_kernel) / baseline_kernel) * 100
if baseline_kernel > 0
else 0.0
)
print("\n" + "=" * 70)
print("Deep Tensor Wrap Overhead Benchmark (--torch-trace):")
print(f" Baseline wall-clock: {baseline_wall:.2f}s")
print(f" Deep-wraps wall-clock: {deep_wall:.2f}s")
print(f" Wall-clock overhead: {wall_overhead:.1f}%")
print(f" Baseline kernel duration: {baseline_kernel:.0f} ns")
print(f" Deep-wraps kernel duration: {deep_kernel:.0f} ns")
print(f" Kernel overhead: {kernel_overhead:.1f}%")
print("=" * 70 + "\n")