Files
TheRock/rocm-systems/projects/rccl/test/transport/NetIbMPI/FaultInjectTests.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

3228 lines
133 KiB
C++

/*************************************************************************
* Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved.
*
* See LICENSE.txt for license information
************************************************************************/
#include "NetIbMPITestBase.hpp"
#include "NetIbCastInspect.hpp"
#include "NetIbFaultInject.hpp"
#include <cerrno> // EAGAIN
#if defined(MPI_TESTS_ENABLED) && defined(ENABLE_FAULT_INJECTION)
// IB WC status codes used by FailoverErrorCodeWhitelist test.
// Defined here as constants to avoid depending on infiniband/verbs.h in tests.
static constexpr int kWcWrFlushErr = 5; // IBV_WC_WR_FLUSH_ERR
static constexpr int kWcRetryExcErr = 12; // IBV_WC_RETRY_EXC_ERR
static constexpr int kWcRemAccessErr = 10; // IBV_WC_REM_ACCESS_ERR
static constexpr int kWcGeneralErr = 22; // IBV_WC_GENERAL_ERR
// Resiliency device state constants (from ncclIbResiliencyDevState enum).
static constexpr int kDevStateOk = 0; // ncclIbResiliencyDevStateOk
static constexpr int kDevStateRecoveryInProgress = 2; // ncclIbResiliencyDevStateRecoveryInProgress
static constexpr int kDevStateRecoveryFailed = 3; // ncclIbResiliencyDevStateRecoveryFailed
static constexpr int kDevStateRecovered = 4; // ncclIbResiliencyDevStateRecovered
static constexpr int kDevStateErrorPermanent = 5; // ncclIbResiliencyDevStateErrorPermanent
// Helper: Create a NIC Fusion merged device from physical devices 0 and 1.
// Returns the merged device index, or -1 if fewer than 2 devices available.
// Both ranks must call this; result is broadcast from rank 0.
static int CreateMergedDeviceForFailover(ncclNet_t* net, int totalDevs) {
int mergedDev = -1;
if (totalDevs >= 2) {
ncclNetVDeviceProps_t vProps = {};
vProps.ndevs = 2;
vProps.devs[0] = 0;
vProps.devs[1] = 1;
net->makeVDevice(&mergedDev, &vProps);
}
// Each rank creates its own merged device from its local physical devices.
// Reduce to the minimum: if any rank lacks >=2 devices, all ranks skip.
int minDev = 0;
MPI_Allreduce(&mergedDev, &minDev, 1, MPI_INT, MPI_MIN, MPI_COMM_WORLD);
if (minDev < 0) return -1;
return mergedDev;
}
// MPI tags used for inter-rank result forwarding (rank 1 → rank 0).
// Keep these distinct from NCCL-level recv tags (per-message ints passed to irecv).
static constexpr int kSchedStateMpiTag = 9880; // ncclIbCastSchedState from FaultInjCastSlowQpRebalances
static constexpr int kFaultResultMpiTag = 9881; // FaultInjectResult from FaultInjCastQpErrorIsFatal
// Carries rank 1's observable state after the fault injection attempt to rank 0
// so all assertions run on rank 0 (the only rank with GTest listeners).
struct FaultInjectResult {
int sendRet; // ncclResult_t from PostSend (cast to int)
int recvRet; // ncclResult_t from PostRecv/Test on the recv side (cast to int)
int fatalCount; // ncclIbCastFaultGetFatalCount result
int phase2FatalCount; // fatal count after a finite injection budget is spent
int clearRet; // ncclResult_t from ncclIbCastFaultClear (cast to int)
int setErrRet; // first non-Success from ncclIbCastFaultSetQpError, or ncclSuccess
int actualNqps; // number of QPs armed with the error fault
};
// =============================================================================
// Test: FaultInjCastQpErrorIsFatal
//
// CAST path. Arm error injection on QP 0. The fault hook fires inside
// IbCastMultiSend before wrap_ibv_post_send and returns ncclSystemError;
// NCCLCHECK propagates it to IbCastIsend → net_->isend caller.
//
// Verifies:
// - isend returns an error OR fatalErrorCount > 0
// =============================================================================
TEST_F(NetIbMPITest, FaultInjCastQpErrorIsFatal) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
CAST_ENV_CHECK_OR_SKIP();
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/0, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 1024;
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>(i & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup: initialise WRR scheduler state before arming the fault.
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, /*tag=*/300, mhandle);
ASSERT_GT(actualNqps, 0);
// rank 1 arms the fault on every active QP so it fires regardless of which
// one the WRR scheduler selects for the next send.
FaultInjectResult r1 = {};
r1.actualNqps = actualNqps;
if (rank == 1) {
r1.setErrRet = static_cast<int>(ncclSuccess);
for (int q = 0; q < actualNqps; ++q) {
ncclResult_t ret = ncclIbCastFaultSetQpError(sendComm, q, /*inject=*/true);
if (ret != ncclSuccess && r1.setErrRet == static_cast<int>(ncclSuccess))
r1.setErrRet = static_cast<int>(ret);
}
}
MPI_Barrier(MPI_COMM_WORLD);
// Track whether the recv request completed so we can drain before CloseRecvComm.
bool recvDone = false;
void* recvReq = nullptr;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {301};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq), ncclSuccess);
for (int poll = 0; poll < 100; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq, &done, &sz) != ncclSuccess) break;
if (done) { recvDone = true; break; }
usleep(kPollIntervalUs);
}
} else {
// IbCastIsend sets *request before IbCastMultiSend is called, so sendReq
// may be non-null even when sendRet != ncclSuccess. Break on either condition.
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, 301, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
int fatalCount = 0;
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 200; poll++) {
int done = 0, sz = 0;
TestRequest(sendReq, &done, &sz);
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
if (done || fatalCount > 0) break;
usleep(kPollIntervalUs);
}
}
r1.sendRet = static_cast<int>(sendRet);
r1.fatalCount = fatalCount;
r1.clearRet = static_cast<int>(ncclIbCastFaultClear(sendComm));
// Ship results to rank 0 for assertions (rank 1 has no GTest listeners).
MPI_Send(&r1, sizeof(r1), MPI_BYTE, 0, kFaultResultMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
MPI_Recv(&r1, sizeof(r1), MPI_BYTE, 1, kFaultResultMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
EXPECT_EQ(r1.setErrRet, static_cast<int>(ncclSuccess))
<< "rank 1: ncclIbCastFaultSetQpError failed with " << r1.setErrRet
<< " (armed " << r1.actualNqps << " QPs)";
bool isendFailed = (r1.sendRet != static_cast<int>(ncclSuccess));
EXPECT_TRUE(isendFailed || r1.fatalCount > 0)
<< "rank 1: expected isend to fail OR fatalErrorCount > 0 after arming all "
<< r1.actualNqps << " CAST QPs with error injection; "
<< "isend returned " << r1.sendRet << ", fatalCount=" << r1.fatalCount;
EXPECT_EQ(r1.clearRet, static_cast<int>(ncclSuccess))
<< "rank 1: ncclIbCastFaultClear failed with " << r1.clearRet;
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !recvDone)
DrainRecvRequest(recvReq);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FaultInjCastSlowQpRebalances
//
// CAST path with WRR enabled. Arm a 10 ms delay on QP 0 and run 500 sends.
// The RTT timer fires every 50 ms (several times during the run) and updates
// initQpTokens based on measured per-QP latency.
//
// The test follows the pattern of CastStressMultiRoundTwoConns:
// - rank 1 reads ncclIbCastSchedState and sends it to rank 0 via MPI
// - rank 0 performs all assertions (scheduler state + data integrity)
//
// Verifies:
// - initQpTokens[0] differs from the initial equal share (RTT timer fired)
// - Data arrives intact on the receiver
// - Requires >= 2 actual QPs; skipped otherwise.
//
// Message size is kept below GetSplitDataMin() so messages always take the
// WRR token path (same rationale as CastStressMultiRoundTwoConns).
// =============================================================================
TEST_F(NetIbMPITest, FaultInjCastSlowQpRebalances) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const int rank = MPIEnvironment::world_rank;
CAST_ENV_CHECK_OR_SKIP();
if (GetSplitDataMin() == 0)
GTEST_SKIP() << "RCCL_IB_QP_SCHED_SPLIT_DATA_MIN=0: kMsgSz = GetSplitDataMin()-1 would underflow";
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/0, &listenComm, &sendComm, &recvComm);
constexpr int kNMsgs = 500;
// Keep below splitDataMin so messages take the WRR token path.
const size_t kMsgSz = std::max<size_t>(64, GetSplitDataMin() - 1);
const size_t kBufSz = static_cast<size_t>(kNMsgs) * kMsgSz;
constexpr int kBaseTag = 4000;
std::vector<char> sendBuf(kBufSz), recvBuf(kBufSz);
for (size_t i = 0; i < kBufSz; i++) sendBuf[i] = static_cast<char>((i * 3 + 7) & 0xFF);
memset(recvBuf.data(), 0, kBufSz);
void* comm = (rank == 0) ? recvComm : sendComm;
char* regBuf = (rank == 0) ? recvBuf.data() : sendBuf.data();
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, regBuf, kBufSz, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup: initialise WRR state and learn actual nqps.
const int actualNqps = GetActualNqps(sendComm, recvComm, regBuf, kMsgSz, kBaseTag - 1, mhandle);
ASSERT_GT(actualNqps, 0);
// Broadcast skip decision from rank 1 (the only rank that owns sendComm
// and can read the real QP count after the warmup send). root=1 is
// intentional: rank 1 sets doSkip, rank 0 receives it.
int doSkip = (actualNqps < 2) ? 1 : 0;
MPI_Bcast(&doSkip, 1, MPI_INT, /*root=*/1, MPI_COMM_WORLD);
if (doSkip) {
GTEST_SKIP() << "Need >= 2 actual QPs for rebalancing test (got " << actualNqps << ")";
}
if (rank == 1) {
// Arm equal-weight tokens as baseline for the RTT comparison.
const std::vector<int> tokens = EqualTokens(actualNqps);
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
// Arm 10 ms delay on QP 0 — makes QP 0 observably slower to the RTT timer.
ASSERT_EQ(ncclIbCastFaultSetQpDelay(sendComm, /*qpIdx=*/0, /*delayUs=*/10000), ncclSuccess);
}
// 500 sends in batches of 16 (mirrors CastStressMultiRoundTwoConns).
constexpr int kBatch = 16;
if (rank == 0) {
for (int base = 0; base < kNMsgs; base += kBatch) {
const int end = std::min(base + kBatch, kNMsgs);
std::vector<void*> reqs(end - base, nullptr);
for (int i = base; i < end; i++) {
void* bufs[1] = {recvBuf.data() + i * kMsgSz};
size_t sizes[1] = {kMsgSz};
int tags[1] = {kBaseTag + i};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &reqs[i - base]),
ncclSuccess);
ASSERT_NE(reqs[i - base], nullptr);
}
for (int i = 0; i < end - base; i++) {
int sz = 0;
ASSERT_EQ(WaitForCompletion(reqs[i], &sz, 10000), ncclSuccess);
}
}
} else {
for (int base = 0; base < kNMsgs; base += kBatch) {
const int end = std::min(base + kBatch, kNMsgs);
std::vector<void*> reqs(end - base, nullptr);
for (int i = base; i < end; i++)
PostSendWithRetry(sendComm, sendBuf.data() + i * kMsgSz, kMsgSz,
kBaseTag + i, mhandle, &reqs[i - base]);
for (int i = 0; i < end - base; i++) {
int sz = 0;
ASSERT_EQ(WaitForCompletion(reqs[i], &sz, 10000), ncclSuccess);
}
}
}
// rank 1 reads scheduler state and ships it to rank 0 for assertions.
// (State lives in sendComm which only rank 1 owns.)
struct ncclIbCastSchedState st = {};
if (rank == 1) {
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
MPI_Send(&st, sizeof(st), MPI_BYTE, 0, kSchedStateMpiTag, MPI_COMM_WORLD);
ASSERT_EQ(ncclIbCastFaultClear(sendComm), ncclSuccess);
} else {
MPI_Recv(&st, sizeof(st), MPI_BYTE, 1, kSchedStateMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0) {
// Data integrity: recvBuf must match what rank 1 sent.
EXPECT_EQ(memcmp(recvBuf.data(), sendBuf.data(), kBufSz), 0)
<< "data corruption with 10 ms QP 0 delay";
// RTT timer check: initQpTokens[0] must have changed from the initial
// equal share. Use EqualTokens() — not integer division — to get the
// exact baseline value that was set by rank 1 before the sends, so the
// comparison is consistent even when 100 % actualNqps != 0.
const std::vector<int> baseTokens = EqualTokens(actualNqps);
EXPECT_LT(st.initQpTokens[0], baseTokens[0])
<< "RTT timer did not reduce QP 0 tokens below equal share (initQpTokens[0]=" << st.initQpTokens[0]
<< ", equalShare=" << baseTokens[0] << "); "
<< "was 10 ms delay insufficient for the RTT timer interval?";
// WRR invariants: token accounting must be consistent.
EXPECT_TRUE(st.schedInit) << "schedInit must be true after 500 sends";
{
int s = 0;
for (int q = 0; q < st.nqps; q++) s += st.activeQpTokens[q];
EXPECT_EQ(s, st.activeTotTokens) << "activeQpTokens sum mismatch";
}
EXPECT_GE(st.activeTotTokens, 0);
EXPECT_LE(st.activeTotTokens, st.initTotTokens);
}
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FaultInjCastDelayDataIntegrity
//
// CAST path. Inject 2 ms delay on QP 0, run 50 sends.
// Verifies: data integrity is preserved despite the artificial per-QP delay.
//
// Unlike FaultInjCastSlowQpRebalances, this test does not aim to trigger
// the RTT timer; it only confirms that usleep in the send path does not
// corrupt or drop data.
// =============================================================================
TEST_F(NetIbMPITest, FaultInjCastDelayDataIntegrity) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const int rank = MPIEnvironment::world_rank;
CAST_ENV_CHECK_OR_SKIP();
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/0, &listenComm, &sendComm, &recvComm);
constexpr int kNMsgs = 50;
constexpr size_t kMsgSz = 8192;
const size_t kBufSz = static_cast<size_t>(kNMsgs) * kMsgSz;
constexpr int kBaseTag = 5000;
std::vector<char> sendBuf(kBufSz), recvBuf(kBufSz);
for (size_t i = 0; i < kBufSz; i++) sendBuf[i] = static_cast<char>((i * 7 + 3) & 0xFF);
memset(recvBuf.data(), 0, kBufSz);
void* comm = (rank == 0) ? recvComm : sendComm;
char* regBuf = (rank == 0) ? recvBuf.data() : sendBuf.data();
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, regBuf, kBufSz, NCCL_PTR_HOST, &mhandle), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm, recvComm, regBuf, kMsgSz, kBaseTag - 1, mhandle);
ASSERT_GT(actualNqps, 0);
if (rank == 1) {
ASSERT_EQ(ncclIbCastFaultSetQpDelay(sendComm, /*qpIdx=*/0, /*delayUs=*/2000), ncclSuccess);
}
CastDoBatchSendRecv(rank, sendComm, recvComm, sendBuf.data(), recvBuf.data(),
kMsgSz, kNMsgs, kBaseTag, mhandle);
if (rank == 1) {
ASSERT_EQ(ncclIbCastFaultClear(sendComm), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0) {
// recvBuf must match the pattern rank 1 sent.
// Both ranks initialise sendBuf with the same deterministic pattern,
// so rank 0's sendBuf is a valid reference without extra MPI traffic.
EXPECT_EQ(memcmp(recvBuf.data(), sendBuf.data(), kBufSz), 0)
<< "data corruption with 2 ms QP 0 delay (50 sends)";
}
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FaultInjCastSingleQpErrorIsFatal
//
// CAST path with WRR enabled. Arm error injection on exactly one QP —
// QP 0 — using ncclIbCastSetTokens to steer all WRR tokens there before the
// fault send. This models the realistic "one link failed" scenario where only
// a specific physical path is broken.
//
// Verifies:
// - isend returns an error OR fatalErrorCount > 0
// - Skipped when actualNqps < 2 (single-QP: no WRR, cursor is always 0
// and FaultInjCastQpErrorIsFatal already covers that case)
// =============================================================================
TEST_F(NetIbMPITest, FaultInjCastSingleQpErrorIsFatal) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
CAST_ENV_CHECK_OR_SKIP();
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/0, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 1024; // below splitDataMin → single-QP WRR path
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>(i & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup: initialise WRR state and determine actual QP count.
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, /*tag=*/400, mhandle);
ASSERT_GT(actualNqps, 0);
// Broadcast skip decision from rank 1 (owns sendComm and the WRR cursor).
int doSkip = (actualNqps < 2) ? 1 : 0;
MPI_Bcast(&doSkip, 1, MPI_INT, /*root=*/1, MPI_COMM_WORLD);
if (doSkip) {
GTEST_SKIP() << "Need >= 2 actual QPs for single-QP fault test (got " << actualNqps
<< "); FaultInjCastQpErrorIsFatal covers the single-QP case";
}
// rank 1: steer WRR onto QP 0 by setting all tokens there, then arm the
// fault on QP 0 only. Using SetTokens makes the chosen QP deterministic
// regardless of where the WRR cursor landed after warmup.
struct FaultInjectResult r1 = {};
r1.actualNqps = actualNqps;
const int targetQp = 0;
if (rank == 1) {
// Concentrate all tokens on QP 0: tokens[0]=1, tokens[1..n-1]=0.
std::vector<int> tokens(actualNqps, 0);
tokens[0] = 1;
EXPECT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
r1.setErrRet = static_cast<int>(ncclIbCastFaultSetQpError(sendComm, targetQp, /*inject=*/true));
}
MPI_Barrier(MPI_COMM_WORLD);
bool recvDone = false;
void* recvReq = nullptr;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {401};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq), ncclSuccess);
for (int poll = 0; poll < 100; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq, &done, &sz) != ncclSuccess) break;
if (done) { recvDone = true; break; }
usleep(kPollIntervalUs);
}
} else {
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, 401, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
int fatalCount = 0;
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 200; poll++) {
int done = 0, sz = 0;
TestRequest(sendReq, &done, &sz);
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
if (done || fatalCount > 0) break;
usleep(kPollIntervalUs);
}
}
r1.sendRet = static_cast<int>(sendRet);
r1.fatalCount = fatalCount;
r1.clearRet = static_cast<int>(ncclIbCastFaultClear(sendComm));
MPI_Send(&r1, sizeof(r1), MPI_BYTE, 0, kFaultResultMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
MPI_Recv(&r1, sizeof(r1), MPI_BYTE, 1, kFaultResultMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
EXPECT_EQ(r1.setErrRet, static_cast<int>(ncclSuccess))
<< "rank 1: ncclIbCastFaultSetQpError failed for QP " << targetQp;
bool isendFailed = (r1.sendRet != static_cast<int>(ncclSuccess));
EXPECT_TRUE(isendFailed || r1.fatalCount > 0)
<< "rank 1: expected isend to fail OR fatalErrorCount > 0 after single-QP "
<< targetQp << " error injection (of " << r1.actualNqps << " total); "
<< "isend returned " << r1.sendRet << ", fatalCount=" << r1.fatalCount;
EXPECT_EQ(r1.clearRet, static_cast<int>(ncclSuccess))
<< "rank 1: ncclIbCastFaultClear failed";
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !recvDone)
DrainRecvRequest(recvReq);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FaultInjCastQpErrorClearRecovers
//
// CAST path. Arm error injection on all QPs, trigger one failed send, then
// call ncclIbCastFaultClear and open a new connection. Verifies that after
// clearing fault state a fresh connection sends and receives N messages
// without errors and with full data integrity.
//
// A new connection is used for the recovery phase because a faulted
// connection increments fatalErrorCount and future IbCastIsend calls check
// that counter before proceeding (NCCL_IB_RETURN_ASYNC_EVENTS=1). The test
// therefore validates that fault injection does not leave persistent state
// that contaminates subsequent connections.
//
// Verifies:
// - After fault injection and FaultClear, a new CAST connection transfers
// data correctly
// - fatalErrorCount on the new connection remains 0
// =============================================================================
TEST_F(NetIbMPITest, FaultInjCastQpErrorClearRecovers) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
CAST_ENV_CHECK_OR_SKIP();
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
// ── Phase 1: inject fault on first connection ─────────────────────────
void* listenComm1 = nullptr;
void* sendComm1 = nullptr;
void* recvComm1 = nullptr;
SetupCastConnection(/*dev=*/0, &listenComm1, &sendComm1, &recvComm1);
constexpr size_t kMsgSize = 1024;
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>(i & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm1 = (rank == 0) ? recvComm1 : sendComm1;
void* buf1 = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle1 = nullptr;
ASSERT_EQ(RegisterMemory(comm1, buf1, kMsgSize, NCCL_PTR_HOST, &mhandle1), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm1, recvComm1, buf1, kMsgSize, /*tag=*/500, mhandle1);
ASSERT_GT(actualNqps, 0);
if (rank == 1) {
for (int q = 0; q < actualNqps; ++q)
ASSERT_EQ(ncclIbCastFaultSetQpError(sendComm1, q, /*inject=*/true), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
// Trigger the fault (rank 0 posts recv, rank 1 posts send that will fail).
// rank 1 forwards its fault result to rank 0 so we can assert the fault
// was actually observed before testing recovery in phase 2.
static constexpr int kPhase1MpiTag = 9882;
FaultInjectResult p1 = {};
void* recvReq1 = nullptr;
bool recvDone1 = false;
if (rank == 0) {
void* bufs[1] = {buf1};
size_t sizes[1] = {kMsgSize};
int tags[1] = {501};
void* handles[1] = {mhandle1};
ASSERT_EQ(PostRecv(recvComm1, 1, bufs, sizes, tags, handles, &recvReq1), ncclSuccess);
for (int poll = 0; poll < 100; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq1, &done, &sz) != ncclSuccess) break;
if (done) { recvDone1 = true; break; }
usleep(kPollIntervalUs);
}
} else {
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm1, buf1, kMsgSize, 501, mhandle1, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
int fatalCount = 0;
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 200; poll++) {
int done = 0, sz = 0;
TestRequest(sendReq, &done, &sz);
ncclIbCastFaultGetFatalCount(sendComm1, &fatalCount);
if (done || fatalCount > 0) break;
usleep(kPollIntervalUs);
}
} else {
ncclIbCastFaultGetFatalCount(sendComm1, &fatalCount);
}
p1.sendRet = static_cast<int>(sendRet);
p1.fatalCount = fatalCount;
EXPECT_EQ(ncclIbCastFaultClear(sendComm1), ncclSuccess);
MPI_Send(&p1, sizeof(p1), MPI_BYTE, 0, kPhase1MpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
MPI_Recv(&p1, sizeof(p1), MPI_BYTE, 1, kPhase1MpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
bool isendFailed = (p1.sendRet != static_cast<int>(ncclSuccess));
EXPECT_TRUE(isendFailed || p1.fatalCount > 0)
<< "Phase 1: fault injection did not trigger — isend returned "
<< p1.sendRet << ", fatalCount=" << p1.fatalCount
<< "; recovery test is meaningless without a confirmed fault";
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !recvDone1)
DrainRecvRequest(recvReq1);
TeardownConnection(recvComm1, listenComm1, sendComm1, mhandle1);
// ── Phase 2: open fresh connection, verify it works cleanly ──────────
constexpr int kNMsgs = 20;
constexpr size_t kMsgSz2 = 4096;
const size_t kBufSz2 = static_cast<size_t>(kNMsgs) * kMsgSz2;
constexpr int kBaseTag = 510;
std::vector<char> sendBuf2(kBufSz2), recvBuf2(kBufSz2);
for (size_t i = 0; i < kBufSz2; i++) sendBuf2[i] = static_cast<char>((i * 5 + 11) & 0xFF);
memset(recvBuf2.data(), 0, kBufSz2);
void* listenComm2 = nullptr;
void* sendComm2 = nullptr;
void* recvComm2 = nullptr;
SetupCastConnection(/*dev=*/0, &listenComm2, &sendComm2, &recvComm2);
void* comm2 = (rank == 0) ? recvComm2 : sendComm2;
char* regBuf2 = (rank == 0) ? recvBuf2.data() : sendBuf2.data();
void* mhandle2 = nullptr;
ASSERT_EQ(RegisterMemory(comm2, regBuf2, kBufSz2, NCCL_PTR_HOST, &mhandle2), ncclSuccess);
CastDoBatchSendRecv(rank, sendComm2, recvComm2,
sendBuf2.data(), recvBuf2.data(),
kMsgSz2, kNMsgs, kBaseTag, mhandle2);
// Verify fatalCount on the new connection is still 0 (forwarded rank 1 → 0).
int newFatalCount = 0;
if (rank == 1) {
ncclIbCastFaultGetFatalCount(sendComm2, &newFatalCount);
}
MPI_Bcast(&newFatalCount, 1, MPI_INT, /*root=*/1, MPI_COMM_WORLD);
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0) {
EXPECT_EQ(memcmp(recvBuf2.data(), sendBuf2.data(), kBufSz2), 0)
<< "data corruption in post-fault recovery connection";
EXPECT_EQ(newFatalCount, 0)
<< "new connection has non-zero fatalErrorCount after FaultClear on previous connection";
}
TeardownConnection(recvComm2, listenComm2, sendComm2, mhandle2);
}
// =============================================================================
// Test: FailoverErrorCodeWhitelist
//
// Validates IbCastResiliencyCheckErrorNotFatal via the ncclIbCastFaultCheckErrorFatal
// wrapper. WR_FLUSH_ERR and RETRY_EXC_ERR must be non-fatal (eligible for
// failover); other error codes must be fatal.
//
// Requires NCCL_IB_RESILIENCY_PORT_FAILOVER=1 so the resiliency context exists.
// Does NOT require ndevs >= 2 — only tests the classification function.
// =============================================================================
TEST_F(NetIbMPITest, FailoverErrorCodeWhitelist) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const char* failoverEnv = getenv("NCCL_IB_RESILIENCY_PORT_FAILOVER");
if (!failoverEnv || strcmp(failoverEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_FAILOVER=1";
}
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/0, &listenComm, &sendComm, &recvComm);
struct WhitelistResult {
int hasResiliency;
int flushIsFatal;
int retryIsFatal;
int remAccessIsFatal;
int generalErrIsFatal;
};
static constexpr int kWhitelistMpiTag = 9885;
WhitelistResult r = {};
if (rank == 1) {
// Check resiliency context exists
struct ncclIbCastResiliencyState resState = {};
r.hasResiliency = (ncclIbCastGetResiliencyState(sendComm, &resState) == ncclSuccess) ? 1 : 0;
if (r.hasResiliency) {
bool isFatal = false;
ncclIbCastFaultCheckErrorFatal(sendComm, kWcWrFlushErr, &isFatal);
r.flushIsFatal = isFatal ? 1 : 0;
ncclIbCastFaultCheckErrorFatal(sendComm, kWcRetryExcErr, &isFatal);
r.retryIsFatal = isFatal ? 1 : 0;
ncclIbCastFaultCheckErrorFatal(sendComm, kWcRemAccessErr, &isFatal);
r.remAccessIsFatal = isFatal ? 1 : 0;
ncclIbCastFaultCheckErrorFatal(sendComm, kWcGeneralErr, &isFatal);
r.generalErrIsFatal = isFatal ? 1 : 0;
}
MPI_Send(&r, sizeof(r), MPI_BYTE, 0, kWhitelistMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
MPI_Recv(&r, sizeof(r), MPI_BYTE, 1, kWhitelistMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
ASSERT_EQ(r.hasResiliency, 1)
<< "Resiliency context not created — is NCCL_IB_RESILIENCY_PORT_FAILOVER=1?";
EXPECT_EQ(r.flushIsFatal, 0)
<< "WR_FLUSH_ERR should be non-fatal (eligible for failover)";
EXPECT_EQ(r.retryIsFatal, 0)
<< "RETRY_EXC_ERR should be non-fatal (eligible for failover)";
EXPECT_EQ(r.remAccessIsFatal, 1)
<< "REM_ACCESS_ERR should be fatal (not in whitelist)";
EXPECT_EQ(r.generalErrIsFatal, 1)
<< "GENERAL_ERR should be fatal (not in whitelist)";
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, nullptr);
}
// =============================================================================
// Test: FailoverCqeErrorRecovered
//
// Core failover test. Requires NIC Fusion (ndevs >= 2) so there is a
// surviving device. Drives one QP to IBV_QPS_ERR via
// ncclIbCastFaultDriveQpToError, producing real WR_FLUSH_ERR CQEs.
// The resiliency state machine should: detect the error, replace the QP,
// probe the receiver, and complete the request via the surviving device.
//
// Requires:
// - NCCL_IB_RESILIENCY_PORT_FAILOVER=1
// - NCCL_IB_MERGE_NICS=1 + NCCL_NET_FORCE_MERGE (or ndevs >= 2 by topology)
// - sentData fix (C2) — without it, probe result is ignored
// =============================================================================
TEST_F(NetIbMPITest, FailoverCqeErrorRecovered) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const char* failoverEnv = getenv("NCCL_IB_RESILIENCY_PORT_FAILOVER");
if (!failoverEnv || strcmp(failoverEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_FAILOVER=1";
}
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
int totalDevs = 0;
AssertInitAndGetDevices(&totalDevs);
int mergedDev = CreateMergedDeviceForFailover(net_, totalDevs);
if (mergedDev < 0) {
GTEST_SKIP() << "Failover requires NIC Fusion (ndevs >= 2). "
<< "Found " << totalDevs << " physical devices.";
}
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/mergedDev, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 8192;
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>((i * 13 + 7) & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, /*tag=*/600, mhandle);
ASSERT_GT(actualNqps, 0);
struct ncclIbCastResiliencyState resState = {};
struct FailoverResult {
int sendRet;
int fatalCount;
int devState0;
int inProgress;
int repostCount;
};
static constexpr int kFailoverMpiTag = 9886;
FailoverResult fr = {};
bool recvDone = false;
void* recvReq = nullptr;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {601};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
// Drive sender QP 0 to ERR. The receiver should handle this via
// negative events accounting (BY_ID matching + resiliency).
if (rank == 1) {
ASSERT_EQ(ncclIbCastFaultDriveQpToError(sendComm, 0), ncclSuccess);
// Post the send — WRs on QP 0 will flush, surviving QPs handle the data
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, 601, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
if (sendRet == ncclSuccess && sendReq != nullptr) {
// Poll IbCastTest until request completes or timeout
for (int poll = 0; poll < 500; poll++) {
int done = 0, sz = 0;
ncclResult_t testRet = TestRequest(sendReq, &done, &sz);
if (testRet != ncclSuccess) { sendRet = testRet; break; }
if (done) break;
usleep(kPollIntervalUs);
}
}
int fatalCount = 0;
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
ncclIbCastGetResiliencyState(sendComm, &resState);
int repostCount = 0;
ncclIbCastGetRepostCount(sendComm, &repostCount);
fr.sendRet = static_cast<int>(sendRet);
fr.fatalCount = fatalCount;
fr.devState0 = resState.devState[0];
fr.inProgress = resState.inProgress ? 1 : 0;
fr.repostCount = repostCount;
MPI_Send(&fr, sizeof(fr), MPI_BYTE, 0, kFailoverMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
// Receiver MUST poll IbCastTest concurrently with sender's failover.
// The sender's probe RDMA-reads receiver's completions[] which is only
// set when the receiver calls IbCastTest → IbCastCompletionEventProcess.
// Poll long enough for sender's failover (~10-15 seconds).
for (int poll = 0; poll < 1500; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq, &done, &sz) != ncclSuccess) break;
if (done) { recvDone = true; break; }
usleep(kPollIntervalUs);
}
MPI_Recv(&fr, sizeof(fr), MPI_BYTE, 1, kFailoverMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
EXPECT_EQ(fr.sendRet, static_cast<int>(ncclSuccess))
<< "Send should complete via surviving device after failover";
EXPECT_EQ(fr.fatalCount, 0)
<< "No fatal error expected — failover should handle the QP error";
// With PORT_RECOVERY enabled, inProgress stays true (recovery ongoing).
// Without recovery, failover completes and inProgress becomes false.
// devState[0] should not be Ok — Error(1) without recovery,
// or RecoveryInProgress(2) if recovery thread picked it up.
EXPECT_NE(fr.devState0, 0)
<< "Device 0 should not be Ok after failover (got " << fr.devState0 << ")";
if (fr.sendRet == static_cast<int>(ncclSuccess)) {
EXPECT_TRUE(recvDone)
<< "Send succeeded but receiver never got the data — data loss";
}
if (recvDone) {
EXPECT_EQ(memcmp(recvBuf.data(), sendBuf.data(), kMsgSize), 0)
<< "Data corruption after failover — receiver got wrong data";
}
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !recvDone)
DrainRecvRequest(recvReq);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FailoverSingleDeviceTopology
//
// Single NIC with PORT_FAILOVER=1. Drive QP to ERR. With only one device,
// there is no surviving device — failover should degrade to fatal error.
// Verifies graceful degradation (no crash, no hang).
// =============================================================================
TEST_F(NetIbMPITest, FailoverSingleDeviceTopology) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const char* failoverEnv = getenv("NCCL_IB_RESILIENCY_PORT_FAILOVER");
if (!failoverEnv || strcmp(failoverEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_FAILOVER=1";
}
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
// Use device 0 (single NIC, ndevs=1) — no NIC Fusion
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/0, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 4096;
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>((i * 11 + 3) & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, /*tag=*/700, mhandle);
ASSERT_GT(actualNqps, 0);
struct SingleDevResult {
int sendRet;
int fatalCount;
};
static constexpr int kSingleDevMpiTag = 9887;
SingleDevResult sr = {};
bool recvDone = false;
void* recvReq = nullptr;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {701};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 1) {
// Drive QP 0 to ERR before sending — with single device,
// all QPs are on device 0, so failover has no surviving device
ASSERT_EQ(ncclIbCastFaultDriveQpToError(sendComm, 0), ncclSuccess);
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, 701, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 200; poll++) {
int done = 0, sz = 0;
ncclResult_t testRet = TestRequest(sendReq, &done, &sz);
if (testRet != ncclSuccess) { sendRet = testRet; break; }
if (done) break;
usleep(kPollIntervalUs);
}
}
int fatalCount = 0;
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
sr.sendRet = static_cast<int>(sendRet);
sr.fatalCount = fatalCount;
MPI_Send(&sr, sizeof(sr), MPI_BYTE, 0, kSingleDevMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
for (int poll = 0; poll < 200; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq, &done, &sz) != ncclSuccess) break;
if (done) { recvDone = true; break; }
usleep(kPollIntervalUs);
}
MPI_Recv(&sr, sizeof(sr), MPI_BYTE, 1, kSingleDevMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
// With single device, failover should detect no surviving device
// and return a fatal error (ncclRemoteError)
bool isendFailed = (sr.sendRet != static_cast<int>(ncclSuccess));
EXPECT_TRUE(isendFailed || sr.fatalCount > 0)
<< "Single-device failover should produce a fatal error or failed send; "
<< "sendRet=" << sr.sendRet << ", fatalCount=" << sr.fatalCount;
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !recvDone)
DrainRecvRequest(recvReq);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FailoverAllDevicesFailed
//
// NIC Fusion (ndevs >= 2) with PORT_FAILOVER=1. Drive ALL QPs on both
// devices to ERR simultaneously. With no surviving device, failover
// should detect the total failure and return a fatal error, no hang.
// =============================================================================
TEST_F(NetIbMPITest, FailoverAllDevicesFailed) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const char* failoverEnv = getenv("NCCL_IB_RESILIENCY_PORT_FAILOVER");
if (!failoverEnv || strcmp(failoverEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_FAILOVER=1";
}
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
int totalDevs = 0;
AssertInitAndGetDevices(&totalDevs);
int mergedDev = CreateMergedDeviceForFailover(net_, totalDevs);
if (mergedDev < 0) {
GTEST_SKIP() << "Requires NIC Fusion (ndevs >= 2). Need at least 2 IB devices.";
}
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/mergedDev, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 4096;
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>((i * 17 + 5) & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, /*tag=*/800, mhandle);
ASSERT_GT(actualNqps, 0);
struct MaxAttemptResult {
int sendRet;
int fatalCount;
};
static constexpr int kMaxAttemptMpiTag = 9888;
MaxAttemptResult mr = {};
bool recvDone = false;
void* recvReq = nullptr;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {801};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 1) {
// Drive ALL QPs to ERR — no surviving device
for (int q = 0; q < actualNqps; q++) {
ASSERT_EQ(ncclIbCastFaultDriveQpToError(sendComm, q), ncclSuccess);
}
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, 801, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 300; poll++) {
int done = 0, sz = 0;
ncclResult_t testRet = TestRequest(sendReq, &done, &sz);
if (testRet != ncclSuccess) { sendRet = testRet; break; }
if (done) break;
usleep(kPollIntervalUs);
}
}
int fatalCount = 0;
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
mr.sendRet = static_cast<int>(sendRet);
mr.fatalCount = fatalCount;
MPI_Send(&mr, sizeof(mr), MPI_BYTE, 0, kMaxAttemptMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
for (int poll = 0; poll < 300; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq, &done, &sz) != ncclSuccess) break;
if (done) { recvDone = true; break; }
usleep(kPollIntervalUs);
}
MPI_Recv(&mr, sizeof(mr), MPI_BYTE, 1, kMaxAttemptMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
bool isendFailed = (mr.sendRet != static_cast<int>(ncclSuccess));
EXPECT_TRUE(isendFailed || mr.fatalCount > 0)
<< "All QPs failed — expected fatal error or failed send; "
<< "sendRet=" << mr.sendRet << ", fatalCount=" << mr.fatalCount;
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !recvDone)
DrainRecvRequest(recvReq);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FailoverLargeMessageDataIntegrity
//
// NIC Fusion (ndevs >= 2). Drive one QP to ERR before sending a large
// message (64 KB). Verifies failover handles multi-chunk data correctly:
// the surviving QPs deliver the data, and the receiver gets the complete
// message with no corruption.
//
// Note: repostCount may be 0 because fault is injected before the send,
// so QP 0 never posts data. Selective retransmit (repostCount > 0) only
// triggers when data is partially sent before the fault — requires
// mid-flight injection which is non-deterministic on fast HW.
// =============================================================================
TEST_F(NetIbMPITest, FailoverLargeMessageDataIntegrity) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const char* failoverEnv = getenv("NCCL_IB_RESILIENCY_PORT_FAILOVER");
if (!failoverEnv || strcmp(failoverEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_FAILOVER=1";
}
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
int totalDevs = 0;
AssertInitAndGetDevices(&totalDevs);
int mergedDev = CreateMergedDeviceForFailover(net_, totalDevs);
if (mergedDev < 0) {
GTEST_SKIP() << "Requires NIC Fusion (ndevs >= 2). Need at least 2 IB devices.";
}
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/mergedDev, &listenComm, &sendComm, &recvComm);
// Large message to ensure data spans multiple QPs
constexpr size_t kMsgSize = 65536;
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>((i * 31 + 17) & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, /*tag=*/900, mhandle);
ASSERT_GT(actualNqps, 0);
struct RetransmitResult {
int sendRet;
int fatalCount;
int repostCount;
int devState0;
};
static constexpr int kRetransmitMpiTag = 9889;
RetransmitResult rr = {};
bool recvDone = false;
void* recvReq = nullptr;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {901};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 1) {
ASSERT_EQ(ncclIbCastFaultDriveQpToError(sendComm, 0), ncclSuccess);
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, 901, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 500; poll++) {
int done = 0, sz = 0;
ncclResult_t testRet = TestRequest(sendReq, &done, &sz);
if (testRet != ncclSuccess) { sendRet = testRet; break; }
if (done) break;
usleep(kPollIntervalUs);
}
}
int fatalCount = 0;
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
struct ncclIbCastResiliencyState resState = {};
ncclIbCastGetResiliencyState(sendComm, &resState);
int repostCount = 0;
ncclIbCastGetRepostCount(sendComm, &repostCount);
rr.sendRet = static_cast<int>(sendRet);
rr.fatalCount = fatalCount;
rr.repostCount = repostCount;
rr.devState0 = resState.devState[0];
MPI_Send(&rr, sizeof(rr), MPI_BYTE, 0, kRetransmitMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
// Receiver must poll concurrently with sender's failover
for (int poll = 0; poll < 1500; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq, &done, &sz) != ncclSuccess) break;
if (done) { recvDone = true; break; }
usleep(kPollIntervalUs);
}
MPI_Recv(&rr, sizeof(rr), MPI_BYTE, 1, kRetransmitMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
EXPECT_EQ(rr.sendRet, static_cast<int>(ncclSuccess))
<< "Send should complete via surviving device after failover";
EXPECT_EQ(rr.fatalCount, 0)
<< "No fatal error expected";
EXPECT_NE(rr.devState0, 0)
<< "Device 0 should not be Ok after failover (got " << rr.devState0 << ")";
if (rr.sendRet == static_cast<int>(ncclSuccess)) {
EXPECT_TRUE(recvDone)
<< "Send succeeded but receiver never got the data — data loss";
}
if (recvDone) {
EXPECT_EQ(memcmp(recvBuf.data(), sendBuf.data(), kMsgSize), 0)
<< "Data corruption after failover on large message";
}
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !recvDone)
DrainRecvRequest(recvReq);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FailoverDeviceOneFailure
//
// Same as FailoverCqeErrorRecovered but fails device 1 (QP index 1)
// instead of device 0 (QP index 0). Validates that failover works
// regardless of which device fails.
// =============================================================================
TEST_F(NetIbMPITest, FailoverDeviceOneFailure) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const char* failoverEnv = getenv("NCCL_IB_RESILIENCY_PORT_FAILOVER");
if (!failoverEnv || strcmp(failoverEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_FAILOVER=1";
}
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
int totalDevs = 0;
AssertInitAndGetDevices(&totalDevs);
int mergedDev = CreateMergedDeviceForFailover(net_, totalDevs);
if (mergedDev < 0) {
GTEST_SKIP() << "Requires NIC Fusion (ndevs >= 2).";
}
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/mergedDev, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 8192;
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>((i * 19 + 11) & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, /*tag=*/1000, mhandle);
ASSERT_GT(actualNqps, 1);
struct {
int sendRet;
int fatalCount;
int devState1;
int inProgress;
} fr = {};
static constexpr int kDev1MpiTag = 9890;
bool recvDone = false;
void* recvReq = nullptr;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {1001};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 1) {
// Fail QP index 1 = device 1 (QPs interleaved: 0=dev0, 1=dev1, 2=dev0...)
ASSERT_EQ(ncclIbCastFaultDriveQpToError(sendComm, 1), ncclSuccess);
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, 1001, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 500; poll++) {
int done = 0, sz = 0;
ncclResult_t testRet = TestRequest(sendReq, &done, &sz);
if (testRet != ncclSuccess) { sendRet = testRet; break; }
if (done) break;
usleep(kPollIntervalUs);
}
}
int fatalCount = 0;
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
struct ncclIbCastResiliencyState resState = {};
ncclIbCastGetResiliencyState(sendComm, &resState);
fr.sendRet = static_cast<int>(sendRet);
fr.fatalCount = fatalCount;
fr.devState1 = resState.devState[1];
fr.inProgress = resState.inProgress ? 1 : 0;
MPI_Send(&fr, sizeof(fr), MPI_BYTE, 0, kDev1MpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
for (int poll = 0; poll < 1500; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq, &done, &sz) != ncclSuccess) break;
if (done) { recvDone = true; break; }
usleep(kPollIntervalUs);
}
MPI_Recv(&fr, sizeof(fr), MPI_BYTE, 1, kDev1MpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
EXPECT_EQ(fr.sendRet, static_cast<int>(ncclSuccess))
<< "Send should complete via device 0 after device 1 failover";
EXPECT_EQ(fr.fatalCount, 0);
EXPECT_NE(fr.devState1, 0) << "Device 1 should not be Ok after failover";
if (fr.sendRet == static_cast<int>(ncclSuccess)) {
EXPECT_TRUE(recvDone) << "Send succeeded but receiver lost data";
}
if (recvDone) {
EXPECT_EQ(memcmp(recvBuf.data(), sendBuf.data(), kMsgSize), 0)
<< "Data corruption after device 1 failover";
}
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !recvDone)
DrainRecvRequest(recvReq);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FailoverMultiRequestInFlight
//
// Post 4 send requests, then fault QP 0. All 4 requests should
// complete via the surviving device. Validates that the resiliency
// state machine handles multiple concurrent failed requests.
// =============================================================================
TEST_F(NetIbMPITest, FailoverMultiRequestInFlight) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const char* failoverEnv = getenv("NCCL_IB_RESILIENCY_PORT_FAILOVER");
if (!failoverEnv || strcmp(failoverEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_FAILOVER=1";
}
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
int totalDevs = 0;
AssertInitAndGetDevices(&totalDevs);
int mergedDev = CreateMergedDeviceForFailover(net_, totalDevs);
if (mergedDev < 0) {
GTEST_SKIP() << "Requires NIC Fusion (ndevs >= 2).";
}
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/mergedDev, &listenComm, &sendComm, &recvComm);
constexpr int kNumReqs = 4;
constexpr size_t kMsgSize = 4096;
const size_t kBufSize = kMsgSize * (kNumReqs + 1);
std::vector<char> sendBuf(kBufSize), recvBuf(kBufSize);
for (size_t i = 0; i < kBufSize; i++) sendBuf[i] = static_cast<char>((i * 29 + 7) & 0xFF);
memset(recvBuf.data(), 0, kBufSize);
void* comm = (rank == 0) ? recvComm : sendComm;
char* regBuf = (rank == 0) ? recvBuf.data() : sendBuf.data();
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, regBuf, kBufSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup
const int actualNqps = GetActualNqps(sendComm, recvComm, regBuf, kMsgSize, /*tag=*/1200, mhandle);
ASSERT_GT(actualNqps, 0);
static constexpr int kMultiMpiTag = 9892;
constexpr int kBaseTag = 1210;
// Post all recvs first
void* recvReqs[kNumReqs] = {};
if (rank == 0) {
for (int i = 0; i < kNumReqs; i++) {
char* buf = regBuf + (i + 1) * kMsgSize;
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {kBaseTag + i};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReqs[i]), ncclSuccess);
}
}
MPI_Barrier(MPI_COMM_WORLD);
struct MultiReqResult {
int sendRet[kNumReqs];
int fatalCount;
};
MultiReqResult mr = {};
if (rank == 1) {
// Fault QP 0 before posting any sends
ASSERT_EQ(ncclIbCastFaultDriveQpToError(sendComm, 0), ncclSuccess);
// Post all 4 sends
void* sendReqs[kNumReqs] = {};
for (int i = 0; i < kNumReqs; i++) {
char* buf = regBuf + (i + 1) * kMsgSize;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, kBaseTag + i, mhandle, &sendReqs[i]);
if (sendRet != ncclSuccess || sendReqs[i] != nullptr) break;
usleep(kPollIntervalUs);
}
mr.sendRet[i] = static_cast<int>(sendRet);
}
// Poll all requests to completion
for (int i = 0; i < kNumReqs; i++) {
if (sendReqs[i] == nullptr) continue;
for (int poll = 0; poll < 500; poll++) {
int done = 0, sz = 0;
ncclResult_t testRet = TestRequest(sendReqs[i], &done, &sz);
if (testRet != ncclSuccess) { mr.sendRet[i] = static_cast<int>(testRet); break; }
if (done) break;
usleep(kPollIntervalUs);
}
}
ncclIbCastFaultGetFatalCount(sendComm, &mr.fatalCount);
MPI_Send(&mr, sizeof(mr), MPI_BYTE, 0, kMultiMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
// Poll all recvs concurrently
bool allDone = false;
for (int poll = 0; poll < 1500 && !allDone; poll++) {
allDone = true;
for (int i = 0; i < kNumReqs; i++) {
if (recvReqs[i] == nullptr) continue;
int done = 0, sz = 0;
TestRequest(recvReqs[i], &done, &sz);
if (done) { recvReqs[i] = nullptr; }
else { allDone = false; }
}
if (!allDone) usleep(kPollIntervalUs);
}
MPI_Recv(&mr, sizeof(mr), MPI_BYTE, 1, kMultiMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
for (int i = 0; i < kNumReqs; i++) {
EXPECT_EQ(mr.sendRet[i], static_cast<int>(ncclSuccess))
<< "Multi-request " << i << " send failed";
}
EXPECT_EQ(mr.fatalCount, 0) << "Fatal error during multi-request failover";
EXPECT_TRUE(allDone) << "Not all receiver requests completed";
if (allDone) {
for (int i = 0; i < kNumReqs; i++) {
size_t offset = (i + 1) * kMsgSize;
EXPECT_EQ(memcmp(recvBuf.data() + offset, sendBuf.data() + offset, kMsgSize), 0)
<< "Data corruption in multi-request " << i;
}
}
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0) {
for (int i = 0; i < kNumReqs; i++) {
if (recvReqs[i]) DrainRecvRequest(recvReqs[i]);
}
}
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: RecoveryThreadStartedOnlyWithParam
//
// Verifies that the PORT_RECOVERY background thread is started only when
// NCCL_IB_RESILIENCY_PORT_RECOVERY=1 is set, and not otherwise.
//
// Two sub-cases:
// 1. With PORT_RECOVERY=1 + PORT_FAILOVER=1: recoveryEnabled must be true.
// 2. Without PORT_RECOVERY (env unset or 0): recoveryEnabled must be false.
//
// Does not require NIC Fusion — we only check the resiliency state struct.
// =============================================================================
TEST_F(NetIbMPITest, RecoveryThreadStartedOnlyWithParam) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const char* failoverEnv = getenv("NCCL_IB_RESILIENCY_PORT_FAILOVER");
const char* recoveryEnv = getenv("NCCL_IB_RESILIENCY_PORT_RECOVERY");
if (!failoverEnv || strcmp(failoverEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_FAILOVER=1";
}
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/0, &listenComm, &sendComm, &recvComm);
struct RecoveryEnabledResult {
int recoveryEnabled;
int getStateRet;
};
static constexpr int kRecoveryEnabledMpiTag = 9893;
RecoveryEnabledResult r = {};
if (rank == 1) {
struct ncclIbCastResiliencyState resState = {};
r.getStateRet = static_cast<int>(ncclIbCastGetResiliencyState(sendComm, &resState));
r.recoveryEnabled = resState.recoveryEnabled ? 1 : 0;
MPI_Send(&r, sizeof(r), MPI_BYTE, 0, kRecoveryEnabledMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
MPI_Recv(&r, sizeof(r), MPI_BYTE, 1, kRecoveryEnabledMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
ASSERT_EQ(r.getStateRet, static_cast<int>(ncclSuccess))
<< "ncclIbCastGetResiliencyState failed — resiliency context not created; "
<< "is NCCL_IB_RESILIENCY_PORT_FAILOVER=1?";
bool recoveryParamSet = (recoveryEnv && strcmp(recoveryEnv, "1") == 0);
if (recoveryParamSet) {
EXPECT_EQ(r.recoveryEnabled, 1)
<< "recoveryEnabled should be true when NCCL_IB_RESILIENCY_PORT_RECOVERY=1";
} else {
EXPECT_EQ(r.recoveryEnabled, 0)
<< "recoveryEnabled should be false when NCCL_IB_RESILIENCY_PORT_RECOVERY is not set";
}
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, nullptr);
}
// =============================================================================
// Test: RecoverySuccessRestoresTraffic
//
// Core PORT_RECOVERY test. Requires NIC Fusion (ndevs >= 2) and both
// PORT_FAILOVER=1 and PORT_RECOVERY=1.
//
// Sequence:
// 1. Drive both sender and receiver data QP 0 to IBV_QPS_ERR — models a
// link failure. Both sides detect WR_FLUSH_ERR, enter failover, then
// recovery. The recovery handshake (alive messages + ACK) requires
// both sides to have recovery contexts.
// 2. Poll ncclIbCastGetResiliencyState on sender until devState[0]
// returns to Ok/Recovered (recovery restored the data QP to RTS).
// 3. Send 20 messages and verify data integrity — confirms restored QPs
// carry sustained live traffic.
// =============================================================================
TEST_F(NetIbMPITest, RecoverySuccessRestoresTraffic) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const char* failoverEnv = getenv("NCCL_IB_RESILIENCY_PORT_FAILOVER");
const char* recoveryEnv = getenv("NCCL_IB_RESILIENCY_PORT_RECOVERY");
if (!failoverEnv || strcmp(failoverEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_FAILOVER=1";
}
if (!recoveryEnv || strcmp(recoveryEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_RECOVERY=1";
}
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
int totalDevs = 0;
AssertInitAndGetDevices(&totalDevs);
int mergedDev = CreateMergedDeviceForFailover(net_, totalDevs);
if (mergedDev < 0) {
GTEST_SKIP() << "Requires NIC Fusion (ndevs >= 2). Found " << totalDevs << " physical devices.";
}
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/mergedDev, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 8192;
constexpr int kPostRecoveryMsgs = 20;
const size_t kBufSize = kMsgSize * (kPostRecoveryMsgs + 1);
std::vector<char> sendBuf(kBufSize), recvBuf(kBufSize);
for (size_t i = 0; i < kBufSize; i++) sendBuf[i] = static_cast<char>((i * 37 + 19) & 0xFF);
memset(recvBuf.data(), 0, kBufSize);
void* comm = (rank == 0) ? recvComm : sendComm;
char* regBuf = (rank == 0) ? recvBuf.data() : sendBuf.data();
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, regBuf, kBufSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm, recvComm, regBuf, kMsgSize, /*tag=*/1300, mhandle);
ASSERT_GT(actualNqps, 0);
// ── Phase 1: drive both sender and receiver QP 0 to ERR ──────────────
// Both sides must detect the failure and enter recovery for the
// handshake to complete (sender=requestor, receiver=responder).
static constexpr int kRecoveryMpiTag = 9894;
static constexpr int kPostRecoveryMpiTag = 9895;
static constexpr int kRecoveryPollIters = 4000; // 4000 * 10ms = 40s
struct FailoverPhaseResult {
int sendRet;
int fatalCount;
int devState0AfterFailover;
};
struct RecoveryPhaseResult {
int recoveryCount0;
};
FailoverPhaseResult fp = {};
RecoveryPhaseResult rp = {};
// Post a recv + send that will trigger CQE errors on both sides after
// we drive their QPs to error.
bool phase1RecvDone = false;
void* recvReq1 = nullptr;
if (rank == 0) {
void* bufs[1] = {regBuf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {1301};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq1), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
// Drive both sender and receiver data QP 0 to ERR — models a link failure
// that breaks QPs on both sides. Both must detect the CQE errors and enter
// recovery for the alive-message handshake to complete.
if (rank == 0) {
ASSERT_EQ(ncclIbCastFaultDriveRecvQpToError(recvComm, 0), ncclSuccess);
} else {
ASSERT_EQ(ncclIbCastFaultDriveQpToError(sendComm, 0), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 1) {
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, regBuf, kMsgSize, 1301, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 500; poll++) {
int done = 0, sz = 0;
ncclResult_t testRet = TestRequest(sendReq, &done, &sz);
if (testRet != ncclSuccess) { sendRet = testRet; break; }
if (done) break;
usleep(kPollIntervalUs);
}
}
int fatalCount = 0;
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
struct ncclIbCastResiliencyState resState = {};
ncclIbCastGetResiliencyState(sendComm, &resState);
fp.sendRet = static_cast<int>(sendRet);
fp.fatalCount = fatalCount;
fp.devState0AfterFailover = resState.devState[0];
MPI_Send(&fp, sizeof(fp), MPI_BYTE, 0, kRecoveryMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
// Poll receiver — the recv will see WR_FLUSH_ERR on QP 0 and enter
// failover+recovery. Data may or may not arrive via surviving device.
for (int poll = 0; poll < 1500; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq1, &done, &sz) != ncclSuccess) break;
if (done) { phase1RecvDone = true; break; }
usleep(kPollIntervalUs);
}
if (!phase1RecvDone) DrainRecvRequest(recvReq1);
MPI_Recv(&fp, sizeof(fp), MPI_BYTE, 1, kRecoveryMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
EXPECT_EQ(fp.sendRet, static_cast<int>(ncclSuccess))
<< "Phase 1: send should complete via surviving device after failover";
EXPECT_EQ(fp.fatalCount, 0)
<< "Phase 1: no fatal error expected";
}
MPI_Barrier(MPI_COMM_WORLD);
// ── Phase 2: wait for recovery to restore devState[0] ──
// Poll devState (set directly by recovery thread) rather than recoveryCount
// (which requires IbCastResiliencyProgress to be called from an active request).
if (rank == 1) {
struct ncclIbCastResiliencyState resState = {};
for (int poll = 0; poll < kRecoveryPollIters; poll++) {
ncclIbCastGetResiliencyState(sendComm, &resState);
if (resState.devState[0] == kDevStateOk || resState.devState[0] == kDevStateRecovered) break;
usleep(10000); // 10 ms
}
rp.recoveryCount0 = resState.devState[0];
MPI_Send(&rp, sizeof(rp), MPI_BYTE, 0, kPostRecoveryMpiTag, MPI_COMM_WORLD);
} else {
MPI_Recv(&rp, sizeof(rp), MPI_BYTE, 1, kPostRecoveryMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
}
MPI_Barrier(MPI_COMM_WORLD);
// ── Phase 3: send 20 messages to verify sustained traffic on restored QPs ──
if (rank == 0) {
EXPECT_TRUE(rp.recoveryCount0 == kDevStateOk || rp.recoveryCount0 == kDevStateRecovered)
<< "Recovery did not restore device 0 within timeout; "
<< "devState[0]=" << rp.recoveryCount0;
}
if (rp.recoveryCount0 < 1) {
if (rank == 0) {
ADD_FAILURE() << "Recovery did not succeed — skipping post-recovery traffic";
}
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
return;
}
constexpr int kBaseTag = 1310;
for (int m = 0; m < kPostRecoveryMsgs; m++) {
char* msgSendBuf = sendBuf.data() + (m + 1) * kMsgSize;
char* msgRecvBuf = recvBuf.data() + (m + 1) * kMsgSize;
void* req = nullptr;
if (rank == 0) {
void* bufs[1] = {msgRecvBuf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {kBaseTag + m};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &req), ncclSuccess);
int sz = 0;
ASSERT_EQ(WaitForCompletion(req, &sz, 10000), ncclSuccess)
<< "Post-recovery message " << m << " recv failed";
} else {
PostSendWithRetry(sendComm, msgSendBuf, kMsgSize, kBaseTag + m, mhandle, &req);
int sz = 0;
ASSERT_EQ(WaitForCompletion(req, &sz, 10000), ncclSuccess)
<< "Post-recovery message " << m << " send failed";
}
}
int fatalCount = 0;
if (rank == 1) {
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
}
MPI_Bcast(&fatalCount, 1, MPI_INT, /*root=*/1, MPI_COMM_WORLD);
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0) {
for (int m = 0; m < kPostRecoveryMsgs; m++) {
size_t offset = (m + 1) * kMsgSize;
EXPECT_EQ(memcmp(recvBuf.data() + offset, sendBuf.data() + offset, kMsgSize), 0)
<< "Data corruption in post-recovery message " << m;
}
EXPECT_EQ(fatalCount, 0)
<< "Fatal error during sustained post-recovery traffic";
}
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: RecoveryPendingWhileLinkDown
//
// Only the sender's data QP 0 is driven to ERR — simulates a link-down
// where the receiver has not yet detected the failure. The sender enters
// recovery and starts sending alive messages, but the receiver never posts
// recv WRs on its recovery QP (no recovery context), so the sender's
// alive messages hang in RNR retry indefinitely.
//
// This is by design: the recovery thread waits indefinitely for the link
// to be restored (the customer may take an arbitrary amount of time to
// fix the physical link). There is no permanent-failure timeout.
//
// Validates: recovery stays in RecoveryInProgress(2), data still flows
// on the surviving device, no crash, no unexpected state transitions.
// =============================================================================
TEST_F(NetIbMPITest, RecoveryPendingWhileLinkDown) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const char* failoverEnv = getenv("NCCL_IB_RESILIENCY_PORT_FAILOVER");
const char* recoveryEnv = getenv("NCCL_IB_RESILIENCY_PORT_RECOVERY");
if (!failoverEnv || strcmp(failoverEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_FAILOVER=1";
}
if (!recoveryEnv || strcmp(recoveryEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_RECOVERY=1";
}
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
int totalDevs = 0;
AssertInitAndGetDevices(&totalDevs);
int mergedDev = CreateMergedDeviceForFailover(net_, totalDevs);
if (mergedDev < 0) {
GTEST_SKIP() << "Requires NIC Fusion (ndevs >= 2). Found " << totalDevs << " physical devices.";
}
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/mergedDev, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 8192;
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>((i * 41 + 23) & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, /*tag=*/1400, mhandle);
ASSERT_GT(actualNqps, 0);
static constexpr int kPendingMpiTag = 9897;
bool phase1RecvDone = false;
void* recvReq = nullptr;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {1401};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
// Only sender QP — receiver stays healthy (link-down simulation)
if (rank == 1) {
ASSERT_EQ(ncclIbCastFaultDriveQpToError(sendComm, 0), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
struct PendingResult {
int sendRet;
int fatalCount;
int devState0;
int outstandingRecovery;
};
PendingResult pr = {};
if (rank == 1) {
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, 1401, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 500; poll++) {
int done = 0, sz = 0;
ncclResult_t testRet = TestRequest(sendReq, &done, &sz);
if (testRet != ncclSuccess) { sendRet = testRet; break; }
if (done) break;
usleep(kPollIntervalUs);
}
}
// Poll until recovery thread starts (devState transitions to
// RecoveryInProgress). Much faster than a fixed sleep on fast HW.
struct ncclIbCastResiliencyState resState = {};
for (int poll = 0; poll < 500; poll++) {
ncclIbCastGetResiliencyState(sendComm, &resState);
if (resState.devState[0] == kDevStateRecoveryInProgress) break;
usleep(10000); // 10ms
}
int fatalCount = 0;
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
ncclIbCastGetResiliencyState(sendComm, &resState);
pr.sendRet = static_cast<int>(sendRet);
pr.fatalCount = fatalCount;
pr.devState0 = resState.devState[0];
pr.outstandingRecovery = resState.outstandingRecovery;
MPI_Send(&pr, sizeof(pr), MPI_BYTE, 0, kPendingMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
for (int poll = 0; poll < 1500; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq, &done, &sz) != ncclSuccess) break;
if (done) { phase1RecvDone = true; break; }
usleep(kPollIntervalUs);
}
MPI_Recv(&pr, sizeof(pr), MPI_BYTE, 1, kPendingMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
// Send should succeed via surviving device (failover works)
EXPECT_EQ(pr.sendRet, static_cast<int>(ncclSuccess))
<< "Send should complete via surviving device";
EXPECT_EQ(pr.fatalCount, 0)
<< "No fatal error expected — failover handles the QP error";
// Recovery should be stuck in RecoveryInProgress(2) — by design,
// the recovery thread waits indefinitely for the link to come back.
EXPECT_EQ(pr.devState0, kDevStateRecoveryInProgress)
<< "Device 0 should be in RecoveryInProgress(2) while link is down; "
<< "got " << pr.devState0;
EXPECT_EQ(pr.outstandingRecovery, 1)
<< "Should have 1 outstanding recovery operation";
if (pr.sendRet == static_cast<int>(ncclSuccess)) {
EXPECT_TRUE(phase1RecvDone)
<< "Send succeeded but receiver never got the data";
}
if (phase1RecvDone) {
EXPECT_EQ(memcmp(recvBuf.data(), sendBuf.data(), kMsgSize), 0)
<< "Data corruption on surviving device after failover";
}
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !phase1RecvDone)
DrainRecvRequest(recvReq);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: RecoveryDeviceOneFailure
//
// Same as RecoverySuccessRestoresTraffic but faults device 1 (QP index 1)
// instead of device 0. QP-to-device mapping is interleaved (QP 0=dev 0,
// QP 1=dev 1, QP 2=dev 0...), so this catches index-arithmetic bugs in
// IbCastPortRecoveryQpsToError, IbCastPortRecoveryQpsRestore, and
// IbCastPortRecoveryContextInit that would only surface with device 1.
// =============================================================================
TEST_F(NetIbMPITest, RecoveryDeviceOneFailure) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const char* failoverEnv = getenv("NCCL_IB_RESILIENCY_PORT_FAILOVER");
const char* recoveryEnv = getenv("NCCL_IB_RESILIENCY_PORT_RECOVERY");
if (!failoverEnv || strcmp(failoverEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_FAILOVER=1";
}
if (!recoveryEnv || strcmp(recoveryEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_RECOVERY=1";
}
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
int totalDevs = 0;
AssertInitAndGetDevices(&totalDevs);
int mergedDev = CreateMergedDeviceForFailover(net_, totalDevs);
if (mergedDev < 0) {
GTEST_SKIP() << "Requires NIC Fusion (ndevs >= 2). Found " << totalDevs << " physical devices.";
}
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/mergedDev, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 8192;
const size_t kBufSize = kMsgSize * 2;
std::vector<char> sendBuf(kBufSize), recvBuf(kBufSize);
for (size_t i = 0; i < kBufSize; i++) sendBuf[i] = static_cast<char>((i * 47 + 31) & 0xFF);
memset(recvBuf.data(), 0, kBufSize);
void* comm = (rank == 0) ? recvComm : sendComm;
char* regBuf = (rank == 0) ? recvBuf.data() : sendBuf.data();
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, regBuf, kBufSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm, recvComm, regBuf, kMsgSize, /*tag=*/1600, mhandle);
ASSERT_GT(actualNqps, 1);
// ── Phase 1: drive both sender and receiver data QP 1 to ERR ─────────
static constexpr int kDev1RecoveryMpiTag = 9900;
static constexpr int kDev1PostRecoveryMpiTag = 9901;
static constexpr int kRecoveryPollIters = 4000; // 40s
struct FailoverPhaseResult {
int sendRet;
int fatalCount;
int devState1AfterFailover;
};
FailoverPhaseResult fp = {};
int devState1AfterRecovery = -1;
bool phase1RecvDone = false;
void* recvReq1 = nullptr;
if (rank == 0) {
void* bufs[1] = {regBuf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {1601};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq1), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
// Fault QP index 1 = device 1 on both sides
if (rank == 0) {
ASSERT_EQ(ncclIbCastFaultDriveRecvQpToError(recvComm, 1), ncclSuccess);
} else {
ASSERT_EQ(ncclIbCastFaultDriveQpToError(sendComm, 1), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 1) {
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, regBuf, kMsgSize, 1601, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 500; poll++) {
int done = 0, sz = 0;
ncclResult_t testRet = TestRequest(sendReq, &done, &sz);
if (testRet != ncclSuccess) { sendRet = testRet; break; }
if (done) break;
usleep(kPollIntervalUs);
}
}
int fatalCount = 0;
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
struct ncclIbCastResiliencyState resState = {};
ncclIbCastGetResiliencyState(sendComm, &resState);
fp.sendRet = static_cast<int>(sendRet);
fp.fatalCount = fatalCount;
fp.devState1AfterFailover = resState.devState[1];
MPI_Send(&fp, sizeof(fp), MPI_BYTE, 0, kDev1RecoveryMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
for (int poll = 0; poll < 1500; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq1, &done, &sz) != ncclSuccess) break;
if (done) { phase1RecvDone = true; break; }
usleep(kPollIntervalUs);
}
if (!phase1RecvDone) DrainRecvRequest(recvReq1);
MPI_Recv(&fp, sizeof(fp), MPI_BYTE, 1, kDev1RecoveryMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
EXPECT_EQ(fp.sendRet, static_cast<int>(ncclSuccess))
<< "Phase 1: send should complete via device 0 after device 1 failover";
EXPECT_EQ(fp.fatalCount, 0)
<< "Phase 1: no fatal error expected";
}
MPI_Barrier(MPI_COMM_WORLD);
// ── Phase 2: wait for recovery on device 1 ──────────────────────────
if (rank == 1) {
struct ncclIbCastResiliencyState resState = {};
for (int poll = 0; poll < kRecoveryPollIters; poll++) {
ncclIbCastGetResiliencyState(sendComm, &resState);
if (resState.devState[1] == kDevStateOk || resState.devState[1] == kDevStateRecovered) break;
usleep(10000);
}
devState1AfterRecovery = resState.devState[1];
MPI_Send(&devState1AfterRecovery, 1, MPI_INT, 0, kDev1PostRecoveryMpiTag, MPI_COMM_WORLD);
} else {
MPI_Recv(&devState1AfterRecovery, 1, MPI_INT, 1, kDev1PostRecoveryMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
}
MPI_Barrier(MPI_COMM_WORLD);
// ── Phase 3: post-recovery send ──────────────────────────────────────
bool phase3RecvDone = false;
void* recvReq3 = nullptr;
if (rank == 0) {
void* bufs[1] = {regBuf + kMsgSize};
size_t sizes[1] = {kMsgSize};
int tags[1] = {1602};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq3), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
struct PostRecoveryResult {
int sendRet;
int fatalCount;
};
static constexpr int kDev1Phase3MpiTag = 9902;
PostRecoveryResult pr = {};
if (rank == 1) {
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, regBuf + kMsgSize, kMsgSize, 1602, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 500; poll++) {
int done = 0, sz = 0;
ncclResult_t testRet = TestRequest(sendReq, &done, &sz);
if (testRet != ncclSuccess) { sendRet = testRet; break; }
if (done) break;
usleep(kPollIntervalUs);
}
}
int fatalCount = 0;
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
pr.sendRet = static_cast<int>(sendRet);
pr.fatalCount = fatalCount;
MPI_Send(&pr, sizeof(pr), MPI_BYTE, 0, kDev1Phase3MpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
for (int poll = 0; poll < 500; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq3, &done, &sz) != ncclSuccess) break;
if (done) { phase3RecvDone = true; break; }
usleep(kPollIntervalUs);
}
MPI_Recv(&pr, sizeof(pr), MPI_BYTE, 1, kDev1Phase3MpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
EXPECT_TRUE(devState1AfterRecovery == kDevStateOk || devState1AfterRecovery == kDevStateRecovered)
<< "Recovery did not restore device 1 within timeout; "
<< "devState[1]=" << devState1AfterRecovery;
EXPECT_EQ(pr.sendRet, static_cast<int>(ncclSuccess))
<< "Post-recovery send failed (sendRet=" << pr.sendRet << ")";
EXPECT_EQ(pr.fatalCount, 0)
<< "Fatal error during post-recovery traffic";
if (pr.sendRet == static_cast<int>(ncclSuccess)) {
EXPECT_TRUE(phase3RecvDone)
<< "Post-recovery send succeeded but receiver did not get data";
}
if (phase3RecvDone) {
size_t offset = kMsgSize;
EXPECT_EQ(memcmp(recvBuf.data() + offset, sendBuf.data() + offset, kMsgSize), 0)
<< "Data corruption in post-recovery message on device 1";
}
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !phase3RecvDone)
DrainRecvRequest(recvReq3);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: RecoveryUdTimeoutExhaustsAttempts
//
// Validates the UD recovery QP retry/timeout cycle. Only the sender's data
// QP is driven to ERR — the receiver never enters recovery. With UD recovery
// QPs, alive messages complete locally (fire-and-forget) but the receiver
// never ACKs. The sender cycles through:
// AliveMessages → Ack (timeout) → retry → ... → RecoveryFailed
//
// This path was unreachable with RC recovery QPs (alive messages hung in
// RNR retry forever). UD makes it testable.
//
// Default timing: 200ms start + 5 * (500ms batch + 5s ack timeout) ≈ 28s
// =============================================================================
TEST_F(NetIbMPITest, RecoveryUdTimeoutExhaustsAttempts) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const char* failoverEnv = getenv("NCCL_IB_RESILIENCY_PORT_FAILOVER");
const char* recoveryEnv = getenv("NCCL_IB_RESILIENCY_PORT_RECOVERY");
if (!failoverEnv || strcmp(failoverEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_FAILOVER=1";
}
if (!recoveryEnv || strcmp(recoveryEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_RECOVERY=1";
}
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
int totalDevs = 0;
AssertInitAndGetDevices(&totalDevs);
int mergedDev = CreateMergedDeviceForFailover(net_, totalDevs);
if (mergedDev < 0) {
GTEST_SKIP() << "Requires NIC Fusion (ndevs >= 2). Found " << totalDevs << " physical devices.";
}
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/mergedDev, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 8192;
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>((i * 53 + 37) & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, /*tag=*/1700, mhandle);
ASSERT_GT(actualNqps, 0);
static constexpr int kTimeoutMpiTag = 9903;
// 200ms start + 5 * (500ms + 5s) ≈ 28s. Poll for 45s to be safe.
static constexpr int kTimeoutPollIters = 4500;
bool phase1RecvDone = false;
void* recvReq = nullptr;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {1701};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 1) {
ASSERT_EQ(ncclIbCastFaultDriveQpToError(sendComm, 0), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
struct TimeoutResult {
int sendRet;
int devState0;
int outstandingRecovery;
};
TimeoutResult tr = {};
if (rank == 1) {
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, 1701, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 500; poll++) {
int done = 0, sz = 0;
ncclResult_t testRet = TestRequest(sendReq, &done, &sz);
if (testRet != ncclSuccess) { sendRet = testRet; break; }
if (done) break;
usleep(kPollIntervalUs);
}
}
// Wait for recovery to exhaust all attempts
struct ncclIbCastResiliencyState resState = {};
for (int poll = 0; poll < kTimeoutPollIters; poll++) {
ncclIbCastGetResiliencyState(sendComm, &resState);
if (resState.devState[0] == kDevStateRecoveryFailed ||
resState.devState[0] == kDevStateErrorPermanent) break;
usleep(10000); // 10ms
}
tr.sendRet = static_cast<int>(sendRet);
tr.devState0 = resState.devState[0];
tr.outstandingRecovery = resState.outstandingRecovery;
MPI_Send(&tr, sizeof(tr), MPI_BYTE, 0, kTimeoutMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
for (int poll = 0; poll < 1500; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq, &done, &sz) != ncclSuccess) break;
if (done) { phase1RecvDone = true; break; }
usleep(kPollIntervalUs);
}
MPI_Recv(&tr, sizeof(tr), MPI_BYTE, 1, kTimeoutMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
EXPECT_EQ(tr.sendRet, static_cast<int>(ncclSuccess))
<< "Send should complete via surviving device";
// Recovery should have failed after exhausting all attempts
EXPECT_TRUE(tr.devState0 == kDevStateRecoveryFailed ||
tr.devState0 == kDevStateErrorPermanent)
<< "Expected RecoveryFailed(3) or ErrorPermanent(5) after UD timeout; "
<< "got devState[0]=" << tr.devState0;
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !phase1RecvDone)
DrainRecvRequest(recvReq);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FaultInjCastOpsPostSendErrno
//
// Ops-overload path: arms the shimmed ibv_post_send to return EAGAIN, so the
// fault fires at the real libibverbs boundary (unlike the pre-call intercept in
// FaultInjCastQpErrorIsFatal).
//
// Verifies: isend returns an error OR fatalErrorCount > 0 when post_send fails.
// Requires: WRR scheduler env vars (CAST_ENV_CHECK_OR_SKIP); no other setup.
// =============================================================================
TEST_F(NetIbMPITest, FaultInjCastOpsPostSendErrno) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
CAST_ENV_CHECK_OR_SKIP();
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/0, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 1024;
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>(i & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup so WRR scheduler state is initialised before arming the fault.
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, /*tag=*/700, mhandle);
ASSERT_GT(actualNqps, 0);
static constexpr int kEagain = EAGAIN;
FaultInjectResult r1 = {};
r1.actualNqps = actualNqps;
if (rank == 1) {
r1.setErrRet = static_cast<int>(ncclSuccess);
for (int q = 0; q < actualNqps; ++q) {
ncclResult_t ret = ncclIbCastFaultOpsSetPostSendError(sendComm, q, kEagain);
if (ret != ncclSuccess && r1.setErrRet == static_cast<int>(ncclSuccess))
r1.setErrRet = static_cast<int>(ret);
}
}
MPI_Barrier(MPI_COMM_WORLD);
bool recvDone = false;
void* recvReq = nullptr;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {701};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq), ncclSuccess);
for (int poll = 0; poll < 100; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq, &done, &sz) != ncclSuccess) break;
if (done) { recvDone = true; break; }
usleep(kPollIntervalUs);
}
} else {
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, 701, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
int fatalCount = 0;
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 200; poll++) {
int done = 0, sz = 0;
ncclResult_t testRet = TestRequest(sendReq, &done, &sz);
if (testRet != ncclSuccess) { sendRet = testRet; break; }
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
if (done || fatalCount > 0) break;
usleep(kPollIntervalUs);
}
}
r1.sendRet = static_cast<int>(sendRet);
r1.fatalCount = fatalCount;
// Disarm before teardown so close paths are clean.
r1.clearRet = static_cast<int>(ncclIbCastFaultOpsClear(sendComm));
MPI_Send(&r1, sizeof(r1), MPI_BYTE, 0, kFaultResultMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
MPI_Recv(&r1, sizeof(r1), MPI_BYTE, 1, kFaultResultMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
EXPECT_EQ(r1.setErrRet, static_cast<int>(ncclSuccess))
<< "rank 1: ncclIbCastFaultOpsSetPostSendError failed with " << r1.setErrRet;
EXPECT_EQ(r1.clearRet, static_cast<int>(ncclSuccess))
<< "rank 1: ncclIbCastFaultOpsClear failed with " << r1.clearRet;
bool isendFailed = (r1.sendRet != static_cast<int>(ncclSuccess));
EXPECT_TRUE(isendFailed || r1.fatalCount > 0)
<< "rank 1: expected isend to fail OR fatalErrorCount > 0 after arming all "
<< r1.actualNqps << " CAST QPs with ops-level post_send EAGAIN; "
<< "isend returned " << r1.sendRet << ", fatalCount=" << r1.fatalCount;
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !recvDone)
DrainRecvRequest(recvReq);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FaultInjCastOpsPollCqFlushNonFatal
//
// Ops-overload path, poll_cq STATUS-REWRITE mode: rewrites real completions on
// sender QP 0 to the whitelisted (recoverable) IBV_WC_WR_FLUSH_ERR, so the
// resiliency layer fails over to the surviving device.
//
// Verifies: transfer fails over to the surviving device with fatalErrorCount == 0
// and data intact.
// Requires: NCCL_IB_RESILIENCY_PORT_FAILOVER=1 and NIC Fusion ndevs >= 2 (else SKIP).
// =============================================================================
TEST_F(NetIbMPITest, FaultInjCastOpsPollCqFlushNonFatal) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const char* failoverEnv = getenv("NCCL_IB_RESILIENCY_PORT_FAILOVER");
if (!failoverEnv || strcmp(failoverEnv, "1") != 0) {
GTEST_SKIP() << "Requires NCCL_IB_RESILIENCY_PORT_FAILOVER=1";
}
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
int totalDevs = 0;
AssertInitAndGetDevices(&totalDevs);
int mergedDev = CreateMergedDeviceForFailover(net_, totalDevs);
if (mergedDev < 0) {
GTEST_SKIP() << "Failover requires NIC Fusion (ndevs >= 2). "
<< "Found " << totalDevs << " physical devices.";
}
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/mergedDev, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 8192;
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>((i * 13 + 7) & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, /*tag=*/710, mhandle);
ASSERT_GT(actualNqps, 0);
struct ncclIbCastResiliencyState resState = {};
struct FailoverResult {
int sendRet;
int fatalCount;
int devState0;
int armRet;
int clearRet;
};
static constexpr int kOpsFailoverMpiTag = 9887;
FailoverResult fr = {};
bool recvDone = false;
void* recvReq = nullptr;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {711};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 1) {
// Rewrite mode: status of real completions on QP 0 becomes WR_FLUSH_ERR.
ncclResult_t armRet = ncclIbCastFaultOpsSetPollCqError(
sendComm, /*qpIdx=*/0, kWcWrFlushErr, /*injectCount=*/-1, /*injectWhenIdle=*/false);
fr.armRet = static_cast<int>(armRet);
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, 711, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 500; poll++) {
int done = 0, sz = 0;
ncclResult_t testRet = TestRequest(sendReq, &done, &sz);
if (testRet != ncclSuccess) { sendRet = testRet; break; }
if (done) break;
usleep(kPollIntervalUs);
}
}
int fatalCount = 0;
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
ncclIbCastGetResiliencyState(sendComm, &resState);
fr.sendRet = static_cast<int>(sendRet);
fr.fatalCount = fatalCount;
fr.devState0 = resState.devState[0];
fr.clearRet = static_cast<int>(ncclIbCastFaultOpsClear(sendComm));
MPI_Send(&fr, sizeof(fr), MPI_BYTE, 0, kOpsFailoverMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
for (int poll = 0; poll < 1500; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq, &done, &sz) != ncclSuccess) break;
if (done) { recvDone = true; break; }
usleep(kPollIntervalUs);
}
MPI_Recv(&fr, sizeof(fr), MPI_BYTE, 1, kOpsFailoverMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
EXPECT_EQ(fr.armRet, static_cast<int>(ncclSuccess))
<< "rank 1: ncclIbCastFaultOpsSetPollCqError failed with " << fr.armRet;
EXPECT_EQ(fr.clearRet, static_cast<int>(ncclSuccess))
<< "rank 1: ncclIbCastFaultOpsClear failed with " << fr.clearRet;
EXPECT_EQ(fr.sendRet, static_cast<int>(ncclSuccess))
<< "Send should complete via surviving device after ops-level CQE rewrite";
EXPECT_EQ(fr.fatalCount, 0)
<< "WR_FLUSH_ERR is whitelisted — no fatal error expected";
EXPECT_NE(fr.devState0, 0)
<< "Device 0 should not be Ok after the injected flush error (got "
<< fr.devState0 << ")";
if (recvDone) {
EXPECT_EQ(memcmp(recvBuf.data(), sendBuf.data(), kMsgSize), 0)
<< "Data corruption after ops-level failover";
}
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !recvDone)
DrainRecvRequest(recvReq);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FaultInjCastOpsPollCqSynthFatal
//
// Ops-overload path, poll_cq SYNTHESIZE mode: on an idle CQ fabricates a
// non-whitelisted IBV_WC_REM_ACCESS_ERR completion. Single device, so there is
// no surviving link to fail over to.
//
// Verifies: isend fails OR fatalErrorCount > 0 (fatal, non-recoverable).
// Requires: WRR scheduler env vars (CAST_ENV_CHECK_OR_SKIP); single device.
// =============================================================================
TEST_F(NetIbMPITest, FaultInjCastOpsPollCqSynthFatal) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
CAST_ENV_CHECK_OR_SKIP();
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/0, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 1024;
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>(i & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, /*tag=*/720, mhandle);
ASSERT_GT(actualNqps, 0);
FaultInjectResult r1 = {};
r1.actualNqps = actualNqps;
if (rank == 1) {
// Synthesize mode: fabricate a single REM_ACCESS_ERR completion when idle.
ncclResult_t ret = ncclIbCastFaultOpsSetPollCqError(
sendComm, /*qpIdx=*/0, kWcRemAccessErr, /*injectCount=*/1, /*injectWhenIdle=*/true);
r1.setErrRet = static_cast<int>(ret);
}
MPI_Barrier(MPI_COMM_WORLD);
bool recvDone = false;
void* recvReq = nullptr;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {721};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq), ncclSuccess);
for (int poll = 0; poll < 100; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq, &done, &sz) != ncclSuccess) break;
if (done) { recvDone = true; break; }
usleep(kPollIntervalUs);
}
} else {
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, 721, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
int fatalCount = 0;
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 300; poll++) {
int done = 0, sz = 0;
ncclResult_t testRet = TestRequest(sendReq, &done, &sz);
if (testRet != ncclSuccess) { sendRet = testRet; break; }
ncclIbCastFaultGetFatalCount(sendComm, &fatalCount);
if (done || fatalCount > 0) break;
usleep(kPollIntervalUs);
}
}
r1.sendRet = static_cast<int>(sendRet);
r1.fatalCount = fatalCount;
r1.clearRet = static_cast<int>(ncclIbCastFaultOpsClear(sendComm));
MPI_Send(&r1, sizeof(r1), MPI_BYTE, 0, kFaultResultMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
MPI_Recv(&r1, sizeof(r1), MPI_BYTE, 1, kFaultResultMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
EXPECT_EQ(r1.setErrRet, static_cast<int>(ncclSuccess))
<< "rank 1: ncclIbCastFaultOpsSetPollCqError failed with " << r1.setErrRet;
EXPECT_EQ(r1.clearRet, static_cast<int>(ncclSuccess))
<< "rank 1: ncclIbCastFaultOpsClear failed with " << r1.clearRet;
bool isendFailed = (r1.sendRet != static_cast<int>(ncclSuccess));
EXPECT_TRUE(isendFailed || r1.fatalCount > 0)
<< "rank 1: expected isend to fail OR fatalErrorCount > 0 after synthesizing a "
<< "non-whitelisted REM_ACCESS_ERR completion; isend returned " << r1.sendRet
<< ", fatalCount=" << r1.fatalCount;
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !recvDone)
DrainRecvRequest(recvReq);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FaultInjCastOpsPostRecvErrno
//
// Ops-overload path, RECEIVER side: the symmetric counterpart of
// FaultInjCastOpsPostSendErrno. Arms the shimmed ibv_post_recv to return EAGAIN
// on every receiver QP, exercising shimPostRecv's errno path (no other test
// reaches it).
//
// Verifies: the receiver reports a PostRecv error when post_recv is faulted on
// all receiver QPs.
// Requires: WRR scheduler env vars (CAST_ENV_CHECK_OR_SKIP); arms all recv QPs.
// =============================================================================
TEST_F(NetIbMPITest, FaultInjCastOpsPostRecvErrno) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
CAST_ENV_CHECK_OR_SKIP();
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/0, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 1024;
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>(i & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup so the connection/QPs are fully established before arming.
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, /*tag=*/730, mhandle);
ASSERT_GT(actualNqps, 0);
static constexpr int kEagain = EAGAIN;
FaultInjectResult r0 = {};
r0.actualNqps = actualNqps;
// Arm post_recv EAGAIN on every receiver QP (rank 0 owns recvComm).
if (rank == 0) {
r0.setErrRet = static_cast<int>(ncclSuccess);
for (int q = 0; q < actualNqps; ++q) {
ncclResult_t ret = ncclIbCastFaultOpsSetPostRecvError(recvComm, q, kEagain);
if (ret != ncclSuccess && r0.setErrRet == static_cast<int>(ncclSuccess))
r0.setErrRet = static_cast<int>(ret);
}
}
MPI_Barrier(MPI_COMM_WORLD);
bool recvDone = false;
void* recvReq = nullptr;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {731};
void* handles[1] = {mhandle};
ncclResult_t recvRet = PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq);
r0.recvRet = static_cast<int>(recvRet);
if (recvRet == ncclSuccess && recvReq != nullptr) {
for (int poll = 0; poll < 200; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq, &done, &sz) != ncclSuccess) {
r0.recvRet = static_cast<int>(ncclSystemError);
break;
}
if (done) { recvDone = true; break; }
usleep(kPollIntervalUs);
}
}
// Disarm before teardown so the close path is clean.
r0.clearRet = static_cast<int>(ncclIbCastFaultOpsClear(recvComm));
} else {
// Sender: best-effort send; do not assert here (rank 0 holds listeners).
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, 731, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 200; poll++) {
int done = 0, sz = 0;
if (TestRequest(sendReq, &done, &sz) != ncclSuccess) break;
if (done) break;
usleep(kPollIntervalUs);
}
}
}
// Rank 0 already has the result locally; no inter-rank ship needed because
// the assertions are recv-side (rank 0).
if (rank == 0) {
EXPECT_EQ(r0.setErrRet, static_cast<int>(ncclSuccess))
<< "rank 0: ncclIbCastFaultOpsSetPostRecvError failed with " << r0.setErrRet;
EXPECT_EQ(r0.clearRet, static_cast<int>(ncclSuccess))
<< "rank 0: ncclIbCastFaultOpsClear failed with " << r0.clearRet;
// post_recv EAGAIN on all receiver QPs: the receive must NOT complete
// cleanly — either PostRecv/Test reported an error, or it never finished.
// Positive signal: post_recv EAGAIN on every receiver QP forces a
// synchronous error, so recv must actually error — a mere "didn't
// finish" (which any timeout satisfies) would let a dead shim pass.
bool recvErrored = (r0.recvRet != static_cast<int>(ncclSuccess));
EXPECT_TRUE(recvErrored)
<< "rank 0: expected recv to error after arming all "
<< r0.actualNqps << " receiver QPs with ops-level post_recv EAGAIN; "
<< "recv post/test returned " << r0.recvRet << ", recvDone=" << recvDone;
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !recvDone)
DrainRecvRequest(recvReq);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FaultInjCastOpsPollCqInjectCountFinite
//
// Ops-overload path, poll_cq REWRITE mode with a FINITE injectCount: arms a
// bounded budget of whitelisted WR_FLUSH_ERR per QP, exercising the shim's
// "pollInjectCount > 0 → decrement" branch (the unlimited/idle tests miss it).
// CAST rotates QP per request as (id + qpIndex) % nqps, so Phase 1 drives
// actualNqps+1 transfers to drain every armed QP before Phase 2 checks recovery.
//
// Verifies: the fault stops after the per-QP budget is spent and a follow-up
// transfer completes cleanly with no new fatal error.
// Requires: WRR scheduler env vars (CAST_ENV_CHECK_OR_SKIP); arms all QPs.
// =============================================================================
TEST_F(NetIbMPITest, FaultInjCastOpsPollCqInjectCountFinite) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
CAST_ENV_CHECK_OR_SKIP();
const int rank = MPIEnvironment::world_rank;
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/0, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 1024;
std::vector<char> sendBuf(kMsgSize), recvBuf(kMsgSize);
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>(i & 0xFF);
memset(recvBuf.data(), 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, /*tag=*/740, mhandle);
ASSERT_GT(actualNqps, 0);
FaultInjectResult r1 = {};
r1.actualNqps = actualNqps;
if (rank == 1) {
// Rewrite mode, FINITE budget per QP, whitelisted status so the connection
// survives and we can verify the post-budget clean path. Arm EVERY QP (not
// just QP 0): a small message rides a single WRR-chosen QP that may not be
// QP 0, so arming all QPs guarantees a real completion gets its status
// rewritten and the finite pollInjectCount>0 decrement branch is exercised.
r1.setErrRet = static_cast<int>(ncclSuccess);
for (int q = 0; q < actualNqps; ++q) {
ncclResult_t ret = ncclIbCastFaultOpsSetPollCqError(
sendComm, q, kWcWrFlushErr, /*injectCount=*/1, /*injectWhenIdle=*/false);
if (ret != ncclSuccess && r1.setErrRet == static_cast<int>(ncclSuccess))
r1.setErrRet = static_cast<int>(ret);
}
}
MPI_Barrier(MPI_COMM_WORLD);
// ---- Phase 1: drain the per-QP fault budget on EVERY QP ----
// CAST selects the QP per request as (req->id + qpIndex) % nqps
// (IbCastCommBaseGetQpForRequest in common_cast.h), so a single transfer
// spends only ONE QP's injectCount=1 budget. We armed every QP, so we must
// drive at least actualNqps sequential transfers — their rotating request
// ids hit each QP at least once — before the budget is fully exhausted.
// Driving fewer would leave some QPs armed, and Phase 2 could still land on
// an unspent QP and see another injected WR_FLUSH_ERR (the flakiness this
// structure removes). The extra round guards against a transfer that splits
// across two QPs and leaves one budget unspent.
const int drainRounds = actualNqps + 1;
for (int i = 0; i < drainRounds; ++i) {
const int tag = 741 + i;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {tag};
void* handles[1] = {mhandle};
void* recvReq = nullptr;
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq), ncclSuccess);
bool done1 = false;
for (int poll = 0; poll < 300; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq, &done, &sz) != ncclSuccess) break;
if (done) { done1 = true; break; }
usleep(kPollIntervalUs);
}
if (!done1) DrainRecvRequest(recvReq);
} else {
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, tag, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 300; poll++) {
int done = 0, sz = 0;
if (TestRequest(sendReq, &done, &sz) != ncclSuccess) break;
if (done) break;
usleep(kPollIntervalUs);
}
}
if (sendRet != ncclSuccess && r1.sendRet == static_cast<int>(ncclSuccess))
r1.sendRet = static_cast<int>(sendRet);
}
MPI_Barrier(MPI_COMM_WORLD);
}
// ---- Phase 2: after the budget is spent, a clean transfer must pass ----
bool phase2RecvDone = false;
void* recvReq2 = nullptr;
int phase2Fatal = 0;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {kMsgSize};
int tags[1] = {800};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &recvReq2), ncclSuccess);
for (int poll = 0; poll < 300; poll++) {
int done = 0, sz = 0;
if (TestRequest(recvReq2, &done, &sz) != ncclSuccess) break;
if (done) { phase2RecvDone = true; break; }
usleep(kPollIntervalUs);
}
} else {
void* sendReq = nullptr;
ncclResult_t sendRet = ncclSuccess;
for (int attempt = 0; attempt < kMaxRetryAttempts; attempt++) {
sendRet = PostSend(sendComm, buf, kMsgSize, 800, mhandle, &sendReq);
if (sendRet != ncclSuccess || sendReq != nullptr) break;
usleep(kPollIntervalUs);
}
if (sendRet == ncclSuccess && sendReq != nullptr) {
for (int poll = 0; poll < 300; poll++) {
int done = 0, sz = 0;
if (TestRequest(sendReq, &done, &sz) != ncclSuccess) break;
if (done) break;
usleep(kPollIntervalUs);
}
}
ncclIbCastFaultGetFatalCount(sendComm, &phase2Fatal);
r1.phase2FatalCount = phase2Fatal;
r1.clearRet = static_cast<int>(ncclIbCastFaultOpsClear(sendComm));
MPI_Send(&r1, sizeof(r1), MPI_BYTE, 0, kFaultResultMpiTag, MPI_COMM_WORLD);
}
if (rank == 0) {
MPI_Recv(&r1, sizeof(r1), MPI_BYTE, 1, kFaultResultMpiTag, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
EXPECT_EQ(r1.setErrRet, static_cast<int>(ncclSuccess))
<< "rank 1: arming finite-count poll_cq fault failed with " << r1.setErrRet;
// Phase 2 (after the per-QP injectCount=1 budget is spent) must be clean:
// the receive completes and no fatal error is recorded on the sender.
EXPECT_TRUE(phase2RecvDone)
<< "phase 2 receive did not complete after the finite fault budget was spent";
EXPECT_EQ(r1.phase2FatalCount, 0)
<< "phase 2 sender saw fatalCount=" << r1.phase2FatalCount
<< " after the finite WR_FLUSH_ERR budget should have been exhausted";
EXPECT_EQ(r1.clearRet, static_cast<int>(ncclSuccess))
<< "rank 1: ncclIbCastFaultOpsClear failed with " << r1.clearRet;
}
MPI_Barrier(MPI_COMM_WORLD);
if (rank == 0 && !phase2RecvDone) DrainRecvRequest(recvReq2);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: FaultInjCastOpsApiInvalidArgs
//
// Unit-style coverage of the ops-fault API guard branches: NULL comm and the
// install/registration lookups that the functional tests never hit (they always
// pass a live, shimmed connection). Calls each API with NULL, then re-arms/clears
// on a live connection to confirm the success guards.
//
// Verifies: each API returns ncclInvalidArgument on NULL comm and ncclSuccess
// on a live connection.
// Requires: WRR scheduler env vars (CAST_ENV_CHECK_OR_SKIP); no data transfer.
// =============================================================================
TEST_F(NetIbMPITest, FaultInjCastOpsApiInvalidArgs) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
CAST_ENV_CHECK_OR_SKIP();
static constexpr int kEagain = EAGAIN;
// ---- NULL comm: every Ops API must reject with ncclInvalidArgument ----
EXPECT_EQ(ncclIbCastFaultOpsSetPostSendError(nullptr, 0, kEagain), ncclInvalidArgument);
EXPECT_EQ(ncclIbCastFaultOpsSetPostRecvError(nullptr, 0, kEagain), ncclInvalidArgument);
EXPECT_EQ(ncclIbCastFaultOpsSetPollCqError(nullptr, 0, kWcWrFlushErr, -1, false),
ncclInvalidArgument);
EXPECT_EQ(ncclIbCastFaultOpsClear(nullptr), ncclInvalidArgument);
// ---- Live connection: success guards (registered context) ----
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(/*dev=*/0, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 1024;
std::vector<char> buf(kMsgSize, 0);
void* comm = (MPIEnvironment::world_rank == 0) ? recvComm : sendComm;
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf.data(), kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Establish QPs so the context is shimmed/registered.
const int actualNqps = GetActualNqps(sendComm, recvComm, buf.data(), kMsgSize,
/*tag=*/750, mhandle);
ASSERT_GT(actualNqps, 0);
if (MPIEnvironment::world_rank == 1) {
// Arm then clear on a registered context: both must succeed.
EXPECT_EQ(ncclIbCastFaultOpsSetPostSendError(sendComm, 0, kEagain), ncclSuccess);
EXPECT_EQ(ncclIbCastFaultOpsSetPollCqError(sendComm, 0, kWcWrFlushErr, 1, false),
ncclSuccess);
EXPECT_EQ(ncclIbCastFaultOpsClear(sendComm), ncclSuccess);
// Clear again (already cleared) must still succeed (idempotent).
EXPECT_EQ(ncclIbCastFaultOpsClear(sendComm), ncclSuccess);
} else {
EXPECT_EQ(ncclIbCastFaultOpsSetPostRecvError(recvComm, 0, kEagain), ncclSuccess);
EXPECT_EQ(ncclIbCastFaultOpsClear(recvComm), ncclSuccess);
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
#endif /* MPI_TESTS_ENABLED && ENABLE_FAULT_INJECTION */