Files
TheRock/rocm-systems/projects/rccl/test/ir-device/tests/conftest.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

422 lines
16 KiB
Python

# *************************************************************************
# * Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved.
# *
# * See LICENSE.txt for license information
# ************************************************************************
"""Shared fixtures for the librccl_device.bc (IR) device tests.
This mirrors test/ext-plugins: the test artifact (here, the GoogleTest
binary built from bindings/ir/test/IR_test.cpp against librccl_device.bc) is
built on demand from a session-scoped fixture and the whole suite is skipped
with a clear reason when its prerequisites are missing — instead of failing
the CI job on machines that did not build with -DEMIT_LLVM_IR=ON.
Prerequisites (set as environment variables, with sensible defaults):
RCCL_DIR RCCL source root (default: repo root, derived)
RCCL_BUILD RCCL CMake build dir (default: $RCCL_DIR/build/release)
ROCM_PATH ROCm install root (default: /opt/rocm)
ARCH offload / bitcode arch (default: gfx942)
GTEST_ROOT GoogleTest prefix (default: $RCCL_BUILD/gtest)
IR_OUTDIR dir for the compiled exe(default: <workdir>/ir_test_build)
The build requires that RCCL was configured/built with:
cmake -DEMIT_LLVM_IR=ON -DBITCODE_LIB_ARCH=<arch> -DBUILD_TESTS=ON ...
so that librccl_device.bc, the hipify-staged headers, generated nccl.h/rccl.h
and GoogleTest are all present.
"""
import glob
import logging
import os
import shutil
import subprocess
from types import SimpleNamespace
import pytest
logger = logging.getLogger(__name__)
WORKDIR = os.getcwd()
# RCCL source root: this file is test/ir-device/tests/conftest.py
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
_DEFAULT_RCCL_DIR = os.path.abspath(os.path.join(_THIS_DIR, "..", "..", ".."))
RCCL_DIR = os.path.abspath(os.environ.get("RCCL_DIR", _DEFAULT_RCCL_DIR))
RCCL_BUILD = os.path.abspath(
os.environ.get("RCCL_BUILD", os.path.join(RCCL_DIR, "build", "release"))
)
ROCM_PATH = os.environ.get("ROCM_PATH", "/opt/rocm")
ARCH = os.environ.get("ARCH", "gfx942")
GTEST_ROOT = os.environ.get("GTEST_ROOT", os.path.join(RCCL_BUILD, "gtest"))
IR_OUTDIR = os.environ.get("IR_OUTDIR", os.path.join(WORKDIR, "ir_test_build"))
IR_DIR = os.path.join(RCCL_DIR, "bindings", "ir")
IR_TEST_SRC = os.path.join(IR_DIR, "test", "IR_test.cpp")
BITCODE = os.path.join(RCCL_BUILD, "lib", "librccl_device.bc")
HIPIFY_INC = os.path.join(RCCL_BUILD, "hipify", "src", "include")
GENERATED_INC = os.path.join(RCCL_BUILD, "include")
HIPCC = os.path.join(ROCM_PATH, "bin", "hipcc")
TEST_EXE = os.path.join(IR_OUTDIR, "IR_test.exe")
# --- Multi-rank GIN/composite barrier functional test (separate MPI binary) ---
# This one needs the host RCCL library + MPI in addition to the bitcode, so it
# is gated independently and skipped (not failed) when those are absent.
GIN_MPI_TEST_SRC = os.path.join(IR_DIR, "test", "IR_gin_mpi_test.cpp")
GIN_MPI_TEST_EXE = os.path.join(IR_OUTDIR, "IR_gin_mpi_test.exe")
MPI_INC = os.environ.get("MPI_INC", "/usr/include/x86_64-linux-gnu/mpich")
NRANKS = int(os.environ.get("IR_GIN_NRANKS", "2"))
# Devices the MPI run may use, one per rank. Defaults to the first NRANKS GPUs
# (0,1,...) so each rank's in-binary hipSetDevice(localRank) lands on its own
# GPU. Override with IR_GIN_GPUS="2,3" to target specific devices.
IR_GIN_GPUS = os.environ.get("IR_GIN_GPUS", ",".join(str(i) for i in range(NRANKS)))
LOGDIR = os.path.join(WORKDIR, "logs")
os.makedirs(LOGDIR, exist_ok=True)
def _find_gtest_libdir():
"""Return the dir holding libgtest.{a,so} under GTEST_ROOT, or None.
Mirrors the RCCL CMake gtest install layout (lib or lib64) and Debian
multiarch installs (lib/<triple>-linux-gnu).
"""
candidates = [
os.path.join(GTEST_ROOT, "lib"),
os.path.join(GTEST_ROOT, "lib64"),
]
candidates += glob.glob(os.path.join(GTEST_ROOT, "lib", "*-linux-gnu"))
for d in candidates:
if os.path.isfile(os.path.join(d, "libgtest.a")) or os.path.isfile(
os.path.join(d, "libgtest.so")
):
return d
return None
def _missing_prerequisite():
"""Return a human-readable reason string if a prerequisite is missing.
Note: librccl_device.bc is intentionally NOT required here — it is built
on demand from the hipify-staged headers (see _build_bitcode). The real
gate is the hipify staging dir, which a prior RCCL CMake build produces
and which is needed both to emit the bitcode and to compile the test.
"""
if not os.path.isfile(IR_TEST_SRC):
return f"IR_test.cpp not found at {IR_TEST_SRC}"
if not os.path.isfile(HIPCC):
return f"hipcc not found at {HIPCC} (set ROCM_PATH)"
if not os.path.isdir(HIPIFY_INC):
return (
f"hipify staging dir not found at {HIPIFY_INC} "
"(build RCCL once so the staged nccl_device headers exist)"
)
gtest_inc = os.path.join(GTEST_ROOT, "include")
if (
not os.path.isfile(os.path.join(gtest_inc, "gtest", "gtest.h"))
or _find_gtest_libdir() is None
):
return f"GoogleTest not found under GTEST_ROOT={GTEST_ROOT}"
if not os.path.isdir("/dev/dri") and not os.path.exists("/dev/kfd"):
return "no AMD GPU device nodes (/dev/kfd, /dev/dri) present"
return None
def _build_bitcode():
"""Emit librccl_device.bc via the standalone bindings/ir Makefile.
Equivalent to building RCCL with -DEMIT_LLVM_IR=ON, but without a full
rebuild: the Makefile reuses the hipify-staged headers an earlier RCCL
build already produced. Returns the build log path; raises on failure.
"""
env = os.environ.copy()
env.update({"ROCM_PATH": ROCM_PATH})
args = [
"make", "-C", IR_DIR,
"EMIT_LLVM_IR=1",
f"BITCODE_LIB_ARCH={ARCH}",
f"BUILDDIR={RCCL_BUILD}",
f"ROCM_PATH={ROCM_PATH}",
"llvm_ir",
]
build_log = os.path.join(LOGDIR, "ir_bitcode_build.log")
with open(build_log, "w") as log:
proc = subprocess.run(
args, env=env, stdout=log, stderr=subprocess.STDOUT,
universal_newlines=True,
)
assert proc.returncode == 0, (
f"Failed to build librccl_device.bc (see {build_log})"
)
assert os.path.isfile(BITCODE), f"Bitcode not produced at {BITCODE}"
return build_log
def _build_test_binary():
"""Compile IR_test.cpp into a GoogleTest binary linked against the bitcode.
Ports the hipcc invocation that previously lived in run_IR_test.sh:
* -Xoffload-linker <bc> routes librccl_device.bc into the AMDGPU
device-side LTO link.
* -plugin-opt=-amdgpu-internalize-symbols=false keeps the exported
device thunks from being re-internalized by AMDGPU LTO.
* -O0 is deliberate: a ROCm 7.x AMDGPU codegen bug breaks the indirect
(vtable) dispatch this test exercises at -O1/-O2 (hang or error 700).
This is a correctness test, not a benchmark, so -O0 is fine.
Returns the build log path; raises on failure.
"""
os.makedirs(IR_OUTDIR, exist_ok=True)
gtest_inc = os.path.join(GTEST_ROOT, "include")
gtest_libdir = _find_gtest_libdir()
args = [
HIPCC,
f"--offload-arch={ARCH}", "-O0",
"-D__HIP_PLATFORM_AMD__=1",
f"-I{HIPIFY_INC}",
f"-I{os.path.join(HIPIFY_INC, 'nccl_device')}",
f"-I{GENERATED_INC}",
f"-I{gtest_inc}",
IR_TEST_SRC,
"-Xoffload-linker", BITCODE,
"-Xoffload-linker", "-plugin-opt=-amdgpu-internalize-symbols=false",
f"-L{gtest_libdir}", "-lgtest_main", "-lgtest", "-lpthread",
"-o", TEST_EXE,
]
build_log = os.path.join(LOGDIR, "ir_test_build.log")
with open(build_log, "w") as log:
log.write("$ " + " ".join(args) + "\n\n")
log.flush()
proc = subprocess.run(
args, env=os.environ.copy(), stdout=log,
stderr=subprocess.STDOUT, universal_newlines=True,
)
assert proc.returncode == 0, f"Failed to compile IR_test (see {build_log})"
assert os.path.isfile(TEST_EXE), f"Test binary not produced at {TEST_EXE}"
return build_log
def _find_rccl_libdir():
"""Return the dir holding the host librccl.so under RCCL_BUILD, or None."""
candidates = [
RCCL_BUILD,
os.path.join(RCCL_BUILD, "lib"),
os.path.join(RCCL_BUILD, "lib64"),
]
for d in candidates:
if glob.glob(os.path.join(d, "librccl.so*")):
return d
return None
def _missing_mpi_prerequisite():
"""Reason string if the MPI GIN functional test cannot be built/run.
Builds on _missing_prerequisite() (hipcc / hipify / gtest / GPU nodes) and
adds the host-library + MPI requirements unique to the multi-rank test. The
GIN-runtime gates (>=2 GPUs, IB GIN, NCCL_GIN_TYPE/CUMEM/INTRANET env) are
enforced inside the test itself via GTEST_SKIP, so they are not duplicated
here — this only covers what is needed to *build and launch* the binary.
"""
base = _missing_prerequisite()
if base:
return base
if not os.path.isfile(GIN_MPI_TEST_SRC):
return f"IR_gin_mpi_test.cpp not found at {GIN_MPI_TEST_SRC}"
if shutil.which("mpirun") is None or shutil.which("mpicxx") is None:
return "MPI not found (mpirun/mpicxx not in PATH)"
if not os.path.isfile(os.path.join(MPI_INC, "mpi.h")):
return f"mpi.h not found at {MPI_INC} (set MPI_INC)"
if _find_rccl_libdir() is None:
return (
f"host librccl.so not found under {RCCL_BUILD} "
"(build RCCL's host library, not just -DEMIT_LLVM_IR=ON)"
)
return None
def _build_gin_mpi_binary():
"""Compile IR_gin_mpi_test.cpp into an MPI GoogleTest binary.
Same device-side bitcode link as IR_test (-Xoffload-linker librccl_device.bc
+ amdgpu-internalize-symbols=false), plus the host RCCL library and MPI.
Returns the build log path; raises on failure.
"""
os.makedirs(IR_OUTDIR, exist_ok=True)
gtest_inc = os.path.join(GTEST_ROOT, "include")
gtest_libdir = _find_gtest_libdir()
rccl_libdir = _find_rccl_libdir()
args = [
HIPCC,
f"--offload-arch={ARCH}", "-O0",
"-D__HIP_PLATFORM_AMD__=1",
f"-I{MPI_INC}",
f"-I{HIPIFY_INC}",
f"-I{os.path.join(HIPIFY_INC, 'nccl_device')}",
f"-I{GENERATED_INC}",
f"-I{gtest_inc}",
GIN_MPI_TEST_SRC,
"-Xoffload-linker", BITCODE,
"-Xoffload-linker", "-plugin-opt=-amdgpu-internalize-symbols=false",
f"-L{rccl_libdir}", f"-Wl,-rpath,{rccl_libdir}", "-lrccl",
"-lmpichcxx", "-lmpich",
f"-L{gtest_libdir}", "-lgtest_main", "-lgtest", "-lpthread",
"-o", GIN_MPI_TEST_EXE,
]
build_log = os.path.join(LOGDIR, "ir_gin_mpi_build.log")
with open(build_log, "w") as log:
log.write("$ " + " ".join(args) + "\n\n")
log.flush()
proc = subprocess.run(
args, env=os.environ.copy(), stdout=log,
stderr=subprocess.STDOUT, universal_newlines=True,
)
assert proc.returncode == 0, (
f"Failed to compile IR_gin_mpi_test (see {build_log})"
)
assert os.path.isfile(GIN_MPI_TEST_EXE), (
f"MPI test binary not produced at {GIN_MPI_TEST_EXE}"
)
return build_log
@pytest.fixture(scope="session")
def ir_gin_mpi_binary():
"""Build the bitcode if needed, then compile the MPI GIN test once.
Skips the whole multi-rank suite when its build/launch prerequisites
(host librccl.so, MPI, hipify staging, GoogleTest, GPU nodes) are missing.
"""
reason = _missing_mpi_prerequisite()
if reason:
pytest.skip(f"IR GIN multi-rank tests skipped: {reason}")
if not os.path.isfile(BITCODE):
logger.info("librccl_device.bc MISSING — building it (arch=%s)...", ARCH)
_build_bitcode()
logger.info("Compiling IR_gin_mpi_test.cpp -> %s ...", GIN_MPI_TEST_EXE)
_build_gin_mpi_binary()
return GIN_MPI_TEST_EXE
@pytest.fixture(scope="session")
def run_gin_mpi_gtest(ir_gin_mpi_binary):
"""Return a helper that launches the MPI GIN test under `mpirun -np N`.
The helper returns (subprocess.CompletedProcess, log_file_path). GIN env
defaults (NCCL_GIN_TYPE=2, NCCL_CUMEM_ENABLE=1, RCCL_ENABLE_INTRANET=1) are
seeded if unset so a single-node 2-GPU run can establish GIN; the binary
itself GTEST_SKIPs cleanly if GIN still cannot initialize. HIP_VISIBLE_DEVICES
is pinned to IR_GIN_GPUS (default the first NRANKS devices) so each rank gets
its own GPU regardless of any ambient single-GPU restriction.
"""
def _run(gtest_filter, log_name):
env = os.environ.copy()
env.setdefault("NCCL_GIN_ENABLE", "1")
env.setdefault("NCCL_GIN_TYPE", "2")
env.setdefault("NCCL_CUMEM_ENABLE", "1")
env.setdefault("RCCL_ENABLE_INTRANET", "1")
env.setdefault("HSA_NO_SCRATCH_RECLAIM", "1")
# Pin GPUs per rank: expose NRANKS distinct devices to the whole run so
# each rank's hipSetDevice(localRank) selects its own. Set explicitly
# (not setdefault) so a restrictive ambient HIP_VISIBLE_DEVICES=0 from
# the shell doesn't collapse every rank onto one GPU and force a skip.
env["HIP_VISIBLE_DEVICES"] = IR_GIN_GPUS
args = [
"mpirun", "-np", str(NRANKS),
ir_gin_mpi_binary,
f"--gtest_filter={gtest_filter}", "--gtest_color=no",
]
log_file = os.path.join(LOGDIR, log_name)
with open(log_file, "w") as log:
log.write("$ " + " ".join(args) + "\n\n")
log.flush()
proc = subprocess.run(
args, env=env, stdout=log, stderr=subprocess.STDOUT,
universal_newlines=True, timeout=300,
)
return proc, log_file
return _run
@pytest.fixture(scope="session")
def ir_test_binary():
"""Build the bitcode if needed, then compile IR_test.cpp against it once.
Skips the entire suite if prerequisites are missing. Returns the path to
the compiled GoogleTest executable.
"""
reason = _missing_prerequisite()
if reason:
pytest.skip(f"IR device tests skipped: {reason}")
# Emit librccl_device.bc on demand (the EMIT_LLVM_IR step) when absent.
if os.path.isfile(BITCODE):
logger.info("librccl_device.bc PRESENT at %s — reusing it", BITCODE)
else:
logger.info(
"librccl_device.bc MISSING at %s — building it (EMIT_LLVM_IR step, "
"arch=%s)...", BITCODE, ARCH)
bc_log = _build_bitcode()
logger.info("librccl_device.bc BUILT at %s (log: %s)", BITCODE, bc_log)
logger.info(
"Compiling IR_test.cpp -> %s (hipcc -O0, arch=%s)...", TEST_EXE, ARCH)
test_log = _build_test_binary()
logger.info("IR_test binary BUILT at %s (log: %s)", TEST_EXE, test_log)
return TEST_EXE
@pytest.fixture(scope="session")
def paths():
return SimpleNamespace(
WORKDIR=WORKDIR,
RCCL_DIR=RCCL_DIR,
RCCL_BUILD=RCCL_BUILD,
ROCM_PATH=ROCM_PATH,
ARCH=ARCH,
GTEST_ROOT=GTEST_ROOT,
IR_OUTDIR=IR_OUTDIR,
BITCODE=BITCODE,
TEST_EXE=TEST_EXE,
LOGDIR=LOGDIR,
)
@pytest.fixture(scope="session")
def run_gtest(ir_test_binary):
"""Return a helper that runs the IR_test binary for a --gtest_filter.
The helper returns (subprocess.CompletedProcess, log_file_path). Output is
tee'd to a log file under logs/ for post-mortem inspection.
"""
def _run(gtest_filter, log_name, gpu=None):
env = os.environ.copy()
env.setdefault("HSA_NO_SCRATCH_RECLAIM", "1")
# Default to a single GPU for determinism unless the caller overrides.
if gpu is not None:
env["HIP_VISIBLE_DEVICES"] = str(gpu)
elif "HIP_VISIBLE_DEVICES" not in env:
env["HIP_VISIBLE_DEVICES"] = "0"
args = [ir_test_binary, f"--gtest_filter={gtest_filter}",
"--gtest_color=no"]
log_file = os.path.join(LOGDIR, log_name)
with open(log_file, "w") as log:
proc = subprocess.run(
args,
env=env,
stdout=log,
stderr=subprocess.STDOUT,
universal_newlines=True,
timeout=120,
)
return proc, log_file
return _run