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
452 lines
18 KiB
Python
452 lines
18 KiB
Python
# Copyright Advanced Micro Devices, Inc.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""Shared utilities for PyTorch testing.
|
|
|
|
Several helpers here manage GPU visibility/isolation through HIP_VISIBLE_DEVICES,
|
|
ROCR_VISIBLE_DEVICES, and GPU_DEVICE_ORDINAL. See
|
|
https://rocm.docs.amd.com/en/latest/conceptual/gpu-isolation.html for the
|
|
multiple ways to achieve isolation of GPUs in the ROCm software stack.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from importlib.metadata import version as get_package_version
|
|
from packaging.version import Version
|
|
|
|
|
|
def reconcile_agent_visibility_env() -> None:
|
|
"""Drop GPU_DEVICE_ORDINAL to avoid a fatal HIP agent-visibility conflict.
|
|
|
|
The runner scripts always set HIP_VISIBLE_DEVICES, which supersedes and
|
|
conflicts with a GPU_DEVICE_ORDINAL injected by the CI runner's isolation
|
|
env-file (HIP aborts fatally on the mismatch).
|
|
"""
|
|
gpu_device_ordinal = os.environ.pop("GPU_DEVICE_ORDINAL", None)
|
|
if gpu_device_ordinal is not None:
|
|
print(
|
|
f"[WARNING] Unset GPU_DEVICE_ORDINAL={gpu_device_ordinal} "
|
|
"(HIP_VISIBLE_DEVICES takes precedence)"
|
|
)
|
|
|
|
|
|
def get_supported_and_visible_gpus() -> tuple[list[str], list[str]]:
|
|
"""Get both supported and visible GPUs in a single subprocess call.
|
|
|
|
Note that the current torch build does not necessarily have
|
|
support for all of the GPUs that are visible.
|
|
|
|
This function runs in a subprocess to avoid initializing CUDA
|
|
in the main process before HIP_VISIBLE_DEVICES is set.
|
|
|
|
Important: If HIP_VISIBLE_DEVICES is already set before calling this script,
|
|
this function will only see GPUs within that constraint. This allows the
|
|
script to work within pre-configured limitations (e.g., in containers).
|
|
|
|
Returns:
|
|
Tuple of (supported_gpus, visible_gpus):
|
|
- supported_gpus: List of AMDGPU archs supported by PyTorch build
|
|
- visible_gpus: List of AMDGPU archs physically visible
|
|
Exits on failure.
|
|
"""
|
|
query_script = """
|
|
import sys
|
|
try:
|
|
import torch
|
|
|
|
if not torch.cuda.is_available():
|
|
print("ERROR:ROCm is not available", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Get supported AMDGPUs (from PyTorch build)
|
|
supported_gpus = torch.cuda.get_arch_list()
|
|
if len(supported_gpus) == 0:
|
|
print("ERROR:No AMD GPUs in PyTorch build", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Get visible GPUs (from hardware)
|
|
visible_gpus = []
|
|
gpu_count = torch.cuda.device_count()
|
|
print(f"GPU count visible for PyTorch: {gpu_count}", file=sys.stderr)
|
|
|
|
for device_idx in range(gpu_count):
|
|
device_id = f"cuda:{device_idx}"
|
|
device = torch.cuda.device(device_id)
|
|
if device:
|
|
device_properties = torch.cuda.get_device_properties(device)
|
|
if device_properties and hasattr(device_properties, 'gcnArchName'):
|
|
# AMD GPUs have gcnArchName
|
|
visible_gpus.append(device_properties.gcnArchName)
|
|
|
|
if len(visible_gpus) == 0:
|
|
print("ERROR:No AMD GPUs with gcnArchName detected", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Output format: SUPPORTED|gpu1,gpu2,gpu3
|
|
# VISIBLE|gpu1,gpu2,gpu3
|
|
print(f"SUPPORTED|{','.join(supported_gpus)}")
|
|
print(f"VISIBLE|{','.join(visible_gpus)}")
|
|
|
|
except Exception as e:
|
|
print(f"ERROR:{e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
"""
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
[sys.executable, "-c", query_script],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
|
|
# Parse the output
|
|
lines = result.stdout.strip().split("\n")
|
|
supported_gpus = []
|
|
visible_gpus = []
|
|
|
|
for line in lines:
|
|
if line.startswith("SUPPORTED|"):
|
|
supported_gpus = line.split("|")[1].split(",")
|
|
elif line.startswith("VISIBLE|"):
|
|
visible_gpus = line.split("|")[1].split(",")
|
|
|
|
if not supported_gpus or not visible_gpus:
|
|
print(f"\n[ERROR] Failed to parse GPU info from subprocess")
|
|
sys.exit(1)
|
|
|
|
return supported_gpus, visible_gpus
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"\n[ERROR] Failed to retrieve GPU info: {e.stderr}")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"\n[ERROR] Unexpected error retrieving GPU info: {e}")
|
|
sys.exit(1)
|
|
|
|
|
|
def get_all_supported_devices(amdgpu_family: str = "") -> dict[str, list[int]]:
|
|
"""Detect supported AMDGPU devices and return mapping of arch to device indices.
|
|
|
|
This function queries available GPUs and returns a mapping of architecture
|
|
names to their system device indices. It does NOT set HIP_VISIBLE_DEVICES;
|
|
callers should set it before running pytest.
|
|
|
|
Args:
|
|
amdgpu_family: AMDGPU family string. Can be:
|
|
- Empty string (default): Auto-detect all visible GPUs supported by PyTorch
|
|
- Specific arch (e.g., "gfx1151"): Find and use matching GPU
|
|
- Wildcard family (e.g., "gfx94X"): Find all matching GPUs
|
|
|
|
Returns:
|
|
Dictionary mapping architecture names to lists of system device indices.
|
|
E.g., {"gfx942": [0, 1], "gfx1100": [2]}
|
|
Exits on failure.
|
|
|
|
Side effects:
|
|
- Reads HIP_VISIBLE_DEVICES if already set (respects pre-configured constraints)
|
|
- If used to set HIP_VISIBLE_DEVICES, this must be done before importing torch in the main process via pytest
|
|
"""
|
|
|
|
# Get the current HIP_VISIBLE_DEVICES to properly map indices
|
|
# If already set (e.g., "2,3,4"), visible GPU indices are remapped (0,1,2)
|
|
# We need to track the original system indices for correct remapping
|
|
current_hip_visible = os.environ.get("HIP_VISIBLE_DEVICES", "")
|
|
if current_hip_visible:
|
|
# Parse existing HIP_VISIBLE_DEVICES to get original system GPU indices
|
|
original_system_indices = [
|
|
int(idx.strip()) for idx in current_hip_visible.split(",")
|
|
]
|
|
print(f"HIP_VISIBLE_DEVICES already set to: {current_hip_visible}")
|
|
else:
|
|
# HIP_VISIBLE_DEVICES not set, no remapping needed
|
|
original_system_indices = None
|
|
|
|
# Query both supported and visible GPUs in a single subprocess call
|
|
# (doesn't initialize CUDA in main process)
|
|
print("Getting GPU information from PyTorch...", end=" ")
|
|
supported_gpus, raw_visible_gpus = get_supported_and_visible_gpus()
|
|
print("done")
|
|
|
|
# Normalize gpu names
|
|
# get_supported_and_visible_gpus() (via device_properties.gcnArchName):
|
|
# Often returns detailed arch names like "gfx942:sramecc+:xnack-" or "gfx1100:xnack-"
|
|
visible_gpus = [gpu.split(":")[0] for gpu in raw_visible_gpus]
|
|
|
|
print(f"Supported AMD GPUs: {supported_gpus}")
|
|
print(f"Visible AMD GPUs: {visible_gpus}")
|
|
|
|
selected_gpu_indices = []
|
|
selected_gpu_archs = []
|
|
|
|
if not amdgpu_family:
|
|
# Mode 1: Auto-detect - use all supported GPUs
|
|
for idx, gpu in enumerate(visible_gpus):
|
|
if gpu in supported_gpus:
|
|
selected_gpu_indices.append(idx)
|
|
selected_gpu_archs.append(gpu)
|
|
if len(selected_gpu_archs) == 0:
|
|
print("[ERROR] No GPU found in visible GPUs that is supported by PyTorch")
|
|
sys.exit(1)
|
|
elif amdgpu_family.split("-")[0].upper().endswith("X"):
|
|
# Mode 2: Wildcard match (e.g., "gfx94X" matches "gfx942", "gfx940", etc.)
|
|
family_part = amdgpu_family.split("-")[0]
|
|
partial_match = family_part[:-1] # Remove the 'X'
|
|
|
|
for idx, gpu in enumerate(visible_gpus):
|
|
if partial_match in gpu and gpu in supported_gpus:
|
|
selected_gpu_indices.append(idx)
|
|
selected_gpu_archs.append(gpu)
|
|
|
|
if len(selected_gpu_archs) == 0:
|
|
print(f"[ERROR] No GPU found matching wildcard pattern '{amdgpu_family}'.")
|
|
sys.exit(1)
|
|
|
|
print(
|
|
f"AMDGPU Arch detected via wildcard match '{partial_match}': "
|
|
f"{selected_gpu_archs} (logical indices {selected_gpu_indices})"
|
|
)
|
|
else:
|
|
# Mode 3: Specific GPU arch - validate it is visible and supported by the current PyTorch build.
|
|
|
|
# We have gfx1151 -> we want to match exactly gfx1151
|
|
# We have gfx950-dcgpu -> we need to match exactly gfx950
|
|
# So remove the suffix after '-'
|
|
pruned_amdgpu_family = amdgpu_family.split("-")[0]
|
|
for idx, gpu in enumerate(visible_gpus):
|
|
if gpu in supported_gpus:
|
|
if gpu == pruned_amdgpu_family or pruned_amdgpu_family in gpu:
|
|
selected_gpu_indices.append(idx)
|
|
selected_gpu_archs.append(gpu)
|
|
|
|
if len(selected_gpu_archs) == 0:
|
|
print(
|
|
f"[ERROR] Requested GPU '{amdgpu_family}' not found in visible GPUs that are supported by PyTorch"
|
|
)
|
|
sys.exit(1)
|
|
|
|
# Map logical indices back to system indices if HIP_VISIBLE_DEVICES was already set
|
|
if original_system_indices is not None:
|
|
# Map: logical index -> original system index
|
|
# e.g., if HIP_VISIBLE_DEVICES="2,3,4" and we selected logical index 0,
|
|
# the system index is 2 (the original system index)
|
|
system_gpu_indices = [
|
|
original_system_indices[idx] for idx in selected_gpu_indices
|
|
]
|
|
else:
|
|
# HIP_VISIBLE_DEVICES not set, no remapping needed
|
|
system_gpu_indices = selected_gpu_indices
|
|
|
|
# Build the result dictionary: arch -> list of system device indices
|
|
result = {}
|
|
for arch, sys_idx in zip(selected_gpu_archs, system_gpu_indices):
|
|
if arch not in result:
|
|
result[arch] = []
|
|
result[arch].append(sys_idx)
|
|
|
|
print(f"Detected PyTorch supported architecture at device indices: {result}")
|
|
return result
|
|
|
|
|
|
def get_unique_supported_devices(amdgpu_family: str = "") -> dict[str, list[int]]:
|
|
"""
|
|
Returns a dictionary mapping each supported architecture to a single device index (the first one for each).
|
|
This is a convenience wrapper over get_all_supported_devices for situations where
|
|
only one device per arch is desired.
|
|
|
|
Args:
|
|
amdgpu_family: Optionally filter by a specific AMDGPU family string or pattern.
|
|
|
|
Returns:
|
|
Dictionary: {arch: [device_index]} for each supported arch (single-element lists).
|
|
"""
|
|
devices_by_arch = get_all_supported_devices(amdgpu_family)
|
|
unique_devices = {
|
|
arch: [indices[0]] for arch, indices in devices_by_arch.items() if indices
|
|
}
|
|
return unique_devices
|
|
|
|
|
|
def set_gpu_execution_policy(
|
|
supported_devices: dict[str, list[int]],
|
|
policy: str,
|
|
offset: int = 0,
|
|
) -> list[tuple[str, int]]:
|
|
"""
|
|
Configures the HIP_VISIBLE_DEVICES environment variable according to a GPU selection policy,
|
|
enabling targeted execution on specific AMD GPUs for PyTorch/pytest runs. This must be run
|
|
*before* torch is imported, because HIP_VISIBLE_DEVICES cannot affect CUDA device visibility after initialization.
|
|
|
|
Args:
|
|
supported_devices (dict[str, list[int]]): Dictionary mapping GPU architectures to lists of device indices.
|
|
Can be obtained from get_all_supported_devices() or get_unique_supported_devices().
|
|
- get_all_supported_devices(): {"gfx942": [0, 1], "gfx1100": [2]}
|
|
- get_unique_supported_devices(): {"gfx942": [0], "gfx1100": [2]}
|
|
policy (str): Device selection policy. Must be one of:
|
|
- "single": Use a single device from the provided devices at the given offset.
|
|
- "all": Use all provided devices. The offset parameter is ignored.
|
|
offset (int): Index offset for selecting device with "single" policy. Ignored for "all" policy.
|
|
Offset is applied to the flattened list of (arch, idx) pairs made from supported_devices dictionary.
|
|
Depending on the function used to get supported_devices:
|
|
- get_all_supported_devices(): Select the device at the given offset.
|
|
Example: {"gfx942": [0, 1], "gfx1100": [2]} => [("gfx942", 0), ("gfx942", 1), ("gfx1100", 2)]
|
|
- get_unique_supported_devices(): Since every architecture has a single device,
|
|
offset effectively allows us to iterate over specific architectures.
|
|
Example: {"gfx942": [0], "gfx1100": [2]} => [("gfx942", 0), ("gfx1100", 2)]
|
|
|
|
Returns:
|
|
list[tuple[str, int]]: A list of (arch, device_index) tuples that were selected and made visible.
|
|
- For policy "single", the list contains a single (arch, idx).
|
|
- For "all", the list contains every (arch, idx) made visible.
|
|
|
|
Raises:
|
|
ValueError: If an invalid policy is supplied or if supported_devices is empty.
|
|
IndexError: If the requested offset exceeds the set of possible devices (only for "single" policy).
|
|
"""
|
|
valid_policies = ("single", "all")
|
|
if policy not in valid_policies:
|
|
raise ValueError(f"Invalid policy '{policy}'. Must be one of {valid_policies}.")
|
|
|
|
if not supported_devices:
|
|
raise ValueError("supported_devices cannot be empty; no devices available.")
|
|
|
|
if policy == "single":
|
|
# Flatten the supported_devices dictionary into pairs of (arch, idx) for each device.
|
|
flat_devices = [
|
|
(arch, idx)
|
|
for arch, indices in supported_devices.items()
|
|
for idx in indices
|
|
]
|
|
if offset < 0 or offset >= len(flat_devices):
|
|
raise IndexError(
|
|
f"Offset {offset} out of range for {len(flat_devices)} total devices"
|
|
)
|
|
arch, device_idx = flat_devices[offset]
|
|
os.environ["HIP_VISIBLE_DEVICES"] = str(device_idx)
|
|
print(f"Policy '{policy}': Using device {device_idx} ({arch})")
|
|
return [(arch, device_idx)]
|
|
|
|
else:
|
|
# "all" policy: Use all supported devices in the provided dictionary
|
|
# Can have multiple devices per specific architecture
|
|
flat_devices = [
|
|
(arch, idx)
|
|
for arch, indices in supported_devices.items()
|
|
for idx in indices
|
|
]
|
|
device_indices_str = ",".join(str(idx) for _, idx in flat_devices)
|
|
os.environ["HIP_VISIBLE_DEVICES"] = device_indices_str
|
|
device_pairs_str = ", ".join(f"{arch}: {idx}" for arch, idx in flat_devices)
|
|
print(f"Policy 'all': Using devices [{device_pairs_str}]")
|
|
return flat_devices
|
|
|
|
|
|
def configure_gpu_visibility(
|
|
amdgpu_family: str,
|
|
device_query: str,
|
|
gpu_policy: str,
|
|
) -> list[str]:
|
|
"""Query candidate GPUs, apply the selection policy, and set HIP_VISIBLE_DEVICES.
|
|
|
|
Combines the two GPU-selection stages shared by the PyTorch test runners:
|
|
stage 1 (``device_query``) builds the candidate set, stage 2 (``gpu_policy``)
|
|
decides how many candidates are made visible.
|
|
|
|
Must run BEFORE torch is imported: once torch.cuda is initialized, changing
|
|
HIP_VISIBLE_DEVICES has no effect.
|
|
|
|
Args:
|
|
amdgpu_family: AMDGPU family filter (empty string auto-detects).
|
|
device_query: Stage-1 candidate selection, "unique" or "all".
|
|
gpu_policy: Stage-2 visibility policy, "single" or "all".
|
|
|
|
Returns:
|
|
Sorted list of the architectures that were made visible.
|
|
"""
|
|
if device_query == "unique":
|
|
supported_devices = get_unique_supported_devices(amdgpu_family)
|
|
else:
|
|
supported_devices = get_all_supported_devices(amdgpu_family)
|
|
|
|
selected_devices = set_gpu_execution_policy(supported_devices, policy=gpu_policy)
|
|
|
|
selected_archs = sorted({arch for arch, _ in selected_devices})
|
|
device_ids = [str(dev_id) for _, dev_id in selected_devices]
|
|
print(
|
|
f"Selected {len(selected_devices)} GPU(s): "
|
|
f"query={device_query}, policy={gpu_policy}, "
|
|
f"arch(es)={', '.join(selected_archs)}, "
|
|
f"device(s)={', '.join(device_ids)}"
|
|
)
|
|
return selected_archs
|
|
|
|
|
|
def detect_pytorch_version() -> str:
|
|
"""Auto-detect the PyTorch version from the installed package.
|
|
|
|
Returns:
|
|
The detected PyTorch version as major.minor (e.g., "2.7").
|
|
"""
|
|
v = Version(get_package_version("torch"))
|
|
return f"{v.major}.{v.minor}"
|
|
|
|
|
|
def check_pytorch_source_version(pytorch_dir: Path, allow_mismatch: bool) -> None:
|
|
"""Verify that the PyTorch test source version matches the installed wheel.
|
|
|
|
Compares the major.minor version from <pytorch_dir>/version.txt against
|
|
the installed torch package. A mismatch causes confusing test failures
|
|
(missing attributes, changed APIs, collection errors) that look like real
|
|
bugs but are just version skew.
|
|
|
|
Args:
|
|
pytorch_dir: Path to the PyTorch source directory.
|
|
|
|
Raises:
|
|
SystemExit: If there is a major.minor version mismatch.
|
|
"""
|
|
version_file = pytorch_dir / "version.txt"
|
|
if not version_file.exists():
|
|
print(
|
|
f"[WARNING] {version_file} not found — cannot verify test source "
|
|
f"version matches installed wheel. Proceeding anyway."
|
|
)
|
|
return
|
|
|
|
source_version = Version(version_file.read_text().strip())
|
|
installed_version = Version(get_package_version("torch"))
|
|
|
|
# Compare major.minor only (ignore patch, pre-release, local segments).
|
|
if source_version.release[:2] != installed_version.release[:2]:
|
|
print(
|
|
f"[ERROR] PyTorch version mismatch!\n"
|
|
f" Test sources: {source_version.major}.{source_version.minor} "
|
|
f"(from {version_file}: {source_version})\n"
|
|
f" Installed wheel: "
|
|
f"{installed_version.major}.{installed_version.minor} "
|
|
f"({installed_version})\n"
|
|
f"\n"
|
|
f"Running tests from a different PyTorch version than the installed\n"
|
|
f"wheel causes misleading failures (missing APIs, changed error\n"
|
|
f"messages, collection errors). Check out matching test sources or\n"
|
|
f"install a matching wheel."
|
|
)
|
|
if allow_mismatch:
|
|
print(
|
|
"[WARNING] allow_mismatch (--allow-version-mismatch) was set, so continuing anyway\n"
|
|
)
|
|
return
|
|
else:
|
|
print(
|
|
"[ERROR] Set allow_mismatch (--allow-version-mismatch) to bypass this check. Exiting"
|
|
)
|
|
sys.exit(1)
|
|
|
|
print(
|
|
f"PyTorch version check OK: source and wheel both "
|
|
f"{installed_version.major}.{installed_version.minor}"
|
|
)
|