Files
TheRock/rocm-systems/projects/rccl/test/GinNcclTimeoutMPITests.cpp
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

638 lines
27 KiB
C++

/*************************************************************************
* Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved.
*
* See LICENSE.txt for license information
************************************************************************/
/**
* @file NcclTimeoutMPITests.cpp
* @brief Consolidated MPI coverage for ncclTimeout across all layers (AICOMNET-193).
*
* Covers three test suites in one file:
*
* TimeoutMPITest — host async-error API on a live comm (no device kernel):
* - AsyncErrorRoundTrip: set ncclTimeout → GetAsyncError → clear → AllReduce OK
* - AsyncErrorWithInFlightOp: inject during AllReduce, verify advisory (no corruption)
* - ErrorStringConsistentAcrossRanks: "timeout" string is uniform on all ranks
*
* LsaBarrierTimeoutMPITest — real LSA device barrier timeout (requires NCCL_CUMEM_ENABLE=1):
* - HealthyBarrierReturnsSuccess: all peers arrive → ncclSuccess
* - AbsentPeerProducesTimeout: one peer absent → ncclTimeout
* - ZeroBudgetAbsentPeerTimesOut: budget=0 + absent → immediate ncclTimeout
* - RepeatedHealthyBarriersSucceed: 64 sequential healthy barriers, no drift
* - RecoversAfterTimeout: timeout then fresh healthy barrier succeeds
* - BackToBackTimeouts: 4 sequential stuck barriers, each yields ncclTimeout
*
* GinBarrierTimeoutMPITest — real GIN device barrier timeout
* (requires NCCL_GIN_TYPE=2, NCCL_CUMEM_ENABLE=1):
* - HealthyBarrierReturnsSuccess
* - AbsentPeerProducesTimeout
* - ZeroBudgetAbsentPeerTimesOut
* - RepeatedHealthyBarriersSucceed: 32 sequential healthy barriers
* - RecoversAfterTimeout
* - BackToBackTimeouts
*
* Timing note: clock64() is arch-dependent. Tests use a generous kHealthy budget
* so healthy cases never false-trip, and rely on "absent peer guarantees expiry"
* for timeout cases — never on a precise cycle-to-wallclock mapping.
*/
#include "MPITestBase.hpp"
#include "ResourceGuards.hpp"
#include "TestChecks.hpp"
#include "nccl.h"
#include "nccl_device.h"
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <string>
#include <hip/hip_runtime.h>
#include <mpi.h>
#ifdef MPI_TESTS_ENABLED
using namespace MPITestConstants;
using namespace RCCLTestGuards;
// ============================================================
// Shared helpers
// ============================================================
namespace {
constexpr uint64_t kHealthyTimeoutCycles = 5000000000ULL;
constexpr uint64_t kShortTimeoutCycles = 200000000ULL;
// --- Collective helpers ---
bool allocFill(float** ptr, int n, float val) {
if (hipMalloc(ptr, n * sizeof(float)) != hipSuccess) return false;
float* h = new float[n];
for (int i = 0; i < n; ++i) h[i] = val;
bool ok = (hipMemcpy(*ptr, h, n * sizeof(float), hipMemcpyHostToDevice) == hipSuccess);
delete[] h;
return ok;
}
bool checkAllReduce(float* dBuf, int n, float fillPerRank, int worldSize) {
float* h = new float[n];
bool ok = (hipMemcpy(h, dBuf, n * sizeof(float), hipMemcpyDeviceToHost) == hipSuccess);
if (ok) {
const float expected = fillPerRank * static_cast<float>(worldSize);
for (int i = 0; i < n; ++i) if (h[i] != expected) { ok = false; break; }
}
delete[] h;
return ok;
}
// --- LSA barrier helpers ---
__global__ void lsaBarrierTimeoutKernel(struct ncclDevComm devComm,
uint64_t timeoutCycles, int* outResult) {
ncclLsaBarrierSession<ncclCoopCta> bar{
ncclCoopCta(), devComm, ncclTeamTagLsa(), /*index=*/0u};
ncclResult_t r = bar.sync(ncclCoopCta(), cuda::memory_order_relaxed, timeoutCycles);
if (threadIdx.x == 0 && blockIdx.x == 0) *outResult = static_cast<int>(r);
}
__global__ void lsaRepeatedBarrierKernel(struct ncclDevComm devComm,
uint64_t timeoutCycles, int iters, int* outCount) {
int successes = 0;
for (int i = 0; i < iters; ++i) {
ncclLsaBarrierSession<ncclCoopCta> bar{
ncclCoopCta(), devComm, ncclTeamTagLsa(), /*index=*/0u};
if (bar.sync(ncclCoopCta(), cuda::memory_order_relaxed, timeoutCycles) == ncclSuccess)
++successes;
}
if (threadIdx.x == 0 && blockIdx.x == 0) *outCount = successes;
}
ncclResult_t createLsaDevComm(ncclComm_t comm, int nBarriers, ncclDevComm* out) {
ncclDevCommRequirements reqs = NCCL_DEV_COMM_REQUIREMENTS_INITIALIZER;
reqs.lsaBarrierCount = nBarriers;
return ncclDevCommCreate(comm, &reqs, out);
}
int runLsaBarrier(ncclDevComm devComm, hipStream_t stream, uint64_t timeoutCycles) {
int* dResult = nullptr;
if (hipMalloc(&dResult, sizeof(int)) != hipSuccess) return -100;
int sentinel = -1;
(void)hipMemcpy(dResult, &sentinel, sizeof(int), hipMemcpyHostToDevice);
lsaBarrierTimeoutKernel<<<1, 256, 0, stream>>>(devComm, timeoutCycles, dResult);
(void)hipStreamSynchronize(stream);
int hResult = -1;
(void)hipMemcpy(&hResult, dResult, sizeof(int), hipMemcpyDeviceToHost);
(void)hipFree(dResult);
return hResult;
}
// --- GIN barrier helpers ---
std::string ginBarrierSkipReason() {
if (const char* e = std::getenv("NCCL_GIN_ENABLE"); e && std::strcmp(e, "0") == 0)
return "GIN explicitly disabled (NCCL_GIN_ENABLE=0)";
const char* ginType = std::getenv("NCCL_GIN_TYPE");
if (!ginType || std::atoi(ginType) != 2)
return "NCCL_GIN_TYPE=2 required";
const char* cumem = std::getenv("NCCL_CUMEM_ENABLE");
if (!cumem || std::strcmp(cumem, "1") != 0)
return "NCCL_CUMEM_ENABLE=1 required";
// Single-node needs intranet mode.
MPI_Comm nodeComm;
MPI_Comm_split_type(MPI_COMM_WORLD, MPI_COMM_TYPE_SHARED, 0, MPI_INFO_NULL, &nodeComm);
int nodeSize = 0, worldSize = 0;
MPI_Comm_size(nodeComm, &nodeSize);
MPI_Comm_size(MPI_COMM_WORLD, &worldSize);
MPI_Comm_free(&nodeComm);
if (nodeSize == worldSize) {
if (const char* e = std::getenv("RCCL_ENABLE_INTRANET"); !e || std::strcmp(e, "1") != 0)
return "Single-node: RCCL_ENABLE_INTRANET=1 required";
}
return "";
}
__global__ void ginBarrierTimeoutKernel(struct ncclDevComm devComm,
uint64_t timeoutCycles, int* outResult) {
ncclGin gin{devComm, /*ginContext=*/0};
ncclGinBarrierSession<ncclCoopCta> bar{
ncclCoopCta(), gin, ncclTeamTagRail{}, /*index=*/0u};
ncclResult_t r = bar.sync(ncclCoopCta(), cuda::memory_order_relaxed,
ncclGinFenceLevel::Relaxed, timeoutCycles);
if (threadIdx.x == 0 && blockIdx.x == 0) *outResult = static_cast<int>(r);
}
__global__ void ginRepeatedBarrierKernel(struct ncclDevComm devComm,
uint64_t timeoutCycles, int iters, int* outCount) {
ncclGin gin{devComm, /*ginContext=*/0};
int successes = 0;
for (int i = 0; i < iters; ++i) {
ncclGinBarrierSession<ncclCoopCta> bar{
ncclCoopCta(), gin, ncclTeamTagRail{}, /*index=*/0u};
if (bar.sync(ncclCoopCta(), cuda::memory_order_relaxed,
ncclGinFenceLevel::Relaxed, timeoutCycles) == ncclSuccess)
++successes;
}
if (threadIdx.x == 0 && blockIdx.x == 0) *outCount = successes;
}
ncclResult_t createGinDevComm(ncclComm_t comm, int nBarriers, ncclDevComm* out) {
ncclDevCommRequirements reqs = NCCL_DEV_COMM_REQUIREMENTS_INITIALIZER;
reqs.railGinBarrierCount = nBarriers;
reqs.ginConnectionType = NCCL_GIN_CONNECTION_RAIL;
return ncclDevCommCreate(comm, &reqs, out);
}
int runGinBarrier(ncclDevComm devComm, hipStream_t stream, uint64_t timeoutCycles) {
int* dResult = nullptr;
if (hipMalloc(&dResult, sizeof(int)) != hipSuccess) return -100;
int sentinel = -1;
(void)hipMemcpy(dResult, &sentinel, sizeof(int), hipMemcpyHostToDevice);
ginBarrierTimeoutKernel<<<1, 256, 0, stream>>>(devComm, timeoutCycles, dResult);
(void)hipStreamSynchronize(stream);
int hResult = -1;
(void)hipMemcpy(&hResult, dResult, sizeof(int), hipMemcpyDeviceToHost);
(void)hipFree(dResult);
return hResult;
}
} // namespace
// ============================================================
// TimeoutMPITest — host async-error API (3 tests)
// ============================================================
class TimeoutMPITest : public MPITestBase {};
// Set ncclTimeout, read it back, clear, verify AllReduce still works.
// Also tests advisory property: injecting during AllReduce doesn't corrupt data.
TEST_F(TimeoutMPITest, AsyncErrorRoundTrip)
{
ASSERT_TRUE(validateTestPrerequisites(2, kNoProcessLimit, kNoPowerOfTwoRequired, 1, kNoNodeLimit));
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
int worldSize = 0;
MPI_Comm_size(MPI_COMM_WORLD, &worldSize);
// Set + verify + clear.
ASSERT_MPI_EQ(ncclSuccess, ncclCommSetAsyncError(comm, ncclTimeout));
ncclResult_t observed = ncclSuccess;
ASSERT_MPI_EQ(ncclSuccess, ncclCommGetAsyncError(comm, &observed));
ASSERT_MPI_EQ(ncclTimeout, observed);
ASSERT_MPI_TRUE(std::strcmp(ncclGetErrorString(observed), "timeout") == 0);
ASSERT_MPI_EQ(ncclSuccess, ncclCommSetAsyncError(comm, ncclSuccess));
MPI_Barrier(MPI_COMM_WORLD);
// Comm must be functional after clearing.
const int n = 1024;
float* sendD = nullptr; float* recvD = nullptr;
ASSERT_TRUE(allocFill(&sendD, n, 1.0f));
ASSERT_TRUE(allocFill(&recvD, n, 0.0f));
SCOPE_EXIT(if (sendD)(void)hipFree(sendD); if (recvD)(void)hipFree(recvD));
ASSERT_MPI_EQ(ncclSuccess, ncclAllReduce(sendD, recvD, n, ncclFloat, ncclSum, comm, stream));
ASSERT_EQ(hipSuccess, hipStreamSynchronize(stream));
ASSERT_MPI_TRUE(checkAllReduce(recvD, n, 1.0f, worldSize));
ncclResult_t after = ncclTimeout;
ASSERT_MPI_EQ(ncclSuccess, ncclCommGetAsyncError(comm, &after));
ASSERT_MPI_EQ(ncclSuccess, after);
}
// ncclTimeout injected while AllReduce is in-flight: advisory only,
// does not corrupt result, clearable, comm healthy afterwards.
TEST_F(TimeoutMPITest, AsyncErrorWithInFlightOp)
{
ASSERT_TRUE(validateTestPrerequisites(2, kNoProcessLimit, kNoPowerOfTwoRequired, 1, kNoNodeLimit));
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
int worldSize = 0;
MPI_Comm_size(MPI_COMM_WORLD, &worldSize);
const int n = 64 * 1024;
float* sendD = nullptr; float* recvD = nullptr;
ASSERT_TRUE(allocFill(&sendD, n, 2.0f));
ASSERT_TRUE(allocFill(&recvD, n, 0.0f));
SCOPE_EXIT(if (sendD)(void)hipFree(sendD); if (recvD)(void)hipFree(recvD));
ASSERT_MPI_EQ(ncclSuccess, ncclAllReduce(sendD, recvD, n, ncclFloat, ncclSum, comm, stream));
ASSERT_MPI_EQ(ncclSuccess, ncclCommSetAsyncError(comm, ncclTimeout)); // injected mid-flight
ASSERT_EQ(hipSuccess, hipStreamSynchronize(stream));
// Data correctness: advisory error must not corrupt the collective result.
ASSERT_MPI_TRUE(checkAllReduce(recvD, n, 2.0f, worldSize));
// Error visible, then clearable.
ncclResult_t state = ncclSuccess;
ASSERT_MPI_EQ(ncclSuccess, ncclCommGetAsyncError(comm, &state));
ASSERT_MPI_EQ(ncclTimeout, state);
ASSERT_MPI_EQ(ncclSuccess, ncclCommSetAsyncError(comm, ncclSuccess));
MPI_Barrier(MPI_COMM_WORLD);
ncclResult_t clean = ncclTimeout;
ASSERT_MPI_EQ(ncclSuccess, ncclCommGetAsyncError(comm, &clean));
ASSERT_MPI_EQ(ncclSuccess, clean);
}
// "timeout" string must be identical on every rank/node.
TEST_F(TimeoutMPITest, ErrorStringConsistentAcrossRanks)
{
ASSERT_TRUE(validateTestPrerequisites(2, kNoProcessLimit, kNoPowerOfTwoRequired, 1, kNoNodeLimit));
const char* s = ncclGetErrorString(ncclTimeout);
ASSERT_MPI_TRUE(s != nullptr && std::strcmp(s, "timeout") == 0);
// Verify every rank produced the same string length via AllReduce(min/max).
// If any rank returns a different string, min != max and the test fails.
int len = static_cast<int>(std::strlen(s));
int lenMin = len, lenMax = len;
MPI_Allreduce(MPI_IN_PLACE, &lenMin, 1, MPI_INT, MPI_MIN, MPI_COMM_WORLD);
MPI_Allreduce(MPI_IN_PLACE, &lenMax, 1, MPI_INT, MPI_MAX, MPI_COMM_WORLD);
ASSERT_MPI_EQ(lenMin, lenMax);
}
// ============================================================
// LsaBarrierTimeoutMPITest — real LSA device barrier (6 tests)
// ============================================================
class LsaBarrierTimeoutMPITest : public MPITestBase {
protected:
std::string skipReason_;
bool setUpLsaDevComm(int nBarriers, ncclComm_t* commOut,
hipStream_t* streamOut, ncclDevComm* devCommOut) {
skipReason_.clear();
if (!validateTestPrerequisites(2, kNoProcessLimit, kNoPowerOfTwoRequired, 1, kNoNodeLimit)) {
ADD_FAILURE() << "Test requires at least 2 MPI processes"; return false;
}
if (createTestCommunicator() != ncclSuccess) {
ADD_FAILURE() << "createTestCommunicator failed"; return false;
}
ncclComm_t comm = getActiveCommunicator();
ncclResult_t devRc = createLsaDevComm(comm, nBarriers, devCommOut);
if (devRc != ncclSuccess) {
skipReason_ = std::string("LSA devComm requires symmetric memory (cuMem, kernel >= 6.8); "
"ncclDevCommCreate returned ") + ncclGetErrorString(devRc);
return false;
}
*commOut = comm; *streamOut = getActiveStream(); return true;
}
};
#define LSA_SETUP_OR_BAIL() \
do { if (!skipReason_.empty()) GTEST_SKIP() << skipReason_; return; } while (0)
TEST_F(LsaBarrierTimeoutMPITest, HealthyBarrierReturnsSuccess)
{
ncclComm_t comm{}; hipStream_t stream{}; ncclDevComm devComm{};
if (!setUpLsaDevComm(1, &comm, &stream, &devComm)) LSA_SETUP_OR_BAIL();
SCOPE_EXIT((void)ncclDevCommDestroy(comm, &devComm));
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_EQ(static_cast<int>(ncclSuccess), runLsaBarrier(devComm, stream, kHealthyTimeoutCycles));
}
TEST_F(LsaBarrierTimeoutMPITest, AbsentPeerProducesTimeout)
{
ncclComm_t comm{}; hipStream_t stream{}; ncclDevComm devComm{};
if (!setUpLsaDevComm(1, &comm, &stream, &devComm)) LSA_SETUP_OR_BAIL();
SCOPE_EXIT((void)ncclDevCommDestroy(comm, &devComm));
int rank = -1, nRanks = -1;
ncclCommUserRank(comm, &rank); ncclCommCount(comm, &nRanks);
// The single-absent-peer timeout only starves reliably with a 2-rank LSA team;
// at N>2 the all-to-all inbox + rolling-epoch wait can complete, so skip instead.
// Decide collectively (max LSA size across the job) so every rank skips or runs
// together — a per-rank skip would deadlock the MPI_Barrier below.
ncclTeam_t lsaTeam = ncclTeamLsa(comm);
int maxLsaNRanks = lsaTeam.nRanks;
MPI_Allreduce(MPI_IN_PLACE, &maxLsaNRanks, 1, MPI_INT, MPI_MAX, MPI_COMM_WORLD);
if (maxLsaNRanks != 2) {
GTEST_SKIP() << "LSA absent-peer timeout requires a 2-rank LSA team; got "
<< maxLsaNRanks << ". Run with 2 ranks sharing one LSA domain.";
}
const bool isAbsent = (lsaTeam.rank == lsaTeam.nRanks - 1);
int hResult = static_cast<int>(ncclSuccess);
if (!isAbsent) {
hResult = runLsaBarrier(devComm, stream, kShortTimeoutCycles);
EXPECT_EQ(static_cast<int>(ncclTimeout), hResult)
<< "LSA rank " << lsaTeam.rank << " expected ncclTimeout, got "
<< ncclGetErrorString(static_cast<ncclResult_t>(hResult));
}
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_TRUE((isAbsent || hResult == static_cast<int>(ncclTimeout)) ? 1 : 0);
}
TEST_F(LsaBarrierTimeoutMPITest, ZeroBudgetAbsentPeerTimesOut)
{
ncclComm_t comm{}; hipStream_t stream{}; ncclDevComm devComm{};
if (!setUpLsaDevComm(1, &comm, &stream, &devComm)) LSA_SETUP_OR_BAIL();
SCOPE_EXIT((void)ncclDevCommDestroy(comm, &devComm));
int rank = -1, nRanks = -1;
ncclCommUserRank(comm, &rank); ncclCommCount(comm, &nRanks);
ncclTeam_t lsaTeam = ncclTeamLsa(comm);
// The single-absent-peer timeout only starves reliably with a 2-rank LSA team;
// at N>2 the all-to-all inbox + rolling-epoch wait can complete, so skip instead.
if (lsaTeam.nRanks != 2) {
GTEST_SKIP() << "LSA absent-peer timeout requires a 2-rank LSA team; got "
<< lsaTeam.nRanks << ". Run with 2 ranks sharing one LSA domain.";
}
const bool isAbsent = (lsaTeam.rank == lsaTeam.nRanks - 1);
int hResult = static_cast<int>(ncclSuccess);
if (!isAbsent) {
hResult = runLsaBarrier(devComm, stream, /*timeoutCycles=*/0ULL);
EXPECT_EQ(static_cast<int>(ncclTimeout), hResult)
<< "LSA rank " << lsaTeam.rank << " expected immediate ncclTimeout, got "
<< ncclGetErrorString(static_cast<ncclResult_t>(hResult));
}
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_TRUE((isAbsent || hResult == static_cast<int>(ncclTimeout)) ? 1 : 0);
}
TEST_F(LsaBarrierTimeoutMPITest, RepeatedHealthyBarriersSucceed)
{
ncclComm_t comm{}; hipStream_t stream{}; ncclDevComm devComm{};
if (!setUpLsaDevComm(1, &comm, &stream, &devComm)) LSA_SETUP_OR_BAIL();
SCOPE_EXIT((void)ncclDevCommDestroy(comm, &devComm));
constexpr int kIters = 64;
int* dCount = nullptr;
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&dCount, sizeof(int)));
SCOPE_EXIT(if (dCount)(void)hipFree(dCount));
int zero = 0;
HIP_TEST_CHECK_GTEST_FAIL(hipMemcpy(dCount, &zero, sizeof(int), hipMemcpyHostToDevice));
MPI_Barrier(MPI_COMM_WORLD);
lsaRepeatedBarrierKernel<<<1, 256, 0, stream>>>(devComm, kHealthyTimeoutCycles, kIters, dCount);
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
int hCount = -1;
HIP_TEST_CHECK_GTEST_FAIL(hipMemcpy(&hCount, dCount, sizeof(int), hipMemcpyDeviceToHost));
ASSERT_MPI_EQ(kIters, hCount);
}
TEST_F(LsaBarrierTimeoutMPITest, RecoversAfterTimeout)
{
ncclComm_t comm{}; hipStream_t stream{}; ncclDevComm devComm{};
if (!setUpLsaDevComm(1, &comm, &stream, &devComm)) LSA_SETUP_OR_BAIL();
SCOPE_EXIT((void)ncclDevCommDestroy(comm, &devComm));
int rank = -1, nRanks = -1;
ncclCommUserRank(comm, &rank); ncclCommCount(comm, &nRanks);
ncclTeam_t lsaTeam = ncclTeamLsa(comm);
// The single-absent-peer timeout only starves reliably with a 2-rank LSA team;
// at N>2 the all-to-all inbox + rolling-epoch wait can complete, so skip instead.
if (lsaTeam.nRanks != 2) {
GTEST_SKIP() << "LSA absent-peer timeout requires a 2-rank LSA team; got "
<< lsaTeam.nRanks << ". Run with 2 ranks sharing one LSA domain.";
}
const bool isAbsent = (lsaTeam.rank == lsaTeam.nRanks - 1);
if (!isAbsent) {
EXPECT_EQ(static_cast<int>(ncclTimeout),
runLsaBarrier(devComm, stream, kShortTimeoutCycles));
}
MPI_Barrier(MPI_COMM_WORLD);
ncclDevComm devComm2{};
ASSERT_MPI_EQ(ncclSuccess, createLsaDevComm(comm, 1, &devComm2));
SCOPE_EXIT((void)ncclDevCommDestroy(comm, &devComm2));
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_EQ(static_cast<int>(ncclSuccess),
runLsaBarrier(devComm2, stream, kHealthyTimeoutCycles));
}
TEST_F(LsaBarrierTimeoutMPITest, BackToBackTimeouts)
{
ncclComm_t comm{}; hipStream_t stream{}; ncclDevComm devComm{};
if (!setUpLsaDevComm(1, &comm, &stream, &devComm)) LSA_SETUP_OR_BAIL();
SCOPE_EXIT((void)ncclDevCommDestroy(comm, &devComm));
int rank = -1, nRanks = -1;
ncclCommUserRank(comm, &rank); ncclCommCount(comm, &nRanks);
ncclTeam_t lsaTeam = ncclTeamLsa(comm);
// The single-absent-peer timeout only starves reliably with a 2-rank LSA team;
// at N>2 the all-to-all inbox + rolling-epoch wait can complete, so skip instead.
if (lsaTeam.nRanks != 2) {
GTEST_SKIP() << "LSA absent-peer timeout requires a 2-rank LSA team; got "
<< lsaTeam.nRanks << ". Run with 2 ranks sharing one LSA domain.";
}
const bool isAbsent = (lsaTeam.rank == lsaTeam.nRanks - 1);
constexpr int kRounds = 4;
int timeouts = 0;
for (int i = 0; i < kRounds; ++i) {
ncclDevComm dc{};
if (createLsaDevComm(comm, 1, &dc) != ncclSuccess) break;
if (!isAbsent) {
int r = runLsaBarrier(dc, stream, kShortTimeoutCycles);
if (r == static_cast<int>(ncclTimeout)) ++timeouts;
}
MPI_Barrier(MPI_COMM_WORLD);
(void)ncclDevCommDestroy(comm, &dc);
MPI_Barrier(MPI_COMM_WORLD);
}
if (!isAbsent) EXPECT_EQ(kRounds, timeouts);
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_TRUE((isAbsent || timeouts == kRounds) ? 1 : 0);
}
// ============================================================
// GinBarrierTimeoutMPITest — real GIN device barrier (6 tests)
// ============================================================
class GinBarrierTimeoutMPITest : public MPITestBase {
protected:
std::string skipReason_;
bool setUpGinDevComm(int nBarriers, ncclComm_t* commOut,
hipStream_t* streamOut, ncclDevComm* devCommOut) {
skipReason_.clear();
if (auto reason = ginBarrierSkipReason(); !reason.empty()) {
skipReason_ = reason; return false;
}
if (!validateTestPrerequisites(2, kNoProcessLimit, kNoPowerOfTwoRequired, 1, kNoNodeLimit)) {
ADD_FAILURE() << "Test requires at least 2 MPI processes"; return false;
}
if (createTestCommunicator() != ncclSuccess) {
ADD_FAILURE() << "createTestCommunicator failed"; return false;
}
ncclComm_t comm = getActiveCommunicator();
ncclResult_t devRc = createGinDevComm(comm, nBarriers, devCommOut);
if (devRc != ncclSuccess) {
skipReason_ = std::string("GIN devComm unavailable; ncclDevCommCreate returned ")
+ ncclGetErrorString(devRc);
return false;
}
*commOut = comm; *streamOut = getActiveStream(); return true;
}
};
#define GIN_SETUP_OR_BAIL() \
do { if (!skipReason_.empty()) GTEST_SKIP() << skipReason_; return; } while (0)
TEST_F(GinBarrierTimeoutMPITest, HealthyBarrierReturnsSuccess)
{
ncclComm_t comm{}; hipStream_t stream{}; ncclDevComm devComm{};
if (!setUpGinDevComm(1, &comm, &stream, &devComm)) GIN_SETUP_OR_BAIL();
SCOPE_EXIT((void)ncclDevCommDestroy(comm, &devComm));
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_EQ(static_cast<int>(ncclSuccess), runGinBarrier(devComm, stream, kHealthyTimeoutCycles));
}
TEST_F(GinBarrierTimeoutMPITest, AbsentPeerProducesTimeout)
{
ncclComm_t comm{}; hipStream_t stream{}; ncclDevComm devComm{};
if (!setUpGinDevComm(1, &comm, &stream, &devComm)) GIN_SETUP_OR_BAIL();
SCOPE_EXIT((void)ncclDevCommDestroy(comm, &devComm));
ncclTeam_t railTeam = ncclTeamRail(comm);
const bool isAbsent = (railTeam.rank == railTeam.nRanks - 1);
int hResult = static_cast<int>(ncclSuccess);
if (!isAbsent) {
hResult = runGinBarrier(devComm, stream, kShortTimeoutCycles);
EXPECT_EQ(static_cast<int>(ncclTimeout), hResult)
<< "Rail rank " << railTeam.rank << " expected ncclTimeout, got "
<< ncclGetErrorString(static_cast<ncclResult_t>(hResult));
(void)hipStreamSynchronize(stream);
}
MPI_Barrier(MPI_COMM_WORLD);
if (isAbsent) {
(void)runGinBarrier(devComm, stream, /*timeoutCycles=*/0ULL);
(void)hipStreamSynchronize(stream);
}
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_TRUE((isAbsent || hResult == static_cast<int>(ncclTimeout)) ? 1 : 0);
}
TEST_F(GinBarrierTimeoutMPITest, ZeroBudgetAbsentPeerTimesOut)
{
ncclComm_t comm{}; hipStream_t stream{}; ncclDevComm devComm{};
if (!setUpGinDevComm(1, &comm, &stream, &devComm)) GIN_SETUP_OR_BAIL();
SCOPE_EXIT((void)ncclDevCommDestroy(comm, &devComm));
ncclTeam_t railTeam = ncclTeamRail(comm);
const bool isAbsent = (railTeam.rank == railTeam.nRanks - 1);
int hResult = static_cast<int>(ncclSuccess);
if (!isAbsent) {
hResult = runGinBarrier(devComm, stream, /*timeoutCycles=*/0ULL);
EXPECT_EQ(static_cast<int>(ncclTimeout), hResult)
<< "Rail rank " << railTeam.rank << " expected immediate ncclTimeout, got "
<< ncclGetErrorString(static_cast<ncclResult_t>(hResult));
(void)hipStreamSynchronize(stream);
}
MPI_Barrier(MPI_COMM_WORLD);
if (isAbsent) {
(void)runGinBarrier(devComm, stream, 0ULL);
(void)hipStreamSynchronize(stream);
}
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_TRUE((isAbsent || hResult == static_cast<int>(ncclTimeout)) ? 1 : 0);
}
TEST_F(GinBarrierTimeoutMPITest, RepeatedHealthyBarriersSucceed)
{
ncclComm_t comm{}; hipStream_t stream{}; ncclDevComm devComm{};
if (!setUpGinDevComm(1, &comm, &stream, &devComm)) GIN_SETUP_OR_BAIL();
SCOPE_EXIT((void)ncclDevCommDestroy(comm, &devComm));
constexpr int kIters = 32;
int* dCount = nullptr;
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&dCount, sizeof(int)));
SCOPE_EXIT(if (dCount)(void)hipFree(dCount));
int zero = 0;
HIP_TEST_CHECK_GTEST_FAIL(hipMemcpy(dCount, &zero, sizeof(int), hipMemcpyHostToDevice));
MPI_Barrier(MPI_COMM_WORLD);
ginRepeatedBarrierKernel<<<1, 256, 0, stream>>>(devComm, kHealthyTimeoutCycles, kIters, dCount);
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
int hCount = -1;
HIP_TEST_CHECK_GTEST_FAIL(hipMemcpy(&hCount, dCount, sizeof(int), hipMemcpyDeviceToHost));
ASSERT_MPI_EQ(kIters, hCount);
}
TEST_F(GinBarrierTimeoutMPITest, RecoversAfterTimeout)
{
ncclComm_t comm{}; hipStream_t stream{}; ncclDevComm devComm{};
if (!setUpGinDevComm(1, &comm, &stream, &devComm)) GIN_SETUP_OR_BAIL();
SCOPE_EXIT((void)ncclDevCommDestroy(comm, &devComm));
ncclTeam_t railTeam = ncclTeamRail(comm);
const bool isAbsent = (railTeam.rank == railTeam.nRanks - 1);
if (!isAbsent) {
EXPECT_EQ(static_cast<int>(ncclTimeout),
runGinBarrier(devComm, stream, kShortTimeoutCycles));
(void)hipStreamSynchronize(stream);
}
MPI_Barrier(MPI_COMM_WORLD);
if (isAbsent) {
(void)runGinBarrier(devComm, stream, 0ULL);
(void)hipStreamSynchronize(stream);
}
MPI_Barrier(MPI_COMM_WORLD);
ncclDevComm devComm2{};
ASSERT_MPI_EQ(ncclSuccess, createGinDevComm(comm, 1, &devComm2));
SCOPE_EXIT((void)ncclDevCommDestroy(comm, &devComm2));
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_EQ(static_cast<int>(ncclSuccess),
runGinBarrier(devComm2, stream, kHealthyTimeoutCycles));
}
TEST_F(GinBarrierTimeoutMPITest, BackToBackTimeouts)
{
ncclComm_t comm{}; hipStream_t stream{}; ncclDevComm devComm{};
if (!setUpGinDevComm(1, &comm, &stream, &devComm)) GIN_SETUP_OR_BAIL();
SCOPE_EXIT((void)ncclDevCommDestroy(comm, &devComm));
ncclTeam_t railTeam = ncclTeamRail(comm);
const bool isAbsent = (railTeam.rank == railTeam.nRanks - 1);
constexpr int kRounds = 4;
int timeouts = 0;
for (int i = 0; i < kRounds; ++i) {
MPI_Barrier(MPI_COMM_WORLD);
ncclDevComm dc{};
if (createGinDevComm(comm, 1, &dc) != ncclSuccess) break;
if (!isAbsent) {
int r = runGinBarrier(dc, stream, kShortTimeoutCycles);
if (r == static_cast<int>(ncclTimeout)) ++timeouts;
(void)hipStreamSynchronize(stream);
}
MPI_Barrier(MPI_COMM_WORLD);
if (isAbsent) {
(void)runGinBarrier(dc, stream, 0ULL);
(void)hipStreamSynchronize(stream);
}
MPI_Barrier(MPI_COMM_WORLD);
(void)ncclDevCommDestroy(comm, &dc);
MPI_Barrier(MPI_COMM_WORLD);
}
if (!isAbsent) EXPECT_EQ(kRounds, timeouts);
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_TRUE((isAbsent || timeouts == kRounds) ? 1 : 0);
}
#endif // MPI_TESTS_ENABLED