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
323 lines
12 KiB
Python
323 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# MIT License
|
|
#
|
|
# Copyright (c) 2024-2025 Advanced Micro Devices, Inc. All rights reserved.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
# THE SOFTWARE.
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import pytest
|
|
import glob
|
|
import subprocess
|
|
|
|
|
|
def get_gpu_node_count():
|
|
"""
|
|
Detect the number of GPU nodes/agents available in the system.
|
|
Returns the count of GPU nodes, or None if detection fails.
|
|
"""
|
|
try:
|
|
# Method 2: Check /sys/class/kfd/kfd/topology/nodes/ for GPU nodes
|
|
# This follows the logic from rocprofiler-sdk/source/lib/rocprofiler-sdk/agent.cpp
|
|
nodes_path = "/sys/class/kfd/kfd/topology/nodes"
|
|
if os.path.exists(nodes_path):
|
|
gpu_count = 0
|
|
node_id = 0
|
|
# Nodes are numbered monotonically starting from 0
|
|
while True:
|
|
node_path = os.path.join(nodes_path, str(node_id))
|
|
# Once we're missing a node folder, there are no more nodes
|
|
if not os.path.exists(node_path):
|
|
break
|
|
|
|
properties_file = os.path.join(node_path, "properties")
|
|
if os.path.exists(properties_file) and os.access(
|
|
properties_file, os.R_OK
|
|
):
|
|
try:
|
|
with open(properties_file, "r") as f:
|
|
content = f.read()
|
|
|
|
# Properties file must be non-empty
|
|
if not content.strip():
|
|
node_id += 1
|
|
continue
|
|
|
|
# Parse properties to find cpu_cores_count and simd_count
|
|
cpu_cores_count = 0
|
|
simd_count = 0
|
|
|
|
for line in content.split("\n"):
|
|
line = line.strip()
|
|
if line.startswith("cpu_cores_count"):
|
|
cpu_cores_count = int(line.split()[1])
|
|
elif line.startswith("simd_count"):
|
|
simd_count = int(line.split()[1])
|
|
|
|
# A node is a GPU if cpu_cores_count == 0 AND simd_count > 0
|
|
if cpu_cores_count == 0 and simd_count > 0:
|
|
gpu_count += 1
|
|
except (IOError, ValueError, IndexError):
|
|
pass
|
|
|
|
node_id += 1
|
|
|
|
if gpu_count > 0:
|
|
return gpu_count
|
|
except (OSError, ValueError):
|
|
pass
|
|
|
|
# If detection fails, return None
|
|
return None
|
|
|
|
|
|
def load_json_file(filepath):
|
|
"""
|
|
Load JSON from a file, handling cases where multiple MPI ranks may have written
|
|
to the same file (resulting in concatenated JSON objects).
|
|
|
|
Returns the first valid JSON object found, or None if no valid JSON exists.
|
|
"""
|
|
try:
|
|
with open(filepath, "r") as f:
|
|
content = f.read()
|
|
|
|
# Try to load as a single JSON object first
|
|
try:
|
|
return json.loads(content)
|
|
except json.JSONDecodeError as e:
|
|
# If that fails, it might be multiple JSON objects concatenated
|
|
# Try to extract the first valid JSON object
|
|
decoder = json.JSONDecoder()
|
|
try:
|
|
obj, idx = decoder.raw_decode(content)
|
|
# Successfully decoded the first JSON object
|
|
return obj
|
|
except json.JSONDecodeError:
|
|
# Even the first object is malformed
|
|
return None
|
|
except (IOError, OSError):
|
|
return None
|
|
|
|
|
|
def get_sdk_data(data):
|
|
"""
|
|
Extract rocprofiler-sdk-tool data from JSON, handling both dict and list structures.
|
|
Some JSON files have rocprofiler-sdk-tool as a list, others as a dict.
|
|
"""
|
|
if data is None or "rocprofiler-sdk-tool" not in data:
|
|
return None
|
|
|
|
sdk_data = data["rocprofiler-sdk-tool"]
|
|
|
|
# Handle list structure - take the first element
|
|
if isinstance(sdk_data, list):
|
|
return sdk_data[0] if len(sdk_data) > 0 else {}
|
|
|
|
# Already a dict
|
|
return sdk_data
|
|
|
|
|
|
def validate_json_file_has_profiling_data(json_file):
|
|
"""
|
|
Validate that a JSON file contains valid profiling data.
|
|
|
|
Loads the JSON file, extracts rocprofiler-sdk-tool data, and verifies that
|
|
it contains either kernel_dispatch or hip_api profiling data.
|
|
|
|
Raises AssertionError if validation fails.
|
|
"""
|
|
data = load_json_file(json_file)
|
|
assert data is not None, f"Failed to load JSON from {json_file}"
|
|
|
|
sdk_data = get_sdk_data(data)
|
|
assert sdk_data is not None, f"Missing rocprofiler-sdk-tool data in {json_file}"
|
|
buffer_records = sdk_data.get("buffer_records", {})
|
|
|
|
# Should have some kernel or HIP API data
|
|
has_data = (
|
|
len(buffer_records.get("kernel_dispatch", [])) > 0
|
|
or len(buffer_records.get("hip_api", [])) > 0
|
|
)
|
|
assert has_data, f"No profiling data found in {json_file}"
|
|
|
|
|
|
def test_mpi_ranks_feature(output_dir, test_mode):
|
|
"""
|
|
Test the --mpi-ranks feature with different scenarios using simple-transpose application.
|
|
|
|
The simple-transpose application runs a simple matrix transpose kernel using HIP.
|
|
It uses the default stream (stream_id == 0) and executes a single matrixTranspose kernel.
|
|
|
|
Test modes:
|
|
- with-mpi-single: MPI run with 4 ranks, profiling only rank 0
|
|
- with-mpi-multiple: MPI run with 4 ranks, profiling ranks 0-1,3
|
|
- without-mpi: Non-MPI run, should generate output regardless
|
|
"""
|
|
|
|
# Detect the number of GPU nodes in the system
|
|
gpu_node_count = get_gpu_node_count()
|
|
is_single_node = gpu_node_count is not None and gpu_node_count <= 1
|
|
|
|
# Find JSON output files
|
|
# For MPI tests: Look only in rank.* subdirectories (to avoid stale files)
|
|
# For non-MPI tests: Look everywhere in the output directory
|
|
if test_mode in ["with-mpi-single", "with-mpi-multiple"]:
|
|
# MPI test - only look in rank.* subdirectories
|
|
json_files = []
|
|
for rank_dir in glob.glob(os.path.join(output_dir, "rank.*")):
|
|
json_files.extend(
|
|
glob.glob(os.path.join(rank_dir, "**/out_results.json"), recursive=True)
|
|
)
|
|
else:
|
|
# Non-MPI test - look everywhere
|
|
json_files = glob.glob(
|
|
os.path.join(output_dir, "**/out_results.json"), recursive=True
|
|
)
|
|
|
|
if test_mode == "with-mpi-single":
|
|
# With --mpi-ranks 0 and 4 MPI ranks, only rank 0 should generate output in rank.0/
|
|
# So we should have exactly 1 JSON file
|
|
assert (
|
|
len(json_files) == 1
|
|
), f"Expected 1 JSON file for rank 0 only, but found {len(json_files)}: {json_files}"
|
|
|
|
# Verify the file is from rank 0
|
|
validate_json_file_has_profiling_data(json_files[0])
|
|
|
|
elif test_mode == "with-mpi-multiple":
|
|
# With --mpi-ranks 0-1,3 and 4 MPI ranks, ranks 0, 1, and 3 should generate output
|
|
# Each rank uses a separate output directory (via the MPI rank env var)
|
|
# so we should always get exactly 3 files
|
|
expected_files = 3
|
|
|
|
if gpu_node_count is not None:
|
|
print(f"INFO: Detected {gpu_node_count} GPU node(s)")
|
|
|
|
# With separate output directories per rank, we should have exactly the expected number
|
|
assert len(json_files) == expected_files, (
|
|
f"Expected {expected_files} JSON files for ranks 0, 1, and 3, "
|
|
f"but found {len(json_files)}: {json_files}"
|
|
)
|
|
|
|
# Verify each file has valid profiling data
|
|
for json_file in json_files:
|
|
validate_json_file_has_profiling_data(json_file)
|
|
|
|
elif test_mode == "without-mpi":
|
|
# Without MPI environment, --mpi-ranks should be ignored and output generated
|
|
# We should have at least 1 JSON file
|
|
assert (
|
|
len(json_files) >= 1
|
|
), f"Expected at least 1 JSON file for non-MPI run, but found {len(json_files)}: {json_files}"
|
|
|
|
# Verify the file has valid profiling data
|
|
validate_json_file_has_profiling_data(json_files[0])
|
|
|
|
else:
|
|
pytest.fail(f"Unknown test mode: {test_mode}")
|
|
|
|
|
|
def test_csv_output_consistency(output_dir, test_mode):
|
|
"""
|
|
Verify that CSV files are also correctly generated/not generated based on rank filtering.
|
|
"""
|
|
|
|
# Find CSV files
|
|
# For MPI tests: Look only in rank.* subdirectories
|
|
# For non-MPI tests: Look everywhere in the output directory
|
|
if test_mode in ["with-mpi-single", "with-mpi-multiple"]:
|
|
# MPI test - only look in rank.* subdirectories
|
|
csv_files = []
|
|
for rank_dir in glob.glob(os.path.join(output_dir, "rank.*")):
|
|
csv_files.extend(
|
|
glob.glob(
|
|
os.path.join(rank_dir, "**/out_kernel_trace.csv"), recursive=True
|
|
)
|
|
)
|
|
else:
|
|
# Non-MPI test - look everywhere
|
|
csv_files = glob.glob(
|
|
os.path.join(output_dir, "**/out_kernel_trace.csv"), recursive=True
|
|
)
|
|
|
|
if test_mode == "with-mpi-single":
|
|
# Only rank 0 should have CSV output
|
|
assert (
|
|
len(csv_files) == 1
|
|
), f"Expected 1 CSV file for rank 0 only, but found {len(csv_files)}: {csv_files}"
|
|
|
|
elif test_mode == "with-mpi-multiple":
|
|
# Each rank has separate output directory, so expect exactly 3 CSV files
|
|
expected_files = 3
|
|
assert len(csv_files) == expected_files, (
|
|
f"Expected {expected_files} CSV files for ranks 0, 1, and 3, "
|
|
f"but found {len(csv_files)}: {csv_files}"
|
|
)
|
|
|
|
elif test_mode == "without-mpi":
|
|
# Non-MPI run should have CSV output
|
|
assert (
|
|
len(csv_files) >= 1
|
|
), f"Expected at least 1 CSV file for non-MPI run, but found {len(csv_files)}: {csv_files}"
|
|
|
|
|
|
def test_no_output_for_filtered_ranks(output_dir, test_mode):
|
|
"""
|
|
Verify that ranks not in the --mpi-ranks list do not generate output.
|
|
Since each rank has a separate output directory (via the MPI rank env var),
|
|
we can check that rank 2's directory doesn't exist or is empty.
|
|
"""
|
|
|
|
if test_mode != "with-mpi-multiple":
|
|
pytest.skip("This test only applies to with-mpi-multiple mode")
|
|
|
|
# Check for rank.2 directory (which should NOT have been created or should be empty)
|
|
rank_2_dir = os.path.join(output_dir, "rank.2")
|
|
|
|
if os.path.exists(rank_2_dir):
|
|
# Directory exists - check if it has any JSON files
|
|
json_files_in_rank_2 = glob.glob(
|
|
os.path.join(rank_2_dir, "**/out_results.json"), recursive=True
|
|
)
|
|
assert (
|
|
len(json_files_in_rank_2) == 0
|
|
), f"Rank 2 should not generate output, but found {len(json_files_in_rank_2)} files: {json_files_in_rank_2}"
|
|
|
|
# Verify that ranks 0, 1, and 3 directories exist with output
|
|
for rank in [0, 1, 3]:
|
|
rank_dir = os.path.join(output_dir, f"rank.{rank}")
|
|
assert os.path.exists(
|
|
rank_dir
|
|
), f"Expected directory for rank {rank} at {rank_dir}"
|
|
json_files = glob.glob(
|
|
os.path.join(rank_dir, "**/out_results.json"), recursive=True
|
|
)
|
|
assert (
|
|
len(json_files) >= 1
|
|
), f"Expected output files for rank {rank} in {rank_dir}"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
exit_code = pytest.main(["-x", __file__] + sys.argv[1:])
|
|
sys.exit(exit_code)
|