# ************************************************************************* # * Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved. # * # * See LICENSE.txt for license information # ************************************************************************ """Functional tests for librccl_device.bc, driven through the GoogleTest binaries built from bindings/ir/test/IR_test.cpp (single process) and bindings/ir/test/IR_gin_mpi_test.cpp (multi-rank, under mpirun). Each pytest case runs a binary restricted to a single --gtest_filter so failures map cleanly to the underlying device API. Outcomes are mapped by `_summarize_gtest` so the pytest result mirrors what GoogleTest actually did: * any failure / non-zero exit -> pytest FAILED * at least one real pass -> pytest PASSED * clean run where every case SKIPPED -> pytest SKIPPED (with the reason) That last rule matters for the GIN/composite cases: when the host lacks the hardware/runtime to actually exercise a barrier (e.g. <2 GPUs, or no cuMem / symmetric-memory support), GoogleTest SKIPs and exits 0 — without this mapping those would misleadingly show as PASSED. """ import re import pytest def _summarize_gtest(proc, log, what): """Translate a GoogleTest binary run into a pytest outcome. Reads the captured log so we can tell a genuine pass from a run in which every selected case was SKIPPED (both exit 0). """ try: with open(log) as fh: text = fh.read() except OSError: text = "" if proc.returncode != 0 or "[ FAILED ]" in text: raise AssertionError(f"{what} failed, see {log}") def _count(pattern): m = re.search(pattern, text) return int(m.group(1)) if m else 0 passed = _count(r"\[ PASSED \] (\d+) test") skipped = _count(r"\[ SKIPPED \] (\d+) test") # Nothing passed but something skipped -> surface a real pytest SKIP with # the GoogleTest skip reason (the line printed right after ": Skipped"). if passed == 0 and skipped > 0: m = re.search(r": Skipped\s*\n([^\[]+)", text) reason = " ".join(m.group(1).split()) if m else "all cases skipped" pytest.skip(f"{what}: {reason} (see {log})") @pytest.mark.ir_device @pytest.mark.peer_pointer def test_get_peer_pointer_team(run_gtest): """[A] ncclGetPeerPointerTeam — 20 pointer-arithmetic cases.""" proc, log = run_gtest("IRDeviceTest.A_GetPeerPointerTeam", "ir_peer_pointer.log") _summarize_gtest(proc, log, "ncclGetPeerPointerTeam") @pytest.mark.ir_device @pytest.mark.coop def test_coop_init_and_accessors(run_gtest): """[B1-B5] ncclCoopAnyInit* + ncclCoopSize/ThreadRank/NumThreads.""" proc, log = run_gtest( "IRDeviceTest.B1_*:IRDeviceTest.B2_*:IRDeviceTest.B3*:" "IRDeviceTest.B4*:IRDeviceTest.B5_*", "ir_coop_init.log", ) _summarize_gtest(proc, log, "ncclCoopAny init/accessors") @pytest.mark.ir_device @pytest.mark.coop def test_coop_sync(run_gtest): """[B6] ncclCoopSync — all five coop types complete.""" proc, log = run_gtest("IRDeviceTest.B6_CoopSync", "ir_coop_sync.log") _summarize_gtest(proc, log, "ncclCoopSync") @pytest.mark.ir_device @pytest.mark.lsa_barrier def test_lsa_barrier_session(run_gtest): """[B7] ncclLsaBarrierSession structural check (+ runtime cases skipped).""" proc, log = run_gtest("IRDeviceTest.B7*", "ir_lsa_barrier.log") _summarize_gtest(proc, log, "ncclLsaBarrierSession") @pytest.mark.ir_device @pytest.mark.gin_barrier @pytest.mark.composite_barrier def test_bucket_c_symbols_linkable(run_gtest): """[C0] All four bucket-C thunks resolve/export from librccl_device.bc. Takes the device address of ncclGinBarrierSession{Init,Sync} and ncclBarrierSession{Init,Sync}; a missing/renamed/internalized export fails the device link or yields a null address. No live resources needed. """ proc, log = run_gtest("IRDeviceTest.C0_BucketCSymbolsLinkable", "ir_bucket_c_symbols.log") _summarize_gtest(proc, log, "bucket C symbol link coverage") @pytest.mark.ir_device @pytest.mark.gin_barrier def test_gin_barrier_session(run_gtest): """[C1] ncclGinBarrierSession structural check (+ runtime case skipped).""" proc, log = run_gtest("IRDeviceTest.C1*", "ir_gin_barrier.log") _summarize_gtest(proc, log, "ncclGinBarrierSession") @pytest.mark.ir_device @pytest.mark.composite_barrier def test_composite_barrier_session(run_gtest): """[C2] composite ncclBarrierSession structural check (+ runtime skipped).""" proc, log = run_gtest("IRDeviceTest.C2*", "ir_composite_barrier.log") _summarize_gtest(proc, log, "ncclBarrierSession") @pytest.mark.ir_device @pytest.mark.gin_barrier @pytest.mark.mpi def test_gin_barrier_session_functional_mpi(run_gin_mpi_gtest): """[C1 functional] ncclGinBarrierSession{Init,Sync} over a live 2-rank GIN rail barrier, under `mpirun -np 2`. Reports SKIPPED (not PASSED) when the host can't actually run it — e.g. fewer than 2 GPUs, or no cuMem/symmetric-memory support. """ proc, log = run_gin_mpi_gtest( "IRGinBarrierMPITest.C1_GinBarrierSession_TwoRanks", "ir_gin_barrier_mpi.log", ) _summarize_gtest(proc, log, "GIN barrier functional (2-rank)") @pytest.mark.ir_device @pytest.mark.composite_barrier @pytest.mark.mpi def test_composite_barrier_session_functional_mpi(run_gin_mpi_gtest): """[C2 functional] composite ncclBarrierSession{Init,Sync} (inner LSA + outer GIN) over a live 2-rank comm. Same skip semantics as the GIN case. """ proc, log = run_gin_mpi_gtest( "IRGinBarrierMPITest.C2_CompositeBarrierSession_TwoRanks", "ir_composite_barrier_mpi.log", ) _summarize_gtest(proc, log, "composite barrier functional (2-rank)")