Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test_component.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ jobs:
SHARD_INDEX: ${{ matrix.shard }}
TOTAL_SHARDS: ${{ fromJSON(inputs.component).total_shards }}
TEST_TYPE: ${{ fromJSON(inputs.component).test_type }}
TEST_COMPONENT: ${{ fromJSON(inputs.component).job_name }}
ROCM_KPACK_DEBUG: "1"
run: |
${{ fromJSON(inputs.component).test_script }}
Expand Down
2 changes: 1 addition & 1 deletion build_tools/github_actions/fetch_test_configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ def _get_script_path(script_name: str) -> str:
"job_name": "miopen",
"fetch_artifact_args": "--blas --miopen --rand --tests",
"timeout_minutes": 60,
"test_script": f"python {_get_script_path('test_miopen.py')}",
"test_script": f"python {_get_script_path('test_runner.py')}",
"platform": ["linux", "windows"],
"total_shards_dict": {
"linux": 4,
Expand Down
34 changes: 34 additions & 0 deletions build_tools/github_actions/github_actions_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,40 @@ def get_first_gpu_architecture(env=None, therock_bin_dir: str | None = None) ->
raise RuntimeError("No GPU architecture found in rocminfo output")


def find_matching_gpu_arch(gpu_arch: str, available_gpu_archs: set[str]) -> str | None:
"""
Find the most specific GPU architecture in the set that matches the given GPU.

Tries in order from most specific to least specific:
# Example:
# find_matching_gpu_arch('gfx1151', {'gfx1151', 'gfx115X', 'gfx11X'}) gives 'gfx1151'
# find_matching_gpu_arch('gfx1151', {'gfx1150', 'gfx94X', 'gfx11X'}) gives 'gfx11X'
- Wildcard matches (gfx115X, gfx11X, etc.)

Returns the matching architecture string or None if no match found.
"""
# First, try exact match
if gpu_arch in available_gpu_archs:
return gpu_arch

# Generate possible wildcard patterns from most specific to least specific
# For gfx1151: try gfx115X, gfx11X
possible_patterns = []
arch_str = gpu_arch

# Generate patterns by replacing characters with X from right to left
for i in range(len(arch_str) - 1, 1, -1):
pattern = arch_str[:i] + "X"
possible_patterns.append(pattern)

# Try each pattern
for pattern in possible_patterns:
if pattern in available_gpu_archs:
return pattern

return None
Comment thread
dileepr1 marked this conversation as resolved.
Resolved


def is_asan():
"""Using artifact_group, determines if this is an asan build"""
ARTIFACT_GROUP = os.getenv("ARTIFACT_GROUP", "")
Expand Down
87 changes: 87 additions & 0 deletions build_tools/github_actions/test_executable_scripts/README.md
Comment thread
dileepr1 marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Test Runner

`test_runner.py` is a **generic test runner** used by GitHub Actions to run component tests (e.g. MIOpen). It relies on a standardized CTest naming and labeling scheme so that only tests valid for the current scenario are run.

This document describes how the mechanism works, what the component must provide, and how it ties into CI.

______________________________________________________________________

## Overview

1. The script is invoked by the **Test component** workflow with env vars set (component name, GPU arch, test type, sharding).
1. The script expects the component under test to have a ctest based interface, with labels corresponding to test categories like quick, standard, comprehensive and full(which can be run for scenarios like smoke test, pre-commit etc)
1. The component can opt to always exclude some tests or based on condition like OS and GPU where the test is getting run
1. The script discovers which **GPU-specific test suites** exist by running `ctest --print-labels` and parsing labels of the form `ex_gpu_{gpu_arch}`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're discussing how to handle systems having variable GPU memory. This is being driven by APUs such as Strix Halo. We probably don't need a complete answer for this PR, but it wouldn't hurt to start thinking about it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The particular issue is that many existing tests assuming having a certain amount of GPU memory. For MIOpen, this wasn't part of the design so we don't know the actual minimum, but roughly 12GB. We were seeing lots of failures on a Strix Halo having 16GB system memory, in which case the memory allocated to GPU was appx 4GB 'dedicated' and 4.9GB 'shared' for a total of 8.9GB.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be easily handled if we know the gfx models and their available memory and the memory needed for each of the tests before hand by using the exclude_gpu_gfx* section in test_categories.yaml.
Automation may be possible by adding the memory details of gpus and tests in the yaml file as well.
But it will need more thought in case the same gpu model can have multiple memory values in the test set up.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, the latter is the case on APU. CPU and GPU memory come from the same physical RAM sticks and the OS divides it up. GPU memory can be user-modified not only by changing the system RAM, but also modifying firmware and/or software settings. The memory configuration has to be queried before each run just prior to launching the tests.

1. It builds and runs a **ctest** command with the right labels and options (category, parallelism, sharding, etc.).

No component-specific logic is required beyond using the expected test names and CTest labels.

______________________________________________________________________

## Test Naming and Labels (Component Contract)

For this runner to work, the component’s CTest configuration must follow this convention.

### CTest labels (discovery)

The script runs `ctest --print-labels --test-dir <component>` and collects every label that starts with **`ex_gpu_`**. The suffix is treated as the GPU architecture (e.g. `ex_gpu_gfx110X` → `gfx110X`, `ex_gpu_gfx950` → `gfx950`). Only labels whose suffix starts with `gfx` are used.

### CTest labels (selection)

- **Category:** Tests are selected by **label** for the run (e.g. “run all tests with label `quick`” or `standard`). The script uses `-L <category>`.
- **GPU-specific suites:** For a given `gpu_arch`, the script looks for a label `ex_gpu_{gpu_arch}` (e.g. `ex_gpu_gfx1150`, `ex_gpu_gfx11X`). It uses `-L ex_gpu_<arch>` when it finds a matching label, or `-LE ex_gpu` to exclude all GPU-specific tests when no match is found or when no GPU is specified.

So the component must assign GPU-specific tests the label `ex_gpu_{gpu_arch}` (and typically a category label like `quick` or `standard`).

______________________________________________________________________

## Execution Flow

1. **Resolve component directory**
Map `TEST_COMPONENT` (e.g. `miopen`) to the test directory name (e.g. `MIOpen`) via `COMPONENT_DIR_MAPPING`. Fail if the test directory does not exist under `THEROCK_BIN_DIR`.
1. **Discover GPU suites**
Run `ctest --print-labels --test-dir {THEROCK_BIN_DIR}/{TEST_COMPONENT}`. Collect every label that starts with `ex_gpu_` and whose suffix starts with `gfx` (e.g. `ex_gpu_gfx110X` → `gfx110X`). This yields the set of **available GPU architectures** for which a label exists.
Comment thread
dileepr1 marked this conversation as resolved.
Resolved
These are the set of GPU's for which some test exclusions apply - i.e there are some tests which should not be run on these GPU models. The exp_gpu\_ labels with these gpu models points to the ctest entries where the tests are excluded.
1. **Choose category**
From `TEST_TYPE`: `smoke` → `quick`, else → `standard`.
1. **Resolve GPU arch**
Parse `AMDGPU_FAMILIES` for the first `gfx...` token (e.g. `gfx1151`). If missing or generic, the script will exclude all GPU-specific tests (`-LE ex_gpu`).
1. **Match GPU to suite**
Using `find_matching_gpu_arch()` (from `github_actions_utils`):

- Prefer **exact** match in the discovered set (e.g. `gfx1151`).
- Else try **wildcard** patterns from most to least specific (e.g. for `gfx1151`: `gfx115X`, then `gfx11X`).
- If a match is found, add `-L ex_gpu_{matching_arch}`; otherwise add `-LE ex_gpu`.

6. **Build ctest command**

- `ctest -L <category>` (and optionally `-L ex_gpu_<arch>` or `-LE ex_gpu`).
- Common options: `--output-on-failure`, `--parallel <N>`, `--test-dir`, `-V`, `--tests-information <SHARD_INDEX>,<TOTAL_SHARDS>`.
- Parallelism: default 8; can be adjusted according to `AMDGPU_FAMILIES`

7. **Run ctest**
Execute the command in `THEROCK_DIR` with the environment that includes `ROCM_PATH`, `GTEST_SHARD_INDEX`, and `GTEST_TOTAL_SHARDS`.

______________________________________________________________________

## CI Integration

- **Workflow:** `.github/workflows/test_component.yml` runs the test step with env vars such as `TEST_COMPONENT`, `TEST_TYPE`, `AMDGPU_FAMILIES`, `SHARD_INDEX`, `TOTAL_SHARDS`.
- **Test script:** For components that use this mechanism, the test script is set to `python .../test_runner.py` in `build_tools/github_actions/fetch_test_configurations.py` (e.g. MIOpen).
- **Sharding:** The workflow matrix uses `shard_arr` from the same config; the script passes sharding to ctest via `--tests-information` and to GTest via `GTEST_SHARD_INDEX` / `GTEST_TOTAL_SHARDS`.

______________________________________________________________________

## Adding a New Component to This Runner

1. **In the component (e.g. CMake/CTest):**

- Name GPU-specific tests `{target}_{category}_{gpu_arch}_suite`.
- Assign labels `ex_gpu_{gpu_arch}` and the category label (`quick` / `standard` or equivalent).

2. **In TheRock:**

- In `test_filters.py`, add the job name → directory mapping in `COMPONENT_DIR_MAPPING`.
- In `fetch_test_configurations.py`, set the component’s `test_script` to `python .../test_filters.py` and set `job_name` (and shards, timeout, etc.) as needed.

After that, the generic flow (discovery → match GPU → run ctest with the right labels) applies without further changes to `test_filters.py` for that component.
253 changes: 253 additions & 0 deletions build_tools/github_actions/test_executable_scripts/test_runner.py
Comment thread
dileepr1 marked this conversation as resolved.
Resolved
Comment thread
dileepr1 marked this conversation as resolved.
Resolved
Comment thread
dileepr1 marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""
This is a generic test runner that can test multiple components.
This works on components in rocm-libraries/rocm-systems which use test_categories.yml for test categorization.

Environment variables used:
TEST_COMPONENT: Job name of the component to test (e.g., "miopen", "rocrand", "hiprand")
This is automatically set by the GitHub Actions workflow from the job_name field.
The script maps these job names to actual test directory names (e.g., "miopen" -> "MIOpen")
Defaults to "miopen" if not set.
TEST_TYPE: "smoke" runs tests with "quick" category, otherwise runs "standard" category
AMDGPU_FAMILIES: Parsed to extract GPU architecture (e.g., "gfx1151")

The script discovers GPU-specific labels via ctest --print-labels and runs the appropriate tests for the current GPU architecture.
"""

import sys
import subprocess
import re
import os

import logging
import shlex
from pathlib import Path

sys.path.insert(0, os.fspath(Path(__file__).resolve().parent.parent))
from github_actions_utils import find_matching_gpu_arch

THEROCK_BIN_DIR = os.getenv("THEROCK_BIN_DIR")
SCRIPT_DIR = Path(__file__).resolve().parent
THEROCK_DIR = SCRIPT_DIR.parent.parent.parent
TEST_TYPE = os.getenv("TEST_TYPE", "smoke")
AMDGPU_FAMILIES = os.getenv("AMDGPU_FAMILIES")

# Map job names to actual test directory names
# The job names come from TEST_COMPONENT env var (set by GitHub Actions workflow)
# and need to be mapped to the actual directory names in THEROCK_BIN_DIR
COMPONENT_DIR_MAPPING = {
"miopen": "MIOpen",
"rocrand": "rocRAND",
"hiprand": "hipRAND",
"rocthrust": "rocthrust",
"rocprim": "rocprim",
"rocwmma": "rocwmma",
"hipcub": "hipcub",
"hipdnn": "hipdnn",
"hipdnn-samples": "hipdnn_samples",
"miopen_plugin": "miopen_legacy_plugin",
# Add more mappings as needed
}

# Get the test component from environment (required - no default)
test_component_job_name = os.getenv("TEST_COMPONENT")
if not test_component_job_name:
print(
"ERROR: TEST_COMPONENT environment variable is required but not set.",
file=sys.stderr,
)
sys.exit(1)

TEST_COMPONENT = COMPONENT_DIR_MAPPING.get(
test_component_job_name, test_component_job_name
)

# GTest sharding
SHARD_INDEX = os.getenv("SHARD_INDEX", 1)
TOTAL_SHARDS = os.getenv("TOTAL_SHARDS", 1)

# CTest parallel jobs (use fewer in less capable platforms)
ctest_parallel_count = 8
if AMDGPU_FAMILIES and "gfx1152" in AMDGPU_FAMILIES:
ctest_parallel_count = 4
elif AMDGPU_FAMILIES and "gfx1153" in AMDGPU_FAMILIES:
ctest_parallel_count = 4

# CTest per-test timeout (default 2 hours, in seconds)
# There should be a timeout set from component level, but this can be used as an override
ctest_timeout_seconds = 7200

environ_vars = os.environ.copy()
# Set the GTEST env vars for Gtest based tests
# Set ROCM_PATH for tests that rely on it
environ_vars["GTEST_SHARD_INDEX"] = str(int(SHARD_INDEX) - 1)
environ_vars["GTEST_TOTAL_SHARDS"] = str(TOTAL_SHARDS)
ROCM_PATH = Path(THEROCK_BIN_DIR).resolve().parent
environ_vars["ROCM_PATH"] = str(ROCM_PATH)

logging.basicConfig(level=logging.INFO)
##############################################


def get_available_gpu_suite_tests():
"""
Get all available GPU architecture labels from ctest --print-labels.

Parses labels of the form ex_gpu_{gpu_arch} (e.g. ex_gpu_gfx110X, ex_gpu_gfx950).
Returns a set of gpu_arch strings (e.g., 'gfx110X', 'gfx115X', 'gfx950').
"""
test_dir = Path(THEROCK_BIN_DIR) / TEST_COMPONENT
if not test_dir.exists() or not test_dir.is_dir():
print(f"Error: Test directory does not exist: {test_dir}", file=sys.stderr)
sys.exit(1)

try:
# Ensure the component has at least one test
list_result = subprocess.run(
["ctest", "-N", "--test-dir", str(test_dir)],
capture_output=True,
text=True,
check=True,
)
total_tests = sum(
1
for line in list_result.stdout.splitlines()
if re.search(r"Test\s+#\d+:", line)
)
if total_tests == 0:
print(
f"Error: No tests found in {test_dir}. Cannot run test suite.",
file=sys.stderr,
)
sys.exit(1)

result = subprocess.run(
["ctest", "--print-labels", "--test-dir", str(test_dir)],
capture_output=True,
text=True,
check=True,
)

gpu_archs = set()
prefix = "ex_gpu_"
for line in result.stdout.splitlines():
label = line.strip()
if label.startswith(prefix):
gpu_arch = label[len(prefix) :]
if gpu_arch.startswith("gfx"):
gpu_archs.add(gpu_arch)

return gpu_archs
except subprocess.CalledProcessError as e:
print(f"Error running ctest --print-labels: {e}", file=sys.stderr)
sys.exit(1)
except FileNotFoundError:
print(
"Error: ctest not found. Make sure CMake/CTest is installed.",
file=sys.stderr,
)
sys.exit(1)


def build_ctest_command(category, gpu_arch, available_gpu_archs):
"""
Build the appropriate ctest command based on the category and GPU architecture.

Returns a list of command arguments suitable for subprocess.run()
"""
cmd = ["ctest", "-L", category]

# Add common ctest parameters
cmd.extend(
[
"--output-on-failure",
"--parallel",
f"{ctest_parallel_count}",
"--timeout",
str(ctest_timeout_seconds),
"--test-dir",
f"{THEROCK_BIN_DIR}/{TEST_COMPONENT}",
"-V", # Always run in verbose mode
# Shards the tests by running a specific set of tests based on starting test (shard_index) and stride (total_shards)
"--tests-information",
f"{SHARD_INDEX},,{TOTAL_SHARDS}",
Comment thread
dileepr1 marked this conversation as resolved.
Resolved
]
)

if gpu_arch.lower() in ["generic", "none", ""]:
# For generic/unspecified GPU, exclude all GPU-specific suite tests
cmd.extend(["-LE", "ex_gpu"])
return cmd

# Find the appropriate GPU suite
matching_arch = find_matching_gpu_arch(gpu_arch, available_gpu_archs)

if matching_arch:
# Run the specific GPU suite using the ex_gpu label
gpu_label = f"ex_gpu_{matching_arch}"
cmd.extend(["-L", gpu_label])
print(f"# Using GPU suite label: {gpu_label}")
else:
# No specific GPU suite found, run standard tests excluding all GPU-specific ones
cmd.extend(["-LE", "ex_gpu"])
print(f"# No GPU suite found for {gpu_arch}, excluding all ex_gpu tests")

return cmd


def main():
# Use only two categories for now - quick and standard - depending on TEST_TYPE.
if TEST_TYPE and TEST_TYPE.lower() == "smoke":
category = "quick"
else:
category = "standard"

# Use AMDGPU_FAMILIES from environment variable, extract gfx<xxx> part
gpu_arch = ""
if AMDGPU_FAMILIES:
# Extract gfx<xxx> pattern from AMDGPU_FAMILIES string
# Pattern matches: gfx followed by alphanumeric characters (e.g., gfx1151, gfx950, gfx11X)
match = re.search(r"gfx[0-9a-zA-Z]+", AMDGPU_FAMILIES)
if match:
gpu_arch = match.group(0)
else:
print(
f"# Warning: Could not extract GPU architecture from AMDGPU_FAMILIES='{AMDGPU_FAMILIES}', using default '{gpu_arch}'"
)

print(
f"# TEST_COMPONENT: {test_component_job_name} -> Test Directory: {TEST_COMPONENT}"
)
print(f"# TEST_TYPE: {TEST_TYPE} -> Category: {category}")
print(f"# AMDGPU_FAMILIES: {AMDGPU_FAMILIES} -> GPU Architecture: {gpu_arch}")
print()

# Get available GPU suite tests from ctest
print("# Discovering available GPU suite tests...")
available_gpu_archs = get_available_gpu_suite_tests()

if available_gpu_archs:
print(f"# Found {len(available_gpu_archs)} GPU suite test(s)")
print(f"# Available GPU architectures: {sorted(available_gpu_archs)}")
else:
print("# Warning: No GPU specific test suites available")
print()

# Build the ctest command
cmd = build_ctest_command(category, gpu_arch, available_gpu_archs)

print(f"# Running: {' '.join(cmd)}")
print()

# Execute the command
try:
logging.info(f"++ Exec [{THEROCK_DIR}]$ {shlex.join(cmd)}")
result = subprocess.run(cmd, cwd=THEROCK_DIR, env=environ_vars, check=False)
return result.returncode
except Exception as e:
print(f"Error running ctest: {e}", file=sys.stderr)
return 1


if __name__ == "__main__":
sys.exit(main())
Loading