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

664 lines
28 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
************************************************************************/
// End-to-end MPI tests for CE collectives via the public RCCL API (AllGather, AlltoAll, Scatter, Gather, Fallback, Stress).
#include "CeTestHelpers.hpp"
#include "DeviceBufferHelpers.hpp"
#include "MPIHelpers.hpp"
#include "MPITestBase.hpp"
#include "ResourceGuards.hpp"
#include "SymmetricBufferHelpers.hpp"
#include "TestChecks.hpp"
#include "rccl/rccl.h"
#include <gtest/gtest.h>
#include <hip/hip_runtime.h>
#include <string>
#ifdef MPI_TESTS_ENABLED
using namespace MPITestConstants;
using namespace RCCLTestHelpers;
namespace CeMPITestConstants
{
constexpr size_t kSmallCount = 4096; // elements per rank for fast tests
constexpr size_t kMediumCount = 65536; // elements per rank for larger tests
constexpr int kMinRanks2 = 2;
constexpr int kMinRanks4 = 4;
constexpr int kMinRanks8 = 8;
constexpr int kStressIters = 20; // back-to-back iterations for stress test
constexpr int kInterleavedIters = 10; // iterations for CE+SM interleaved stress test
} // namespace CeMPITestConstants
using namespace CeMPITestConstants;
// Base fixture for all CE MPI tests; captures NCCL INFO per rank for log-marker assertions.
// Tests always run: on CE-capable systems they assert the CE path; on others they assert
// the SM fallback path. isCeExpected() is the single gate for which assertion to make.
class CeMPITest : public MPITestBase
{
protected:
std::unique_ptr<MPIHelpers::MpiEnvGuard> debugGuard_;
std::unique_ptr<MPIHelpers::MpiEnvGuard> debugSubsysGuard_;
std::unique_ptr<MPIHelpers::TestLogAssertionContext> logCtx_;
void SetUp() override
{
MPITestBase::SetUp();
// Log capture is always enabled so that assertCEPathTaken / assertCEPathNotTaken
// can inspect the NCCL debug output regardless of whether CE is active.
debugGuard_ = std::make_unique<MPIHelpers::MpiEnvGuard>("NCCL_DEBUG", "INFO");
debugSubsysGuard_ = std::make_unique<MPIHelpers::MpiEnvGuard>("NCCL_DEBUG_SUBSYS", "ALL");
logCtx_ = std::make_unique<MPIHelpers::TestLogAssertionContext>(
MPIHelpers::makeCombinedAssertionLogOptions(getTestMpiRank()));
}
void TearDown() override
{
// Destroy communicator before logCtx_ so its log lines flush to the debug file first.
MPITestBase::TearDown();
logCtx_.reset();
debugSubsysGuard_.reset();
debugGuard_.reset();
}
// Returns true when all CE prerequisites are met (driver + env vars).
// Delegates to isCeDispatchConfigured() in CeTestHelpers.hpp — the single
// source of truth shared with CeInternalMPITests.cpp.
bool isCeExpected() const { return isCeDispatchConfigured(); }
// CE log markers: "Init CE" = CE initialised; "CE: rank" = CE path taken for a collective.
enum class CeLogStatus { Taken, InitOnlyNoOp, NotInitialized };
static CeLogStatus checkCeLog(const std::string& log)
{
if(log.find("Init CE") == std::string::npos)
return CeLogStatus::NotInitialized;
if(log.find("CE: rank") != std::string::npos)
return CeLogStatus::Taken;
return CeLogStatus::InitOnlyNoOp;
}
// Merge debug file + per-rank stderr (covers ncclDebugInit race on first test).
std::string readAllLogs() const
{
return logCtx_->readNcclDebugLog() + logCtx_->readPerRankStderrLog();
}
// Assert the CE dispatch path was taken, or — when CE is not expected on this
// system/configuration — assert the SM fallback path and print an info line.
void assertCEPathTaken(const char* context)
{
const std::string log = readAllLogs();
CeLogStatus status = checkCeLog(log);
if(isCeExpected())
{
EXPECT_EQ(status, CeLogStatus::Taken)
<< context
<< ": \"CE: rank\" absent from NCCL log"
" (NCCL_CTA_POLICY=2 set but CE dispatch path was not taken)";
if(status == CeLogStatus::Taken)
TEST_INFO("%s: assertion passed — CE dispatch path taken", context);
}
else
{
EXPECT_EQ(log.find("CE: rank"), std::string::npos)
<< context
<< ": \"CE: rank\" found in NCCL log but CE was not expected"
" (driver or env-var prerequisites not met)";
TEST_INFO("%s: assertion passed — SM fallback path (CE not available/configured)",
context);
}
}
// Root-only variant: only root emits "CE: rank" (non-root has numOps=0 in Scatter/Gather).
void assertCEPathTakenOnRoot(int rootRank, int myRank, const char* context)
{
const std::string log = readAllLogs();
CeLogStatus status = checkCeLog(log);
if(isCeExpected())
{
if(myRank == rootRank)
{
EXPECT_EQ(status, CeLogStatus::Taken)
<< context
<< ": \"CE: rank\" absent from NCCL log on root rank " << myRank
<< " (NCCL_CTA_POLICY=2 set but CE dispatch path was not taken)";
if(status == CeLogStatus::Taken)
TEST_INFO("%s: assertion passed — root rank %d CE dispatch path taken",
context, myRank);
}
else
{
TEST_INFO("%s: rank %d non-root — no CE ops expected (correct)", context, myRank);
}
}
else
{
EXPECT_EQ(log.find("CE: rank"), std::string::npos)
<< context
<< ": \"CE: rank\" found in NCCL log but CE was not expected";
TEST_INFO("%s: rank %d assertion passed — SM fallback path (CE not available/configured)",
context, myRank);
}
}
// Assert CE was explicitly NOT taken. Prints an info line in all cases.
void assertCEPathNotTaken(const char* context)
{
const std::string log = readAllLogs();
EXPECT_EQ(log.find("CE: rank"), std::string::npos)
<< context << ": \"CE: rank\" found in NCCL log but was not expected";
TEST_INFO("%s: assertion passed — CE path not taken (expected for this test)", context);
}
// Assert the specific CE batch path sub-variant that was (or was not) taken.
//
// When CE is expected:
// withIntraBatchSync=true → asserts "Batch path with intraBatchSync"
// withIntraBatchSync=false → asserts "Batch path without intraBatchSync"
// When CE is not expected the check is skipped (SM fallback emits neither line).
//
// Call after assertCEPathTaken() so the "CE: rank" assertion already passed.
void assertCEBatchPath(bool withIntraBatchSync, const char* context)
{
if(!isCeExpected())
return;
const std::string needle = withIntraBatchSync
? "Batch path with intraBatchSync"
: "Batch path without intraBatchSync";
const std::string log = readAllLogs();
EXPECT_NE(log.find(needle), std::string::npos)
<< context << ": expected \"" << needle << "\" not found in NCCL log";
TEST_INFO("%s: batch path assertion passed — %s", context, needle.c_str());
}
// Root-only variant: only the root rank emits the batch path log line
// (non-root ranks have numOps=0 for Scatter/Gather and emit nothing).
void assertCEBatchPathOnRoot(bool withIntraBatchSync,
int rootRank, int myRank,
const char* context)
{
if(!isCeExpected() || myRank != rootRank)
return;
assertCEBatchPath(withIntraBatchSync, context);
}
// Data helpers: fill/verify with float(rank+1) per-rank or block-index pattern.
// Thin fixture wrappers; the underlying logic lives in CeTestHelpers.hpp so that
// CeInternalMPITests.cpp can call the same helpers without going through this fixture.
void fillRankScalar(void* buf, size_t nElem, int rank)
{
ASSERT_EQ(hipSuccess, ceFillRankScalarFloat(buf, nElem, rank));
}
void fillBlockPattern(void* buf, size_t totalElem, size_t elemsPerBlock)
{
ASSERT_EQ(
hipSuccess,
initializeBufferWithPattern<float>(buf, totalElem, [elemsPerBlock](size_t i) {
return static_cast<float>(i / elemsPerBlock + 1);
}));
}
bool verifyBlockPattern(const void* buf, size_t totalElem, size_t elemsPerBlock)
{
return ceVerifyBlockPatternFloat(buf, totalElem, elemsPerBlock);
}
bool verifyRankScalar(const void* buf, size_t nElem, int rank)
{
return verifyBufferData<float>(buf, nElem,
[rank](size_t) { return static_cast<float>(rank + 1); });
}
// Symmetric buffer wrapper bound to the active communicator; call after createTestCommunicator().
using SymBuf = RCCLTestHelpers::SymBuf;
ncclResult_t allocSymBuf(size_t bytes, SymBuf& sb)
{
return RCCLTestHelpers::ncclSymBufAlloc(getActiveCommunicator(), bytes, sb);
}
};
// ===========================================================================
// CeMPI_AllGather – ncclAllGather CE correctness + log verification
// ===========================================================================
class CeMPI_AllGather : public CeMPITest
{
protected:
void runAllGather(int minRanks, size_t count, const char* testId)
{
if(!validateTestPrerequisites(minRanks))
GTEST_SKIP() << "Need >= " << minRanks << " MPI ranks";
ASSERT_EQ(ncclSuccess, createTestCommunicator());
int rank{}, nRanks{};
ncclCommUserRank(getActiveCommunicator(), &rank);
ncclCommCount(getActiveCommunicator(), &nRanks);
SymBuf sendSym, recvSym;
ASSERT_EQ(ncclSuccess, allocSymBuf(count * sizeof(float), sendSym));
ASSERT_EQ(ncclSuccess, allocSymBuf(count * nRanks * sizeof(float), recvSym));
fillRankScalar(sendSym.ptr, count, rank);
ASSERT_EQ(ncclSuccess,
ncclAllGather(sendSym.ptr, recvSym.ptr, count, ncclFloat32,
getActiveCommunicator(), getActiveStream()));
ASSERT_EQ(hipSuccess, hipStreamSynchronize(getActiveStream()));
ASSERT_TRUE(verifyBlockPattern(recvSym.ptr, count * nRanks, count))
<< "Rank " << rank << ": AllGather data verification failed";
assertCEPathTaken(testId);
// numOps = nRanks (one copy per rank including self); chunkBytes = count * sizeof(float)
assertCEBatchPath(ceExpectIntraBatchSync(nRanks, count * sizeof(float)), testId);
}
};
// CE-MPI-AG-01: 2 ranks, small buffers.
TEST_F(CeMPI_AllGather, TwoRanks) { runAllGather(kMinRanks2, kSmallCount, "CeMPI_AllGather/TwoRanks"); }
// CE-MPI-AG-02: 4 ranks, medium buffers.
TEST_F(CeMPI_AllGather, FourRanks) { runAllGather(kMinRanks4, kMediumCount, "CeMPI_AllGather/FourRanks"); }
// CE-MPI-AG-03: 8 ranks (default production topology), medium buffers.
TEST_F(CeMPI_AllGather, EightRanks) { runAllGather(kMinRanks8, kMediumCount, "CeMPI_AllGather/EightRanks"); }
// CE-MPI-AG-04: Edge case — single element per rank.
TEST_F(CeMPI_AllGather, SingleElement) { runAllGather(kMinRanks2, 1, "CeMPI_AllGather/SingleElement"); }
// ===========================================================================
// CeMPI_AlltoAll – ncclAlltoAll CE correctness + log verification
// ===========================================================================
class CeMPI_AlltoAll : public CeMPITest
{
protected:
// count is per-rank-per-destination; total send/recv buffer = count * nRanks.
void runAlltoAll(int minRanks, size_t count, const char* testId)
{
if(!validateTestPrerequisites(minRanks))
GTEST_SKIP() << "Need >= " << minRanks << " MPI ranks";
ASSERT_EQ(ncclSuccess, createTestCommunicator());
int rank{}, nRanks{};
ncclCommUserRank(getActiveCommunicator(), &rank);
ncclCommCount(getActiveCommunicator(), &nRanks);
const size_t totalElem = count * static_cast<size_t>(nRanks);
SymBuf sendSym, recvSym;
ASSERT_EQ(ncclSuccess, allocSymBuf(totalElem * sizeof(float), sendSym));
ASSERT_EQ(ncclSuccess, allocSymBuf(totalElem * sizeof(float), recvSym));
fillRankScalar(sendSym.ptr, totalElem, rank);
ASSERT_EQ(ncclSuccess,
ncclAlltoAll(sendSym.ptr, recvSym.ptr, count, ncclFloat32,
getActiveCommunicator(), getActiveStream()));
ASSERT_EQ(hipSuccess, hipStreamSynchronize(getActiveStream()));
ASSERT_TRUE(verifyBlockPattern(recvSym.ptr, totalElem, count))
<< "Rank " << rank << ": AlltoAll data verification failed";
assertCEPathTaken(testId);
// numOps = nRanks (one per destination rank); chunkBytes = count * sizeof(float)
assertCEBatchPath(ceExpectIntraBatchSync(nRanks, count * sizeof(float)), testId);
}
};
// CE-MPI-A2A-01: 2 ranks, small buffers.
TEST_F(CeMPI_AlltoAll, TwoRanks) { runAlltoAll(kMinRanks2, kSmallCount, "CeMPI_AlltoAll/TwoRanks"); }
// CE-MPI-A2A-02: 4 ranks, medium buffers.
TEST_F(CeMPI_AlltoAll, FourRanks) { runAlltoAll(kMinRanks4, kMediumCount, "CeMPI_AlltoAll/FourRanks"); }
// CE-MPI-A2A-03: 8 ranks (default production topology), medium buffers.
TEST_F(CeMPI_AlltoAll, EightRanks) { runAlltoAll(kMinRanks8, kMediumCount, "CeMPI_AlltoAll/EightRanks"); }
// CE-MPI-A2A-04: Odd rank count (3) — non-power-of-two op layout vs 2/4/8 ranks.
TEST_F(CeMPI_AlltoAll, ThreeRanks) { runAlltoAll(3, kSmallCount, "CeMPI_AlltoAll/ThreeRanks"); }
// ===========================================================================
// CeMPI_Scatter – ncclScatter CE correctness + log verification
// ===========================================================================
class CeMPI_Scatter : public CeMPITest
{
protected:
// Root sends block r to rank r; non-root send bufs are registered but ignored.
void runScatter(int minRanks, size_t count, int root, const char* testId)
{
if(!validateTestPrerequisites(minRanks))
GTEST_SKIP() << "Need >= " << minRanks << " MPI ranks";
ASSERT_EQ(ncclSuccess, createTestCommunicator());
int rank{}, nRanks{};
ncclCommUserRank(getActiveCommunicator(), &rank);
ncclCommCount(getActiveCommunicator(), &nRanks);
SymBuf sendSym, recvSym;
ASSERT_EQ(ncclSuccess,
allocSymBuf(count * static_cast<size_t>(nRanks) * sizeof(float), sendSym));
ASSERT_EQ(ncclSuccess, allocSymBuf(count * sizeof(float), recvSym));
if(rank == root)
fillBlockPattern(sendSym.ptr, count * static_cast<size_t>(nRanks), count);
ASSERT_EQ(ncclSuccess,
ncclScatter(sendSym.ptr, recvSym.ptr, count, ncclFloat32, root,
getActiveCommunicator(), getActiveStream()));
ASSERT_EQ(hipSuccess, hipStreamSynchronize(getActiveStream()));
ASSERT_TRUE(verifyRankScalar(recvSym.ptr, count, rank))
<< "Rank " << rank << ": Scatter data verification failed";
assertCEPathTakenOnRoot(root, rank, testId);
// Root sends nRanks chunks; chunkBytes = count * sizeof(float)
assertCEBatchPathOnRoot(ceExpectIntraBatchSync(nRanks, count * sizeof(float)),
root, rank, testId);
}
};
// CE-MPI-SCT-01: 4 ranks, root = 0.
TEST_F(CeMPI_Scatter, FourRanksRoot0) { runScatter(kMinRanks4, kSmallCount, 0, "CeMPI_Scatter/FourRanksRoot0"); }
// CE-MPI-SCT-02: 4 ranks, non-zero root (root = 1).
TEST_F(CeMPI_Scatter, FourRanksRoot1) { runScatter(kMinRanks4, kSmallCount, 1, "CeMPI_Scatter/FourRanksRoot1"); }
// CE-MPI-SCT-03: 8 ranks (default production topology), root = 0.
TEST_F(CeMPI_Scatter, EightRanksRoot0) { runScatter(kMinRanks8, kSmallCount, 0, "CeMPI_Scatter/EightRanksRoot0"); }
// ===========================================================================
// CeMPI_Gather – ncclGather CE correctness + log verification
// ===========================================================================
class CeMPI_Gather : public CeMPITest
{
protected:
// All ranks send; root gathers into block-pattern recvbuf; non-root recv bufs registered.
void runGather(int minRanks, size_t count, int root, const char* testId)
{
if(!validateTestPrerequisites(minRanks))
GTEST_SKIP() << "Need >= " << minRanks << " MPI ranks";
ASSERT_EQ(ncclSuccess, createTestCommunicator());
int rank{}, nRanks{};
ncclCommUserRank(getActiveCommunicator(), &rank);
ncclCommCount(getActiveCommunicator(), &nRanks);
SymBuf sendSym, recvSym;
ASSERT_EQ(ncclSuccess, allocSymBuf(count * sizeof(float), sendSym));
ASSERT_EQ(ncclSuccess,
allocSymBuf(count * static_cast<size_t>(nRanks) * sizeof(float), recvSym));
fillRankScalar(sendSym.ptr, count, rank);
ASSERT_EQ(ncclSuccess,
ncclGather(sendSym.ptr, recvSym.ptr, count, ncclFloat32, root,
getActiveCommunicator(), getActiveStream()));
ASSERT_EQ(hipSuccess, hipStreamSynchronize(getActiveStream()));
if(rank == root)
{
ASSERT_TRUE(verifyBlockPattern(recvSym.ptr, count * static_cast<size_t>(nRanks), count))
<< "Rank " << rank << " (root): Gather data verification failed";
}
assertCEPathTakenOnRoot(root, rank, testId);
// Root receives nRanks chunks; chunkBytes = count * sizeof(float)
assertCEBatchPathOnRoot(ceExpectIntraBatchSync(nRanks, count * sizeof(float)),
root, rank, testId);
}
};
// CE-MPI-GTH-01: 4 ranks, root = 0.
TEST_F(CeMPI_Gather, FourRanksRoot0) { runGather(kMinRanks4, kSmallCount, 0, "CeMPI_Gather/FourRanksRoot0"); }
// CE-MPI-GTH-02: 4 ranks, non-zero root (root = 1).
TEST_F(CeMPI_Gather, FourRanksRoot1) { runGather(kMinRanks4, kSmallCount, 1, "CeMPI_Gather/FourRanksRoot1"); }
// CE-MPI-GTH-03: 8 ranks (default production topology), root = 0.
TEST_F(CeMPI_Gather, EightRanksRoot0) { runGather(kMinRanks8, kSmallCount, 0, "CeMPI_Gather/EightRanksRoot0"); }
// ===========================================================================
// CeMPI_Fallback – CE not taken for AllReduce; SM path when CE env is off
// ===========================================================================
class CeMPI_Fallback : public CeMPITest
{};
// CE-MPI-FALLBACK-01: AllReduce never uses CE (not a CE collective).
// Plain hipMalloc avoids the symmetric-SM kernel path (absent with GENERATE_SYM_KERNELS=OFF).
TEST_F(CeMPI_Fallback, AllReduceNeverUsesCE)
{
using namespace RCCLTestGuards;
if(!validateTestPrerequisites(kMinRanks2))
GTEST_SKIP() << "Need >= 2 MPI ranks";
ASSERT_EQ(ncclSuccess, createTestCommunicator());
int rank{}, nRanks{};
ncclCommUserRank(getActiveCommunicator(), &rank);
ncclCommCount(getActiveCommunicator(), &nRanks);
const size_t count = kSmallCount;
const size_t bytes = count * sizeof(float);
// Plain hipMalloc (not symmetric) is intentional: avoids the sym-SM kernel path.
// DeviceBufferAutoGuard ensures hipFree on all exit paths including early ASSERTs.
void* sendBuf = nullptr;
ASSERT_EQ(hipSuccess, hipMalloc(&sendBuf, bytes));
DeviceBufferAutoGuard sendGuard(sendBuf);
void* recvBuf = nullptr;
ASSERT_EQ(hipSuccess, hipMalloc(&recvBuf, bytes));
DeviceBufferAutoGuard recvGuard(recvBuf);
fillRankScalar(sendBuf, count, rank);
ASSERT_EQ(ncclSuccess,
ncclAllReduce(sendBuf, recvBuf, count, ncclFloat32, ncclSum,
getActiveCommunicator(), getActiveStream()));
ASSERT_EQ(hipSuccess, hipStreamSynchronize(getActiveStream()));
const float expected = static_cast<float>(nRanks * (nRanks + 1) / 2);
ASSERT_TRUE(verifyBufferData<float>(recvBuf, count,
[expected](size_t) { return expected; }))
<< "Rank " << rank << ": AllReduce data verification failed";
assertCEPathNotTaken("CeMPI_Fallback/AllReduceNeverUsesCE");
}
// CE-MPI-FALLBACK-02: AllGather with plain hipMalloc (no symmetric window) falls back to SM.
// NCCL_CTA_POLICY is process-cached; bypassing CE via unregistered buffers is the correct approach.
TEST_F(CeMPI_Fallback, AllGatherFallbackToSMWhenCEDisabled)
{
using namespace RCCLTestGuards;
if(!validateTestPrerequisites(kMinRanks2))
GTEST_SKIP() << "Need >= 2 MPI ranks";
ASSERT_EQ(ncclSuccess, createTestCommunicator());
int rank{}, nRanks{};
ncclCommUserRank(getActiveCommunicator(), &rank);
ncclCommCount(getActiveCommunicator(), &nRanks);
const size_t count = kSmallCount;
const size_t bytes = count * sizeof(float);
// Plain hipMalloc (not symmetric) is intentional: ncclDevrFindWindow returns NULL
// → CE dispatch condition false. Guards ensure cleanup on all exit paths.
void* sendBuf = nullptr;
ASSERT_EQ(hipSuccess, hipMalloc(&sendBuf, bytes));
DeviceBufferAutoGuard sendGuard(sendBuf);
void* recvBuf = nullptr;
ASSERT_EQ(hipSuccess, hipMalloc(&recvBuf, bytes * static_cast<size_t>(nRanks)));
DeviceBufferAutoGuard recvGuard(recvBuf);
fillRankScalar(sendBuf, count, rank);
ASSERT_EQ(ncclSuccess,
ncclAllGather(sendBuf, recvBuf, count, ncclFloat32,
getActiveCommunicator(), getActiveStream()));
ASSERT_EQ(hipSuccess, hipStreamSynchronize(getActiveStream()));
ASSERT_TRUE(verifyBlockPattern(recvBuf, count * static_cast<size_t>(nRanks), count))
<< "Rank " << rank << ": AllGather (SM path) data verification failed";
assertCEPathNotTaken("CeMPI_Fallback/AllGatherFallbackToSMWhenCEDisabled");
}
// CE-MPI-FALLBACK-03: Only send registered; recvWin == NULL → CE dispatch skipped, SM ring used.
TEST_F(CeMPI_Fallback, AllGatherRecvNotRegisteredFallsBackToSM)
{
using namespace RCCLTestGuards;
if(!validateTestPrerequisites(kMinRanks2))
GTEST_SKIP() << "Need >= 2 MPI ranks";
ASSERT_EQ(ncclSuccess, createTestCommunicator());
int rank{}, nRanks{};
ncclCommUserRank(getActiveCommunicator(), &rank);
ncclCommCount(getActiveCommunicator(), &nRanks);
const size_t count = kSmallCount;
const size_t bytes = count * sizeof(float);
// Send is a symmetric window; recv is plain hipMalloc (intentional) → CE condition fails.
SymBuf sendSym;
ASSERT_EQ(ncclSuccess, allocSymBuf(bytes, sendSym));
void* recvBuf = nullptr;
ASSERT_EQ(hipSuccess, hipMalloc(&recvBuf, bytes * static_cast<size_t>(nRanks)));
DeviceBufferAutoGuard recvGuard(recvBuf);
fillRankScalar(sendSym.ptr, count, rank);
ASSERT_EQ(ncclSuccess,
ncclAllGather(sendSym.ptr, recvBuf, count, ncclFloat32,
getActiveCommunicator(), getActiveStream()));
ASSERT_EQ(hipSuccess, hipStreamSynchronize(getActiveStream()));
ASSERT_TRUE(verifyBlockPattern(recvBuf, count * static_cast<size_t>(nRanks), count))
<< "Rank " << rank << ": AllGather (partial-reg SM fallback) data verification failed";
assertCEPathNotTaken("CeMPI_Fallback/AllGatherRecvNotRegisteredFallsBackToSM");
}
// ===========================================================================
// CeMPI_Stress – back-to-back repetitions stress test
// ===========================================================================
class CeMPI_Stress : public CeMPITest
{};
// CE-MPI-STRESS-01: 20 consecutive AllGather calls; validates ceSeqNum never desyncs.
TEST_F(CeMPI_Stress, AllGatherBackToBack20x)
{
if(!validateTestPrerequisites(kMinRanks2))
GTEST_SKIP() << "Need >= 2 MPI ranks";
ASSERT_EQ(ncclSuccess, createTestCommunicator());
int rank{}, nRanks{};
ncclCommUserRank(getActiveCommunicator(), &rank);
ncclCommCount(getActiveCommunicator(), &nRanks);
const size_t count = kSmallCount;
SymBuf sendSym, recvSym;
ASSERT_EQ(ncclSuccess, allocSymBuf(count * sizeof(float), sendSym));
ASSERT_EQ(ncclSuccess, allocSymBuf(count * nRanks * sizeof(float), recvSym));
void* sendBuf = sendSym.ptr;
void* recvBuf = recvSym.ptr;
fillRankScalar(sendBuf, count, rank);
for(int iter = 0; iter < kStressIters; ++iter)
{
ASSERT_EQ(ncclSuccess,
ncclAllGather(sendBuf, recvBuf, count, ncclFloat32,
getActiveCommunicator(), getActiveStream()))
<< "Iteration " << iter << " failed";
ASSERT_EQ(hipSuccess, hipStreamSynchronize(getActiveStream()))
<< "Stream sync failed at iteration " << iter;
ASSERT_TRUE(verifyBlockPattern(recvBuf, count * nRanks, count))
<< "Rank " << rank << ": AllGather data wrong at iteration " << iter;
}
assertCEPathTaken("CeMPI_Stress/AllGatherBackToBack20x");
}
// CE-MPI-STRESS-02: Interleave CE AllGather and SM AllReduce on same comm.
// Validates that CE DMA work and SM kernel work ordered on the same comm do
// not corrupt each other's state.
TEST_F(CeMPI_Stress, InterleavedCEAllGatherAndSMAllReduce)
{
if(!validateTestPrerequisites(kMinRanks2))
GTEST_SKIP() << "Need >= 2 MPI ranks";
ASSERT_EQ(ncclSuccess, createTestCommunicator());
int rank{}, nRanks{};
ncclCommUserRank(getActiveCommunicator(), &rank);
ncclCommCount(getActiveCommunicator(), &nRanks);
const size_t count = kSmallCount;
SymBuf agSendSym, agRecvSym;
ASSERT_EQ(ncclSuccess, allocSymBuf(count * sizeof(float), agSendSym));
ASSERT_EQ(ncclSuccess, allocSymBuf(count * nRanks * sizeof(float), agRecvSym));
void* agSend = agSendSym.ptr;
void* agRecv = agRecvSym.ptr;
// AllReduce uses plain hipMalloc (intentional): symmetric buffers would route to a
// sym-SM kernel absent with GENERATE_SYM_KERNELS=OFF → ncclUnhandledCudaError.
// DeviceBufferAutoGuard ensures cleanup on all exit paths.
void* arSend = nullptr;
ASSERT_EQ(hipSuccess, hipMalloc(&arSend, count * sizeof(float)));
RCCLTestGuards::DeviceBufferAutoGuard arSendGuard(arSend);
void* arRecv = nullptr;
ASSERT_EQ(hipSuccess, hipMalloc(&arRecv, count * sizeof(float)));
RCCLTestGuards::DeviceBufferAutoGuard arRecvGuard(arRecv);
fillRankScalar(agSend, count, rank);
fillRankScalar(arSend, count, rank);
const float arExpected = static_cast<float>(nRanks * (nRanks + 1) / 2);
for(int iter = 0; iter < kInterleavedIters; ++iter)
{
ASSERT_EQ(ncclSuccess,
ncclAllGather(agSend, agRecv, count, ncclFloat32,
getActiveCommunicator(), getActiveStream()))
<< "AllGather iter " << iter;
ASSERT_EQ(hipSuccess, hipStreamSynchronize(getActiveStream()));
ASSERT_TRUE(verifyBlockPattern(agRecv, count * nRanks, count))
<< "AllGather data wrong at iter " << iter;
ASSERT_EQ(ncclSuccess,
ncclAllReduce(arSend, arRecv, count, ncclFloat32, ncclSum,
getActiveCommunicator(), getActiveStream()))
<< "AllReduce iter " << iter;
ASSERT_EQ(hipSuccess, hipStreamSynchronize(getActiveStream()));
ASSERT_TRUE(verifyBufferData<float>(arRecv, count,
[arExpected](size_t) { return arExpected; }))
<< "AllReduce data wrong at iter " << iter;
}
assertCEPathTaken("CeMPI_Stress/InterleavedCEAllGatherAndSMAllReduce");
}
#endif // MPI_TESTS_ENABLED