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

1484 lines
63 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*************************************************************************
* Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved.
*
* See LICENSE.txt for license information
************************************************************************/
#include "NetIbMPITestBase.hpp"
#include "NetIbCastInspect.hpp"
#include <initializer_list>
#ifdef MPI_TESTS_ENABLED
// =============================================================================
// Test: CastEqualWeightsTwoQPsTokenCounts
//
// White-box: request 2 QPs, equal weights (schedWeight=0), WRR+split mode.
// Actual nqps determined at runtime (ionic caps at 1, mlx5 supports 2+).
// Verifies: initTokens.totTokens=100, per-QP tokens equal, sum invariant.
// =============================================================================
TEST_F(NetIbMPITest, CastEqualWeightsTwoQPsTokenCounts) {
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 size_t kMsgSize = 1024;
char sendBuf[kMsgSize], recvBuf[kMsgSize];
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>(i & 0xFF);
memset(recvBuf, 0, sizeof(recvBuf));
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf) : static_cast<void*>(sendBuf);
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup: learn real nqps (NCCL_PARAM cache may cap below requested 2).
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, 122, mhandle);
ASSERT_GT(actualNqps, 0);
// Arm equal-weight tokens.
if (rank == 1) {
const std::vector<int> tokens = EqualTokens(actualNqps);
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
}
CastDoSendRecv(rank, sendComm, recvComm, buf, kMsgSize, 123, mhandle);
if (rank == 0)
EXPECT_EQ(memcmp(sendBuf, recvBuf, kMsgSize), 0) << "data mismatch";
if (rank == 1) {
struct ncclIbCastSchedState state = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &state), ncclSuccess);
ExpectEqualWeightInitTokens(state, actualNqps);
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastWeightsDistributionOneRound
//
// White-box: Verifies that exactly totTokens sends exhaust all WRR tokens.
// Two phases on the same connection:
// Phase 1: equal weights (100/nqps each) — totTokens sends → activeTokens == 0
// Phase 2: unequal weights (triangular: higher-indexed QPs get fewer tokens)
// — totTokens sends → activeTokens == 0
// SetTokens resets both init and active tokens, so no reconnect needed.
// =============================================================================
TEST_F(NetIbMPITest, CastWeightsDistributionOneRound) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const int rank = MPIEnvironment::world_rank;
CAST_ENV_CHECK_OR_SKIP();
CAST_REQUIRE_UPDATE_INTERVAL_OR_SKIP(10000000);
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(0, &listenComm, &sendComm, &recvComm);
constexpr int kTotTokens = 100;
constexpr size_t kMsgSz = 64;
// Buffer sized for kTotTokens messages.
char sendBuf[kTotTokens * kMsgSz], recvBuf[kTotTokens * kMsgSz];
for (size_t i = 0; i < sizeof(sendBuf); i++) sendBuf[i] = static_cast<char>(i & 0xFF);
memset(recvBuf, 0, sizeof(recvBuf));
void* comm = (rank == 0) ? recvComm : sendComm;
void* baseBuf = (rank == 0) ? static_cast<void*>(recvBuf) : static_cast<void*>(sendBuf);
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, baseBuf, sizeof(sendBuf), NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup: learn real nqps before arming tokens.
const int actualNqps = GetActualNqps(sendComm, recvComm, baseBuf, kMsgSz, 199, mhandle);
ASSERT_GT(actualNqps, 0);
// Phase 1: equal weights, kTotTokens sends exhaust one full WRR round.
if (rank == 1) {
const std::vector<int> tokens = EqualTokens(actualNqps, kTotTokens);
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
}
CastDoBatchSendRecv(rank, sendComm, recvComm, sendBuf, recvBuf, kMsgSz, kTotTokens, 200, mhandle);
if (rank == 1) {
struct ncclIbCastSchedState state = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &state), ncclSuccess);
ASSERT_TRUE(state.schedInit);
EXPECT_EQ(state.activeTotTokens, 0) << "equal weights: after one full round active tokens must be 0";
}
MPI_Barrier(MPI_COMM_WORLD);
// Phase 2: unequal weights (triangular: token[i] ∝ nqps-i), kTotTokens sends.
if (rank == 1) {
const int weightSum = actualNqps * (actualNqps + 1) / 2;
std::vector<int> tokens(actualNqps);
int allocated = 0;
for (int i = 0; i < actualNqps - 1; i++) {
tokens[i] = kTotTokens * (actualNqps - i) / weightSum;
allocated += tokens[i];
}
tokens[actualNqps - 1] = kTotTokens - allocated;
// For nqps=1 the sequence is trivially {100}; strictly-decreasing check only for nqps>1.
if (actualNqps > 1) {
for (int i = 0; i < actualNqps - 1; i++)
ASSERT_GT(tokens[i], tokens[i + 1])
<< "triangular sequence not strictly decreasing for nqps=" << actualNqps;
}
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
}
CastDoBatchSendRecv(rank, sendComm, recvComm, sendBuf, recvBuf, kMsgSz, kTotTokens, 300, mhandle);
if (rank == 1) {
struct ncclIbCastSchedState state = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &state), ncclSuccess);
ASSERT_TRUE(state.schedInit);
EXPECT_EQ(state.activeTotTokens, 0) << "unequal weights: after one full round active tokens must be 0";
}
// Forward rank-1 gtest failures to rank 0 so the test's exit code reflects them.
// (rank 1 has its default gtest listeners detached by main_mpi.cpp.)
{
int total_failed = 0; std::string msg;
if (rank == 1) {
auto* r = ::testing::UnitTest::GetInstance()->current_test_info()->result();
for (int i = 0; i < r->total_part_count(); i++) {
const auto& p = r->GetTestPartResult(i);
if (p.failed()) { total_failed++; msg += std::string(" - ") + (p.summary() ? p.summary() : "") + "\n"; }
}
int len = (int)msg.size();
MPI_Send(&total_failed, 1, MPI_INT, 0, 9620, MPI_COMM_WORLD);
MPI_Send(&len, 1, MPI_INT, 0, 9621, MPI_COMM_WORLD);
if (len > 0) MPI_Send(msg.data(), len, MPI_CHAR, 0, 9622, MPI_COMM_WORLD);
} else if (rank == 0) {
int len = 0;
MPI_Recv(&total_failed, 1, MPI_INT, 1, 9620, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Recv(&len, 1, MPI_INT, 1, 9621, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
if (len > 0) {
msg.assign(len, '\0');
MPI_Recv(msg.data(), len, MPI_CHAR, 1, 9622, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
ADD_FAILURE() << "rank 1 reported " << total_failed << " failure(s):\n" << msg;
}
}
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastTokenSumInvariantAfterConsumption
//
// White-box: Equal tokens. After 10 sends: initTokens immutable,
// sum(activeQpTokens)==activeTotTokens.
// =============================================================================
TEST_F(NetIbMPITest, CastTokenSumInvariantAfterConsumption) {
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(0, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 128;
constexpr int kNMsgs = 10;
char sendBuf[kMsgSize * kNMsgs], recvBuf[kMsgSize * kNMsgs];
for (size_t i = 0; i < sizeof(sendBuf); i++) sendBuf[i] = static_cast<char>(i & 0xFF);
memset(recvBuf, 0, sizeof(recvBuf));
void* comm = (rank == 0) ? recvComm : sendComm;
void* baseBuf = (rank == 0) ? static_cast<void*>(recvBuf) : static_cast<void*>(sendBuf);
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, baseBuf, sizeof(sendBuf), NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup: learn real nqps.
const int actualNqps = GetActualNqps(sendComm, recvComm, baseBuf, kMsgSize, 699, mhandle);
ASSERT_GT(actualNqps, 0);
if (rank == 1) {
const std::vector<int> tokens = EqualTokens(actualNqps);
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
}
CastDoBatchSendRecv(rank, sendComm, recvComm, sendBuf, recvBuf, kMsgSize, kNMsgs, 700, mhandle);
if (rank == 1) {
struct ncclIbCastSchedState state = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &state), ncclSuccess);
ExpectEqualWeightInitTokens(state, actualNqps);
ExpectActiveTokenSumInvariant(state);
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastSingleQPBypassesWrr
//
// White-box: nqps=1. WRR must be bypassed (schedInit stays false).
// =============================================================================
TEST_F(NetIbMPITest, CastSingleQPBypassesWrr) {
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(0, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 256;
char sendBuf[kMsgSize], recvBuf[kMsgSize];
for (size_t i = 0; i < kMsgSize; i++) sendBuf[i] = static_cast<char>((i * 3) & 0xFF);
memset(recvBuf, 0, sizeof(recvBuf));
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf) : static_cast<void*>(sendBuf);
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
CastDoSendRecv(rank, sendComm, recvComm, buf, kMsgSize, 800, mhandle);
if (rank == 0)
EXPECT_EQ(memcmp(sendBuf, recvBuf, kMsgSize), 0) << "data mismatch";
if (rank == 1) {
struct ncclIbCastSchedState state = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &state), ncclSuccess);
if (state.nqps <= 1) {
EXPECT_FALSE(state.schedInit) << "WRR must be bypassed for nqps=1";
}
// else: nqps was cached to a higher value; skip the bypass assertion.
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastSchedParmsReflectEnvVars
//
// White-box: schedParms inside sendComm must match env vars:
// enable=true, doWrr=true, splitData=false, splitDataMin from env.
// =============================================================================
TEST_F(NetIbMPITest, CastSchedParmsReflectEnvVars) {
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: library ignores zero, "
"state.splitDataMin would not match";
}
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(0, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 64;
char sendBuf[kMsgSize], recvBuf[kMsgSize];
memset(sendBuf, 0xAB, sizeof(sendBuf));
memset(recvBuf, 0, sizeof(recvBuf));
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf) : static_cast<void*>(sendBuf);
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
CastDoSendRecv(rank, sendComm, recvComm, buf, kMsgSize, 900, mhandle);
if (rank == 1) {
struct ncclIbCastSchedState state = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &state), ncclSuccess);
EXPECT_TRUE(state.schedEnable);
EXPECT_TRUE(state.doWrr);
EXPECT_EQ(state.splitDataMin, GetSplitDataMin());
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastCursorWrapsAtNqpsBoundary
//
// White-box: actual nqps determined at runtime. Tokens={0,...,0,1} (only last QP).
// The WRR while(1) loop must skip QP0..nqps-2 and select QP[nqps-1].
// After selection cursor advances to 0.
// =============================================================================
TEST_F(NetIbMPITest, CastCursorWrapsAtNqpsBoundary) {
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(0, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSize = 128;
char sendBuf[kMsgSize], recvBuf[kMsgSize];
memset(sendBuf, 0xCC, kMsgSize);
memset(recvBuf, 0, kMsgSize);
void* comm = (rank == 0) ? recvComm : sendComm;
void* buf = (rank == 0) ? static_cast<void*>(recvBuf) : static_cast<void*>(sendBuf);
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kMsgSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup: learn real nqps.
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSize, 999, mhandle);
ASSERT_GT(actualNqps, 0);
// Set single token on the last QP; all others get 0.
if (rank == 1) {
std::vector<int> tokens(actualNqps, 0);
tokens[actualNqps - 1] = 1;
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
}
// The send must land on QP[nqps-1] and wrap cursor to 0.
CastDoSendRecv(rank, sendComm, recvComm, buf, kMsgSize, 1000, mhandle);
if (rank == 1) {
struct ncclIbCastSchedState state = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &state), ncclSuccess);
ASSERT_TRUE(state.schedInit);
EXPECT_EQ(state.activeTotTokens, 0);
EXPECT_EQ(state.qpIndex, 0);
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastMaxQPCount128
//
// Run exactly actualNqps sends — one full WRR round.
// Verify every QP was selected exactly once: activeQpTokens[i]==0 for all i,
// activeTotTokens==0, and qpIndex==0 (cursor wrapped back to start).
// =============================================================================
TEST_F(NetIbMPITest, CastMaxQPCount128) {
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(0, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSz = 32;
constexpr size_t kBufSz = (NCCL_IB_MAX_QPS + 1) * kMsgSz;
constexpr int kBaseTag = 1300;
std::vector<char> sendBuf(kBufSz, 0);
std::vector<char> recvBuf(kBufSz, 0);
for (size_t i = 0; i < kBufSz; i++) sendBuf[i] = static_cast<char>(i & 0xFF);
void* comm = (rank == 0) ? recvComm : sendComm;
void* baseBuf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, baseBuf, kBufSz, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup: learn real nqps (driver caps 128 at hardware limit).
const int actualNqps = GetActualNqps(sendComm, recvComm, baseBuf, kMsgSz, 999, mhandle);
ASSERT_GT(actualNqps, 0);
// Set 1 token per QP — each QP selected exactly once across kNMsgs sends.
if (rank == 1) {
std::vector<int> tokens(actualNqps, 1);
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
}
// Post all actualNqps sends/recvs concurrently (skip slot 0 used by warmup).
CastDoBatchSendRecv(rank, sendComm, recvComm,
sendBuf.data() + kMsgSz, recvBuf.data() + kMsgSz,
kMsgSz, actualNqps, kBaseTag, mhandle);
if (rank == 1) {
struct ncclIbCastSchedState state = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &state), ncclSuccess);
ASSERT_TRUE(state.schedInit);
for (int i = 0; i < state.nqps; i++)
EXPECT_EQ(state.initQpTokens[i], 1) << "QP " << i << " initToken must be 1";
EXPECT_EQ(state.activeTotTokens, 0);
for (int i = 0; i < state.nqps; i++)
EXPECT_EQ(state.activeQpTokens[i], 0) << "QP " << i << " activeToken must be 0";
EXPECT_EQ(state.qpIndex, 0);
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastFourQPsMonotonicOrder
//
// White-box: request 4 QPs; actual nqps determined at runtime.
// Assign tokens as a strictly-decreasing sequence summing to 100 using
// triangular proportions: token[i] = round(100 * (nqps-i) / weightSum),
// where weightSum = nqps*(nqps+1)/2; the last token absorbs rounding residual.
// For nqps=1: sequence is trivially {100}; monotonic check is skipped.
// Run exactly 100 sends (= totTokens). Verify:
// - initQpTokens are strictly decreasing (nqps>1)
// - activeTotTokens == 0 after exactly totTokens sends
// - activeQpTokens[i] == 0 for all i
// =============================================================================
TEST_F(NetIbMPITest, CastFourQPsMonotonicOrder) {
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(0, &listenComm, &sendComm, &recvComm);
constexpr int kNMsgs = 100;
constexpr size_t kMsgSz = 32;
constexpr size_t kBufSz = (kNMsgs + 1) * kMsgSz;
constexpr int kBaseTag = 1500;
std::vector<char> sendBuf(kBufSz, 0);
std::vector<char> recvBuf(kBufSz, 0);
for (size_t i = 0; i < kBufSz; i++) sendBuf[i] = static_cast<char>(i & 0xFF);
void* comm = (rank == 0) ? recvComm : sendComm;
void* baseBuf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, baseBuf, kBufSz, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup: learn real nqps.
const int actualNqps = GetActualNqps(sendComm, recvComm, baseBuf, kMsgSz, 998, mhandle);
ASSERT_GT(actualNqps, 0);
// Build strictly-decreasing token sequence summing to kNMsgs.
if (rank == 1) {
const int weightSum = actualNqps * (actualNqps + 1) / 2;
std::vector<int> tokens(actualNqps);
int allocated = 0;
for (int i = 0; i < actualNqps - 1; i++) {
tokens[i] = kNMsgs * (actualNqps - i) / weightSum;
allocated += tokens[i];
}
tokens[actualNqps - 1] = kNMsgs - allocated;
if (actualNqps > 1) {
bool isDecreasing = true;
for (int i = 0; i < actualNqps - 1; i++)
if (tokens[i] <= tokens[i + 1]) { isDecreasing = false; break; }
ASSERT_TRUE(isDecreasing)
<< "Generated token sequence is not strictly decreasing for nqps=" << actualNqps;
}
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
}
// Verify initTokens immediately after SetTokens (before timer can overwrite).
if (rank == 1) {
struct ncclIbCastSchedState probe = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &probe), ncclSuccess);
ASSERT_TRUE(probe.schedInit);
if (actualNqps > 1) {
for (int i = 0; i < probe.nqps - 1; i++)
EXPECT_GT(probe.initQpTokens[i], probe.initQpTokens[i + 1])
<< "initTokens not strictly decreasing at index " << i;
}
}
// Phase 1: exactly kNMsgs sends = one full WRR round.
CastDoBatchSendRecv(rank, sendComm, recvComm,
sendBuf.data() + kMsgSz, recvBuf.data() + kMsgSz,
kMsgSz, kNMsgs, kBaseTag, mhandle);
if (rank == 1) {
struct ncclIbCastSchedState state = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &state), ncclSuccess);
ASSERT_TRUE(state.schedInit);
EXPECT_EQ(state.activeTotTokens, 0);
for (int i = 0; i < state.nqps; i++)
EXPECT_EQ(state.activeQpTokens[i], 0)
<< "QP " << i << " activeToken must be 0 after full round";
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastSplitDataThresholdBoundary
//
// White-box: splitDataMin from env. Per-QP threshold = splitDataMin * nqps.
// size >= threshold → split path → 0 WRR tokens consumed
// size < threshold → WRR path → 1 WRR token consumed
// Actual nqps determines threshold dynamically.
// =============================================================================
TEST_F(NetIbMPITest, CastSplitDataThresholdBoundary) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const int rank = MPIEnvironment::world_rank;
CAST_ENV_CHECK_OR_SKIP();
{
const char* v = getenv("NCCL_IB_SPLIT_DATA_ON_QPS");
if (!v || strcmp(v, "1") != 0)
GTEST_SKIP() << "CastSplitDataThresholdBoundary requires NCCL_IB_SPLIT_DATA_ON_QPS=1";
}
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(0, &listenComm, &sendComm, &recvComm);
void* comm = (rank == 0) ? recvComm : sendComm;
// Phase 0: small warmup buffer to learn actualNqps before allocating the real buffer.
constexpr size_t kWarmupSz = 128;
std::vector<char> warmupBuf(kWarmupSz, 0);
void* warmupBase = warmupBuf.data();
void* warmupHandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, warmupBase, kWarmupSz, NCCL_PTR_HOST, &warmupHandle), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm, recvComm, warmupBase, 64, 1599, warmupHandle);
ASSERT_GT(actualNqps, 0);
ASSERT_EQ(DeregisterMemory(comm, warmupHandle), ncclSuccess);
// threshold = splitDataMin * nqps (from net_ib_cast.cc: dataPerQp = size*nreqs/nqps)
const size_t kSplitDataMin = GetSplitDataMin();
if (kSplitDataMin == 0) {
// With splitDataMin=0 every message goes through the split path regardless of size.
// There is no WRR boundary to test, so skip rather than crash on zero-size buffer.
GTEST_SKIP() << "RCCL_IB_QP_SCHED_SPLIT_DATA_MIN=0: no WRR/split boundary exists";
}
const size_t kThreshold = kSplitDataMin * static_cast<size_t>(actualNqps);
const size_t kSplitSz = kThreshold; // dataPerQp == splitDataMin → split
const size_t kWrrSz = kThreshold - 1; // dataPerQp < splitDataMin → WRR
// Allocate and register the real buffer sized for the actual threshold.
const size_t kBufSz = kThreshold;
std::vector<char> sendBuf(kBufSz, 0x5A);
std::vector<char> recvBuf(kBufSz, 0x00);
void* baseBuf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, baseBuf, kBufSz, NCCL_PTR_HOST, &mhandle), ncclSuccess);
if (rank == 1) {
const std::vector<int> tokens = EqualTokens(actualNqps);
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
}
// Large send: split path — activeTotTokens must NOT change.
int activeTotBeforeSplit = 0;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
activeTotBeforeSplit = st.activeTotTokens;
}
CastDoSendRecv(rank, sendComm, recvComm, baseBuf, kSplitSz, 1600, mhandle);
int activeTotAfterSplit = 0;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
ASSERT_TRUE(st.schedInit);
EXPECT_EQ(st.activeTotTokens, activeTotBeforeSplit)
<< "split path must not consume WRR tokens";
activeTotAfterSplit = st.activeTotTokens;
}
// Small send: WRR path — activeTotTokens must decrease by exactly 1.
CastDoSendRecv(rank, sendComm, recvComm, baseBuf, kWrrSz, 1601, mhandle);
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
ASSERT_TRUE(st.schedInit);
int delta = activeTotAfterSplit - st.activeTotTokens;
EXPECT_EQ(delta, 1) << "WRR path must consume exactly one token";
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastAlternatingWrrNonWrr
//
// White-box: Toggle doWrr mid-test on an established connection.
// Phase 1 (doWrr=true): 10 sends → 10 WRR tokens consumed
// Phase 2 (doWrr=false): 10 sends → 0 WRR tokens consumed
// Phase 3 (doWrr=true): 10 sends → 10 WRR tokens consumed
// =============================================================================
TEST_F(NetIbMPITest, CastAlternatingWrrNonWrr) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const int rank = MPIEnvironment::world_rank;
CAST_ENV_CHECK_OR_SKIP();
CAST_REQUIRE_UPDATE_INTERVAL_OR_SKIP(10000000);
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(0, &listenComm, &sendComm, &recvComm);
constexpr int kPhase = 10;
constexpr size_t kMsgSz = 64;
constexpr int kBaseTag = 1700;
char sendBuf[kPhase * 3 * kMsgSz], recvBuf[kPhase * 3 * kMsgSz];
memset(sendBuf, 0xAA, sizeof(sendBuf));
memset(recvBuf, 0, sizeof(recvBuf));
void* comm = (rank == 0) ? recvComm : sendComm;
void* baseBuf = (rank == 0) ? static_cast<void*>(recvBuf) : static_cast<void*>(sendBuf);
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, baseBuf, sizeof(sendBuf), NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup: learn real nqps.
const int actualNqps = GetActualNqps(sendComm, recvComm, baseBuf, kMsgSz, 1699, mhandle);
ASSERT_GT(actualNqps, 0);
if (rank == 1) {
const std::vector<int> tokens = EqualTokens(actualNqps);
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
}
auto doPhase = [&](int phaseBase) {
CastDoBatchSendRecv(rank, sendComm, recvComm,
sendBuf + phaseBase * kMsgSz, recvBuf + phaseBase * kMsgSz,
kMsgSz, kPhase, kBaseTag + phaseBase, mhandle);
};
// Phase 1: WRR on
int activeTotBefore1 = 0;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
activeTotBefore1 = st.activeTotTokens;
}
doPhase(0);
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
int consumed = activeTotBefore1 - st.activeTotTokens;
ASSERT_TRUE(st.schedInit);
EXPECT_EQ(consumed, kPhase) << "phase 1: expected " << kPhase << " WRR selections";
ASSERT_EQ(ncclIbCastSetSchedParms(sendComm, true, false, false, GetSplitDataMin()), ncclSuccess);
}
int activeTotBefore2 = 0;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
activeTotBefore2 = st.activeTotTokens;
}
doPhase(kPhase);
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
int consumed = activeTotBefore2 - st.activeTotTokens;
EXPECT_EQ(consumed, 0) << "phase 2: doWrr=false must not consume WRR tokens";
ASSERT_EQ(ncclIbCastSetSchedParms(sendComm, true, true, false, GetSplitDataMin()), ncclSuccess);
}
int activeTotBefore3 = 0;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
activeTotBefore3 = st.activeTotTokens;
}
doPhase(kPhase * 2);
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
int consumed = activeTotBefore3 - st.activeTotTokens;
EXPECT_EQ(consumed, kPhase) << "phase 3: expected " << kPhase << " WRR selections";
}
// Forward rank-1 gtest failures to rank 0 (default listeners detached on non-zero ranks).
{
int total_failed = 0; std::string msg;
if (rank == 1) {
auto* r = ::testing::UnitTest::GetInstance()->current_test_info()->result();
for (int i = 0; i < r->total_part_count(); i++) {
const auto& p = r->GetTestPartResult(i);
if (p.failed()) { total_failed++; msg += std::string(" - ") + (p.summary() ? p.summary() : "") + "\n"; }
}
int len = (int)msg.size();
MPI_Send(&total_failed, 1, MPI_INT, 0, 9630, MPI_COMM_WORLD);
MPI_Send(&len, 1, MPI_INT, 0, 9631, MPI_COMM_WORLD);
if (len > 0) MPI_Send(msg.data(), len, MPI_CHAR, 0, 9632, MPI_COMM_WORLD);
} else if (rank == 0) {
int len = 0;
MPI_Recv(&total_failed, 1, MPI_INT, 1, 9630, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Recv(&len, 1, MPI_INT, 1, 9631, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
if (len > 0) {
msg.assign(len, '\0');
MPI_Recv(msg.data(), len, MPI_CHAR, 1, 9632, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
ADD_FAILURE() << "rank 1 reported " << total_failed << " failure(s):\n" << msg;
}
}
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastEnableDisableSplitData
//
// White-box: Toggle splitData on an established connection.
// Message size = splitDataMin * nqps (at the per-QP threshold boundary).
// Phase 1 (splitData=false): oneQp WRR → 1 token consumed
// Phase 2 (splitData=true): split path → 0 tokens consumed
// Phase 3 (splitData=false): oneQp WRR → 1 token consumed
// =============================================================================
TEST_F(NetIbMPITest, CastEnableDisableSplitData) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const int rank = MPIEnvironment::world_rank;
CAST_ENV_CHECK_OR_SKIP();
CAST_REQUIRE_UPDATE_INTERVAL_OR_SKIP(10000000);
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(0, &listenComm, &sendComm, &recvComm);
void* comm = (rank == 0) ? recvComm : sendComm;
// Phase 0: small warmup buffer to learn actualNqps before allocating the real buffer.
constexpr size_t kWarmupSz = 128;
std::vector<char> warmupBuf(kWarmupSz, 0);
void* warmupBase = warmupBuf.data();
void* warmupHandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, warmupBase, kWarmupSz, NCCL_PTR_HOST, &warmupHandle), ncclSuccess);
const int actualNqps = GetActualNqps(sendComm, recvComm, warmupBase, 64, 1799, warmupHandle);
ASSERT_GT(actualNqps, 0);
ASSERT_EQ(DeregisterMemory(comm, warmupHandle), ncclSuccess);
// Message at the split threshold: dataPerQp = splitDataMin → split when splitData=true.
const size_t kSplitDataMin = GetSplitDataMin();
if (kSplitDataMin == 0) {
GTEST_SKIP() << "RCCL_IB_QP_SCHED_SPLIT_DATA_MIN=0: no WRR/split boundary exists";
}
const size_t kMsgSz = kSplitDataMin * static_cast<size_t>(actualNqps);
// Allocate and register the real buffer sized for the actual threshold.
std::vector<char> sendBuf(kMsgSz, 0xBC);
std::vector<char> recvBuf(kMsgSz, 0x00);
void* baseBuf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, baseBuf, kMsgSz, NCCL_PTR_HOST, &mhandle), ncclSuccess);
if (rank == 1) {
const std::vector<int> tokens = EqualTokens(actualNqps);
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
ASSERT_EQ(ncclIbCastSetSchedParms(sendComm, true, true, false, kSplitDataMin), ncclSuccess);
}
// Phase 1: splitData=false → oneQp WRR → 1 token consumed
int activeTotBefore1ed = 0;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
activeTotBefore1ed = st.activeTotTokens;
}
CastDoSendRecv(rank, sendComm, recvComm, baseBuf, kMsgSz, 1800, mhandle);
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
ASSERT_TRUE(st.schedInit);
EXPECT_EQ(activeTotBefore1ed - st.activeTotTokens, 1);
ASSERT_EQ(ncclIbCastSetSchedParms(sendComm, true, true, true, kSplitDataMin), ncclSuccess);
}
// Phase 2: splitData=true → split path → 0 tokens consumed
int activeTotBefore2ed = 0;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
activeTotBefore2ed = st.activeTotTokens;
}
CastDoSendRecv(rank, sendComm, recvComm, baseBuf, kMsgSz, 1801, mhandle);
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
EXPECT_EQ(activeTotBefore2ed - st.activeTotTokens, 0)
<< "split path must not consume tokens";
ASSERT_EQ(ncclIbCastSetSchedParms(sendComm, true, true, false, kSplitDataMin), ncclSuccess);
}
// Phase 3: splitData=false → oneQp WRR → 1 token consumed
int activeTotBefore3ed = 0;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
activeTotBefore3ed = st.activeTotTokens;
}
CastDoSendRecv(rank, sendComm, recvComm, baseBuf, kMsgSz, 1802, mhandle);
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
EXPECT_EQ(activeTotBefore3ed - st.activeTotTokens, 1);
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastEnableDisableSched
//
// White-box: Toggle schedEnable on an established connection.
// Message is small (dataPerQp < splitDataMin) with splitData=true.
// enable=true → WRR oneQp path → 1 token consumed
// enable=false → scheduler disabled, all-QP path → 0 tokens consumed
// enable=true → WRR resumes → 1 token consumed
// =============================================================================
TEST_F(NetIbMPITest, CastEnableDisableSched) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const int rank = MPIEnvironment::world_rank;
CAST_ENV_CHECK_OR_SKIP();
CAST_REQUIRE_UPDATE_INTERVAL_OR_SKIP(10000000);
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(0, &listenComm, &sendComm, &recvComm);
// Must be strictly below splitDataMin so dataPerQp < splitDataMin for any nqps,
// ensuring messages take the WRR path (not the split path).
if (GetSplitDataMin() == 0) {
GTEST_SKIP() << "RCCL_IB_QP_SCHED_SPLIT_DATA_MIN=0: no WRR/split boundary exists";
}
const size_t kMsgSz = std::max<size_t>(64, GetSplitDataMin() - 1);
std::vector<char> sendBuf(kMsgSz, 0xCD);
std::vector<char> recvBuf(kMsgSz, 0);
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, kMsgSz, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup: learn real nqps.
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kMsgSz, 1899, mhandle);
ASSERT_GT(actualNqps, 0);
if (rank == 1) {
const std::vector<int> tokens = EqualTokens(actualNqps);
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
}
// Phase 1: enable=true → WRR path
int activeTotBefore1es = 0;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
activeTotBefore1es = st.activeTotTokens;
}
CastDoSendRecv(rank, sendComm, recvComm, buf, kMsgSz, 1900, mhandle);
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
ASSERT_TRUE(st.schedInit);
EXPECT_EQ(activeTotBefore1es - st.activeTotTokens, 1);
ASSERT_EQ(ncclIbCastSetSchedParms(sendComm, false, true, true, GetSplitDataMin()), ncclSuccess);
}
// Phase 2: enable=false → bypass (all-QP path)
int activeTotBefore2es = 0;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
activeTotBefore2es = st.activeTotTokens;
}
CastDoSendRecv(rank, sendComm, recvComm, buf, kMsgSz, 1901, mhandle);
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
EXPECT_EQ(activeTotBefore2es - st.activeTotTokens, 0)
<< "enable=false must not consume WRR tokens";
ASSERT_EQ(ncclIbCastSetSchedParms(sendComm, true, true, true, GetSplitDataMin()), ncclSuccess);
}
// Phase 3: enable=true → WRR resumes
int activeTotBefore3es = 0;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
activeTotBefore3es = st.activeTotTokens;
}
CastDoSendRecv(rank, sendComm, recvComm, buf, kMsgSz, 1902, mhandle);
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
EXPECT_EQ(activeTotBefore3es - st.activeTotTokens, 1);
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastSendRecvMultipleSizes
//
// splitData=true, splitDataMin from env. Threshold = splitDataMin * nqps.
// WRR sizes (< threshold): each consumes 1 WRR token.
// Split sizes (>= threshold): 0 WRR tokens consumed.
// Data integrity verified for all sizes.
// =============================================================================
TEST_F(NetIbMPITest, CastSendRecvMultipleSizes) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const int rank = MPIEnvironment::world_rank;
CAST_ENV_CHECK_OR_SKIP();
{
const char* v = getenv("NCCL_IB_SPLIT_DATA_ON_QPS");
if (!v || strcmp(v, "1") != 0)
GTEST_SKIP() << "CastSendRecvMultipleSizes requires NCCL_IB_SPLIT_DATA_ON_QPS=1";
}
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(0, &listenComm, &sendComm, &recvComm);
constexpr size_t kBufSz = 524288; // 512 KB — fits all test sizes
std::vector<char> sendBuf(kBufSz);
std::vector<char> recvBuf(kBufSz);
for (size_t i = 0; i < kBufSz; i++) sendBuf[i] = static_cast<char>(i & 0xFF);
void* comm = (rank == 0) ? recvComm : sendComm;
void* baseBuf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, baseBuf, kBufSz, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup: learn real nqps to compute split threshold.
const int actualNqps = GetActualNqps(sendComm, recvComm, baseBuf, 64, 1999, mhandle);
ASSERT_GT(actualNqps, 0);
const size_t kSplitDataMin = GetSplitDataMin();
if (kSplitDataMin == 0) {
GTEST_SKIP() << "RCCL_IB_QP_SCHED_SPLIT_DATA_MIN=0: no WRR/split boundary exists";
}
const size_t kThreshold = kSplitDataMin * static_cast<size_t>(actualNqps);
ASSERT_LE(kThreshold, kBufSz) << "buffer too small for threshold=" << kThreshold;
if (rank == 1) {
const std::vector<int> tokens = EqualTokens(actualNqps);
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
}
// Sizes below threshold → WRR path (1 token consumed each).
// All entries must be strictly below kThreshold; filter out any that aren't
// (e.g. 512 and 4096 may exceed kThreshold when splitDataMin is small).
std::vector<size_t> kWrrSizes;
for (size_t sz : std::initializer_list<size_t>{512, 4096, kSplitDataMin, kThreshold - 1}) {
if (sz > 0 && sz < kThreshold && sz <= kBufSz)
kWrrSizes.push_back(sz);
}
// Deduplicate (kSplitDataMin or 512 might equal kThreshold-1).
kWrrSizes.erase(std::unique(kWrrSizes.begin(), kWrrSizes.end()), kWrrSizes.end());
// Sizes at/above threshold → split path (0 tokens consumed).
const std::vector<size_t> kSplitSizes = {kThreshold, kThreshold * 2};
int baseTag = 2000;
for (size_t sz : kWrrSizes) {
if (sz > kBufSz) continue; // skip if buffer too small (shouldn't happen)
int prevActiveTot = 0;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
prevActiveTot = st.activeTotTokens;
}
CastDoSendRecv(rank, sendComm, recvComm, baseBuf, sz, baseTag++, mhandle);
if (rank == 0)
EXPECT_EQ(memcmp(sendBuf.data(), recvBuf.data(), sz), 0) << "data mismatch at size " << sz;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
EXPECT_EQ(prevActiveTot - st.activeTotTokens, 1)
<< "WRR path must consume exactly 1 token for size=" << sz;
}
}
for (size_t sz : kSplitSizes) {
if (sz > kBufSz) continue;
int prevActiveTot = 0;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
prevActiveTot = st.activeTotTokens;
}
CastDoSendRecv(rank, sendComm, recvComm, baseBuf, sz, baseTag++, mhandle);
if (rank == 0)
EXPECT_EQ(memcmp(sendBuf.data(), recvBuf.data(), sz), 0) << "data mismatch at size " << sz;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
EXPECT_EQ(prevActiveTot - st.activeTotTokens, 0)
<< "split path must not consume WRR tokens for size=" << sz;
}
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastLargeTransfer
//
// Black/white-box: 16 MB transfer with splitData=true.
// dataPerQp = 16MB / nqps >> splitDataMin → split path taken.
// Verify: data integrity + 0 WRR tokens consumed.
// =============================================================================
TEST_F(NetIbMPITest, CastLargeTransfer) {
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
false, kMinGpusPerNode, kNoNodeLimit))
<< "Test requires exactly " << kExactTwoProcesses << " processes";
const int rank = MPIEnvironment::world_rank;
CAST_ENV_CHECK_OR_SKIP();
{
const char* v = getenv("NCCL_IB_SPLIT_DATA_ON_QPS");
if (!v || strcmp(v, "1") != 0)
GTEST_SKIP() << "CastLargeTransfer requires NCCL_IB_SPLIT_DATA_ON_QPS=1";
}
net_ = &netIbCast;
AssertInitAndGetDevices(nullptr);
void* listenComm = nullptr;
void* sendComm = nullptr;
void* recvComm = nullptr;
SetupCastConnection(0, &listenComm, &sendComm, &recvComm);
constexpr size_t kMsgSz = kLargeBufferSize; // 16 MB
std::vector<char> sendBuf(kMsgSz);
std::vector<char> recvBuf(kMsgSz, 0);
for (size_t i = 0; i < kMsgSz; i++) sendBuf[i] = static_cast<char>(i & 0xFF);
void* comm = (rank == 0) ? recvComm : sendComm;
void* baseBuf = (rank == 0) ? static_cast<void*>(recvBuf.data())
: static_cast<void*>(sendBuf.data());
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, baseBuf, kMsgSz, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup: learn real nqps.
const int actualNqps = GetActualNqps(sendComm, recvComm, baseBuf, 64, 2099, mhandle);
ASSERT_GT(actualNqps, 0);
if (rank == 1) {
const std::vector<int> tokens = EqualTokens(actualNqps);
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
}
int activeTotBeforeLarge = 0;
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
activeTotBeforeLarge = st.activeTotTokens;
}
CastDoSendRecv(rank, sendComm, recvComm, baseBuf, kMsgSz, 2100, mhandle);
if (rank == 0)
EXPECT_EQ(memcmp(sendBuf.data(), recvBuf.data(), kMsgSz), 0) << "data mismatch";
if (rank == 1) {
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
ASSERT_TRUE(st.schedInit);
EXPECT_EQ(activeTotBeforeLarge - st.activeTotTokens, 0)
<< "16 MB transfer must use split path, not WRR";
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastSendRecvZeroSize
//
// White-box: Zero-byte send with splitData=true.
// dataPerQp = 0 < splitDataMin → oneQp WRR path (regardless of nqps).
// Verify: send/recv complete with ncclSuccess, received size=0, 1 WRR token consumed.
// =============================================================================
TEST_F(NetIbMPITest, CastSendRecvZeroSize) {
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(0, &listenComm, &sendComm, &recvComm);
constexpr size_t kRegSz = 64;
char buf[kRegSz];
memset(buf, 0, kRegSz);
void* comm = (rank == 0) ? recvComm : sendComm;
void* mhandle = nullptr;
ASSERT_EQ(RegisterMemory(comm, buf, kRegSz, NCCL_PTR_HOST, &mhandle), ncclSuccess);
// Warmup: learn real nqps.
const int actualNqps = GetActualNqps(sendComm, recvComm, buf, kRegSz, 2199, mhandle);
ASSERT_GT(actualNqps, 0);
if (rank == 1) {
const std::vector<int> tokens = EqualTokens(actualNqps);
ASSERT_EQ(ncclIbCastSetTokens(sendComm, tokens.data(), actualNqps), ncclSuccess);
}
void* req = nullptr;
int sz = -1;
if (rank == 0) {
void* bufs[1] = {buf};
size_t sizes[1] = {0};
int tags[1] = {2200};
void* handles[1] = {mhandle};
ASSERT_EQ(PostRecv(recvComm, 1, bufs, sizes, tags, handles, &req), ncclSuccess);
ASSERT_NE(req, nullptr);
ASSERT_EQ(WaitForCompletion(req, &sz, 10000), ncclSuccess);
EXPECT_EQ(sz, 0) << "received size must be 0";
} else {
PostSendWithRetry(sendComm, buf, 0, 2200, mhandle, &req);
ASSERT_EQ(WaitForCompletion(req, &sz, 10000), ncclSuccess);
struct ncclIbCastSchedState st = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComm, &st), ncclSuccess);
ASSERT_TRUE(st.schedInit);
EXPECT_EQ(st.initTotTokens - st.activeTotTokens, 1)
<< "zero-size send must still trigger WRR (dataPerQp=0 < splitDataMin)";
}
MPI_Barrier(MPI_COMM_WORLD);
TeardownConnection(recvComm, listenComm, sendComm, mhandle);
}
// =============================================================================
// Test: CastStressMultiRoundTwoConns
//
// Stress test: 100 independent connections running concurrently, 5000 sends each
// (WRR small-message phase), followed by a ramping-size phase that exercises the
// WRR/split boundary across {splitDataMin/4, /2, *1, *4, *16} with 20 rounds each.
// Actual nqps determined at runtime. Works with nqps=1 (no WRR timer check) or
// nqps>1 (RTT timer fires and rewrites initTokens from asymmetric initial values).
//
// Invariants asserted:
// - schedInit == true on conn[0].
// - sum(activeQpTokens[i]) == activeTotTokens for conn[0].
// - 0 <= activeTotTokens <= initTotTokens for conn[0].
// - If nqps > 1: initQpTokens changed from asymmetric initial values (timer fired).
// =============================================================================
TEST_F(NetIbMPITest, CastStressMultiRoundTwoConns) {
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);
constexpr int kNConns = 100;
std::vector<void*> listenComms(kNConns, nullptr);
std::vector<void*> sendComms(kNConns, nullptr);
std::vector<void*> recvComms(kNConns, nullptr);
for (int c = 0; c < kNConns; c++)
SetupCastConnection(/*dev=*/0, &listenComms[c], &sendComms[c], &recvComms[c]);
// Scale msgs per connection inversely with connection count so total work stays constant.
constexpr int kNMsgsTotal = 10000;
constexpr int kNMsgs = kNMsgsTotal / kNConns; // 100 msgs × 100 conns = 10000 total
if (GetSplitDataMin() == 0) {
GTEST_SKIP() << "RCCL_IB_QP_SCHED_SPLIT_DATA_MIN=0: no WRR/split boundary exists";
}
const size_t kMsgSz = std::max<size_t>(64, GetSplitDataMin() - 1);
const size_t kBufSz = static_cast<size_t>(kNMsgs) * kMsgSz;
// One send/recv buffer pair per connection.
std::vector<std::vector<char>> sendBufs(kNConns, std::vector<char>(kBufSz));
std::vector<std::vector<char>> recvBufs(kNConns, std::vector<char>(kBufSz));
for (int c = 0; c < kNConns; c++) {
for (size_t i = 0; i < kBufSz; i++) {
sendBufs[c][i] = static_cast<char>(((i + c) * 3 + 7) & 0xFF);
}
memset(recvBufs[c].data(), 0, kBufSz);
}
std::vector<void*> mhandles(kNConns, nullptr);
for (int c = 0; c < kNConns; c++) {
void* comm = (rank == 0) ? recvComms[c] : sendComms[c];
char* regBuf = (rank == 0) ? recvBufs[c].data() : sendBufs[c].data();
ASSERT_EQ(RegisterMemory(comm, regBuf, kBufSz, NCCL_PTR_HOST, &mhandles[c]), ncclSuccess);
}
// Warmup on conn[0]: learn real nqps. All conns share same NCCL_PARAM → same nqps.
const int actualNqps = GetActualNqps(sendComms[0], recvComms[0],
(rank == 0) ? recvBufs[0].data() : sendBufs[0].data(),
kMsgSz, 3999, mhandles[0]);
ASSERT_GT(actualNqps, 0);
// Arm tokens asymmetrically on conn[0] only (timer-fired check uses conn[0]).
if (rank == 1) {
std::vector<int> tokens(actualNqps);
if (actualNqps == 1) {
tokens[0] = 100;
} else {
tokens[0] = 70;
tokens[1] = 30;
for (int i = 2; i < actualNqps; i++) tokens[i] = 50;
}
ASSERT_EQ(ncclIbCastSetTokens(sendComms[0], tokens.data(), actualNqps), ncclSuccess);
}
constexpr int kBatch = 16;
constexpr int kTagBase = 10000; // tag = kTagBase + c * kNMsgs + i
// ── Phase 1: small-message WRR stress ────────────────────────────────────
if (rank == 0) {
for (int base = 0; base < kNMsgs; base += kBatch) {
const int end = std::min(base + kBatch, kNMsgs);
const int bsz = end - base;
std::vector<std::vector<void*>> reqs(kNConns, std::vector<void*>(bsz, nullptr));
for (int c = 0; c < kNConns; c++) {
for (int i = base; i < end; i++) {
void* bufs[1] = {recvBufs[c].data() + i * kMsgSz};
size_t sizes[1] = {kMsgSz};
int tags[1] = {kTagBase + c * kNMsgs + i};
void* handles[1] = {mhandles[c]};
ASSERT_EQ(PostRecv(recvComms[c], 1, bufs, sizes, tags, handles,
&reqs[c][i - base]), ncclSuccess);
ASSERT_NE(reqs[c][i - base], nullptr);
}
}
for (int c = 0; c < kNConns; c++) {
for (int i = 0; i < bsz; i++) {
int sz = 0;
ASSERT_EQ(WaitForCompletion(reqs[c][i], &sz, 10000), ncclSuccess);
}
}
}
for (int c = 0; c < kNConns; c++) {
EXPECT_EQ(memcmp(recvBufs[c].data(), sendBufs[c].data(), kBufSz), 0)
<< "phase1 data corruption on conn " << c;
}
} else {
for (int base = 0; base < kNMsgs; base += kBatch) {
const int end = std::min(base + kBatch, kNMsgs);
const int bsz = end - base;
std::vector<std::vector<void*>> reqs(kNConns, std::vector<void*>(bsz, nullptr));
for (int c = 0; c < kNConns; c++) {
for (int i = base; i < end; i++)
PostSendWithRetry(sendComms[c], sendBufs[c].data() + i * kMsgSz, kMsgSz,
kTagBase + c * kNMsgs + i, mhandles[c], &reqs[c][i - base]);
}
for (int c = 0; c < kNConns; c++) {
for (int i = 0; i < bsz; i++) {
int sz = 0;
ASSERT_EQ(WaitForCompletion(reqs[c][i], &sz, 10000), ncclSuccess);
}
}
}
}
// Collect sched state from conn[0] after phase 1.
struct ncclIbCastSchedState stAfterPhase1 = {};
if (rank == 0) {
MPI_Recv(&stAfterPhase1, sizeof(stAfterPhase1), MPI_BYTE, 1, 9870, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
int nqps = 0;
MPI_Recv(&nqps, 1, MPI_INT, 1, 9872, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
EXPECT_TRUE(stAfterPhase1.schedInit) << "conn[0]: schedInit must be true after phase 1";
{
int s = 0;
for (int q = 0; q < stAfterPhase1.nqps; q++) s += stAfterPhase1.activeQpTokens[q];
EXPECT_EQ(s, stAfterPhase1.activeTotTokens) << "conn[0]: token sum mismatch";
}
EXPECT_GE(stAfterPhase1.activeTotTokens, 0);
EXPECT_LE(stAfterPhase1.activeTotTokens, stAfterPhase1.initTotTokens);
if (nqps > 1) {
EXPECT_NE(stAfterPhase1.initQpTokens[0], 70) << "conn[0]: RTT timer did not fire";
EXPECT_NE(stAfterPhase1.initQpTokens[1], 30) << "conn[0]: RTT timer did not fire";
}
} else {
struct ncclIbCastSchedState st0 = {};
ASSERT_EQ(ncclIbCastGetSchedState(sendComms[0], &st0), ncclSuccess);
MPI_Send(&st0, sizeof(st0), MPI_BYTE, 0, 9870, MPI_COMM_WORLD);
MPI_Send(&actualNqps, 1, MPI_INT, 0, 9872, MPI_COMM_WORLD);
}
MPI_Barrier(MPI_COMM_WORLD);
// ── Phase 2: ramping-size stress ─────────────────────────────────────────
// Sizes span WRR and split paths: /4, /2, *1, *4, *16 relative to splitDataMin.
const uint32_t sdm = GetSplitDataMin();
const std::vector<size_t> kRampSizes = {
std::max<size_t>(64, sdm / 4),
std::max<size_t>(64, sdm / 2),
static_cast<size_t>(sdm),
static_cast<size_t>(sdm) * 4,
static_cast<size_t>(sdm) * 16,
};
constexpr int kLargeRounds = 20;
const size_t kRampBufSz = kRampSizes.back(); // one MR per conn, sized for largest
std::vector<std::vector<char>> rampSend(kNConns, std::vector<char>(kRampBufSz));
std::vector<std::vector<char>> rampRecv(kNConns, std::vector<char>(kRampBufSz));
for (int c = 0; c < kNConns; c++) {
for (size_t i = 0; i < kRampBufSz; i++)
rampSend[c][i] = static_cast<char>(((i + c) * 7 + 3) & 0xFF);
memset(rampRecv[c].data(), 0, kRampBufSz);
}
std::vector<void*> rampHandles(kNConns, nullptr);
for (int c = 0; c < kNConns; c++) {
void* comm = (rank == 0) ? recvComms[c] : sendComms[c];
char* regBuf = (rank == 0) ? rampRecv[c].data() : rampSend[c].data();
ASSERT_EQ(RegisterMemory(comm, regBuf, kRampBufSz, NCCL_PTR_HOST, &rampHandles[c]),
ncclSuccess);
}
// Tag space: 20000 + sizeIdx*kNConns*kLargeRounds + c*kLargeRounds + round
constexpr int kRampTagBase = 20000;
for (int si = 0; si < static_cast<int>(kRampSizes.size()); si++) {
const size_t sz = kRampSizes[si];
for (int round = 0; round < kLargeRounds; round++) {
for (int c = 0; c < kNConns; c++) {
const int tag = kRampTagBase + si * kNConns * kLargeRounds
+ c * kLargeRounds + round;
CastDoSendRecv(rank, sendComms[c], recvComms[c],
(rank == 0) ? rampRecv[c].data() : rampSend[c].data(),
sz, tag, rampHandles[c]);
}
}
}
if (rank == 0) {
for (int c = 0; c < kNConns; c++) {
EXPECT_EQ(memcmp(rampRecv[c].data(), rampSend[c].data(), kRampBufSz), 0)
<< "phase2 data corruption on conn " << c;
}
}
MPI_Barrier(MPI_COMM_WORLD);
// ── Teardown ─────────────────────────────────────────────────────────────
for (int c = 0; c < kNConns; c++) {
void* comm = (rank == 0) ? recvComms[c] : sendComms[c];
ASSERT_EQ(DeregisterMemory(comm, rampHandles[c]), ncclSuccess);
ASSERT_EQ(DeregisterMemory(comm, mhandles[c]), ncclSuccess);
if (rank == 0) {
ASSERT_EQ(CloseRecvComm(recvComms[c]), ncclSuccess);
ASSERT_EQ(CloseListenComm(listenComms[c]), ncclSuccess);
} else {
ASSERT_EQ(CloseSendComm(sendComms[c]), ncclSuccess);
}
}
MPI_Barrier(MPI_COMM_WORLD);
}
#endif // MPI_TESTS_ENABLED