Files
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

461 lines
15 KiB
Python

#!/usr/bin/env python3
"""Run PyTorch c10d NCCL distributed tests against CI-built RCCL.
This script handles:
1. Discovering the CI-built librccl.so in the artifact directory
2. Verifying that LD_LIBRARY_PATH overrides PyTorch's bundled RCCL
3. Cloning the matching PyTorch test sources (sparse checkout)
4. Running pytest on test_c10d_nccl.py
Usage from GitHub Actions:
python .github/scripts/test_pytorch_c10d.py \
--artifact-dir ./build \
--pytorch-src ./pytorch-src \
--results-log ./pytorch_c10d_results.log
"""
import argparse
import logging
import os
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from rccl_ci_utils import (
find_rccl_library,
parse_junit_xml,
send_email_report,
send_teams_webhook,
set_github_output,
verify_rccl_override,
write_github_summary,
)
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
log = logging.getLogger(__name__)
SMOKE_TESTS = [
"test_all_reduce_coalesced_nccl",
"test_all_reduce_coalesced_manager_nccl",
"test_allgather_base",
"test_all_gather_into_tensor_coalesced_manager_nccl",
"test_broadcast_coalesced_nccl",
"test_broadcast_subgroup",
"test_reduce_scatter_base_k",
"test_reduce_scatter_tensor_coalesced",
"test_non_blocking_p2p",
"test_send_recv_subgroup",
"test_nccl_barrier_device_ids",
"test_reduce_subgroup",
"test_scatter_subgroup",
"test_gather_subgroup",
"test_all_to_all_single",
"test_init_wo_backend_str",
"test_new_group",
"test_pass_nccl_options_high_priority_stream",
"test_set_process_group_desc",
"test_tensor_dtype_complex",
"test_batch_send_recv_subgroup",
"test_collectives",
]
def find_rocm_lib_dir(artifact_dir: Path) -> Path | None:
"""Find the dist/rocm/lib directory in artifacts."""
for d in artifact_dir.rglob("dist/rocm/lib"):
if d.is_dir():
log.info("Found ROCm lib dir: %s", d)
return d
return None
def setup_ld_library_path(rccl_lib_dir: Path, rocm_lib_dir: Path | None) -> str:
"""Prepend RCCL and ROCm lib dirs to LD_LIBRARY_PATH."""
parts = [str(rccl_lib_dir.resolve())]
if rocm_lib_dir:
parts.append(str(rocm_lib_dir.resolve()))
existing = os.environ.get("LD_LIBRARY_PATH", "")
if existing:
parts.append(existing)
new_path = ":".join(parts)
os.environ["LD_LIBRARY_PATH"] = new_path
log.info("LD_LIBRARY_PATH=%s", new_path)
return new_path
def clone_pytorch_test_sources(pytorch_src: Path) -> None:
"""Sparse-clone PyTorch test sources matching the installed torch version.
For release builds (e.g. 2.5.0), clones at the matching tag.
For nightly builds (e.g. 2.14.0a0+rocm7.15.0a20260712), clones using
--shallow-since to get commits around the build date, then checks out the
commit closest to that date so test sources match the installed wheel.
"""
import re
from datetime import datetime, timedelta
import torch
torch_version = torch.__version__
base_version = torch_version.split("+")[0]
log.info("PyTorch version: %s", torch_version)
git_ref = f"v{base_version}"
result = subprocess.run(
["git", "ls-remote", "--tags", "https://github.com/pytorch/pytorch.git", git_ref],
capture_output=True,
text=True,
)
date_match = re.search(r"(\d{8})", torch_version)
use_date_pinning = False
if result.stdout.strip():
log.info("Found tag %s", git_ref)
elif date_match:
build_date = date_match.group(1)
dt = datetime.strptime(build_date, "%Y%m%d")
shallow_since = (dt - timedelta(days=2)).strftime("%Y-%m-%d")
log.info("Tag %s not found; nightly build date %s", git_ref, build_date)
git_ref = "nightly"
use_date_pinning = True
else:
log.info("Tag %s not found, using nightly branch HEAD", git_ref)
git_ref = "nightly"
if use_date_pinning:
log.info("Cloning PyTorch (ref=%s, shallow-since=%s, sparse) into %s",
git_ref, shallow_since, pytorch_src)
subprocess.run(
[
"git", "clone",
f"--branch={git_ref}",
f"--shallow-since={shallow_since}",
"--filter=blob:none",
"--sparse",
"https://github.com/pytorch/pytorch.git",
str(pytorch_src),
],
check=True,
)
else:
log.info("Cloning PyTorch (ref=%s, depth=1, sparse) into %s", git_ref, pytorch_src)
subprocess.run(
[
"git", "clone",
"--depth=1",
f"--branch={git_ref}",
"--filter=blob:none",
"--sparse",
"https://github.com/pytorch/pytorch.git",
str(pytorch_src),
],
check=True,
)
subprocess.run(
["git", "sparse-checkout", "set", "test/"],
cwd=pytorch_src,
check=True,
)
if use_date_pinning:
before = (dt + timedelta(days=1)).strftime("%Y-%m-%dT00:00:00")
result = subprocess.run(
["git", "log", f"--before={before}", "--format=%H", "-1"],
cwd=pytorch_src,
capture_output=True,
text=True,
)
if result.returncode == 0 and result.stdout.strip():
commit = result.stdout.strip()
log.info("Checking out commit %s (latest before %s)", commit[:12], before)
subprocess.run(
["git", "checkout", commit],
cwd=pytorch_src,
check=True,
)
else:
log.warning("Could not find commit before %s, using HEAD of nightly", before)
test_file = pytorch_src / "test" / "distributed" / "test_c10d_nccl.py"
if not test_file.exists():
log.error("test_c10d_nccl.py not found after clone")
sys.exit(1)
log.info("Test sources ready: %s", test_file)
def patch_missing_torch_modules() -> None:
"""Create stubs for internal torch modules missing from nightly wheels."""
import torch
torch_dir = Path(torch.__file__).parent
strobelight_dir = torch_dir / "_strobelight"
profiler_file = strobelight_dir / "compile_time_profiler.py"
if not profiler_file.exists():
log.info("Creating stub for torch._strobelight (missing from nightly wheel)")
strobelight_dir.mkdir(parents=True, exist_ok=True)
(strobelight_dir / "__init__.py").write_text("")
profiler_file.write_text(
"class StrobelightCompileTimeProfiler:\n"
" def __enter__(self): return self\n"
" def __exit__(self, *a): pass\n"
)
def print_environment_info() -> None:
"""Print GPU and environment details for CI logs."""
import torch
log.info("PyTorch: %s", torch.__version__)
log.info("CUDA/HIP available: %s", torch.cuda.is_available())
log.info("GPU count: %s", torch.cuda.device_count())
for i in range(torch.cuda.device_count()):
log.info(" GPU %d: %s", i, torch.cuda.get_device_name(i))
log.info("LD_LIBRARY_PATH: %s", os.environ.get("LD_LIBRARY_PATH", ""))
def run_tests(pytorch_src: Path, results_log: Path, test_scope: str = "smoke") -> tuple[int, dict]:
"""Run pytest on test_c10d_nccl.py and return (exit_code, summary_dict)."""
miopen_cache = tempfile.mkdtemp(prefix="miopen_cache_")
os.environ["MIOPEN_USER_DB_PATH"] = miopen_cache
env = os.environ.copy()
env["PYTHONPATH"] = str(pytorch_src / "test") + ":" + env.get("PYTHONPATH", "")
junit_xml = results_log.parent / "pytorch_c10d_results.xml"
if test_scope == "smoke":
k_expr = " or ".join(SMOKE_TESTS)
timeout = "60"
log.info("Running smoke tests (%d tests)", len(SMOKE_TESTS))
else:
k_expr = "not NCCLTraceTestDumpOnTimeout"
timeout = "600"
log.info("Running all tests (excluding NCCLTraceTestDumpOnTimeout)")
cmd = [
sys.executable,
"-m",
"pytest",
str(pytorch_src / "test" / "distributed" / "test_c10d_nccl.py"),
"-v",
f"--timeout={timeout}",
"--tb=short",
f"--junitxml={junit_xml}",
"-k",
k_expr,
]
log.info("Running: %s", " ".join(cmd))
with open(results_log, "w") as log_file:
proc = subprocess.Popen(
cmd,
cwd=pytorch_src,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
for line in proc.stdout:
sys.stdout.write(line)
sys.stdout.flush()
log_file.write(line)
proc.wait()
log.info("Test exit code: %d", proc.returncode)
log.info("Results written to: %s", results_log)
exit_code = proc.returncode
passed_tests = []
failed_tests = []
error_details = []
tests_run = 0
summary_line = ""
if junit_xml.exists():
log.info("Parsing JUnit XML: %s", junit_xml)
junit = parse_junit_xml(junit_xml)
passed_tests = junit["passed"]
failed_tests = junit["failed"]
error_details = junit["error_details"]
tests_run = junit["tests_run"]
parts = []
if passed_tests:
parts.append(f"{len(passed_tests)} passed")
if failed_tests:
parts.append(f"{len(failed_tests)} failed")
summary_line = ", ".join(parts)
if error_details:
log.info("Failure/error details from JUnit XML:")
for detail in error_details:
log.info(" %s", detail)
else:
log.warning("JUnit XML not found at %s, falling back to exit code only", junit_xml)
if test_scope == "smoke" and tests_run < len(SMOKE_TESTS):
log.error(
"Expected %d smoke tests but only %d were collected — "
"test names may have changed in the nightly",
len(SMOKE_TESTS),
tests_run,
)
exit_code = 1
summary = {
"exit_code": exit_code,
"test_scope": test_scope,
"passed": passed_tests,
"failed": failed_tests,
"summary_line": summary_line,
"tests_run": tests_run,
"expected_tests": len(SMOKE_TESTS) if test_scope == "smoke" else None,
}
return exit_code, summary
def generate_summary_report(summary: dict, rccl_lib: Path) -> str:
"""Generate a plain-text summary report."""
import torch
status = "PASSED" if summary["exit_code"] == 0 else "FAILED"
gpu_info = []
for i in range(torch.cuda.device_count()):
gpu_info.append(f" GPU {i}: {torch.cuda.get_device_name(i)}")
lines = [
f"RCCL PyTorch c10d Test Report",
f"{'=' * 40}",
f"Status: {status}",
f"Test scope: {summary['test_scope']}",
f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}",
f"",
f"PyTorch: {torch.__version__}",
f"RCCL: {rccl_lib}",
f"GPUs: {torch.cuda.device_count()}x {torch.cuda.get_device_name(0)}",
f"",
f"Results: {summary['summary_line']}",
]
if summary.get("expected_tests") is not None:
lines.append(f"Collected: {summary['tests_run']}/{summary['expected_tests']} expected smoke tests")
lines.append("")
if summary["failed"]:
lines.append(f"FAILED tests ({len(summary['failed'])}):")
for name in summary["failed"]:
lines.append(f" FAIL {name}")
lines.append("")
if summary["passed"]:
lines.append(f"PASSED tests ({len(summary['passed'])}):")
for name, duration in summary["passed"]:
lines.append(f" OK {name:60s} {duration}")
lines.append("")
run_url = os.environ.get("GITHUB_SERVER_URL", "")
repo = os.environ.get("GITHUB_REPOSITORY", "")
run_id = os.environ.get("GITHUB_RUN_ID", "")
if run_url and repo and run_id:
lines.append(f"CI run: {run_url}/{repo}/actions/runs/{run_id}")
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--artifact-dir",
type=Path,
required=True,
help="Directory containing CI-built artifacts",
)
parser.add_argument(
"--pytorch-src",
type=Path,
required=True,
help="Directory to clone PyTorch test sources into",
)
parser.add_argument(
"--results-log",
type=Path,
default=Path("pytorch_c10d_results.log"),
help="Path for test results log file",
)
parser.add_argument(
"--test-scope",
choices=["smoke", "all"],
default="smoke",
help="Run smoke tests (22 curated tests, ~3min) or all tests (default: smoke)",
)
parser.add_argument(
"--notify-email",
type=str,
default="",
help="Send summary report to this email address",
)
parser.add_argument(
"--notify-webhook",
type=str,
default="",
help="Send summary report to this Teams webhook URL",
)
parser.add_argument(
"--discover-only",
action="store_true",
help="Only discover library paths and set GITHUB_OUTPUT, then exit",
)
args = parser.parse_args()
# Step 1: Discover RCCL library path
rccl_lib = find_rccl_library(args.artifact_dir)
rccl_lib_dir = rccl_lib.parent
rocm_lib_dir = find_rocm_lib_dir(args.artifact_dir)
set_github_output("RCCL_LIB_DIR", str(rccl_lib_dir))
if rocm_lib_dir:
set_github_output("ROCM_LIB_DIR", str(rocm_lib_dir))
if args.discover_only:
return
# Step 2: Set up LD_LIBRARY_PATH and verify override
setup_ld_library_path(rccl_lib_dir, rocm_lib_dir)
verify_rccl_override(rccl_lib_dir)
# Step 3: Clone PyTorch test sources
clone_pytorch_test_sources(args.pytorch_src)
# Step 4: Patch missing modules, print environment info, and run tests
patch_missing_torch_modules()
print_environment_info()
exit_code, summary = run_tests(args.pytorch_src, args.results_log, args.test_scope)
# Step 5: Generate and distribute summary report
report = generate_summary_report(summary, rccl_lib)
log.info("\n%s", report)
write_github_summary(report)
summary_path = args.results_log.parent / "pytorch_c10d_summary.txt"
summary_path.write_text(report)
log.info("Summary written to: %s", summary_path)
status = "PASSED" if exit_code == 0 else "FAILED"
if args.notify_email:
send_email_report(report, args.notify_email, status,
subject_prefix="RCCL PyTorch c10d Test")
if args.notify_webhook:
send_teams_webhook(report, args.notify_webhook, status,
subject_prefix="RCCL PyTorch c10d Test")
sys.exit(exit_code)
if __name__ == "__main__":
main()