Files
TheRock/rocm-systems/projects/rocprofiler-systems/tests/validate-unified-memory.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

466 lines
15 KiB
Python

#!/usr/bin/env python3
# Copyright (c) Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
from __future__ import annotations
import argparse
import json
import mmap
import os
import re
import sys
from pathlib import Path
from typing import Any, Optional
# BSD sysexits.h codes — portable across platforms (os.EX_* is POSIX-only)
EXIT_OK = 0
EXIT_USAGE = 64
EXIT_DATAERR = 65
EXIT_NOINPUT = 66
PERFETTO_FAULT_TRACK_NAME = b"Unified Memory Page Faults"
PERFETTO_MIGRATION_THROUGHPUT_TRACK_NAME = b"Unified Memory Migration Throughput"
REQUIRED_MIGRATION_DIRECTIONS = [
"host_to_device",
"device_to_host",
"device_to_device",
]
REQUIRED_MIGRATION_STATS = [
"count",
"total_size_bytes",
"min_size_bytes",
"max_size_bytes",
"avg_size_bytes",
"total_time_ns",
"migration_throughput_gbps",
]
def print_help() -> None:
"""Print the help message"""
print(f"""
Unified Memory Output Validation Tool
DESCRIPTION:
This tool validates unified memory profiling output files (text and JSON formats).
It checks for required fields, proper structure, and expected content.
USAGE:
{os.path.basename(__file__)} --txt-file <path> --json-file <path>
{os.path.basename(__file__)} --output-dir <dir>
ARGUMENT MODES (mutually exclusive):
--txt-file PATH --json-file PATH Explicit file paths
--output-dir DIR Single-level lookup of unified_memory*.{{txt,json}}
OPTIONAL ARGUMENTS:
-h, --help Show this help message and exit
EXAMPLES:
{os.path.basename(__file__)} --txt-file unified_memory.txt --json-file unified_memory.json
{os.path.basename(__file__)} --output-dir rocprof-sys-tests-output/unified-memory
VALIDATION CHECKS:
- Text output format and required headers
- JSON structure and required fields
- Device information completeness
- Migration statistics presence
- Fault-only Perfetto trace does not emit migration-throughput tracks
EXIT CODES:
0 - All validations passed successfully
64 - Usage error (EX_USAGE)
65 - Validation failures detected (EX_DATAERR)
66 - Required input file not found (EX_NOINPUT)
""")
def validate_text_output(filepath: Path) -> bool:
"""
Validates the text output file for unified memory profiling.
Checks for required headers and migration direction data.
Args:
filepath: Path object pointing to the unified_memory.txt file
Returns:
bool: True if validation passes, False otherwise
"""
print(f"Validating text output: {filepath}")
if not filepath.exists():
print(f"Error: File not found: {filepath}")
return False
content = filepath.read_text()
base_headers = ["Unified Memory profiling result", "Total Page Faults"]
missing = [h for h in base_headers if h not in content]
if missing:
print(f"Error: Missing required headers in text output: {missing}")
return False
has_device_block = 'Device "' in content
if has_device_block:
migration_headers = [
"Count",
"Avg Size",
"Total Size",
"Migration Throughput",
]
missing = [h for h in migration_headers if h not in content]
if missing:
print(f"Error: Missing migration headers in text output: {missing}")
return False
migration_directions = ["Host To Device", "Device To Host", "Device To Device"]
if not any(direction in content for direction in migration_directions):
print("Error: Device block present but no migration statistics found")
return False
else:
match = re.search(r"Total Page Faults:\s*(\d+)", content)
if not match or int(match.group(1)) == 0:
print(
"Error: No migrations and no page faults captured; report should be empty"
)
return False
print("Text output validation passed")
return True
def load_json_output(filepath: Path) -> Optional[dict[str, Any]]:
"""Load a unified-memory JSON output file."""
try:
with open(filepath, encoding="utf-8") as f:
data = json.load(f)
except (OSError, UnicodeDecodeError) as e:
print(f"Error: Could not read JSON file: {e}")
return None
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON format: {e}")
return None
if not isinstance(data, dict):
print("Error: JSON root must be an object")
return None
return data
def validate_json_data(data: dict[str, Any]) -> bool:
"""
Validates parsed JSON output for unified memory profiling.
Checks for proper structure, required fields, and device information.
Returns:
bool: True if validation passes, False otherwise
"""
if "devices" not in data:
print("Error: Missing 'devices' array in JSON output")
return False
if "summary" not in data:
print("Error: Missing 'summary' object in JSON output")
return False
summary = data["summary"]
if not isinstance(summary, dict):
print("Error: 'summary' must be an object")
return False
required_summary_fields = [
"xnack_enabled",
"total_page_faults",
"migration_triggers",
]
missing_fields = [field for field in required_summary_fields if field not in summary]
if missing_fields:
print(f"Error: Missing summary fields: {missing_fields}")
return False
devices = data["devices"]
if not isinstance(devices, list):
print("Error: 'devices' must be an array")
return False
if len(devices) == 0:
print("Warning: No devices in output (may be expected if no migrations occurred)")
for i, device in enumerate(devices):
if not isinstance(device, dict):
print(f"Error: Device {i} must be an object")
return False
required_device_fields = ["device_id", "device_name", "migrations"]
missing = [field for field in required_device_fields if field not in device]
if missing:
print(f"Error: Device {i} missing fields: {missing}")
return False
migrations = device["migrations"]
if not isinstance(migrations, dict):
print(f"Error: Device {i} 'migrations' must be an object")
return False
for direction in REQUIRED_MIGRATION_DIRECTIONS:
if direction not in migrations:
print(f"Error: Device {i} missing migration direction: {direction}")
return False
stats = migrations[direction]
if not isinstance(stats, dict):
print(f"Error: Device {i}, {direction} stats must be an object")
return False
missing_stats = [
field for field in REQUIRED_MIGRATION_STATS if field not in stats
]
if missing_stats:
print(f"Error: Device {i}, {direction} missing stats: {missing_stats}")
return False
triggers = summary["migration_triggers"]
if not isinstance(triggers, dict):
print("Error: 'migration_triggers' must be an object")
return False
required_trigger_fields = [
"gpu_page_fault",
"cpu_page_fault",
"prefetch",
"ttm_eviction",
"unknown",
]
missing_trigger_fields = [
field for field in required_trigger_fields if field not in triggers
]
if missing_trigger_fields:
print(f"Error: Missing migration_triggers fields: {missing_trigger_fields}")
return False
print("JSON output validation passed")
print(f" Devices: {len(devices)}")
print(f" XNACK enabled: {summary['xnack_enabled']}")
print(f" Total page faults: {summary['total_page_faults']}")
print(f" Migration triggers: {triggers}")
return True
def resolve_perfetto_trace(output_dir: Path) -> Optional[Path]:
"""Resolve a Perfetto trace in output_dir when one was generated."""
matches = sorted(output_dir.glob("perfetto-trace*.proto"))
if not matches:
matches = sorted(output_dir.glob("perfetto*.proto"))
if len(matches) > 1:
print(f"Warning: multiple Perfetto traces in {output_dir}; using {matches[0]}")
return matches[0] if matches else None
def is_integer(value: object) -> bool:
"""Return true for JSON integer values, excluding booleans."""
return isinstance(value, int) and not isinstance(value, bool)
def has_observed_migrations(devices: list[Any]) -> bool:
"""Return true if any device migration bucket has a positive count."""
for device in devices:
# The CLI calls this only after validate_json_data() succeeds. Treat
# malformed entries as zero here so this helper stays predicate-only.
if not isinstance(device, dict):
continue
migrations = device.get("migrations")
if not isinstance(migrations, dict):
continue
for direction in REQUIRED_MIGRATION_DIRECTIONS:
stats = migrations.get(direction)
if not isinstance(stats, dict):
continue
count = stats.get("count", 0)
if is_integer(count) and count > 0:
return True
return False
def is_fault_only_output(data: dict[str, Any]) -> bool:
"""Return true when reports contain page faults but no observed migrations."""
devices = data.get("devices")
summary = data.get("summary")
if not isinstance(devices, list) or not isinstance(summary, dict):
return False
total_page_faults = summary.get("total_page_faults", 0)
return (
is_integer(total_page_faults)
and total_page_faults > 0
and not has_observed_migrations(devices)
)
def validate_perfetto_fault_only_trace(output_dir: Path, data: dict[str, Any]) -> bool:
"""Validate Perfetto track names for fault-only unified-memory output."""
if not is_fault_only_output(data):
print("Perfetto fault-only validation skipped (migration data present)")
return True
perfetto_trace = resolve_perfetto_trace(output_dir)
if perfetto_trace is None:
print("Warning: no Perfetto trace found; skipping Perfetto track validation")
return True
if perfetto_trace.stat().st_size == 0:
print("Error: Perfetto trace is empty")
return False
print(f"Validating Perfetto fault-only tracks: {perfetto_trace}")
# Lightweight track-name smoke check. This avoids requiring trace_processor
# in every validation environment while still catching accidental track
# creation in the common case. mmap keeps large traces off the Python heap;
# use find() because mmap's `in` operator matches single bytes, not byte
# sequences.
with open(perfetto_trace, "rb") as f, mmap.mmap(
f.fileno(), 0, access=mmap.ACCESS_READ
) as content:
if content.find(PERFETTO_FAULT_TRACK_NAME) == -1:
print(
"Error: fault-only Perfetto trace is missing unified-memory fault track"
)
return False
if content.find(PERFETTO_MIGRATION_THROUGHPUT_TRACK_NAME) != -1:
print(
"Error: fault-only Perfetto trace contains unified-memory migration "
"throughput track"
)
return False
print("Perfetto fault-only track validation passed")
return True
def resolve_from_dir(output_dir: Path) -> tuple[Optional[Path], Optional[Path]]:
"""
Resolve unified_memory*.txt and unified_memory*.json in output_dir.
Single-level lookup only (no recursion). Used for tests where the PID-suffixed
filename is unknown at configure time.
"""
txt_matches = sorted(output_dir.glob("unified_memory*.txt"))
json_matches = sorted(output_dir.glob("unified_memory*.json"))
txt_file = txt_matches[0] if txt_matches else None
json_file = json_matches[0] if json_matches else None
if len(txt_matches) > 1:
print(
f"Warning: multiple unified_memory*.txt files in {output_dir}; using {txt_file}"
)
if len(json_matches) > 1:
print(
f"Warning: multiple unified_memory*.json files in {output_dir}; using {json_file}"
)
return txt_file, json_file
if __name__ == "__main__":
# Handle help manually so that --help doesn't fail on missing required args
if any(arg in ("-h", "--help") for arg in sys.argv[1:]):
print_help()
sys.exit(EXIT_OK)
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument(
"--txt-file",
type=Path,
help="Explicit path to unified_memory.txt (use with --json-file)",
)
parser.add_argument(
"--json-file",
type=Path,
help="Explicit path to unified_memory.json (use with --txt-file)",
)
parser.add_argument(
"--output-dir",
type=Path,
help="Directory containing unified_memory*.{txt,json} (single-level lookup)",
)
args = parser.parse_args()
explicit_mode = args.txt_file is not None and args.json_file is not None
dir_mode = args.output_dir is not None
# Require exactly one mode: bool equality is true when both are set or neither is set
if explicit_mode == dir_mode:
print(
"Error: provide either (--txt-file AND --json-file) OR --output-dir, not both"
)
print_help()
sys.exit(EXIT_USAGE)
if dir_mode:
if not args.output_dir.is_dir():
print(
f"Error: --output-dir does not exist or is not a directory: {args.output_dir}"
)
sys.exit(EXIT_NOINPUT)
txt_file, json_file = resolve_from_dir(args.output_dir)
if txt_file is None:
print(f"Error: no unified_memory*.txt found in {args.output_dir}")
sys.exit(EXIT_NOINPUT)
if json_file is None:
print(f"Error: no unified_memory*.json found in {args.output_dir}")
sys.exit(EXIT_NOINPUT)
else:
txt_file = args.txt_file
json_file = args.json_file
if not txt_file.exists():
print(f"Error: Could not find {txt_file}")
sys.exit(EXIT_NOINPUT)
if not json_file.exists():
print(f"Error: Could not find {json_file}")
sys.exit(EXIT_NOINPUT)
print("Validating unified memory outputs:")
print(f" Text: {txt_file}")
print(f" JSON: {json_file}")
print()
print("Starting unified memory output validation...")
txt_valid = validate_text_output(txt_file)
print(f"Validating JSON output: {json_file}")
json_data = load_json_output(json_file)
json_valid = json_data is not None and validate_json_data(json_data)
perfetto_valid = True
if json_valid:
assert json_data is not None
perfetto_valid = validate_perfetto_fault_only_trace(json_file.parent, json_data)
print()
if txt_valid and json_valid and perfetto_valid:
print("All validation checks passed")
print(f"{txt_file} and {json_file} validated")
sys.exit(EXIT_OK)
else:
print("Some validation checks failed")
print("Failure validating unified memory outputs")
sys.exit(EXIT_DATAERR)