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
423 lines
14 KiB
Python
423 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""Run JAX collective smoke tests against CI-built RCCL.
|
|
|
|
This script handles:
|
|
1. Discovering the CI-built librccl.so in the artifact directory
|
|
2. Prepending its directory to LD_LIBRARY_PATH so JAX loads it
|
|
3. Cloning the matching JAX test sources (sparse checkout)
|
|
4. Running pytest on pmap_test.py and shard_map_test.py
|
|
|
|
Usage from GitHub Actions:
|
|
python .github/scripts/test_jax_collective.py \
|
|
--artifact-dir ./build \
|
|
--jax-src ./jax-src \
|
|
--results-log ./jax_collective_results.log
|
|
"""
|
|
|
|
import argparse
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
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__)
|
|
|
|
JAX_REPO = "https://github.com/ROCm/jax.git"
|
|
|
|
SMOKE_TEST_FILES = [
|
|
"tests/pmap_test.py",
|
|
"tests/shard_map_test.py",
|
|
]
|
|
|
|
SMOKE_TEST_KEYWORDS = [
|
|
"PythonPmapTest and testBasic",
|
|
"PythonPmapTest and testGather and not testGatherBool and not testGatherNeg and not testGatherTiled and not testGatherReplica",
|
|
"PythonPmapTest and testReduceScatter and not Tiled and not Replica",
|
|
"PythonPmapTest and testCollectivePermute and not Grad and not Cyclic",
|
|
"PythonPmapTest and testAllToAll and not Replica and not Vmap and not Grad",
|
|
"ShardMapTest and test_all_gather and not invariant and not axis_index",
|
|
"ShardMapTest and test_matmul_reduce_scatter",
|
|
"ShardMapTest and test_collective_permute and not multiple",
|
|
"ShardMapTest and test_axis_index and not basic and not twoaxes and not eager",
|
|
"ShardMapTest and test_all_to_all and not axis_index and not grad",
|
|
]
|
|
|
|
XLA_ENV = {
|
|
"XLA_PYTHON_CLIENT_ALLOCATOR": "default",
|
|
"XLA_PYTHON_CLIENT_PREALLOCATE": "false",
|
|
"XLA_FLAGS": (
|
|
"--xla_gpu_force_compilation_parallelism=1 "
|
|
"--xla_gpu_enable_nccl_comm_splitting=false "
|
|
# Empty value disables all command buffer types (HIP graphs) on ROCm.
|
|
"--xla_gpu_enable_command_buffer= "
|
|
"--xla_gpu_enable_cublaslt=false"
|
|
),
|
|
}
|
|
|
|
|
|
def find_lib_dirs(artifact_dir: Path) -> list[Path]:
|
|
"""Find all directories containing .so files in the artifact tree."""
|
|
lib_dirs: set[Path] = set()
|
|
for so_file in artifact_dir.rglob("*.so"):
|
|
lib_dirs.add(so_file.parent.resolve())
|
|
for so_file in artifact_dir.rglob("*.so.*"):
|
|
lib_dirs.add(so_file.parent.resolve())
|
|
sorted_dirs = sorted(lib_dirs)
|
|
for d in sorted_dirs:
|
|
count = sum(1 for f in d.iterdir() if ".so" in f.name)
|
|
log.info("Found lib dir: %s (%d libs)", d, count)
|
|
return sorted_dirs
|
|
|
|
|
|
def populate_rocm_lib_dir(lib_dirs: list[Path]) -> None:
|
|
"""Populate /opt/rocm/lib with symlinks to artifact libraries.
|
|
|
|
JAX's xla_rocm_plugin.so has RUNPATH including /opt/rocm/lib. With ELF
|
|
RUNPATH semantics, transitive dependencies are resolved via RUNPATH
|
|
rather than LD_LIBRARY_PATH. In a container with no system ROCm, we
|
|
populate /opt/rocm/lib so the loader can find them.
|
|
"""
|
|
rocm_lib = Path("/opt/rocm/lib")
|
|
try:
|
|
rocm_lib.mkdir(parents=True, exist_ok=True)
|
|
except PermissionError:
|
|
log.warning("Cannot create %s — not running as root", rocm_lib)
|
|
return
|
|
|
|
count = 0
|
|
for d in lib_dirs:
|
|
for so_file in d.iterdir():
|
|
if ".so" not in so_file.name:
|
|
continue
|
|
target = rocm_lib / so_file.name
|
|
if target.exists() or target.is_symlink():
|
|
continue
|
|
target.symlink_to(so_file.resolve())
|
|
count += 1
|
|
log.info("Created %d symlinks in %s", count, rocm_lib)
|
|
|
|
|
|
def setup_ld_library_path(lib_dirs: list[Path]) -> str:
|
|
"""Prepend all artifact lib dirs to LD_LIBRARY_PATH."""
|
|
parts = [str(d) for d in lib_dirs]
|
|
rocm_lib = Path("/opt/rocm/lib")
|
|
if rocm_lib.is_dir():
|
|
parts.insert(0, str(rocm_lib))
|
|
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 setup_xla_environment() -> None:
|
|
"""Set XLA environment variables required for JAX on ROCm."""
|
|
for key, value in XLA_ENV.items():
|
|
os.environ[key] = value
|
|
log.info("Set %s=%s", key, value)
|
|
|
|
|
|
def clone_jax_test_sources(jax_src: Path) -> None:
|
|
"""Sparse-clone ROCm/jax to get test sources matching the installed version.
|
|
|
|
Tries to find a tag matching the installed JAX version (e.g. jax-v0.5.3).
|
|
Falls back to the default branch HEAD for nightly/dev builds.
|
|
"""
|
|
if jax_src.exists() and (jax_src / "tests" / "pmap_test.py").exists():
|
|
log.info("JAX test sources already present at %s, skipping clone", jax_src)
|
|
return
|
|
|
|
import jax
|
|
jax_version = jax.__version__
|
|
base_version = jax_version.split(".dev")[0].split("+")[0]
|
|
git_ref = f"jax-v{base_version}"
|
|
log.info("JAX version: %s, trying tag: %s", jax_version, git_ref)
|
|
|
|
result = subprocess.run(
|
|
["git", "ls-remote", "--tags", JAX_REPO, git_ref],
|
|
capture_output=True, text=True,
|
|
)
|
|
if not result.stdout.strip():
|
|
log.info("Tag %s not found, using default branch HEAD", git_ref)
|
|
git_ref = None
|
|
|
|
clone_cmd = [
|
|
"git", "clone",
|
|
"--depth=1",
|
|
"--filter=blob:none",
|
|
"--sparse",
|
|
JAX_REPO,
|
|
str(jax_src),
|
|
]
|
|
if git_ref:
|
|
clone_cmd.insert(-1, f"--branch={git_ref}")
|
|
|
|
log.info("Cloning ROCm/jax (ref=%s, sparse) into %s", git_ref or "HEAD", jax_src)
|
|
subprocess.run(clone_cmd, check=True)
|
|
subprocess.run(
|
|
["git", "sparse-checkout", "set", "tests/", "build/", "jax/"],
|
|
cwd=jax_src,
|
|
check=True,
|
|
)
|
|
|
|
test_file = jax_src / "tests" / "pmap_test.py"
|
|
if not test_file.exists():
|
|
log.error("pmap_test.py not found after clone")
|
|
sys.exit(1)
|
|
log.info("JAX test sources ready: %s", jax_src)
|
|
|
|
|
|
def print_environment_info() -> None:
|
|
"""Print GPU and environment details for CI logs."""
|
|
log.info("--- Environment Info ---")
|
|
try:
|
|
import jax
|
|
log.info("JAX version: %s", jax.__version__)
|
|
devices = jax.devices()
|
|
log.info("Devices: %s", devices)
|
|
log.info("Device count: %d", jax.device_count())
|
|
log.info("Local device count: %d", jax.local_device_count())
|
|
gpu_devices = [d for d in devices if d.platform != "cpu"]
|
|
if not gpu_devices:
|
|
log.error("No GPU devices found — JAX fell back to CPU only")
|
|
log.error("Check that ROCm libraries are on LD_LIBRARY_PATH")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
log.error("Failed to query JAX devices: %s", e)
|
|
sys.exit(1)
|
|
|
|
log.info("LD_LIBRARY_PATH: %s", os.environ.get("LD_LIBRARY_PATH", ""))
|
|
for key in sorted(XLA_ENV):
|
|
log.info("%s=%s", key, os.environ.get(key, "<unset>"))
|
|
log.info("--- End Environment Info ---")
|
|
|
|
|
|
def run_tests(jax_src: Path, results_log: Path) -> tuple[int, dict]:
|
|
"""Run pytest on the 10 collective smoke tests and return (exit_code, summary)."""
|
|
junit_xml = results_log.parent / "jax_collective_results.xml"
|
|
|
|
k_expr = " or ".join(f"({kw})" for kw in SMOKE_TEST_KEYWORDS)
|
|
cmd = [
|
|
sys.executable, "-m", "pytest",
|
|
"-sv",
|
|
"--timeout=120",
|
|
"--tb=short",
|
|
f"--junitxml={junit_xml}",
|
|
"-k", k_expr,
|
|
] + SMOKE_TEST_FILES
|
|
|
|
log.info("Running: %s", " ".join(cmd))
|
|
|
|
with open(results_log, "w") as log_file:
|
|
proc = subprocess.Popen(
|
|
cmd,
|
|
cwd=jax_src,
|
|
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 = []
|
|
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"]
|
|
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 junit["error_details"]:
|
|
log.info("Failure/error details from JUnit XML:")
|
|
for detail in junit["error_details"]:
|
|
log.info(" %s", detail)
|
|
else:
|
|
log.warning("JUnit XML not found at %s, falling back to exit code only", junit_xml)
|
|
|
|
if tests_run < len(SMOKE_TEST_KEYWORDS):
|
|
log.error(
|
|
"Expected at least %d smoke tests but only %d were collected — "
|
|
"tests may have been skipped or deselected",
|
|
len(SMOKE_TEST_KEYWORDS),
|
|
tests_run,
|
|
)
|
|
exit_code = 1
|
|
|
|
summary = {
|
|
"exit_code": exit_code,
|
|
"passed": passed_tests,
|
|
"failed": failed_tests,
|
|
"summary_line": summary_line,
|
|
"tests_run": tests_run,
|
|
"expected_tests": len(SMOKE_TEST_KEYWORDS),
|
|
}
|
|
return exit_code, summary
|
|
|
|
|
|
def generate_summary_report(summary: dict, rccl_lib: Path) -> str:
|
|
"""Generate a plain-text summary report."""
|
|
import jax
|
|
|
|
status = "PASSED" if summary["exit_code"] == 0 else "FAILED"
|
|
devices = jax.devices()
|
|
gpu_name = str(devices[0]) if devices else "unknown"
|
|
|
|
lines = [
|
|
"RCCL JAX Collective Test Report",
|
|
"=" * 40,
|
|
f"Status: {status}",
|
|
f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}",
|
|
"",
|
|
f"JAX: {jax.__version__}",
|
|
f"RCCL: {rccl_lib}",
|
|
f"GPUs: {len(devices)}x {gpu_name}",
|
|
"",
|
|
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(
|
|
"--jax-src",
|
|
type=Path,
|
|
required=True,
|
|
help="Directory to clone JAX test sources into",
|
|
)
|
|
parser.add_argument(
|
|
"--results-log",
|
|
type=Path,
|
|
default=Path("jax_collective_results.log"),
|
|
help="Path for test results log file",
|
|
)
|
|
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 and all lib dirs in artifacts
|
|
rccl_lib = find_rccl_library(args.artifact_dir)
|
|
rccl_lib_dir = rccl_lib.parent
|
|
lib_dirs = find_lib_dirs(args.artifact_dir)
|
|
|
|
set_github_output("RCCL_LIB_DIR", str(rccl_lib_dir))
|
|
|
|
if args.discover_only:
|
|
return
|
|
|
|
# Step 2: Set up library paths and verify override
|
|
populate_rocm_lib_dir(lib_dirs)
|
|
setup_ld_library_path(lib_dirs)
|
|
verify_rccl_override(rccl_lib_dir)
|
|
|
|
# Step 3: Set XLA environment variables
|
|
setup_xla_environment()
|
|
|
|
# Step 4: Clone JAX test sources
|
|
clone_jax_test_sources(args.jax_src)
|
|
|
|
# Step 5: Print environment info and run tests
|
|
print_environment_info()
|
|
exit_code, summary = run_tests(args.jax_src, args.results_log)
|
|
|
|
# Step 6: 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 / "jax_collective_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 JAX Collective Test")
|
|
if args.notify_webhook:
|
|
send_teams_webhook(report, args.notify_webhook, status,
|
|
subject_prefix="RCCL JAX Collective Test")
|
|
|
|
sys.exit(exit_code)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|