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
202 lines
7.9 KiB
Python
202 lines
7.9 KiB
Python
# *************************************************************************
|
|
# * Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
# *
|
|
# * See LICENSE.txt for license information
|
|
# ************************************************************************
|
|
|
|
import os
|
|
import pytest
|
|
import json
|
|
import glob
|
|
from types import SimpleNamespace
|
|
|
|
# Required fields for inspector JSONL output validation
|
|
INSPECTOR_HEADER_FIELDS = {"id": str, "rank": int, "n_ranks": int, "nnodes": int}
|
|
INSPECTOR_METADATA_FIELDS = {
|
|
"inspector_output_format_version": str,
|
|
"rec_mechanism": str,
|
|
"dump_timestamp_us": int,
|
|
"hostname": str,
|
|
"pid": int,
|
|
}
|
|
INSPECTOR_COLL_PERF_FIELDS = {
|
|
"coll": str,
|
|
"coll_sn": int,
|
|
"coll_msg_size_bytes": int,
|
|
"coll_exec_time_us": int,
|
|
"coll_timing_source": str,
|
|
"coll_algobw_gbs": (int, float),
|
|
"coll_busbw_gbs": (int, float),
|
|
}
|
|
|
|
|
|
def validate_inspector_log_line(line):
|
|
"""Validate a single inspector JSONL line. Returns (is_valid, record_dict_or_None, error_message)."""
|
|
try:
|
|
record = json.loads(line.strip())
|
|
except json.JSONDecodeError as e:
|
|
return False, None, f"Invalid JSON: {e}"
|
|
|
|
# Check top-level sections
|
|
for section in ("header", "metadata", "coll_perf"):
|
|
if section not in record:
|
|
return False, record, f"Missing top-level section: '{section}'"
|
|
|
|
# Validate header fields
|
|
for field, expected_type in INSPECTOR_HEADER_FIELDS.items():
|
|
if field not in record["header"]:
|
|
return False, record, f"Missing header field: '{field}'"
|
|
if not isinstance(record["header"][field], expected_type):
|
|
return False, record, f"header.{field} has wrong type: expected {expected_type.__name__}, got {type(record['header'][field]).__name__}"
|
|
|
|
# Validate metadata fields
|
|
for field, expected_type in INSPECTOR_METADATA_FIELDS.items():
|
|
if field not in record["metadata"]:
|
|
return False, record, f"Missing metadata field: '{field}'"
|
|
if not isinstance(record["metadata"][field], expected_type):
|
|
return False, record, f"metadata.{field} has wrong type: expected {expected_type.__name__}, got {type(record['metadata'][field]).__name__}"
|
|
|
|
# Validate coll_perf fields
|
|
for field, expected_type in INSPECTOR_COLL_PERF_FIELDS.items():
|
|
if field not in record["coll_perf"]:
|
|
return False, record, f"Missing coll_perf field: '{field}'"
|
|
if not isinstance(record["coll_perf"][field], expected_type):
|
|
return False, record, f"coll_perf.{field} has wrong type: expected {expected_type}, got {type(record['coll_perf'][field]).__name__}"
|
|
|
|
# Sanity checks on values
|
|
if record["coll_perf"]["coll_exec_time_us"] < 0:
|
|
return False, record, "coll_perf.coll_exec_time_us is negative"
|
|
if record["coll_perf"]["coll_msg_size_bytes"] < 0:
|
|
return False, record, "coll_perf.coll_msg_size_bytes is negative"
|
|
if record["coll_perf"]["coll_algobw_gbs"] < 0:
|
|
return False, record, "coll_perf.coll_algobw_gbs is negative"
|
|
if record["coll_perf"]["coll_busbw_gbs"] < 0:
|
|
return False, record, "coll_perf.coll_busbw_gbs is negative"
|
|
if record["coll_perf"]["coll_timing_source"] not in ("kernel_gpu", "kernel_cpu", "collective_cpu"):
|
|
return False, record, f"coll_perf.coll_timing_source has unexpected value: '{record['coll_perf']['coll_timing_source']}'"
|
|
|
|
return True, record, "OK"
|
|
|
|
|
|
def validate_inspector_log_file(filepath):
|
|
"""Validate an entire inspector JSONL log file. Returns (is_valid, num_records, list_of_errors)."""
|
|
if not os.path.exists(filepath):
|
|
return False, 0, ["File does not exist"]
|
|
|
|
errors = []
|
|
num_records = 0
|
|
|
|
with open(filepath, 'r') as f:
|
|
for lineno, line in enumerate(f, 1):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
is_valid, _, error_msg = validate_inspector_log_line(line)
|
|
if is_valid:
|
|
num_records += 1
|
|
else:
|
|
errors.append(f"Line {lineno}: {error_msg}")
|
|
|
|
if num_records == 0:
|
|
errors.append("No valid records found in file")
|
|
|
|
return len(errors) == 0, num_records, errors
|
|
|
|
|
|
def count_inspector_records(filepath, coll=None):
|
|
"""Count records in an inspector log file, optionally filtered by collective type."""
|
|
if not os.path.exists(filepath):
|
|
return 0
|
|
|
|
count = 0
|
|
try:
|
|
with open(filepath, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
record = json.loads(line)
|
|
if coll is None or record.get("coll_perf", {}).get("coll") == coll:
|
|
count += 1
|
|
except json.JSONDecodeError:
|
|
continue
|
|
except Exception:
|
|
pass
|
|
return count
|
|
|
|
|
|
def validate_inspector_verbose_record(record):
|
|
"""Validate that an inspector record contains verbose event trace fields. Returns (is_valid, error_message)."""
|
|
coll_perf = record.get("coll_perf", {})
|
|
|
|
# Check event_trace_sn
|
|
if "event_trace_sn" not in coll_perf:
|
|
return False, "Missing coll_perf.event_trace_sn"
|
|
trace_sn = coll_perf["event_trace_sn"]
|
|
if not isinstance(trace_sn, dict):
|
|
return False, "coll_perf.event_trace_sn is not a dict"
|
|
for field in ("coll_start_sn", "coll_stop_sn"):
|
|
if field not in trace_sn:
|
|
return False, f"Missing event_trace_sn.{field}"
|
|
|
|
# Check event_trace_ts
|
|
if "event_trace_ts" not in coll_perf:
|
|
return False, "Missing coll_perf.event_trace_ts"
|
|
trace_ts = coll_perf["event_trace_ts"]
|
|
if not isinstance(trace_ts, dict):
|
|
return False, "coll_perf.event_trace_ts is not a dict"
|
|
for field in ("coll_start_ts", "coll_stop_ts"):
|
|
if field not in trace_ts:
|
|
return False, f"Missing event_trace_ts.{field}"
|
|
|
|
# Check kernel_events in both trace_sn and trace_ts
|
|
if "kernel_events" in trace_sn:
|
|
if not isinstance(trace_sn["kernel_events"], list):
|
|
return False, "event_trace_sn.kernel_events is not a list"
|
|
for i, ke in enumerate(trace_sn["kernel_events"]):
|
|
for field in ("channel_id", "kernel_start_sn", "kernel_stop_sn"):
|
|
if field not in ke:
|
|
return False, f"Missing event_trace_sn.kernel_events[{i}].{field}"
|
|
|
|
if "kernel_events" in trace_ts:
|
|
if not isinstance(trace_ts["kernel_events"], list):
|
|
return False, "event_trace_ts.kernel_events is not a list"
|
|
for i, ke in enumerate(trace_ts["kernel_events"]):
|
|
for field in ("channel_id", "kernel_start_ts", "kernel_stop_ts"):
|
|
if field not in ke:
|
|
return False, f"Missing event_trace_ts.kernel_events[{i}].{field}"
|
|
|
|
return True, "OK"
|
|
|
|
|
|
# Pytest Fixture - bundles all inspector helpers into a single fixture
|
|
@pytest.fixture(scope="session")
|
|
def inspector_helpers():
|
|
"""Provides inspector-specific helper functions and constants."""
|
|
return SimpleNamespace(
|
|
validate_inspector_log_line=validate_inspector_log_line,
|
|
validate_inspector_log_file=validate_inspector_log_file,
|
|
count_inspector_records=count_inspector_records,
|
|
validate_inspector_verbose_record=validate_inspector_verbose_record,
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def clear_inspector_dump(request, paths):
|
|
"""Automatically clear inspector dump folder once before ext_inspector tests."""
|
|
has_inspector_tests = any(
|
|
item.get_closest_marker("ext_inspector")
|
|
for item in request.session.items
|
|
)
|
|
|
|
if has_inspector_tests:
|
|
# Clear all .log files in the inspector dump directory (including subdirectories)
|
|
pattern = os.path.join(paths.INSPECTOR_DUMP_DIR, "**", "*.log")
|
|
for log_file in glob.glob(pattern, recursive=True):
|
|
try:
|
|
os.remove(log_file)
|
|
except OSError:
|
|
pass # Ignore errors if file doesn't exist or can't be removed
|
|
|