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

1835 lines
67 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
************************************************************************/
/**
* @file HostApiMPITests.cpp
* @brief P0 unit tests for RCCL's one-sided RMA Host API
*
* Tests cover:
* W1 - WindowRegisterDeregister: collective register/deregister lifecycle
* P1 - SinglePutRank0ToRank1: basic ncclPutSignal + ncclWaitSignal
* P2 - PutWithNonZeroOffset: peerWinOffset at kSize/2
* P3 - PutMultipleDataTypes: float32, int32, float16 (raw bytes)
* S1 - SignalOnlyNoData: ncclSignal with no data transfer
* WS2 - WaitSignalFenceSemantics: three back-to-back puts, opCnt=3
* O1 - DataVisibilityAfterSync: explicit host read of fine-grain memory
* E1 - PutSignalNullLocalbuff: null localbuf → error (or skip)
* E2 - PutSignalNullWindow: null peerWin → error
* E3 - PutSignalOffsetOutOfBounds: offset past end → error (or skip)
* E4 - PutSignalInvalidSigIdx: sigIdx=1 → error (or skip)
* M1 - TwoCommunicatorsIndependentWindows: two comms, independent windows
* M2 - StressManySmallPuts: 100×64-byte PUTs, opCnt=100 wait
* P6 - PutToSelf: PUT to own window (peer=self loopback)
* W3 - DoubleDeregister: ncclCommWindowDeregister twice on same handle
* W4 - DestroyCommWithoutDeregister: window auto-freed during commFree
*
* Constraints (proxy GIN path, current API limits):
* sigIdx = 0, ctx = 0, flags = 0, winFlags = winMode()
* (NCCL_WIN_COLL_SYMMETRIC when NCCL_CUMEM_ENABLE=1, else NCCL_WIN_DEFAULT)
*
* API signatures (from src/nccl.h.in):
* ncclResult_t ncclMemAlloc(void** ptr, size_t size);
* ncclResult_t ncclMemFree(void* ptr);
* ncclResult_t ncclCommWindowRegister(ncclComm_t comm, void* buff, size_t size,
* ncclWindow_t* win, int winFlags);
* ncclResult_t ncclCommWindowDeregister(ncclComm_t comm, ncclWindow_t win);
* ncclResult_t ncclPutSignal(const void* localbuff, size_t count,
* ncclDataType_t datatype, int peer,
* ncclWindow_t peerWin, size_t peerWinOffset,
* int sigIdx, int ctx, unsigned int flags,
* ncclComm_t comm, hipStream_t stream);
* ncclResult_t ncclSignal(int peer, int sigIdx, int ctx, unsigned int flags,
* ncclComm_t comm, hipStream_t stream);
* ncclResult_t ncclWaitSignal(int nDesc, ncclWaitSignalDesc_t* signalDescs,
* ncclComm_t comm, hipStream_t stream);
*
* Run (example):
* mpirun -np 2 ./rccl-UnitTestsMPI --gtest_filter=HostApiTest.*
*/
#if defined(MPI_TESTS_ENABLED) && defined(RCCL_ENABLE_HOST_API_TESTS)
#include "MPITestBase.hpp"
#include "MPIHelpers.hpp"
#include "ResourceGuards.hpp"
#include "HostApiHelpers.hpp"
#include "TestChecks.hpp"
#include <hip/hip_runtime.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <cstdlib>
#include <chrono>
using namespace MPITestConstants;
using namespace RCCLTestGuards;
using namespace RCCLHostApiHelpers;
// ---------------------------------------------------------------------------
// busy wait kernel
// ---------------------------------------------------------------------------
__global__ void hostApiSpinKernel(unsigned long long cycles, volatile int* sink)
{
unsigned long long start = clock64();
int acc = 0;
while(static_cast<unsigned long long>(clock64() - start) < cycles)
acc++;
if(sink)
*sink = acc;
}
namespace RcclUnitTesting
{
// ============================================================================
// Test fixture
// ============================================================================
/**
* @class HostApiTest
* @brief GTest fixture for RCCL Host API (one-sided RMA) tests.
*
* SetUp creates a communicator for all ranks; individual tests call
* validateTestPrerequisites() to skip when rank count is insufficient.
*/
class HostApiTest : public MPITestBase
{
protected:
void SetUp() override
{
MPITestBase::SetUp();
ASSERT_EQ(ncclSuccess, createTestCommunicator());
}
// Convenience: get rank and world size from the active communicator.
int rank() const
{
int r = -1;
ncclCommUserRank(const_cast<HostApiTest*>(this)->getActiveCommunicator(), &r);
return r;
}
int nRanks() const
{
int n = -1;
ncclCommCount(const_cast<HostApiTest*>(this)->getActiveCommunicator(), &n);
return n;
}
};
// ============================================================================
// Constants shared across tests
// ============================================================================
namespace
{
constexpr size_t kTransferSize = 256 * 1024; // 256 KiB per PUT
// Window is large enough to hold the receive region AND the send region so
// that the source buffer for ncclPutSignal is always part of a registered
// window (required by the proxy GIN path).
constexpr size_t kOneMB = 2 * kTransferSize; // send + recv regions
constexpr size_t kSendOffset = 0; // sender carves src from here
constexpr size_t kRecvOffset = kTransferSize; // receiver data lands here
constexpr int kSigIdx = 0;
constexpr int kCtx = 0;
constexpr unsigned int kFlags = 0;
// Window registration flag, selected at run time from NCCL_CUMEM_ENABLE.
// NCCL_CUMEM_ENABLE=1 -> NCCL_WIN_COLL_SYMMETRIC (Put/Signal data path works)
// NCCL_CUMEM_ENABLE=0 -> NCCL_WIN_DEFAULT (non-symmetric fallback)
inline int winMode()
{
static int cumem_ = -1;
if (cumem_ < 0) {
const char* e = getenv("NCCL_CUMEM_ENABLE");
cumem_ = e ? (atoi(e) != 0) : true;
}
return cumem_ ? NCCL_WIN_COLL_SYMMETRIC : NCCL_WIN_DEFAULT;
}
// Calibrate how many clock64() cycles approximate targetMs of GPU busy wait.
inline unsigned long long calibrateSpinCycles(hipStream_t s, volatile int* sink,
double targetMs)
{
const unsigned long long probe = 1500000ULL * 1000ULL; // ~1 ms @ 1.5 GHz guess
auto t0 = std::chrono::steady_clock::now();
hipLaunchKernelGGL(hostApiSpinKernel, dim3(1), dim3(1), 0, s, probe, sink);
(void)hipStreamSynchronize(s);
auto t1 = std::chrono::steady_clock::now();
double ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
if(ms < 1e-3)
ms = 1e-3;
return static_cast<unsigned long long>((probe / ms) * targetMs);
}
} // namespace
// ============================================================================
// W1 — WindowRegisterDeregister
// ============================================================================
/**
* @test HostApiTest.WindowRegisterDeregister
* @brief Verify collective ncclCommWindowRegister / ncclCommWindowDeregister lifecycle.
*
* All ranks allocate a fine-grain buffer, collectively register a window,
* skip if the system does not support windows, then deregister.
*/
TEST_F(HostApiTest, WindowRegisterDeregister)
{
if(!validateTestPrerequisites(/*min=*/2))
{
GTEST_SKIP() << "Need at least 2 MPI processes";
}
const int myRank = rank();
// Allocate fine-grain buffer (CPU-accessible on ROCm).
void* buf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&buf, kOneMB));
auto bufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(buf); });
// Collective window registration.
ncclWindow_t win = nullptr;
NcclWindowGuard wg(getActiveCommunicator(), buf, kOneMB, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
// NcclWindowGuard destructor calls ncclCommWindowDeregister.
TEST_INFO("W1 rank %d: window registered and will be deregistered by guard.", myRank);
}
// ============================================================================
// P1 — SinglePutRank0ToRank1
// ============================================================================
/**
* @test HostApiTest.SinglePutRank0ToRank1
* @brief Basic ncclPutSignal (rank 0 → rank 1) + ncclWaitSignal (rank 1).
*
* Rank 0 fills a source buffer with FillBuf, issues ncclPutSignal to
* rank 1's window. Rank 1 issues ncclWaitSignal(opCnt=1). After
* hipStreamSynchronize rank 1 verifies the window buffer.
*/
TEST_F(HostApiTest, SinglePutRank0ToRank1)
{
if(!validateTestPrerequisites(/*min=*/2, /*max=*/2))
{
GTEST_SKIP() << "Need exactly 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
// Both ranks allocate + register a window.
// The window is split: [kSendOffset, kTransferSize) is the sender's source
// region; [kRecvOffset, kRecvOffset+kTransferSize) is the receiver's dest.
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kOneMB));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kOneMB, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
// Rank 0: fill the send region of its own window and PUT into rank 1's recv region.
ncclResult_t putRes = ncclSuccess;
if(myRank == 0)
{
void* srcBuf = static_cast<uint8_t*>(winBuf) + kSendOffset;
FillBuf(srcBuf, kTransferSize, /*senderRank=*/0);
putRes = ncclPutSignal(
srcBuf, kTransferSize, ncclUint8,
/*peer=*/1, win, /*peerWinOffset=*/kRecvOffset,
kSigIdx, kCtx, kFlags, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, putRes);
// Rank 1: wait for 1 signal from rank 0.
ncclResult_t waitRes = ncclSuccess;
if(myRank == 1)
{
ncclWaitSignalDesc_t desc{/*opCnt=*/1, /*peer=*/0, kSigIdx, kCtx};
waitRes = ncclWaitSignal(/*nDesc=*/1, &desc, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, waitRes);
// Both ranks synchronize the stream.
{
hipError_t _sync_err = hipStreamSynchronize(stream);
if (_sync_err != hipSuccess) {
fprintf(stderr, "Rank %d: hipStreamSynchronize FAILED: err=%d (%s)\n",
myRank, (int)_sync_err, hipGetErrorString(_sync_err));
fflush(stderr);
}
ASSERT_MPI_EQ(hipSuccess, _sync_err);
}
bool ok = (myRank != 1) ||
VerifyBuf(static_cast<const uint8_t*>(winBuf) + kRecvOffset, kTransferSize, /*seed=*/0);
ASSERT_MPI_TRUE(ok);
TEST_INFO("P1 rank %d: SinglePutRank0ToRank1 passed.", myRank);
}
// ============================================================================
// A1 — PutSignalEnqueueIsAsync
// ============================================================================
/**
* @test HostApiTest.PutSignalEnqueueIsAsync
* @brief ncclPutSignal host-side enqueue must be asynchronous (no implicit
* CPU-GPU synchronization) even while the target stream is busy.
*
* Regression test:
* defect was an implicit stream synchronization inside the CE / putSignal
* enqueue path caused by staging the signal value through a pageable host
* memcpy.
*/
TEST_F(HostApiTest, PutSignalEnqueueIsAsync)
{
constexpr double kSpinMs = 80.0; // how long the busy wait kernel runs
constexpr double kBlockFraction = 0.5; // host-return >= this*kSpinMs => blocked
constexpr int kAsyncNumPuts = 16; // puts issued per measurement
constexpr size_t kAsyncPutElems = 256; // uint8 elements per put (small)
if(!validateTestPrerequisites(/*min=*/2, /*max=*/2))
{
GTEST_SKIP() << "Need exactly 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
const int peer = (myRank + 1) % 2;
const size_t winBytes = static_cast<size_t>(kAsyncNumPuts) * kAsyncPutElems;
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, winBytes));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, winBytes, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
// Sink for the busy wait kernel (device scratch).
void* sink = nullptr;
ASSERT_MPI_EQ(hipSuccess, hipMalloc(&sink, sizeof(int)));
auto sinkGuard = makeScopeGuard([&]() { if(sink) (void)hipFree(sink); });
// Warm up the putSignal machinery once.
ncclResult_t warmRes = ncclPutSignal(
winBuf, kAsyncPutElems, ncclUint8, peer, win, /*peerWinOffset=*/0,
kSigIdx, kCtx, kFlags, comm, stream);
ASSERT_MPI_EQ(ncclSuccess, warmRes);
{
ncclWaitSignalDesc_t d{/*opCnt=*/1, /*peer=*/peer, kSigIdx, kCtx};
ASSERT_MPI_EQ(ncclSuccess, ncclWaitSignal(1, &d, comm, stream));
}
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
MPI_Barrier(MPI_COMM_WORLD);
const unsigned long long spinCycles =
calibrateSpinCycles(stream, static_cast<volatile int*>(sink), kSpinMs);
// Measurement: occupy the stream, then time a batch of putSignal enqueues.
MPI_Barrier(MPI_COMM_WORLD);
// NOTE: sink value and winBuf have no meaningfull values, thus should not be checked
hipLaunchKernelGGL(hostApiSpinKernel, dim3(1), dim3(1), 0, stream,
spinCycles, static_cast<volatile int*>(sink));
auto t0 = std::chrono::steady_clock::now();
ncclResult_t putRes = ncclSuccess;
for(int i = 0; i < kAsyncNumPuts && putRes == ncclSuccess; ++i)
{
size_t off = static_cast<size_t>(i) * kAsyncPutElems;
putRes = ncclPutSignal(
static_cast<uint8_t*>(winBuf) + off, kAsyncPutElems, ncclUint8,
peer, win, off, kSigIdx, kCtx, kFlags, comm, stream);
}
auto t1 = std::chrono::steady_clock::now();
const double hostMs = std::chrono::duration<double, std::milli>(t1 - t0).count();
ASSERT_MPI_EQ(ncclSuccess, putRes);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
MPI_Barrier(MPI_COMM_WORLD);
const double blockThresholdMs = kSpinMs * kBlockFraction;
const bool async = hostMs < blockThresholdMs;
TEST_INFO("A1 rank %d -> peer %d: %d putSignal enqueues host-return %.3f ms "
"(stream busy ~%.1f ms) => %s",
myRank, peer, kAsyncNumPuts, hostMs, kSpinMs,
async ? "async" : "BLOCKED (implicit sync)");
ASSERT_MPI_TRUE(async);
}
// ============================================================================
// P2 — PutWithNonZeroOffset
// ============================================================================
/**
* @test HostApiTest.PutWithNonZeroOffset
* @brief PUT with peerWinOffset = kOneMB/2.
*
* Same as P1 but the destination window offset is placed at the midpoint of
* the window. Rank 1 verifies only the offset region.
*/
TEST_F(HostApiTest, PutWithNonZeroOffset)
{
if(!validateTestPrerequisites(/*min=*/2, /*max=*/2))
{
GTEST_SKIP() << "Need exactly 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
// Window layout: [kSendOffset..kTransferSize) = send region,
// [kRecvOffset..kRecvOffset+kTransferSize) = recv region.
// The PUT targets kRecvOffset in rank 1's window.
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kOneMB));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kOneMB, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
// Zero the recv region on rank 1 so stale bytes are detectable.
if(myRank == 1)
FillSentinel(static_cast<uint8_t*>(winBuf) + kRecvOffset, kTransferSize, 0);
ncclResult_t putRes = ncclSuccess;
if(myRank == 0)
{
void* srcBuf = static_cast<uint8_t*>(winBuf) + kSendOffset;
FillBuf(srcBuf, kTransferSize, /*senderRank=*/0);
putRes = ncclPutSignal(
srcBuf, kTransferSize, ncclUint8,
/*peer=*/1, win, /*peerWinOffset=*/kRecvOffset,
kSigIdx, kCtx, kFlags, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, putRes);
ncclResult_t waitRes = ncclSuccess;
if(myRank == 1)
{
ncclWaitSignalDesc_t desc{1, 0, kSigIdx, kCtx};
waitRes = ncclWaitSignal(1, &desc, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, waitRes);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
const uint8_t* recvBuf = static_cast<const uint8_t*>(winBuf) + kRecvOffset;
bool ok = (myRank != 1) || VerifyBuf(recvBuf, kTransferSize, /*senderRank=*/0);
ASSERT_MPI_TRUE(ok);
TEST_INFO("P2 rank %d: PutWithNonZeroOffset passed.", myRank);
}
// ============================================================================
// P3 — PutMultipleDataTypes
// ============================================================================
/**
* @test HostApiTest.PutMultipleDataTypes
* @brief PUT 256 elements of float32, int32, and float16 and verify raw bytes.
*
* The test uses ncclPutSignal with the correct element type. Verification
* is done via FillBuf / VerifyBuf on the raw bytes of each element array.
*/
TEST_F(HostApiTest, PutMultipleDataTypes)
{
if(!validateTestPrerequisites(/*min=*/2, /*max=*/2))
{
GTEST_SKIP() << "Need exactly 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
const size_t kElem = 256;
// Describe the three types: {ncclDataType, element_size, recv offset in window}
// All recv offsets are placed in the upper half of the window (>= kRecvOffset).
struct TypeDesc { ncclDataType_t type; size_t elemSz; size_t recvOff; };
const TypeDesc types[] = {
{ ncclFloat32, sizeof(float), kRecvOffset + 0 },
{ ncclInt32, sizeof(int32_t), kRecvOffset + 64 * 1024 },
{ ncclFloat16, sizeof(uint16_t), kRecvOffset + 128 * 1024 },
};
const int nTypes = static_cast<int>(sizeof(types) / sizeof(types[0]));
// Window: lower half = send region, upper half = recv regions for all types.
// Ensure the window is large enough for all recv regions.
const size_t maxRecvEnd = types[nTypes - 1].recvOff
+ kElem * types[nTypes - 1].elemSz;
const size_t kWinSize = maxRecvEnd;
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kWinSize));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kWinSize, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
// For each type: fill → PUT (rank 0) / WaitSignal (rank 1) → sync → verify.
for(int t = 0; t < nTypes; ++t)
{
const size_t byteCount = kElem * types[t].elemSz;
ncclResult_t putRes = ncclSuccess;
if(myRank == 0)
{
void* srcBuf = static_cast<uint8_t*>(winBuf) + kSendOffset;
FillBuf(srcBuf, byteCount, /*senderRank=*/0);
putRes = ncclPutSignal(
srcBuf, kElem, types[t].type,
/*peer=*/1, win, types[t].recvOff,
kSigIdx, kCtx, kFlags, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, putRes);
ncclResult_t waitRes = ncclSuccess;
if(myRank == 1)
{
ncclWaitSignalDesc_t desc{1, 0, kSigIdx, kCtx};
waitRes = ncclWaitSignal(1, &desc, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, waitRes);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
// ASSERT_MPI_TRUE must be called by all ranks.
const uint8_t* base = static_cast<const uint8_t*>(winBuf);
bool ok = (myRank != 1) || VerifyBuf(base + types[t].recvOff, byteCount, /*senderRank=*/0);
ASSERT_MPI_TRUE(ok);
TEST_INFO("P3 rank %d: type[%d] (elemSz=%zu) passed.", myRank, t, types[t].elemSz);
}
}
// ============================================================================
// S1 — SignalOnlyNoData
// ============================================================================
/**
* @test HostApiTest.SignalOnlyNoData
* @brief ncclSignal with no data transfer; window contents must be unchanged.
*
* Rank 1 pre-fills its window with sentinel value 0xAB.
* Rank 0 issues ncclSignal(peer=1, sigIdx=0, ctx=0, flags=0).
* Rank 1 issues ncclWaitSignal(opCnt=1, peer=0).
* After sync rank 1 verifies every byte is still 0xAB.
*/
TEST_F(HostApiTest, SignalOnlyNoData)
{
if(!validateTestPrerequisites(/*min=*/2, /*max=*/2))
{
GTEST_SKIP() << "Need exactly 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
const uint8_t kSentinel = 0xAB;
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kOneMB));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kOneMB, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
// Rank 1: pre-fill window with sentinel before the signal.
if(myRank == 1)
FillSentinel(winBuf, kOneMB, kSentinel);
// Rank 0: signal only (no data).
ncclResult_t sigRes = ncclSuccess;
if(myRank == 0)
sigRes = ncclSignal(/*peer=*/1, kSigIdx, kCtx, kFlags, comm, stream);
ASSERT_MPI_EQ(ncclSuccess, sigRes);
// Rank 1: wait for the signal.
ncclResult_t waitRes = ncclSuccess;
if(myRank == 1)
{
ncclWaitSignalDesc_t desc{1, 0, kSigIdx, kCtx};
waitRes = ncclWaitSignal(1, &desc, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, waitRes);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
// Rank 1: window must still be 0xAB throughout (no data was transferred).
bool allSentinel = (myRank != 1) || AllSentinel(winBuf, kOneMB, kSentinel);
ASSERT_MPI_TRUE(allSentinel);
TEST_INFO("S1 rank %d: SignalOnlyNoData passed.", myRank);
}
// ============================================================================
// WS2 — WaitSignalFenceSemantics
// ============================================================================
/**
* @test HostApiTest.WaitSignalFenceSemantics
* @brief Three consecutive PUTs to different window offsets; WaitSignal(opCnt=3).
*
* Rank 0 issues three ncclPutSignal calls to rank 1's window at offsets
* [0, kTransferSize, 2*kTransferSize], each with a different pattern
* (senderRank encoded as 10, 20, 30 to distinguish regions).
* Rank 1 issues ncclWaitSignal with opCnt=3.
* After sync rank 1 verifies all three regions.
*
* Note: VerifyBuf uses seed as the pattern index. We pass synthetic seed
* values (10, 20, 30) to distinguish the three regions.
*/
TEST_F(HostApiTest, WaitSignalFenceSemantics)
{
if(!validateTestPrerequisites(/*min=*/2, /*max=*/2))
{
GTEST_SKIP() << "Need exactly 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
const int kNumPuts = 3;
// Window layout (rank 0): [kSendOffset .. kSendOffset+kTransferSize) is the
// send region (reused for each PUT); recv region not used by rank 0.
// Window layout (rank 1): [0 .. kNumPuts*kTransferSize) is the receive area.
// We need kSendOffset + kTransferSize <= total, and kNumPuts*kTransferSize
// for the receive side. Use kNumPuts+1 slots so rank 0's send region is
// beyond the receive area.
const size_t kWinSize = (kNumPuts + 1) * kTransferSize;
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kWinSize));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kWinSize, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
// Synthetic seed values — must match fill and verify on each side.
const int seeds[kNumPuts] = {10, 20, 30};
// Rank 0 carves its send buffer from the last slot of its own window.
const size_t kSendSlot = static_cast<size_t>(kNumPuts) * kTransferSize;
ncclResult_t putRes = ncclSuccess;
if(myRank == 0)
{
uint8_t* sendRegion = static_cast<uint8_t*>(winBuf) + kSendSlot;
for(int i = 0; i < kNumPuts && putRes == ncclSuccess; ++i)
{
FillBuf(sendRegion, kTransferSize, seeds[i]);
size_t peerOff = static_cast<size_t>(i) * kTransferSize;
putRes = ncclPutSignal(
sendRegion, kTransferSize, ncclUint8,
/*peer=*/1, win, peerOff,
kSigIdx, kCtx, kFlags, comm, stream);
}
}
ASSERT_MPI_EQ(ncclSuccess, putRes);
ncclResult_t waitRes = ncclSuccess;
if(myRank == 1)
{
ncclWaitSignalDesc_t desc{kNumPuts, 0, kSigIdx, kCtx};
waitRes = ncclWaitSignal(1, &desc, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, waitRes);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
bool allOk = true;
if(myRank == 1)
{
const uint8_t* base = static_cast<const uint8_t*>(winBuf);
for(int i = 0; i < kNumPuts; ++i)
{
size_t off = static_cast<size_t>(i) * kTransferSize;
if(!VerifyBuf(base + off, kTransferSize, seeds[i]))
{
allOk = false;
break;
}
}
}
ASSERT_MPI_TRUE(allOk);
TEST_INFO("WS2 rank %d: WaitSignalFenceSemantics passed.", myRank);
}
// ============================================================================
// O1 — DataVisibilityAfterSync
// ============================================================================
/**
* @test HostApiTest.DataVisibilityAfterSync
* @brief Explicit host read of fine-grain window memory after stream sync.
*
* Mirrors P1 but after hipStreamSynchronize the receiver copies the
* fine-grain window pointer into a std::vector<uint8_t> on the host
* VerifyBuf copies device→host via hipMemcpy into a staging buffer and checks
* bytes there.
*/
TEST_F(HostApiTest, DataVisibilityAfterSync)
{
if(!validateTestPrerequisites(/*min=*/2, /*max=*/2))
{
GTEST_SKIP() << "Need exactly 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kOneMB));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kOneMB, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
// Rank 0 uses its own registered window as the source buffer.
// Layout: [kSendOffset, kSendOffset+kTransferSize) = send region (rank 0)
// [kRecvOffset, kRecvOffset+kTransferSize) = recv region (rank 1)
ncclResult_t putRes = ncclSuccess;
if(myRank == 0)
{
void* sendRegion = static_cast<uint8_t*>(winBuf) + kSendOffset;
FillBuf(sendRegion, kTransferSize, 0);
putRes = ncclPutSignal(sendRegion, kTransferSize, ncclUint8,
1, win, kRecvOffset, kSigIdx, kCtx, kFlags, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, putRes);
ncclResult_t waitRes = ncclSuccess;
if(myRank == 1)
{
ncclWaitSignalDesc_t desc{1, 0, kSigIdx, kCtx};
waitRes = ncclWaitSignal(1, &desc, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, waitRes);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
bool ok = (myRank != 1) ||
VerifyBuf(static_cast<const uint8_t*>(winBuf) + kRecvOffset, kTransferSize, /*seed=*/0);
ASSERT_MPI_TRUE(ok);
TEST_INFO("O1 rank %d: DataVisibilityAfterSync passed.", myRank);
}
// ============================================================================
// E1 — PutSignalNullLocalbuff
// ============================================================================
/**
* @test HostApiTest.PutSignalNullLocalbuff
* @brief ncclPutSignal with null localbuf must return an error.
*
* Only rank 0 calls the API (non-collective immediate check). Rank 1 does
* nothing to avoid deadlock. If argcheck is not implemented the test skips.
*/
TEST_F(HostApiTest, PutSignalNullLocalbuff)
{
if(!validateTestPrerequisites(/*min=*/2, /*max=*/2))
{
GTEST_SKIP() << "Need exactly 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
// Both ranks must register a window (collective).
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kOneMB));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kOneMB, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
ncclResult_t res = ncclInvalidArgument;
if(myRank == 0)
{
// Pass nullptr as localbuf.
res = ncclPutSignal(
/*localbuff=*/nullptr, /*count=*/1, ncclFloat32,
/*peer=*/1, win, /*peerWinOffset=*/0,
kSigIdx, kCtx, kFlags, comm, stream);
}
ASSERT_MPI_NE(ncclSuccess, res);
TEST_INFO("E1 rank %d: PutSignalNullLocalbuff done.", myRank);
}
// ============================================================================
// E2 — PutSignalNullWindow
// ============================================================================
/**
* @test HostApiTest.PutSignalNullWindow
* @brief ncclPutSignal with null peerWin must return an error.
*
* Non-collective: only rank 0 calls the API.
*/
TEST_F(HostApiTest, PutSignalNullWindow)
{
if(!validateTestPrerequisites(/*min=*/2, /*max=*/2))
{
GTEST_SKIP() << "Need exactly 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
// Allocate a valid source buffer on rank 0.
void* srcBuf = nullptr;
if(myRank == 0)
{
ASSERT_EQ(ncclSuccess, allocFineGrainBuffer(&srcBuf, kOneMB));
}
auto srcBufGuard = makeScopeGuard([&]() { if(srcBuf) freeFineGrainBuffer(srcBuf); });
ncclResult_t res = ncclInvalidArgument;
if(myRank == 0)
{
res = ncclPutSignal(
srcBuf, /*count=*/1, ncclFloat32,
/*peer=*/1, /*peerWin=*/nullptr, /*peerWinOffset=*/0,
kSigIdx, kCtx, kFlags, comm, stream);
}
ASSERT_MPI_NE(ncclSuccess, res);
TEST_INFO("E2 rank %d: PutSignalNullWindow done.", myRank);
}
// ============================================================================
// E3 — PutSignalOffsetOutOfBounds
// ============================================================================
/**
* @test HostApiTest.PutSignalOffsetOutOfBounds
* @brief ncclPutSignal with peerWinOffset == kOneMB (past end) must error.
*
* Both ranks register a 1 MiB window. Rank 0 attempts a PUT at offset
* equal to the window size (one byte past the end).
*/
TEST_F(HostApiTest, PutSignalOffsetOutOfBounds)
{
if(!validateTestPrerequisites(/*min=*/2, /*max=*/2))
{
GTEST_SKIP() << "Need exactly 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kOneMB));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kOneMB, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
ncclResult_t res = ncclInvalidArgument;
if(myRank == 0)
{
// peerWinOffset = kOneMB means the first byte of the PUT is exactly at
// the end of the window — out of bounds.
void* srcBuf = static_cast<uint8_t*>(winBuf) + kSendOffset;
res = ncclPutSignal(
srcBuf, kTransferSize, ncclUint8,
/*peer=*/1, win, /*peerWinOffset=*/kOneMB,
kSigIdx, kCtx, kFlags, comm, stream);
}
ASSERT_MPI_NE(ncclSuccess, res);
TEST_INFO("E3 rank %d: PutSignalOffsetOutOfBounds done.", myRank);
}
// ============================================================================
// E4 — PutSignalInvalidSigIdx
// ============================================================================
/**
* @test HostApiTest.PutSignalInvalidSigIdx
* @brief ncclPutSignal with sigIdx=1 (reserved, must be 0) should error.
*
* Non-collective: only rank 0 calls the API. Skip if not validated.
*/
TEST_F(HostApiTest, PutSignalInvalidSigIdx)
{
if(!validateTestPrerequisites(/*min=*/2, /*max=*/2))
{
GTEST_SKIP() << "Need exactly 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kOneMB));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kOneMB, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
ncclResult_t res = ncclInvalidArgument;
if(myRank == 0)
{
void* srcBuf = static_cast<uint8_t*>(winBuf) + kSendOffset;
res = ncclPutSignal(
srcBuf, kTransferSize, ncclUint8,
/*peer=*/1, win, /*peerWinOffset=*/0,
/*sigIdx=*/1, kCtx, kFlags, comm, stream);
}
ASSERT_MPI_NE(ncclSuccess, res);
TEST_INFO("E4 rank %d: PutSignalInvalidSigIdx done.", myRank);
}
// ============================================================================
// S2 — SignalCumulativeFence
// ============================================================================
/**
* @test HostApiTest.SignalCumulativeFence
* @brief ncclPutSignal then ncclSignal from rank 0; rank 1 waits with opCnt=2.
*
* Rank 0 issues one ncclPutSignal (256 bytes to rank 1's window at offset 0)
* then one ncclSignal (no data) to rank 1, both before rank 1 waits.
* Rank 1 waits with opCnt=2 (fence: 1 PUT + 1 SIGNAL).
* After sync rank 1 verifies the PUT data.
*/
TEST_F(HostApiTest, SignalCumulativeFence)
{
if(!validateTestPrerequisites(/*min=*/2))
{
GTEST_SKIP() << "Need at least 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
const size_t kSize = 256;
// Window large enough for both send region (rank 0) and recv region (rank 1).
// kSendOffset=0, kRecvOffset=kTransferSize.
const size_t kWinSize = kOneMB;
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kWinSize));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kWinSize, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
ncclResult_t sendRes = ncclSuccess;
if(myRank == 0)
{
void* sendRegion = static_cast<uint8_t*>(winBuf) + kSendOffset;
FillBuf(sendRegion, kSize, /*senderRank=*/0);
sendRes = ncclGroupStart();
if(sendRes == ncclSuccess)
{
ncclResult_t r1 = ncclPutSignal(
sendRegion, kSize, ncclUint8,
/*peer=*/1, win, /*peerWinOffset=*/kRecvOffset,
kSigIdx, kCtx, kFlags, comm, stream);
ncclResult_t r2 = ncclSignal(/*peer=*/1, kSigIdx, kCtx, kFlags, comm, stream);
sendRes = ncclGroupEnd();
if(sendRes == ncclSuccess) sendRes = r1;
if(sendRes == ncclSuccess) sendRes = r2;
}
}
ASSERT_MPI_EQ(ncclSuccess, sendRes);
MPI_Barrier(MPI_COMM_WORLD);
ncclResult_t waitRes = ncclSuccess;
if(myRank == 1)
{
ncclWaitSignalDesc_t desc{/*opCnt=*/2, /*peer=*/0, kSigIdx, kCtx};
waitRes = ncclWaitSignal(/*nDesc=*/1, &desc, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, waitRes);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
bool ok = (myRank != 1) ||
VerifyBuf(static_cast<uint8_t*>(winBuf) + kRecvOffset, kSize, /*senderRank=*/0);
ASSERT_MPI_TRUE(ok);
TEST_INFO("S2 rank %d: SignalCumulativeFence passed.", myRank);
}
// ============================================================================
// WS3 — MultipleSendersOneReceiver
// ============================================================================
/**
* @test HostApiTest.MultipleSendersOneReceiver
* @brief Two senders (rank 0, rank 1) put to disjoint offsets of rank 2's window.
*
* Rank 0 PUTs 256 bytes at offset 0; rank 1 PUTs 256 bytes at offset 4096.
* Rank 2 issues two separate ncclWaitSignal calls (one per sender, opCnt=1 each).
* After sync rank 2 verifies both regions.
*
* Window layout: [0..kSize) and [4096..4096+kSize) are receive slots for rank 2.
* Senders carve their send buffer from offset kSendSlot in their own window.
*/
TEST_F(HostApiTest, MultipleSendersOneReceiver)
{
if(!validateTestPrerequisites(/*min=*/3))
{
GTEST_SKIP() << "Need at least 3 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
const size_t kSize = 256;
// 8192 covers the two recv slots at 0 and 4096; add kSize more for the send
// region so senders can carve from their own window beyond the recv area.
const size_t kSendSlot = 8192;
const size_t kWinSize = kSendSlot + kSize;
// All ranks register a window (collective).
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kWinSize));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kWinSize, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
ncclResult_t sendRes = ncclSuccess;
if(myRank == 0)
{
void* sendRegion = static_cast<uint8_t*>(winBuf) + kSendSlot;
FillBuf(sendRegion, kSize, /*senderRank=*/0);
sendRes = ncclPutSignal(sendRegion, kSize, ncclUint8,
/*peer=*/2, win, /*peerWinOffset=*/0,
kSigIdx, kCtx, kFlags, comm, stream);
}
else if(myRank == 1)
{
void* sendRegion = static_cast<uint8_t*>(winBuf) + kSendSlot;
FillBuf(sendRegion, kSize, /*senderRank=*/1);
sendRes = ncclPutSignal(sendRegion, kSize, ncclUint8,
/*peer=*/2, win, /*peerWinOffset=*/4096,
kSigIdx, kCtx, kFlags, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, sendRes);
ncclResult_t waitRes = ncclSuccess;
if(myRank == 2)
{
ncclWaitSignalDesc_t d0{/*opCnt=*/1, /*peer=*/0, kSigIdx, kCtx};
waitRes = ncclWaitSignal(1, &d0, comm, stream);
if(waitRes == ncclSuccess)
{
ncclWaitSignalDesc_t d1{/*opCnt=*/1, /*peer=*/1, kSigIdx, kCtx};
waitRes = ncclWaitSignal(1, &d1, comm, stream);
}
}
ASSERT_MPI_EQ(ncclSuccess, waitRes);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
bool allOk = true;
if(myRank == 2)
{
const uint8_t* base = static_cast<const uint8_t*>(winBuf);
allOk = VerifyBuf(base + 0, kSize, /*senderRank=*/0) &&
VerifyBuf(base + 4096, kSize, /*senderRank=*/1);
}
ASSERT_MPI_TRUE(allOk);
TEST_INFO("WS3 rank %d: MultipleSendersOneReceiver passed.", myRank);
}
// ============================================================================
// WS4 — WaitSignalMultipleDescriptors
// ============================================================================
/**
* @test HostApiTest.WaitSignalMultipleDescriptors
* @brief ncclWaitSignal called once with an array of 2 descriptors (nDesc=2).
*
* Same topology as WS3 (ranks 0 and 1 → rank 2) but rank 2 passes both
* descriptors in a single ncclWaitSignal(comm, 2, descs, stream) call.
*
* Window layout matches WS3: recv slots at 0 and 4096; send region at kSendSlot.
*/
TEST_F(HostApiTest, WaitSignalMultipleDescriptors)
{
if(!validateTestPrerequisites(/*min=*/3))
{
GTEST_SKIP() << "Need at least 3 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
const size_t kSize = 256;
const size_t kSendSlot = 8192;
const size_t kWinSize = kSendSlot + kSize;
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kWinSize));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kWinSize, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
ncclResult_t sendRes = ncclSuccess;
if(myRank == 0)
{
void* sendRegion = static_cast<uint8_t*>(winBuf) + kSendSlot;
FillBuf(sendRegion, kSize, /*senderRank=*/0);
sendRes = ncclPutSignal(sendRegion, kSize, ncclUint8,
/*peer=*/2, win, /*peerWinOffset=*/0,
kSigIdx, kCtx, kFlags, comm, stream);
}
else if(myRank == 1)
{
void* sendRegion = static_cast<uint8_t*>(winBuf) + kSendSlot;
FillBuf(sendRegion, kSize, /*senderRank=*/1);
sendRes = ncclPutSignal(sendRegion, kSize, ncclUint8,
/*peer=*/2, win, /*peerWinOffset=*/4096,
kSigIdx, kCtx, kFlags, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, sendRes);
ncclResult_t waitRes = ncclSuccess;
if(myRank == 2)
{
// Both descriptors in one call.
ncclWaitSignalDesc_t descs[2];
descs[0] = {/*opCnt=*/1, /*peer=*/0, kSigIdx, kCtx};
descs[1] = {/*opCnt=*/1, /*peer=*/1, kSigIdx, kCtx};
waitRes = ncclWaitSignal(/*nDesc=*/2, &descs[0], comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, waitRes);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
bool allOk = true;
if(myRank == 2)
{
const uint8_t* base = static_cast<const uint8_t*>(winBuf);
allOk = VerifyBuf(base + 0, kSize, /*senderRank=*/0) &&
VerifyBuf(base + 4096, kSize, /*senderRank=*/1);
}
ASSERT_MPI_TRUE(allOk);
TEST_INFO("WS4 rank %d: WaitSignalMultipleDescriptors passed.", myRank);
}
// ============================================================================
// P4 — LargePut
// ============================================================================
/**
* @test HostApiTest.LargePut
* @brief PUT 256 MiB from rank 0 to rank 1's window; spot-check first and last 64 bytes.
*
* We use 256 MiB rather than the full 1 GiB to avoid OOM on hardware with
* limited fine-grain / pinned memory capacity while still exercising a
* large-transfer code path.
*/
TEST_F(HostApiTest, LargePut)
{
if(!validateTestPrerequisites(/*min=*/2))
{
GTEST_SKIP() << "Need at least 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
// 256 MiB — large but avoids OOM on most ROCm systems.
const size_t kLargeSize = 256ULL * 1024 * 1024;
const uint8_t kByte = 0xAB;
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kLargeSize));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kLargeSize, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
ncclResult_t putRes = ncclSuccess;
if(myRank == 0)
{
FillSentinel(winBuf, kLargeSize, kByte);
putRes = ncclPutSignal(winBuf, kLargeSize, ncclUint8,
/*peer=*/1, win, /*peerWinOffset=*/0,
kSigIdx, kCtx, kFlags, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, putRes);
ncclResult_t waitRes = ncclSuccess;
if(myRank == 1)
{
ncclWaitSignalDesc_t desc{/*opCnt=*/1, /*peer=*/0, kSigIdx, kCtx};
waitRes = ncclWaitSignal(1, &desc, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, waitRes);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
bool ok = (myRank != 1) || AllSentinel(winBuf, kLargeSize, kByte);
ASSERT_MPI_TRUE(ok);
TEST_INFO("P4 rank %d: LargePut passed.", myRank);
}
// ============================================================================
// P5 — AllToAllPut
// ============================================================================
/**
* @test HostApiTest.AllToAllPut
* @brief Each rank i PUTs to rank (i+1)%N and waits for rank (i-1+N)%N.
*
* All ranks register 4096-byte windows. Each rank fills 256 bytes with its
* own rank pattern, PUTs to the next rank (offset 0), then waits for the
* previous rank's signal. After sync each rank verifies the received data.
*/
TEST_F(HostApiTest, AllToAllPut)
{
if(!validateTestPrerequisites(/*min=*/2))
{
GTEST_SKIP() << "Need at least 2 MPI processes";
}
const int myRank = rank();
const int nRanks_ = nRanks();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
const size_t kSize = 256;
// Window layout: [0..kSize) = recv slot; [kSize..2*kSize) = send region.
// Both fit within 4096, so kWinSize=4096 is fine.
const size_t kRecvSlot = 0;
const size_t kSendSlot = kSize;
const size_t kWinSize = 4096;
const int sendTo = (myRank + 1) % nRanks_;
const int recvFrom = (myRank + nRanks_ - 1) % nRanks_;
// All ranks register a window.
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kWinSize));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kWinSize, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
// Carve the send buffer from this rank's own registered window.
void* sendRegion = static_cast<uint8_t*>(winBuf) + kSendSlot;
FillBuf(sendRegion, kSize, myRank);
// Batch PUT + WAIT in a group.
ASSERT_MPI_EQ(ncclSuccess, ncclGroupStart());
ncclResult_t rPut = ncclPutSignal(
sendRegion, kSize, ncclUint8,
sendTo, win, /*peerWinOffset=*/kRecvSlot,
kSigIdx, kCtx, kFlags, comm, stream);
ncclWaitSignalDesc_t desc{/*opCnt=*/1, recvFrom, kSigIdx, kCtx};
ncclResult_t rWait = ncclWaitSignal(1, &desc, comm, stream);
ASSERT_MPI_EQ(ncclSuccess, ncclGroupEnd());
ASSERT_MPI_EQ(ncclSuccess, rPut);
ASSERT_MPI_EQ(ncclSuccess, rWait);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
// Verify: we should have received recvFrom's pattern at kRecvSlot.
bool ok = VerifyBuf(static_cast<uint8_t*>(winBuf) + kRecvSlot, kSize, recvFrom);
ASSERT_MPI_TRUE(ok);
TEST_INFO("P5 rank %d: AllToAllPut passed (recv from rank %d).", myRank, recvFrom);
}
// ============================================================================
// O2 — SignalImpliesPriorPutsDelivered
// ============================================================================
/**
* @test HostApiTest.SignalImpliesPriorPutsDelivered
* @brief Two ncclPutSignal calls from rank 0; rank 1 waits with opCnt=2.
*
* Each ncclPutSignal implicitly delivers a signal. Two calls = opCnt 2.
* Rank 1 verifies both data regions after sync.
*/
TEST_F(HostApiTest, SignalImpliesPriorPutsDelivered)
{
if(!validateTestPrerequisites(/*min=*/2))
{
GTEST_SKIP() << "Need at least 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
const size_t kSize = 256;
const size_t kWinSize = kOneMB;
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kWinSize));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kWinSize, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
// Rank 0 carves two send regions from its own registered window.
// Layout: [kSendOffset .. kSendOffset+kSize) = src0
// [kSendOffset+kSize .. kSendOffset+2*kSize) = src1
// Rank 1 receives at kRecvOffset and kRecvOffset+512.
ncclResult_t putRes = ncclSuccess;
if(myRank == 0)
{
void* src0 = static_cast<uint8_t*>(winBuf) + kSendOffset;
void* src1 = static_cast<uint8_t*>(winBuf) + kSendOffset + kSize;
FillBuf(src0, kSize, /*senderRank=*/0);
FillBuf(src1, kSize, /*senderRank=*/10);
putRes = ncclPutSignal(src0, kSize, ncclUint8,
/*peer=*/1, win, /*peerWinOffset=*/kRecvOffset,
kSigIdx, kCtx, kFlags, comm, stream);
if(putRes == ncclSuccess)
putRes = ncclPutSignal(src1, kSize, ncclUint8,
/*peer=*/1, win, /*peerWinOffset=*/kRecvOffset + 512,
kSigIdx, kCtx, kFlags, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, putRes);
ncclResult_t waitRes = ncclSuccess;
if(myRank == 1)
{
ncclWaitSignalDesc_t desc{/*opCnt=*/2, /*peer=*/0, kSigIdx, kCtx};
waitRes = ncclWaitSignal(1, &desc, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, waitRes);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
bool allOk = true;
if(myRank == 1)
{
const uint8_t* base = static_cast<const uint8_t*>(winBuf);
allOk = VerifyBuf(base + kRecvOffset, kSize, /*senderRank=*/0) &&
VerifyBuf(base + kRecvOffset + 512, kSize, /*senderRank=*/10);
}
ASSERT_MPI_TRUE(allOk);
TEST_INFO("O2 rank %d: SignalImpliesPriorPutsDelivered passed.", myRank);
}
// ============================================================================
// E5 — PutSignalInvalidCtx
// ============================================================================
/**
* @test HostApiTest.PutSignalInvalidCtx
* @brief ncclPutSignal with ctx=1 (reserved, must be 0) should error.
*
* Non-collective: only rank 0 calls the API. Skip if argcheck is not
* implemented (i.e., the call returns ncclSuccess).
*/
TEST_F(HostApiTest, PutSignalInvalidCtx)
{
if(!validateTestPrerequisites(/*min=*/2))
{
GTEST_SKIP() << "Need at least 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kOneMB));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kOneMB, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
ncclResult_t res = ncclInvalidArgument;
if(myRank == 0)
{
void* srcBuf = static_cast<uint8_t*>(winBuf) + kSendOffset;
res = ncclPutSignal(
srcBuf, kTransferSize, ncclUint8,
/*peer=*/1, win, /*peerWinOffset=*/0,
kSigIdx, /*ctx=*/1, kFlags, comm, stream);
}
ASSERT_MPI_NE(ncclSuccess, res);
TEST_INFO("E5 rank %d: PutSignalInvalidCtx done.", myRank);
}
// ============================================================================
// E6 — WaitSignalNullDescs
// ============================================================================
/**
* @test HostApiTest.WaitSignalNullDescs
* @brief ncclWaitSignal(nDesc=1, nullptr, ...) must return ncclInvalidArgument.
*
* Each rank calls independently (non-collective). Skip if not validated.
*/
TEST_F(HostApiTest, WaitSignalNullDescs)
{
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
ncclResult_t res = ncclWaitSignal(/*nDesc=*/1, /*signalDescs=*/nullptr, comm, stream);
EXPECT_EQ(ncclInvalidArgument, res)
<< "E6: expected ncclInvalidArgument for null descs with nDesc=1";
TEST_INFO("E6 rank %d: WaitSignalNullDescs done.", myRank);
}
// ============================================================================
// E7 — WaitSignalZeroDesc
// ============================================================================
/**
* @test HostApiTest.WaitSignalZeroDesc
* @brief ncclWaitSignal(nDesc=0, nullptr, ...) — zero descriptors is a no-op.
*
* Expect ncclSuccess (or ncclInvalidArgument — both are acceptable).
* No stream sync or data transfer involved.
*/
TEST_F(HostApiTest, WaitSignalZeroDesc)
{
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
ncclResult_t res = ncclWaitSignal(/*nDesc=*/0, /*signalDescs=*/nullptr, comm, stream);
EXPECT_TRUE(res == ncclSuccess || res == ncclInvalidArgument)
<< "E7: expected ncclSuccess or ncclInvalidArgument for nDesc=0, got "
<< static_cast<int>(res);
TEST_INFO("E7 rank %d: WaitSignalZeroDesc done (result=%d).", myRank, static_cast<int>(res));
}
// ============================================================================
// M1 — TwoCommunicatorsIndependentWindows
// ============================================================================
/**
* @test HostApiTest.TwoCommunicatorsIndependentWindows
* @brief Two independent communicators each own a window; PUTs on each must not interfere.
*
* The fixture's SetUp creates communicator 1. This test creates communicator 2
* manually. Each communicator registers its own buffer/window, rank 0 PUTs to
* rank 1 through each independently (different seeds), rank 1 waits and verifies
* both receive regions.
*/
TEST_F(HostApiTest, TwoCommunicatorsIndependentWindows)
{
if(!validateTestPrerequisites(/*min=*/2, /*max=*/2))
{
GTEST_SKIP() << "Need exactly 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm1 = getActiveCommunicator();
hipStream_t stream1 = getActiveStream();
// ---- Create a second communicator manually ----
ncclUniqueId id2{};
if(myRank == 0)
{
ASSERT_EQ(ncclSuccess, ncclGetUniqueId(&id2));
}
MPI_Bcast(&id2, sizeof(id2), MPI_BYTE, 0, MPI_COMM_WORLD);
ncclComm_t comm2 = nullptr;
ASSERT_MPI_EQ(ncclSuccess, ncclCommInitRank(&comm2, nRanks(), id2, myRank));
auto comm2Guard = makeScopeGuard([&]() { if(comm2) ncclCommDestroy(comm2); });
hipStream_t stream2 = nullptr;
ASSERT_MPI_EQ(hipSuccess, hipStreamCreate(&stream2));
auto stream2Guard = makeScopeGuard([&]() { if(stream2) hipStreamDestroy(stream2); });
// ---- Comm 1: buffer, window, PUT ----
void* buf1 = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&buf1, kOneMB));
auto buf1Guard = makeScopeGuard([&]() { freeFineGrainBuffer(buf1); });
ncclWindow_t win1 = nullptr;
NcclWindowGuard wg1(comm1, buf1, kOneMB, &win1, winMode());
ASSERT_MPI_NE(win1, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg1.initResult());
// ---- Comm 2: buffer, window, PUT ----
void* buf2 = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&buf2, kOneMB));
auto buf2Guard = makeScopeGuard([&]() { freeFineGrainBuffer(buf2); });
ncclWindow_t win2 = nullptr;
NcclWindowGuard wg2(comm2, buf2, kOneMB, &win2, winMode());
ASSERT_MPI_NE(win2, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg2.initResult());
const int kSeed1 = 42;
const int kSeed2 = 99;
// Rank 0: PUT through comm1 and comm2
ncclResult_t put1Res = ncclSuccess;
ncclResult_t put2Res = ncclSuccess;
if(myRank == 0)
{
void* src1 = static_cast<uint8_t*>(buf1) + kSendOffset;
FillBuf(src1, kTransferSize, kSeed1);
put1Res = ncclPutSignal(src1, kTransferSize, ncclUint8,
/*peer=*/1, win1, kRecvOffset,
kSigIdx, kCtx, kFlags, comm1, stream1);
void* src2 = static_cast<uint8_t*>(buf2) + kSendOffset;
FillBuf(src2, kTransferSize, kSeed2);
put2Res = ncclPutSignal(src2, kTransferSize, ncclUint8,
/*peer=*/1, win2, kRecvOffset,
kSigIdx, kCtx, kFlags, comm2, stream2);
}
ASSERT_MPI_EQ(ncclSuccess, put1Res);
ASSERT_MPI_EQ(ncclSuccess, put2Res);
// Rank 1: wait on each communicator independently
ncclResult_t wait1Res = ncclSuccess;
ncclResult_t wait2Res = ncclSuccess;
if(myRank == 1)
{
ncclWaitSignalDesc_t d1{1, 0, kSigIdx, kCtx};
wait1Res = ncclWaitSignal(1, &d1, comm1, stream1);
ncclWaitSignalDesc_t d2{1, 0, kSigIdx, kCtx};
wait2Res = ncclWaitSignal(1, &d2, comm2, stream2);
}
ASSERT_MPI_EQ(ncclSuccess, wait1Res);
ASSERT_MPI_EQ(ncclSuccess, wait2Res);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream1));
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream2));
bool allOk = true;
if(myRank == 1)
{
allOk = VerifyBuf(static_cast<uint8_t*>(buf1) + kRecvOffset, kTransferSize, kSeed1) &&
VerifyBuf(static_cast<uint8_t*>(buf2) + kRecvOffset, kTransferSize, kSeed2);
}
ASSERT_MPI_TRUE(allOk);
TEST_INFO("M1 rank %d: TwoCommunicatorsIndependentWindows passed.", myRank);
}
// ============================================================================
// M2 — StressManySmallPuts
// ============================================================================
/**
* @test HostApiTest.StressManySmallPuts
* @brief 100 small (64-byte) PUTs to distinct offsets; rank 1 waits with opCnt=100.
*
* Stress-tests the signal counter path and many-operation queueing.
* Each PUT uses seed=i so every 64-byte slot carries a distinguishable pattern.
*/
TEST_F(HostApiTest, StressManySmallPuts)
{
if(!validateTestPrerequisites(/*min=*/2, /*max=*/2))
{
GTEST_SKIP() << "Need exactly 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
const int kNumPuts = 100;
const size_t kPutSize = 64;
const size_t kRecvArea = static_cast<size_t>(kNumPuts) * kPutSize;
const size_t kSendSlot = kRecvArea;
const size_t kWinSize = kRecvArea + kPutSize;
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kWinSize));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kWinSize, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
ncclResult_t putRes = ncclSuccess;
if(myRank == 0)
{
uint8_t* sendRegion = static_cast<uint8_t*>(winBuf) + kSendSlot;
for(int i = 0; i < kNumPuts && putRes == ncclSuccess; ++i)
{
FillBuf(sendRegion, kPutSize, /*seed=*/i);
size_t peerOff = static_cast<size_t>(i) * kPutSize;
putRes = ncclPutSignal(sendRegion, kPutSize, ncclUint8,
/*peer=*/1, win, peerOff,
kSigIdx, kCtx, kFlags, comm, stream);
}
}
ASSERT_MPI_EQ(ncclSuccess, putRes);
ncclResult_t waitRes = ncclSuccess;
if(myRank == 1)
{
ncclWaitSignalDesc_t desc{kNumPuts, 0, kSigIdx, kCtx};
waitRes = ncclWaitSignal(1, &desc, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, waitRes);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
bool allOk = true;
if(myRank == 1)
{
const uint8_t* base = static_cast<const uint8_t*>(winBuf);
for(int i = 0; i < kNumPuts; ++i)
{
size_t off = static_cast<size_t>(i) * kPutSize;
if(!VerifyBuf(base + off, kPutSize, /*seed=*/i))
{
allOk = false;
break;
}
}
}
ASSERT_MPI_TRUE(allOk);
TEST_INFO("M2 rank %d: StressManySmallPuts passed (%d x %zu bytes).",
myRank, kNumPuts, kPutSize);
}
// ============================================================================
// P6 — PutToSelf
// ============================================================================
/**
* @test HostApiTest.PutToSelf
* @brief Rank 0 PUTs to its own window (peer = self), testing the loopback path.
*
* All ranks register a window (collective), but only rank 0 performs the
* PUT + WaitSignal cycle targeting itself. Other ranks idle but participate
* in collective assertions (ASSERT_MPI_*) to avoid deadlock.
*/
TEST_F(HostApiTest, PutToSelf)
{
if(!validateTestPrerequisites(/*min=*/2))
{
GTEST_SKIP() << "Need at least 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kOneMB));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
NcclWindowGuard wg(comm, winBuf, kOneMB, &win, winMode());
ASSERT_MPI_NE(win, nullptr);
ASSERT_MPI_EQ(ncclSuccess, wg.initResult());
const int kSelfSeed = 77;
ncclResult_t putRes = ncclSuccess;
if(myRank == 0)
{
void* sendRegion = static_cast<uint8_t*>(winBuf) + kSendOffset;
FillBuf(sendRegion, kTransferSize, kSelfSeed);
putRes = ncclPutSignal(sendRegion, kTransferSize, ncclUint8,
/*peer=*/0, win, kRecvOffset,
kSigIdx, kCtx, kFlags, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, putRes);
ncclResult_t waitRes = ncclSuccess;
if(myRank == 0)
{
ncclWaitSignalDesc_t desc{1, 0, kSigIdx, kCtx};
waitRes = ncclWaitSignal(1, &desc, comm, stream);
}
ASSERT_MPI_EQ(ncclSuccess, waitRes);
ASSERT_MPI_EQ(hipSuccess, hipStreamSynchronize(stream));
bool ok = (myRank != 0) ||
VerifyBuf(static_cast<uint8_t*>(winBuf) + kRecvOffset, kTransferSize, kSelfSeed);
ASSERT_MPI_TRUE(ok);
TEST_INFO("P6 rank %d: PutToSelf passed.", myRank);
}
// ============================================================================
// W3 — DoubleDeregister
// ============================================================================
/**
* @test HostApiTest.DoubleDeregister
* @brief Calling ncclCommWindowDeregister twice on the same handle must not crash.
*
* The first deregister should succeed. The second should return an error
* (or at least not ncclSuccess). No NcclWindowGuard is used because we
* need manual control over the deregister calls.
*/
TEST_F(HostApiTest, DoubleDeregister)
{
if(!validateTestPrerequisites(/*min=*/2))
{
GTEST_SKIP() << "Need at least 2 MPI processes";
}
const int myRank = rank();
ncclComm_t comm = getActiveCommunicator();
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kOneMB));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
ASSERT_MPI_EQ(ncclSuccess,
ncclCommWindowRegister(comm, winBuf, kOneMB, &win, winMode()));
ASSERT_MPI_NE(win, nullptr);
// First deregister — should succeed.
ASSERT_MPI_EQ(ncclSuccess, ncclCommWindowDeregister(comm, win));
// Second deregister on the stale handle — expect failure (not ncclSuccess).
ncclResult_t res2 = ncclCommWindowDeregister(comm, win);
ASSERT_MPI_NE(ncclSuccess, res2);
TEST_INFO("W3 rank %d: DoubleDeregister passed (second deregister returned %d).",
myRank, static_cast<int>(res2));
}
// ============================================================================
// W4 — DestroyCommWithoutDeregister
// ============================================================================
/**
* @test HostApiTest.DestroyCommWithoutDeregister
* @brief A registered window must be freed automatically when the communicator
* is destroyed, even if the user never calls ncclCommWindowDeregister.
*
* Validates the NCCL fix "Free symmetric window objects automatically during
* commFree" — ncclCommDestroy drives the internal commFree -> ncclDevrFinalize
* path, whose window drain loop releases any still-registered windows.
*
* Uses a dedicated communicator (not the fixture's shared one) so it can call
* ncclCommDestroy explicitly and assert it still returns ncclSuccess.
*/
TEST_F(HostApiTest, DestroyCommWithoutDeregister)
{
if(!validateTestPrerequisites(/*min=*/2))
{
GTEST_SKIP() << "Need at least 2 MPI processes";
}
const int worldRank = rank();
const int worldSize = nRanks();
// Build a communicator we fully own so we can destroy it explicitly and
// observe the result of commFree -> ncclDevrFinalize.
ncclUniqueId id{};
ncclResult_t idRes = ncclSuccess; // non-root ranks stay success
if(worldRank == 0)
{
idRes = ncclGetUniqueId(&id);
}
MPI_Bcast(&id, sizeof(id), MPI_BYTE, 0, MPI_COMM_WORLD);
// Collective assert (must be called by every rank, not just root).
ASSERT_MPI_EQ(ncclSuccess, idRes);
ncclComm_t ownComm = nullptr;
ASSERT_MPI_EQ(ncclSuccess, ncclCommInitRank(&ownComm, worldSize, id, worldRank));
ASSERT_MPI_NE(ownComm, nullptr);
// Safety net: if a later assertion aborts the test before the intentional
// destroy below, this guard still tears the comm down. It is cleared after
// the explicit ncclCommDestroy to avoid a double-destroy.
auto ownCommGuard = makeScopeGuard([&]() { if(ownComm) (void)ncclCommDestroy(ownComm); });
// Allocate a fine-grain buffer and register it as a window.
void* winBuf = nullptr;
ASSERT_MPI_EQ(ncclSuccess, allocFineGrainBuffer(&winBuf, kOneMB));
auto winBufGuard = makeScopeGuard([&]() { freeFineGrainBuffer(winBuf); });
ncclWindow_t win = nullptr;
ASSERT_MPI_EQ(ncclSuccess,
ncclCommWindowRegister(ownComm, winBuf, kOneMB, &win, winMode()));
ASSERT_MPI_NE(win, nullptr);
// Intentionally skip ncclCommWindowDeregister — this is the whole point of
// the test. Destroying the comm must auto-free the leftover window objects
// during commFree -> ncclDevrFinalize and therefore still succeed.
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_EQ(ncclSuccess, ncclCommDestroy(ownComm));
ownComm = nullptr; // destroyed explicitly; disarm the guard
MPI_Barrier(MPI_COMM_WORLD);
TEST_INFO("W4 rank %d: DestroyCommWithoutDeregister passed.", worldRank);
}
} // namespace RcclUnitTesting
#endif // MPI_TESTS_ENABLED