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
461 lines
15 KiB
Python
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()
|