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
1727 lines
66 KiB
C++
1727 lines
66 KiB
C++
/*************************************************************************
|
|
* Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved.
|
|
*
|
|
* See LICENSE.txt for license information
|
|
************************************************************************/
|
|
|
|
#include "NetIbMPITestBase.hpp"
|
|
#include <sstream>
|
|
#include <array>
|
|
|
|
#ifdef MPI_TESTS_ENABLED
|
|
|
|
// Initialization Tests
|
|
|
|
TEST_F(NetIbMPITest, InitializePlugin) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI, MPITestConstants::kNoProcessLimit,
|
|
kRequirePowerOfTwo, 1, kNoNodeLimit))
|
|
<< "Test requirements not met";
|
|
|
|
ncclResult_t result = InitNetIb();
|
|
ASSERT_EQ(result, ncclSuccess) << "Failed to initialize NET IB plugin";
|
|
EXPECT_NE(initCtx_, nullptr) << "Init context should be set on success";
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, GetDeviceCount) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI, MPITestConstants::kNoProcessLimit,
|
|
kRequirePowerOfTwo, 1, kNoNodeLimit))
|
|
<< "Test requirements not met";
|
|
|
|
ASSERT_EQ(InitNetIb(), ncclSuccess);
|
|
|
|
int ndev = 0;
|
|
EXPECT_EQ(GetDeviceCount(&ndev), ncclSuccess);
|
|
EXPECT_GT(ndev, 0) << "No IB devices found";
|
|
|
|
if (MPIEnvironment::world_rank == 0) {
|
|
TEST_INFO("Found %d IB device(s)", ndev);
|
|
}
|
|
}
|
|
|
|
// Device Properties Tests
|
|
|
|
TEST_F(NetIbMPITest, GetDeviceProperties) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI, MPITestConstants::kNoProcessLimit,
|
|
kRequirePowerOfTwo, 1, kNoNodeLimit))
|
|
<< "Test requirements not met";
|
|
|
|
int ndev = 0;
|
|
AssertInitAndGetDevices(&ndev);
|
|
|
|
for (int i = 0; i < ndev; i++) {
|
|
ncclNetProperties_t props;
|
|
memset(&props, 0, sizeof(props));
|
|
|
|
EXPECT_EQ(GetDeviceProperties(i, &props), ncclSuccess)
|
|
<< "Failed to get properties for device " << i;
|
|
|
|
EXPECT_NE(props.name, nullptr) << "Device " << i << " has NULL name";
|
|
EXPECT_GT(props.speed, 0) << "Device " << i << " has invalid speed";
|
|
EXPECT_NE(props.pciPath, nullptr) << "Device " << i << " has NULL pciPath";
|
|
|
|
if (MPIEnvironment::world_rank == 0) {
|
|
TEST_INFO("Device %d: name=%s speed=%d pciPath=%s",
|
|
i, props.name, props.speed, props.pciPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, GetDevicePropertiesInvalidDevice) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI, MPITestConstants::kNoProcessLimit,
|
|
kRequirePowerOfTwo, 1, kNoNodeLimit))
|
|
<< "Test requirements not met";
|
|
|
|
int ndev = 0;
|
|
AssertInitAndGetDevices(&ndev);
|
|
|
|
ncclNetProperties_t props;
|
|
|
|
// Invalid device ID (too large)
|
|
ncclResult_t result = GetDeviceProperties(ndev + kInvalidDeviceOffset, &props);
|
|
EXPECT_NE(result, ncclSuccess) << "Should fail for invalid device ID";
|
|
}
|
|
|
|
// Connection Setup Tests
|
|
|
|
TEST_F(NetIbMPITest, ListenAndConnect) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
int ndev = 0;
|
|
AssertInitAndGetDevices(&ndev);
|
|
|
|
const int rank = MPIEnvironment::world_rank;
|
|
ConnectionPair pair;
|
|
NetConnectionGuard connGuard(net_);
|
|
SetupConnectionWithGuard(0, pair, connGuard);
|
|
|
|
if (rank == 0) {
|
|
EXPECT_NE(pair.recvComm, nullptr) << "Recv comm should be established";
|
|
EXPECT_NE(pair.listenComm, nullptr) << "Listen comm should exist";
|
|
} else {
|
|
EXPECT_NE(pair.sendComm, nullptr) << "Send comm should be established";
|
|
}
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, ConnectWithInvalidHandle) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI, MPITestConstants::kNoProcessLimit,
|
|
kRequirePowerOfTwo, 1, kNoNodeLimit))
|
|
<< "Test requirements not met";
|
|
|
|
int ndev = 0;
|
|
AssertInitAndGetDevices(&ndev);
|
|
|
|
ncclNetHandle_t invalidHandle;
|
|
memset(&invalidHandle, 0xFF, sizeof(invalidHandle));
|
|
void* sendComm = nullptr;
|
|
|
|
// Negative test: Connect with garbage handle
|
|
ncclResult_t result = ConnectToRemote(0, &invalidHandle, &sendComm);
|
|
EXPECT_EQ(result, ncclInternalError) << "Should fail with invalid handle";
|
|
}
|
|
|
|
// Memory Registration Tests
|
|
|
|
TEST_F(NetIbMPITest, RegisterHostMemory) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
int ndev = 0;
|
|
AssertInitAndGetDevices(&ndev);
|
|
|
|
const int rank = MPIEnvironment::world_rank;
|
|
ConnectionPair pair;
|
|
NetConnectionGuard connGuard(net_);
|
|
SetupConnectionWithGuard(0, pair, connGuard);
|
|
|
|
const size_t bufferSize = kSmallBufferSize;
|
|
void* buffer = malloc(bufferSize);
|
|
ASSERT_NE(buffer, nullptr);
|
|
auto bufferGuard = makeHostBufferAutoGuard(buffer);
|
|
|
|
void* mhandle = nullptr;
|
|
void* comm = (rank == 0) ? pair.recvComm : pair.sendComm;
|
|
|
|
EXPECT_EQ(RegisterMemory(comm, buffer, bufferSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
|
|
EXPECT_NE(mhandle, nullptr);
|
|
|
|
// Use NetMHandleGuard for automatic deregistration before connection closes
|
|
NetMHandleGuard mhandleGuard(mhandle, NetMHandleDeleter(net_, comm));
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, RegisterGpuMemory) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
int ndev = 0;
|
|
AssertInitAndGetDevices(&ndev);
|
|
|
|
const int rank = MPIEnvironment::world_rank;
|
|
ConnectionPair pair;
|
|
NetConnectionGuard connGuard(net_);
|
|
SetupConnectionWithGuard(0, pair, connGuard);
|
|
|
|
const size_t bufferSize = kSmallBufferSize;
|
|
void* buffer = nullptr;
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&buffer, bufferSize));
|
|
auto bufferGuard = makeDeviceBufferAutoGuard(buffer);
|
|
|
|
void* mhandle = nullptr;
|
|
void* comm = (rank == 0) ? pair.recvComm : pair.sendComm;
|
|
|
|
EXPECT_EQ(RegisterMemory(comm, buffer, bufferSize, NCCL_PTR_CUDA, &mhandle), ncclSuccess);
|
|
EXPECT_NE(mhandle, nullptr);
|
|
|
|
// Use NetMHandleGuard for automatic deregistration before connection closes
|
|
NetMHandleGuard mhandleGuard(mhandle, NetMHandleDeleter(net_, comm));
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, RegisterMemoryNullPointer) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
int ndev = 0;
|
|
AssertInitAndGetDevices(&ndev);
|
|
|
|
const int rank = MPIEnvironment::world_rank;
|
|
ConnectionPair pair;
|
|
NetConnectionGuard connGuard(net_);
|
|
SetupConnectionWithGuard(0, pair, connGuard);
|
|
|
|
void* mhandle = nullptr;
|
|
void* comm = (rank == 0) ? pair.recvComm : pair.sendComm;
|
|
|
|
// Negative test: NULL buffer pointer
|
|
ncclResult_t result = RegisterMemory(comm, nullptr, 4096, NCCL_PTR_HOST, &mhandle);
|
|
EXPECT_NE(result, ncclSuccess) << "Should fail with NULL buffer";
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, DeregisterNullHandle) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
int ndev = 0;
|
|
AssertInitAndGetDevices(&ndev);
|
|
|
|
const int rank = MPIEnvironment::world_rank;
|
|
ConnectionPair pair;
|
|
NetConnectionGuard connGuard(net_);
|
|
SetupConnectionWithGuard(0, pair, connGuard);
|
|
|
|
void* comm = (rank == 0) ? pair.recvComm : pair.sendComm;
|
|
|
|
// Edge case: Deregister NULL handle (should be no-op)
|
|
EXPECT_EQ(DeregisterMemory(comm, nullptr), ncclSuccess);
|
|
}
|
|
|
|
// Send/Recv Tests
|
|
|
|
TEST_F(NetIbMPITest, SimpleSendRecv) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
int ndev = 0;
|
|
AssertInitAndGetDevices(&ndev);
|
|
|
|
const int rank = MPIEnvironment::world_rank;
|
|
ConnectionPair pair;
|
|
NetConnectionGuard connGuard(net_);
|
|
SetupConnectionWithGuard(0, pair, connGuard);
|
|
|
|
const size_t bufferSize = kSmallBufferSize;
|
|
const int tag = 42;
|
|
|
|
void* buffer = malloc(bufferSize);
|
|
ASSERT_NE(buffer, nullptr);
|
|
auto bufferGuard = makeHostBufferAutoGuard(buffer);
|
|
|
|
void* mhandle = nullptr;
|
|
void* comm = (rank == 0) ? pair.recvComm : pair.sendComm;
|
|
ASSERT_EQ(RegisterMemory(comm, buffer, bufferSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
|
|
|
|
// Use NetMHandleGuard for automatic cleanup on failure (exception safety)
|
|
NetMHandleGuard mhandleGuard(mhandle, NetMHandleDeleter(net_, comm));
|
|
|
|
void* request = nullptr;
|
|
|
|
if (rank == 0) {
|
|
// Receiver
|
|
PostSingleRecv(pair.recvComm, buffer, bufferSize, tag, mhandle, &request);
|
|
} else {
|
|
// Sender
|
|
fillHostBufferWithPattern<uint8_t>(buffer, bufferSize, makeBytePattern(rank));
|
|
PostSendWithRetry(pair.sendComm, buffer, bufferSize, tag, mhandle, &request);
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
// Wait for completion
|
|
int sizes[1] = {0};
|
|
ASSERT_NE(request, nullptr) << "Request must be non-NULL before waiting";
|
|
ASSERT_EQ(WaitForCompletion(request, sizes), ncclSuccess);
|
|
|
|
if (rank == 0) {
|
|
EXPECT_EQ(sizes[0], bufferSize) << "Received size mismatch";
|
|
|
|
// Verify received data
|
|
int senderRank = 1; // Data was sent by rank 1
|
|
EXPECT_TRUE(verifyHostBufferData<uint8_t>(buffer, bufferSize, makeBytePattern(senderRank))) << "Data validation failed";
|
|
}
|
|
|
|
// NetMHandleGuard will automatically deregister memory when test scope ends
|
|
// Destructor order ensures MR is deregistered before connection closes:
|
|
// 1. mhandleGuard destructor (deregisters MR)
|
|
// 2. bufferGuard destructor (frees buffer)
|
|
// 3. connGuard destructor (closes connection)
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, SendRecvMultipleSizes) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
int ndev = 0;
|
|
AssertInitAndGetDevices(&ndev);
|
|
|
|
const int rank = MPIEnvironment::world_rank;
|
|
ConnectionPair pair;
|
|
NetConnectionGuard connGuard(net_);
|
|
SetupConnectionWithGuard(0, pair, connGuard);
|
|
|
|
// Test various sizes
|
|
std::vector<size_t> testSizes = {1, 64, 256, 1024, 4096, 16384, 65536};
|
|
|
|
for (size_t size : testSizes) {
|
|
const int tag = 100;
|
|
const int seed = 2000 + static_cast<int>(size); // Unique seed per size
|
|
|
|
void* buffer = malloc(size);
|
|
ASSERT_NE(buffer, nullptr);
|
|
auto bufferGuard = makeHostBufferAutoGuard(buffer); // Local guard for loop iteration
|
|
|
|
void* mhandle = nullptr;
|
|
void* comm = (rank == 0) ? pair.recvComm : pair.sendComm;
|
|
ASSERT_EQ(RegisterMemory(comm, buffer, size, NCCL_PTR_HOST, &mhandle), ncclSuccess);
|
|
NetMHandleGuard mhandleGuard(mhandle, NetMHandleDeleter(net_, comm));
|
|
|
|
void* request = nullptr;
|
|
|
|
if (rank == 0) {
|
|
memset(buffer, 0, size);
|
|
PostSingleRecv(pair.recvComm, buffer, size, tag, mhandle, &request);
|
|
ASSERT_NE(request, nullptr) << "Recv request should never be NULL";
|
|
} else {
|
|
fillHostBufferWithPattern<uint8_t>(buffer, size, makeBytePattern(seed));
|
|
PostSendWithRetry(pair.sendComm, buffer, size, tag, mhandle, &request);
|
|
}
|
|
|
|
// Barrier 1: Ensure both ranks have posted their operations before waiting
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
// Wait for completion
|
|
int sizes[1] = {0};
|
|
ASSERT_EQ(WaitForCompletion(request, sizes), ncclSuccess);
|
|
|
|
// Barrier 2: CRITICAL - Ensure BOTH ranks have completed before EITHER continues
|
|
// This prevents rank A from starting next transfer while rank B is still
|
|
// completing current transfer, which would cause request object reuse race conditions
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
if (rank == 0) {
|
|
EXPECT_EQ(sizes[0], size) << "Size mismatch for transfer of " << size << " bytes";
|
|
EXPECT_TRUE(verifyHostBufferData<uint8_t>(buffer, size, makeBytePattern(seed))) << "Data validation failed for size " << size;
|
|
}
|
|
|
|
// NetMHandleGuard will automatically deregister at end of loop iteration
|
|
}
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, SendRecvZeroSize) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
int ndev = 0;
|
|
AssertInitAndGetDevices(&ndev);
|
|
|
|
const int rank = MPIEnvironment::world_rank;
|
|
ConnectionPair pair;
|
|
NetConnectionGuard connGuard(net_);
|
|
SetupConnectionWithGuard(0, pair, connGuard);
|
|
|
|
const size_t bufferSize = kSmallBufferSize;
|
|
const int tag = 50;
|
|
|
|
void* buffer = malloc(bufferSize);
|
|
ASSERT_NE(buffer, nullptr);
|
|
auto bufferGuard = makeHostBufferAutoGuard(buffer);
|
|
|
|
void* mhandle = nullptr;
|
|
void* comm = (rank == 0) ? pair.recvComm : pair.sendComm;
|
|
ASSERT_EQ(RegisterMemory(comm, buffer, bufferSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
|
|
NetMHandleGuard mhandleGuard(mhandle, NetMHandleDeleter(net_, comm));
|
|
|
|
void* request = nullptr;
|
|
|
|
if (rank == 0) {
|
|
// Receiver - expect zero bytes
|
|
PostSingleRecv(pair.recvComm, buffer, bufferSize, tag, mhandle, &request);
|
|
} else {
|
|
// Sender - send zero bytes
|
|
PostSendWithRetry(pair.sendComm, buffer, 0, tag, mhandle, &request);
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
// Wait for completion
|
|
int sizes[1] = {-1};
|
|
ASSERT_NE(request, nullptr) << "Request must be non-NULL before waiting";
|
|
ASSERT_EQ(WaitForCompletion(request, sizes), ncclSuccess);
|
|
|
|
if (rank == 0) {
|
|
EXPECT_EQ(sizes[0], 0) << "Should receive zero bytes";
|
|
}
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, FlushAfterRecv) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
ASSERT_EQ(InitNetIb(), ncclSuccess);
|
|
|
|
int ndev = 0;
|
|
ASSERT_EQ(GetDeviceCount(&ndev), ncclSuccess);
|
|
ASSERT_GT(ndev, 0);
|
|
|
|
// Check if GDR is available
|
|
ncclNetProperties_t props;
|
|
ASSERT_EQ(GetDeviceProperties(0, &props), ncclSuccess);
|
|
|
|
if (!(props.ptrSupport & NCCL_PTR_CUDA)) {
|
|
GTEST_SKIP() << "GDR not supported, skipping flush test";
|
|
}
|
|
|
|
ConnectionPair pair;
|
|
int rank = MPIEnvironment::world_rank;
|
|
int peerRank = (rank + 1) % 2;
|
|
|
|
ASSERT_EQ(SetupConnection(0, pair, rank, peerRank), ncclSuccess);
|
|
|
|
// Guard connections for automatic cleanup
|
|
NetConnectionGuard connGuard(net_);
|
|
if (rank == 0) {
|
|
connGuard.setRecvComm(pair.recvComm);
|
|
connGuard.setListenComm(pair.listenComm);
|
|
} else {
|
|
connGuard.setSendComm(pair.sendComm);
|
|
}
|
|
|
|
const size_t bufferSize = kSmallBufferSize;
|
|
const int tag = 200;
|
|
|
|
void* buffer = nullptr;
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&buffer, bufferSize));
|
|
auto bufferGuard = makeDeviceBufferAutoGuard(buffer);
|
|
|
|
void* mhandle = nullptr;
|
|
void* comm = (rank == 0) ? pair.recvComm : pair.sendComm;
|
|
ASSERT_EQ(RegisterMemory(comm, buffer, bufferSize, NCCL_PTR_CUDA, &mhandle), ncclSuccess);
|
|
NetMHandleGuard mhandleGuard(mhandle, NetMHandleDeleter(net_, comm));
|
|
|
|
void* request = nullptr;
|
|
|
|
if (rank == 0) {
|
|
// Receiver
|
|
void* recvBuffers[1] = {buffer};
|
|
size_t recvSizes[1] = {bufferSize};
|
|
int recvTags[1] = {tag};
|
|
void* recvHandles[1] = {mhandle};
|
|
|
|
ASSERT_EQ(PostRecv(pair.recvComm, 1, recvBuffers, recvSizes, recvTags,
|
|
recvHandles, &request), ncclSuccess);
|
|
} else {
|
|
// Sender
|
|
ASSERT_EQ(initializeBufferWithPattern<uint8_t>(buffer, bufferSize, makeBytePattern(rank)), hipSuccess);
|
|
|
|
PostSendWithRetry(pair.sendComm, buffer, bufferSize, tag, mhandle, &request);
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
// Wait for completion
|
|
int sizes[1] = {0};
|
|
ASSERT_NE(request, nullptr) << "Request must be non-NULL before waiting";
|
|
ASSERT_EQ(WaitForCompletion(request, sizes), ncclSuccess);
|
|
|
|
if (rank == 0) {
|
|
// Issue flush
|
|
void* flushBuffers[1] = {buffer};
|
|
int flushSizes[1] = {static_cast<int>(bufferSize)};
|
|
void* flushHandles[1] = {mhandle};
|
|
void* flushRequest = nullptr;
|
|
|
|
ncclResult_t result = FlushRecv(pair.recvComm, 1, flushBuffers, flushSizes,
|
|
flushHandles, &flushRequest);
|
|
|
|
if (result == ncclSuccess && flushRequest != nullptr) {
|
|
ASSERT_EQ(WaitForCompletion(flushRequest, nullptr), ncclSuccess);
|
|
}
|
|
}
|
|
|
|
// NetMHandleGuard will automatically deregister at scope end
|
|
}
|
|
|
|
// Stress and Edge Case Tests
|
|
|
|
// Tests multiple sequential transfers on the same connection using host memory.
|
|
// This test validates that request objects can be properly reused across multiple
|
|
// send/recv operations without resource exhaustion or state corruption.
|
|
//
|
|
// SYNCHRONIZATION STRATEGY:
|
|
// Two barriers per iteration ensure proper ordering:
|
|
// 1. After Post: Ensures both ranks have posted before either waits
|
|
// 2. After Completion: CRITICAL - Ensures BOTH ranks complete before EITHER
|
|
// starts next iteration. Without this, rapid request reuse causes races.
|
|
//
|
|
// NULL REQUEST HANDLING:
|
|
// NET IB isend() can return ncclSuccess with NULL request when the FIFO
|
|
// isn't ready yet (receiver's irecv RDMA write hasn't reached sender yet).
|
|
// This is NOT an error - it means "try again". The sender must retry until
|
|
// it gets a valid request pointer. This is normal NET IB protocol behavior.
|
|
//
|
|
// NOTE: Flush (iflush) is intentionally NOT called because:
|
|
// 1. Flush is only needed for GPU Direct RDMA to ensure data visibility
|
|
// 2. For NCCL_PTR_HOST transfers, flush is unnecessary
|
|
TEST_F(NetIbMPITest, MultipleSequentialTransfers) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
int ndev = 0;
|
|
AssertInitAndGetDevices(&ndev);
|
|
|
|
const int rank = MPIEnvironment::world_rank;
|
|
ConnectionPair pair;
|
|
NetConnectionGuard connGuard(net_);
|
|
SetupConnectionWithGuard(0, pair, connGuard);
|
|
|
|
const size_t bufferSize = kSmallBufferSize;
|
|
const int numTransfers = kNumSequentialTransfers;
|
|
|
|
void* sendBuffer = nullptr;
|
|
void* recvBuffer = nullptr;
|
|
HostBufferAutoGuard sendBufferGuard(nullptr);
|
|
HostBufferAutoGuard recvBufferGuard(nullptr);
|
|
|
|
if (rank == 0) {
|
|
recvBuffer = malloc(bufferSize);
|
|
ASSERT_NE(recvBuffer, nullptr);
|
|
recvBufferGuard = makeHostBufferAutoGuard(recvBuffer);
|
|
} else {
|
|
sendBuffer = malloc(bufferSize);
|
|
ASSERT_NE(sendBuffer, nullptr);
|
|
sendBufferGuard = makeHostBufferAutoGuard(sendBuffer);
|
|
}
|
|
|
|
void* mhandle = nullptr;
|
|
void* buffer = (rank == 0) ? recvBuffer : sendBuffer;
|
|
void* comm = (rank == 0) ? pair.recvComm : pair.sendComm;
|
|
ASSERT_EQ(RegisterMemory(comm, buffer, bufferSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
|
|
NetMHandleGuard mhandleGuard(mhandle, NetMHandleDeleter(net_, comm));
|
|
|
|
for (int i = 0; i < numTransfers; i++) {
|
|
const int tag = kTransferTagBase + i;
|
|
const int seed = kBaseSeedOffset + i; // Unique seed for each transfer
|
|
void* request = nullptr;
|
|
|
|
if (rank == 0) {
|
|
memset(recvBuffer, 0, bufferSize);
|
|
PostSingleRecv(pair.recvComm, recvBuffer, bufferSize, tag, mhandle, &request);
|
|
ASSERT_NE(request, nullptr) << "Recv request should never be NULL";
|
|
} else {
|
|
fillHostBufferWithPattern<uint8_t>(sendBuffer, bufferSize, makeBytePattern(seed));
|
|
PostSendWithRetry(pair.sendComm, sendBuffer, bufferSize, tag, mhandle, &request);
|
|
}
|
|
|
|
// Barrier 1: Ensure both ranks have posted their operations before waiting
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
// Wait for completion
|
|
int sizes[1] = {0};
|
|
ASSERT_EQ(WaitForCompletion(request, sizes), ncclSuccess);
|
|
|
|
// Barrier 2: CRITICAL - Ensure BOTH ranks have completed before EITHER continues
|
|
// This prevents rank A from starting transfer N+1 while rank B is still
|
|
// completing transfer N, which would cause request object reuse race conditions
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
if (rank == 0) {
|
|
EXPECT_EQ(sizes[0], bufferSize) << "Transfer " << i << " size mismatch";
|
|
|
|
EXPECT_TRUE(verifyHostBufferData<uint8_t>(recvBuffer, bufferSize, makeBytePattern(seed))) << "Transfer " << i << " data validation failed (seed=" << seed << ")";
|
|
|
|
// NOTE: Flush is NOT called for host memory transfers
|
|
// Flush (iflush) is only needed for GPU Direct RDMA to ensure data visibility on GPU.
|
|
// For NCCL_PTR_HOST transfers, flush is unnecessary and calling it can cause
|
|
// race conditions when request objects are rapidly reused.
|
|
// The NET IB implementation will no-op the flush call for host memory anyway.
|
|
}
|
|
}
|
|
|
|
// NetMHandleGuard will automatically deregister at scope end
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, LargeTransfer) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
int ndev = 0;
|
|
AssertInitAndGetDevices(&ndev);
|
|
|
|
const int rank = MPIEnvironment::world_rank;
|
|
ConnectionPair pair;
|
|
NetConnectionGuard connGuard(net_);
|
|
SetupConnectionWithGuard(0, pair, connGuard);
|
|
|
|
const size_t bufferSize = kLargeBufferSize; // 16 MB
|
|
const int tag = 400;
|
|
|
|
void* buffer = malloc(bufferSize);
|
|
ASSERT_NE(buffer, nullptr);
|
|
auto bufferGuard = makeHostBufferAutoGuard(buffer);
|
|
|
|
void* mhandle = nullptr;
|
|
void* comm = (rank == 0) ? pair.recvComm : pair.sendComm;
|
|
ASSERT_EQ(RegisterMemory(comm, buffer, bufferSize, NCCL_PTR_HOST, &mhandle), ncclSuccess);
|
|
NetMHandleGuard mhandleGuard(mhandle, NetMHandleDeleter(net_, comm));
|
|
|
|
void* request = nullptr;
|
|
|
|
if (rank == 0) {
|
|
// Receiver
|
|
PostSingleRecv(pair.recvComm, buffer, bufferSize, tag, mhandle, &request);
|
|
} else {
|
|
// Sender
|
|
fillHostBufferWithPattern<uint8_t>(buffer, bufferSize, makeBytePattern(rank));
|
|
PostSendWithRetry(pair.sendComm, buffer, bufferSize, tag, mhandle, &request);
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
// Wait for completion with longer timeout for large transfer
|
|
int sizes[1] = {0};
|
|
ASSERT_NE(request, nullptr) << "Request must be non-NULL before waiting";
|
|
ASSERT_EQ(WaitForCompletion(request, sizes, kLargeTransferTimeout), ncclSuccess);
|
|
|
|
if (rank == 0) {
|
|
EXPECT_EQ(sizes[0], bufferSize) << "Large transfer size mismatch";
|
|
|
|
// Verify received data
|
|
int senderRank = 1; // Data was sent by rank 1
|
|
EXPECT_TRUE(verifyHostBufferData<uint8_t>(buffer, bufferSize, makeBytePattern(senderRank))) << "Large transfer data validation failed";
|
|
}
|
|
|
|
// NetMHandleGuard will automatically deregister at scope end
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, CloseWithoutWaitingForCompletion) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
int ndev = 0;
|
|
AssertInitAndGetDevices(&ndev);
|
|
|
|
ConnectionPair pair;
|
|
NetConnectionGuard connGuard(net_);
|
|
SetupConnectionWithGuard(0, pair, connGuard);
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, ListenCloseListen) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
int rank = MPIEnvironment::world_rank;
|
|
int peerRank = (rank + 1) % 2;
|
|
|
|
ASSERT_EQ(InitNetIb(), ncclSuccess);
|
|
|
|
int mergedDev = CreateMergedDevice(4, rank);
|
|
if (mergedDev == -1) {
|
|
GTEST_SKIP() << "Failed to create merged device";
|
|
}
|
|
|
|
for (int iter = 0; iter < 3; iter++) {
|
|
// Iteration 3: abandoned listen-close
|
|
if (iter == 2) {
|
|
if (rank == 0) {
|
|
void* abandonedListen = nullptr;
|
|
ncclNetHandle_t abandonedHandle;
|
|
ASSERT_EQ(CreateListenComm(mergedDev, &abandonedHandle, &abandonedListen),
|
|
ncclSuccess)
|
|
<< "Iter 3: abandoned listen failed";
|
|
ASSERT_NE(abandonedListen, nullptr);
|
|
ASSERT_EQ(CloseListenComm(abandonedListen), ncclSuccess)
|
|
<< "Iter 3: close abandoned listen failed";
|
|
}
|
|
}
|
|
|
|
ConnectionPair pair;
|
|
ncclNetHandle_t handle;
|
|
|
|
if (rank == 0) {
|
|
ASSERT_EQ(CreateListenComm(mergedDev, &handle, &pair.listenComm), ncclSuccess)
|
|
<< "Listen failed iter " << iter;
|
|
ASSERT_NE(pair.listenComm, nullptr);
|
|
}
|
|
|
|
if (rank == 0) {
|
|
MPI_Send(&handle, sizeof(handle), MPI_BYTE, peerRank, 0, MPI_COMM_WORLD);
|
|
} else {
|
|
MPI_Recv(&handle, sizeof(handle), MPI_BYTE, peerRank, 0,
|
|
MPI_COMM_WORLD, MPI_STATUS_IGNORE);
|
|
}
|
|
|
|
int connectFailed = 0;
|
|
if (rank == 0) {
|
|
ncclResult_t r = ncclSuccess;
|
|
while (!pair.recvComm && r == ncclSuccess)
|
|
r = AcceptConnection(pair.listenComm, &pair.recvComm);
|
|
if (r != ncclSuccess || !pair.recvComm) connectFailed = 1;
|
|
} else {
|
|
ncclResult_t r = ncclSuccess;
|
|
while (!pair.sendComm && r == ncclSuccess)
|
|
r = ConnectToRemote(mergedDev, &handle, &pair.sendComm);
|
|
if (r != ncclSuccess || !pair.sendComm) connectFailed = 1;
|
|
}
|
|
MPI_Allreduce(MPI_IN_PLACE, &connectFailed, 1, MPI_INT, MPI_MAX, MPI_COMM_WORLD);
|
|
if (connectFailed) {
|
|
if (rank == 0 && pair.listenComm) CloseListenComm(pair.listenComm);
|
|
FAIL() << "IB QP connect/accept failed (cross-subnet node pair)";
|
|
}
|
|
|
|
// Close: data comms first, then listen
|
|
if (rank == 0) {
|
|
ASSERT_EQ(CloseRecvComm(pair.recvComm), ncclSuccess)
|
|
<< "CloseRecvComm failed iter " << iter;
|
|
} else {
|
|
ASSERT_EQ(CloseSendComm(pair.sendComm), ncclSuccess)
|
|
<< "CloseSendComm failed iter " << iter;
|
|
}
|
|
|
|
if (rank == 0) {
|
|
ASSERT_EQ(CloseListenComm(pair.listenComm), ncclSuccess)
|
|
<< "CloseListenComm failed iter " << iter;
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, MultipleSimultaneousListens) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
int rank = MPIEnvironment::world_rank;
|
|
int peerRank = (rank + 1) % 2;
|
|
|
|
ASSERT_EQ(InitNetIb(), ncclSuccess);
|
|
int ndev = 0;
|
|
ASSERT_EQ(GetDeviceCount(&ndev), ncclSuccess);
|
|
ASSERT_GT(ndev, 0);
|
|
|
|
std::vector<ncclNetProperties_t> props(ndev);
|
|
std::vector<int> physDevs;
|
|
for (int i = 0; i < ndev; i++) {
|
|
memset(&props[i], 0, sizeof(ncclNetProperties_t));
|
|
ASSERT_EQ(GetDeviceProperties(i, &props[i]), ncclSuccess);
|
|
if (!props[i].name || !strchr(props[i].name, '+'))
|
|
physDevs.push_back(i);
|
|
}
|
|
|
|
if (physDevs.empty()) {
|
|
GTEST_SKIP() << "Failed to find network devices";
|
|
}
|
|
|
|
int targetSpeed = props[physDevs[0]].speed;
|
|
std::vector<int> compat;
|
|
for (int d : physDevs)
|
|
if (props[d].speed == targetSpeed) compat.push_back(d);
|
|
|
|
int mergedDevA = CreateMergedDevice(2, rank);
|
|
int mergedDevB = CreateMergedDevice(3, rank, 2);
|
|
if (mergedDevA == -1 || mergedDevB == -1) {
|
|
GTEST_SKIP() << "Failed to create merged device";
|
|
}
|
|
|
|
// Physical device = first NIC from merged A's group
|
|
int physDev = compat[0];
|
|
|
|
static constexpr size_t kTransferSize = 4096;
|
|
static constexpr int kNumListens = 4;
|
|
|
|
// 4 listens: 2 on merged A, 1 on merged B, 1 on physical NIC (member of merged A)
|
|
struct ListenInfo {
|
|
int dev;
|
|
const char* label;
|
|
};
|
|
ListenInfo listens[kNumListens] = {
|
|
{mergedDevA, "MergedA-1"},
|
|
{mergedDevA, "MergedA-2"},
|
|
{mergedDevB, "MergedB"},
|
|
{physDev, "PhysDev"},
|
|
};
|
|
|
|
void* listenComms[kNumListens] = {};
|
|
void* recvComms[kNumListens] = {};
|
|
void* sendComms[kNumListens] = {};
|
|
ncclNetHandle_t handles[kNumListens] = {};
|
|
|
|
// === Phase 1: Rank 0 creates ALL 4 listens simultaneously ===
|
|
if (rank == 0) {
|
|
for (int i = 0; i < kNumListens; i++) {
|
|
ASSERT_EQ(CreateListenComm(listens[i].dev, &handles[i], &listenComms[i]),
|
|
ncclSuccess)
|
|
<< "CreateListenComm failed for " << listens[i].label;
|
|
ASSERT_NE(listenComms[i], nullptr)
|
|
<< "listenComm NULL for " << listens[i].label;
|
|
}
|
|
|
|
// Verify all handles are pairwise unique
|
|
for (int i = 0; i < kNumListens; i++) {
|
|
for (int j = i + 1; j < kNumListens; j++) {
|
|
bool eq = (memcmp(&handles[i], &handles[j], sizeof(ncclNetHandle_t)) == 0);
|
|
EXPECT_FALSE(eq)
|
|
<< "Handles identical for " << listens[i].label
|
|
<< " and " << listens[j].label;
|
|
}
|
|
}
|
|
|
|
// Send all handles to rank 1
|
|
for (int i = 0; i < kNumListens; i++) {
|
|
MPI_Send(&handles[i], sizeof(ncclNetHandle_t), MPI_BYTE,
|
|
peerRank, 60 + i, MPI_COMM_WORLD);
|
|
}
|
|
} else {
|
|
for (int i = 0; i < kNumListens; i++) {
|
|
MPI_Recv(&handles[i], sizeof(ncclNetHandle_t), MPI_BYTE,
|
|
peerRank, 60 + i, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
|
|
}
|
|
}
|
|
|
|
// === Phase 2: ALL connect + accept ===
|
|
int phase2Failed = 0;
|
|
if (rank == 0) {
|
|
for (int i = 0; i < kNumListens && !phase2Failed; i++) {
|
|
ncclResult_t r = ncclSuccess;
|
|
while (!recvComms[i] && r == ncclSuccess)
|
|
r = AcceptConnection(listenComms[i], &recvComms[i]);
|
|
if (r != ncclSuccess || !recvComms[i])
|
|
phase2Failed = 1;
|
|
}
|
|
} else {
|
|
for (int i = 0; i < kNumListens && !phase2Failed; i++) {
|
|
ncclResult_t r = ncclSuccess;
|
|
while (!sendComms[i] && r == ncclSuccess)
|
|
r = ConnectToRemote(listens[i].dev, &handles[i], &sendComms[i]);
|
|
if (r != ncclSuccess || !sendComms[i])
|
|
phase2Failed = 1;
|
|
}
|
|
}
|
|
MPI_Allreduce(MPI_IN_PLACE, &phase2Failed, 1, MPI_INT, MPI_MAX, MPI_COMM_WORLD);
|
|
if (phase2Failed) {
|
|
GTEST_SKIP() << "IB QP connect/accept failed (cross-subnet node pair)";
|
|
}
|
|
|
|
// === Phase 3: Transfer on ALL 4 connections ===
|
|
for (int c = 0; c < kNumListens; c++) {
|
|
void* comm = (rank == 0) ? recvComms[c] : sendComms[c];
|
|
int seed = 2000 + c * 1000;
|
|
int tag = 200 + c;
|
|
|
|
void* buf = malloc(kTransferSize);
|
|
ASSERT_NE(buf, nullptr);
|
|
auto bufGuard = RCCLTestGuards::makeHostBufferAutoGuard(buf);
|
|
void* mhandle = nullptr;
|
|
ASSERT_EQ(RegisterMemory(comm, buf, kTransferSize, NCCL_PTR_HOST, &mhandle),
|
|
ncclSuccess);
|
|
|
|
if (rank == 1) {
|
|
fillHostBufferWithPattern<uint8_t>(buf, kTransferSize, makeBytePattern(seed));
|
|
} else {
|
|
memset(buf, 0xDE, kTransferSize);
|
|
}
|
|
|
|
void* req = nullptr;
|
|
if (rank == 0) {
|
|
void* rb[1] = {buf}; size_t rs[1] = {kTransferSize};
|
|
int rt[1] = {tag}; void* rh[1] = {mhandle};
|
|
ASSERT_EQ(PostRecv(recvComms[c], 1, rb, rs, rt, rh, &req), ncclSuccess);
|
|
ASSERT_NE(req, nullptr);
|
|
}
|
|
|
|
if (rank == 1) {
|
|
void* sendReq = nullptr;
|
|
PostSendWithRetry(sendComms[c], buf, kTransferSize, tag, mhandle, &sendReq);
|
|
}
|
|
|
|
if (rank == 0) {
|
|
int csz[1] = {0};
|
|
ASSERT_EQ(WaitForCompletion(req, csz, kLargeTransferTimeoutMs), ncclSuccess)
|
|
<< listens[c].label << " recv timed out";
|
|
EXPECT_EQ(csz[0], (int)kTransferSize)
|
|
<< listens[c].label << " size mismatch";
|
|
|
|
size_t errIdx = 0;
|
|
uint8_t errExp = 0, errGot = 0;
|
|
bool ok = verifyHostBufferData<uint8_t>(buf, kTransferSize,
|
|
makeBytePattern(seed), 0, 0.0, &errIdx, &errExp, &errGot);
|
|
EXPECT_TRUE(ok) << listens[c].label << " data mismatch at byte " << errIdx
|
|
<< ": expected " << (int)errExp << " got " << (int)errGot;
|
|
}
|
|
|
|
ASSERT_EQ(DeregisterMemory(comm, mhandle), ncclSuccess);
|
|
}
|
|
|
|
// === Phase 4: Close ALL ===
|
|
if (rank == 0) {
|
|
for (int i = 0; i < kNumListens; i++)
|
|
ASSERT_EQ(CloseRecvComm(recvComms[i]), ncclSuccess)
|
|
<< "CloseRecvComm failed for " << listens[i].label;
|
|
} else {
|
|
for (int i = 0; i < kNumListens; i++)
|
|
ASSERT_EQ(CloseSendComm(sendComms[i]), ncclSuccess)
|
|
<< "CloseSendComm failed for " << listens[i].label;
|
|
}
|
|
|
|
if (rank == 0) {
|
|
for (int i = 0; i < kNumListens; i++)
|
|
ASSERT_EQ(CloseListenComm(listenComms[i]), ncclSuccess)
|
|
<< "CloseListenComm failed for " << listens[i].label;
|
|
}
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, MultipleSequentialConnections) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
ASSERT_EQ(InitNetIb(), ncclSuccess);
|
|
|
|
int ndev = 0;
|
|
ASSERT_EQ(GetDeviceCount(&ndev), ncclSuccess);
|
|
ASSERT_GT(ndev, 0);
|
|
|
|
int rank = MPIEnvironment::world_rank;
|
|
int peerRank = (rank + 1) % 2;
|
|
|
|
// Open and close multiple connections sequentially to test resource cleanup
|
|
const int kNumIterations = 5;
|
|
for (int iter = 0; iter < kNumIterations; iter++) {
|
|
ConnectionPair pair;
|
|
ASSERT_EQ(SetupConnection(0, pair, rank, peerRank), ncclSuccess)
|
|
<< "Failed to setup connection on iteration " << iter;
|
|
|
|
NetConnectionGuard connGuard(net_);
|
|
if (rank == 0) {
|
|
connGuard.setRecvComm(pair.recvComm);
|
|
connGuard.setListenComm(pair.listenComm);
|
|
EXPECT_NE(pair.recvComm, nullptr);
|
|
} else {
|
|
connGuard.setSendComm(pair.sendComm);
|
|
EXPECT_NE(pair.sendComm, nullptr);
|
|
}
|
|
// connGuard destructor closes connection
|
|
}
|
|
|
|
if (MPIEnvironment::world_rank == 0) {
|
|
TEST_INFO("Successfully completed %d sequential connection cycles", kNumIterations);
|
|
}
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, RapidConnectDisconnect) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " processes";
|
|
|
|
int rank = MPIEnvironment::world_rank;
|
|
int peerRank = (rank + 1) % 2;
|
|
|
|
ASSERT_EQ(InitNetIb(), ncclSuccess);
|
|
|
|
int ndev = 0;
|
|
ASSERT_EQ(GetDeviceCount(&ndev), ncclSuccess);
|
|
ASSERT_GT(ndev, 0);
|
|
|
|
std::vector<ncclNetProperties_t> props(ndev);
|
|
std::vector<int> physDevs;
|
|
for (int i = 0; i < ndev; i++) {
|
|
memset(&props[i], 0, sizeof(ncclNetProperties_t));
|
|
ASSERT_EQ(GetDeviceProperties(i, &props[i]), ncclSuccess);
|
|
if (!props[i].name || !strchr(props[i].name, '+'))
|
|
physDevs.push_back(i);
|
|
}
|
|
ASSERT_FALSE(physDevs.empty()) << "No physical devices found";
|
|
|
|
int mergedDev = CreateMergedDevice(3, rank);
|
|
if (mergedDev == -1) {
|
|
GTEST_SKIP() << "Failed to create merged device";
|
|
}
|
|
|
|
|
|
static constexpr int kIterations = 100;
|
|
|
|
struct RdmaResourceCounts {
|
|
int qp = -1;
|
|
int cq = -1;
|
|
int mr = -1;
|
|
int pd = -1;
|
|
};
|
|
|
|
auto execCommand = [](const char* cmd) -> std::string {
|
|
std::array<char, 256> buf{};
|
|
std::string out;
|
|
|
|
FILE* pipe = popen(cmd, "r");
|
|
if (!pipe) return out;
|
|
|
|
while (fgets(buf.data(), buf.size(), pipe) != nullptr)
|
|
out += buf.data();
|
|
|
|
pclose(pipe);
|
|
return out;
|
|
};
|
|
|
|
auto countNonEmptyLines = [](const std::string& text) -> int {
|
|
std::istringstream iss(text);
|
|
std::string line;
|
|
int count = 0;
|
|
while (std::getline(iss, line)) {
|
|
if (!line.empty()) count++;
|
|
}
|
|
return count;
|
|
};
|
|
|
|
auto readRdmaResourceCounts = [&]() -> RdmaResourceCounts {
|
|
RdmaResourceCounts counts;
|
|
|
|
std::string probe =
|
|
execCommand("sh -c 'rdma resource show qp >/dev/null 2>&1 && "
|
|
"rdma resource show cq >/dev/null 2>&1 && "
|
|
"rdma resource show mr >/dev/null 2>&1 && "
|
|
"rdma resource show pd >/dev/null 2>&1 && echo OK'");
|
|
if (probe.find("OK") == std::string::npos) return counts;
|
|
|
|
std::string qpOut = execCommand("rdma resource show qp 2>/dev/null");
|
|
std::string cqOut = execCommand("rdma resource show cq 2>/dev/null");
|
|
std::string mrOut = execCommand("rdma resource show mr 2>/dev/null");
|
|
std::string pdOut = execCommand("rdma resource show pd 2>/dev/null");
|
|
|
|
if (qpOut.empty() || cqOut.empty() || mrOut.empty() || pdOut.empty()) return counts;
|
|
|
|
counts.qp = countNonEmptyLines(qpOut);
|
|
counts.cq = countNonEmptyLines(cqOut);
|
|
counts.mr = countNonEmptyLines(mrOut);
|
|
counts.pd = countNonEmptyLines(pdOut);
|
|
|
|
return counts;
|
|
};
|
|
|
|
auto countsAvailable = [](const RdmaResourceCounts& c) -> bool {
|
|
return c.qp >= 0 && c.cq >= 0 && c.mr >= 0 && c.pd >= 0;
|
|
};
|
|
|
|
RdmaResourceCounts before = readRdmaResourceCounts();
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
for (int iter = 0; iter < kIterations; iter++) {
|
|
void* listenComm = nullptr;
|
|
void* recvComm = nullptr;
|
|
void* sendComm = nullptr;
|
|
ncclNetHandle_t handle;
|
|
memset(&handle, 0, sizeof(handle));
|
|
|
|
if (rank == 0) {
|
|
ASSERT_EQ(CreateListenComm(mergedDev, &handle, &listenComm), ncclSuccess)
|
|
<< "CreateListenComm failed at iteration " << iter;
|
|
ASSERT_NE(listenComm, nullptr)
|
|
<< "listenComm is NULL at iteration " << iter;
|
|
|
|
MPI_Send(&handle, sizeof(ncclNetHandle_t), MPI_BYTE,
|
|
peerRank, 7000 + iter, MPI_COMM_WORLD);
|
|
} else {
|
|
MPI_Recv(&handle, sizeof(ncclNetHandle_t), MPI_BYTE,
|
|
peerRank, 7000 + iter, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
if (rank == 0) {
|
|
int attempts = 0;
|
|
while (!recvComm && attempts < kMaxRetryAttempts) {
|
|
ASSERT_EQ(AcceptConnection(listenComm, &recvComm), ncclSuccess)
|
|
<< "AcceptConnection failed at iteration " << iter;
|
|
if (!recvComm) usleep(kPollIntervalUs);
|
|
attempts++;
|
|
}
|
|
ASSERT_NE(recvComm, nullptr)
|
|
<< "AcceptConnection did not complete at iteration " << iter;
|
|
} else {
|
|
int attempts = 0;
|
|
while (!sendComm && attempts < kMaxRetryAttempts) {
|
|
ASSERT_EQ(ConnectToRemote(mergedDev, &handle, &sendComm), ncclSuccess)
|
|
<< "ConnectToRemote failed at iteration " << iter;
|
|
if (!sendComm) usleep(kPollIntervalUs);
|
|
attempts++;
|
|
}
|
|
ASSERT_NE(sendComm, nullptr)
|
|
<< "ConnectToRemote did not complete at iteration " << iter;
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
if (rank == 0) {
|
|
ASSERT_EQ(CloseRecvComm(recvComm), ncclSuccess)
|
|
<< "CloseRecvComm failed at iteration " << iter;
|
|
ASSERT_EQ(CloseListenComm(listenComm), ncclSuccess)
|
|
<< "CloseListenComm failed at iteration " << iter;
|
|
} else {
|
|
ASSERT_EQ(CloseSendComm(sendComm), ncclSuccess)
|
|
<< "CloseSendComm failed at iteration " << iter;
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
RdmaResourceCounts after = readRdmaResourceCounts();
|
|
|
|
if (countsAvailable(before) && countsAvailable(after)) {
|
|
EXPECT_EQ(after.qp, before.qp)
|
|
<< "QP leak detected on rank " << rank
|
|
<< ": before=" << before.qp << " after=" << after.qp;
|
|
EXPECT_EQ(after.cq, before.cq)
|
|
<< "CQ leak detected on rank " << rank
|
|
<< ": before=" << before.cq << " after=" << after.cq;
|
|
EXPECT_EQ(after.mr, before.mr)
|
|
<< "MR leak detected on rank " << rank
|
|
<< ": before=" << before.mr << " after=" << after.mr;
|
|
EXPECT_EQ(after.pd, before.pd)
|
|
<< "PD leak detected on rank " << rank
|
|
<< ": before=" << before.pd << " after=" << after.pd;
|
|
} else {
|
|
GTEST_SKIP() << "RDMA resource counters unavailable; rapid connect/disconnect "
|
|
<< "behavior was tested, but QP/CQ/MR/PD leak check was skipped";
|
|
}
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, MultiRecv) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly 2 processes";
|
|
|
|
int rank = MPIEnvironment::world_rank;
|
|
int peerRank = (rank + 1) % 2;
|
|
|
|
ASSERT_EQ(InitNetIb(), ncclSuccess);
|
|
|
|
static constexpr int kWidth = 8;
|
|
static constexpr int kNumBatches = 255;
|
|
static constexpr int kBaseTag = 8000;
|
|
static constexpr size_t kSizes[kWidth] = {
|
|
1, 64, 256, 1024, 4096, 16384, 65536, 131072
|
|
};
|
|
|
|
auto FillPattern = [&](void* buf, size_t size, int batch, int slot) {
|
|
fillHostBufferWithPattern<uint8_t>(buf, size, [batch, slot](size_t j) {
|
|
return static_cast<uint8_t>((batch * 17 + slot * 31 + j) % 256);
|
|
});
|
|
};
|
|
|
|
auto CheckPattern = [&](void* buf, size_t size, int batch, int slot) -> bool {
|
|
return verifyHostBufferData<uint8_t>(buf, size, [batch, slot](size_t j) {
|
|
return static_cast<uint8_t>((batch * 17 + slot * 31 + j) % 256);
|
|
});
|
|
};
|
|
|
|
ConnectionPair pair;
|
|
ASSERT_EQ(SetupConnection(0, pair, rank, peerRank), ncclSuccess);
|
|
void* recvComm = pair.recvComm;
|
|
void* sendComm = pair.sendComm;
|
|
|
|
NetConnectionGuard connGuard(net_);
|
|
if (rank == 0) {
|
|
connGuard.setRecvComm(pair.recvComm);
|
|
connGuard.setListenComm(pair.listenComm);
|
|
} else {
|
|
connGuard.setSendComm(pair.sendComm);
|
|
}
|
|
|
|
for (int batch = 0; batch < kNumBatches; batch++) {
|
|
size_t sizes[kWidth];
|
|
int tags[kWidth];
|
|
for (int i = 0; i < kWidth; i++) {
|
|
sizes[i] = kSizes[i];
|
|
tags[i] = kBaseTag + batch * kWidth + i;
|
|
}
|
|
|
|
if (rank == 0) {
|
|
void* bufs[kWidth] = {};
|
|
void* mhs[kWidth] = {};
|
|
int recvSizes[kWidth] = {};
|
|
|
|
auto cleanup = makeScopeGuard([&]() {
|
|
for (int i = 0; i < kWidth; i++) {
|
|
if (mhs[i]) { DeregisterMemory(recvComm, mhs[i]); mhs[i] = nullptr; }
|
|
if (bufs[i]) { free(bufs[i]); bufs[i] = nullptr; }
|
|
}
|
|
});
|
|
|
|
for (int i = 0; i < kWidth; i++) {
|
|
bufs[i] = malloc(sizes[i]);
|
|
ASSERT_NE(bufs[i], nullptr)
|
|
<< "malloc failed for recv batch=" << batch << " slot=" << i;
|
|
memset(bufs[i], 0xCC, sizes[i]);
|
|
|
|
ASSERT_EQ(RegisterMemory(recvComm, bufs[i], sizes[i], NCCL_PTR_HOST, &mhs[i]),
|
|
ncclSuccess)
|
|
<< "RegisterMemory failed for recv batch=" << batch << " slot=" << i;
|
|
ASSERT_NE(mhs[i], nullptr);
|
|
}
|
|
|
|
void* req = nullptr;
|
|
ASSERT_EQ(PostRecv(recvComm, kWidth, bufs, sizes, tags, mhs, &req), ncclSuccess)
|
|
<< "PostRecv failed for batch=" << batch;
|
|
ASSERT_NE(req, nullptr) << "PostRecv returned null request for batch=" << batch;
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
ASSERT_EQ(WaitForCompletion(req, recvSizes, kLargeTransferTimeoutMs), ncclSuccess)
|
|
<< "WaitForCompletion failed for recv batch=" << batch;
|
|
|
|
for (int i = 0; i < kWidth; i++) {
|
|
EXPECT_EQ(recvSizes[i], static_cast<int>(sizes[i]))
|
|
<< "recv size mismatch at batch=" << batch << " slot=" << i;
|
|
|
|
EXPECT_TRUE(CheckPattern(bufs[i], sizes[i], batch, i))
|
|
<< "data mismatch at batch=" << batch
|
|
<< " slot=" << i
|
|
<< " size=" << sizes[i];
|
|
}
|
|
|
|
// cleanup guard fires here, deregistering MRs and freeing buffers
|
|
} else {
|
|
void* bufs[kWidth] = {};
|
|
void* mhs[kWidth] = {};
|
|
void* reqs[kWidth] = {};
|
|
|
|
auto cleanup = makeScopeGuard([&]() {
|
|
for (int i = 0; i < kWidth; i++) {
|
|
if (mhs[i]) { DeregisterMemory(sendComm, mhs[i]); mhs[i] = nullptr; }
|
|
if (bufs[i]) { free(bufs[i]); bufs[i] = nullptr; }
|
|
}
|
|
});
|
|
|
|
for (int i = 0; i < kWidth; i++) {
|
|
bufs[i] = malloc(sizes[i]);
|
|
ASSERT_NE(bufs[i], nullptr)
|
|
<< "malloc failed for send batch=" << batch << " slot=" << i;
|
|
|
|
FillPattern(bufs[i], sizes[i], batch, i);
|
|
|
|
ASSERT_EQ(RegisterMemory(sendComm, bufs[i], sizes[i], NCCL_PTR_HOST, &mhs[i]),
|
|
ncclSuccess)
|
|
<< "RegisterMemory failed for send batch=" << batch << " slot=" << i;
|
|
ASSERT_NE(mhs[i], nullptr);
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
for (int i = 0; i < kWidth; i++) {
|
|
reqs[i] = nullptr;
|
|
PostSendWithRetry(sendComm, bufs[i], sizes[i], tags[i], mhs[i], &reqs[i]);
|
|
ASSERT_NE(reqs[i], nullptr)
|
|
<< "PostSend returned null request for batch=" << batch
|
|
<< " slot=" << i;
|
|
}
|
|
|
|
for (int i = 0; i < kWidth; i++) {
|
|
int sentSize[1] = {0}; // send request yields one size entry
|
|
ASSERT_EQ(WaitForCompletion(reqs[i], sentSize, kLargeTransferTimeoutMs), ncclSuccess)
|
|
<< "send completion failed for batch=" << batch << " slot=" << i;
|
|
}
|
|
|
|
// cleanup guard fires here, deregistering MRs and freeing buffers
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
// connGuard closes comms on scope exit
|
|
}
|
|
|
|
TEST_F(NetIbMPITest, MultiRecvShuffled) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly 2 processes";
|
|
|
|
int rank = MPIEnvironment::world_rank;
|
|
int peerRank = (rank + 1) % 2;
|
|
|
|
ASSERT_EQ(InitNetIb(), ncclSuccess);
|
|
|
|
static constexpr int kWidth = 8;
|
|
static constexpr int kNumBatches = 255;
|
|
static constexpr int kBaseTag = 12000;
|
|
// stride 100 between batches keeps tags visually distinct in debug logs
|
|
static constexpr int kTagStride = 100;
|
|
static constexpr size_t kBaseSizes[kWidth] = {
|
|
1, 64, 256, 1024, 4096, 16384, 65536, 131072
|
|
};
|
|
|
|
// kRecvOrder[slot] = msgId posted at that recv slot (recv posts in slot order)
|
|
static constexpr int kRecvOrder[kWidth] = {3, 0, 6, 1, 7, 2, 5, 4};
|
|
// kSendOrder[issueIndex] = msgId to send at that issue position
|
|
static constexpr int kSendOrder[kWidth] = {5, 2, 7, 0, 6, 3, 1, 4};
|
|
|
|
auto FillPattern = [&](void* buf, size_t size, int batch, int msgId) {
|
|
fillHostBufferWithPattern<uint8_t>(buf, size, [batch, msgId](size_t j) {
|
|
return static_cast<uint8_t>((batch * 19 + msgId * 37 + j) % 256);
|
|
});
|
|
};
|
|
|
|
auto CheckPattern = [&](void* buf, size_t size, int batch, int msgId) -> bool {
|
|
return verifyHostBufferData<uint8_t>(buf, size, [batch, msgId](size_t j) {
|
|
return static_cast<uint8_t>((batch * 19 + msgId * 37 + j) % 256);
|
|
});
|
|
};
|
|
|
|
ConnectionPair pair;
|
|
ASSERT_EQ(SetupConnection(0, pair, rank, peerRank), ncclSuccess);
|
|
void* recvComm = pair.recvComm;
|
|
void* sendComm = pair.sendComm;
|
|
|
|
NetConnectionGuard connGuard(net_);
|
|
if (rank == 0) {
|
|
connGuard.setRecvComm(pair.recvComm);
|
|
connGuard.setListenComm(pair.listenComm);
|
|
} else {
|
|
connGuard.setSendComm(pair.sendComm);
|
|
}
|
|
|
|
for (int batch = 0; batch < kNumBatches; batch++) {
|
|
int msgTags[kWidth];
|
|
size_t msgSizes[kWidth];
|
|
for (int msgId = 0; msgId < kWidth; msgId++) {
|
|
msgTags[msgId] = kBaseTag + batch * kTagStride + msgId;
|
|
msgSizes[msgId] = kBaseSizes[msgId];
|
|
}
|
|
|
|
if (rank == 0) {
|
|
void* bufs[kWidth] = {};
|
|
void* mhs[kWidth] = {};
|
|
size_t recvSizesArg[kWidth];
|
|
int recvTags[kWidth];
|
|
int recvSizesOut[kWidth] = {};
|
|
|
|
auto cleanup = makeScopeGuard([&]() {
|
|
for (int i = 0; i < kWidth; i++) {
|
|
if (mhs[i]) { DeregisterMemory(recvComm, mhs[i]); mhs[i] = nullptr; }
|
|
if (bufs[i]) { free(bufs[i]); bufs[i] = nullptr; }
|
|
}
|
|
});
|
|
|
|
for (int slot = 0; slot < kWidth; slot++) {
|
|
int msgId = kRecvOrder[slot];
|
|
recvTags[slot] = msgTags[msgId];
|
|
recvSizesArg[slot] = msgSizes[msgId];
|
|
|
|
bufs[slot] = malloc(recvSizesArg[slot]);
|
|
ASSERT_NE(bufs[slot], nullptr)
|
|
<< "malloc failed for recv batch=" << batch << " slot=" << slot;
|
|
memset(bufs[slot], 0xCC, recvSizesArg[slot]);
|
|
|
|
ASSERT_EQ(RegisterMemory(recvComm, bufs[slot], recvSizesArg[slot],
|
|
NCCL_PTR_HOST, &mhs[slot]),
|
|
ncclSuccess)
|
|
<< "RegisterMemory failed for recv batch=" << batch << " slot=" << slot;
|
|
ASSERT_NE(mhs[slot], nullptr);
|
|
}
|
|
|
|
void* req = nullptr;
|
|
ASSERT_EQ(PostRecv(recvComm, kWidth, bufs, recvSizesArg, recvTags, mhs, &req),
|
|
ncclSuccess)
|
|
<< "PostRecv failed for batch=" << batch;
|
|
ASSERT_NE(req, nullptr) << "PostRecv returned null request for batch=" << batch;
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
ASSERT_EQ(WaitForCompletion(req, recvSizesOut, kLargeTransferTimeoutMs), ncclSuccess)
|
|
<< "WaitForCompletion failed for recv batch=" << batch;
|
|
|
|
for (int slot = 0; slot < kWidth; slot++) {
|
|
int msgId = kRecvOrder[slot];
|
|
EXPECT_EQ(recvSizesOut[slot], static_cast<int>(msgSizes[msgId]))
|
|
<< "recv size mismatch at batch=" << batch
|
|
<< " slot=" << slot
|
|
<< " msgId=" << msgId;
|
|
|
|
EXPECT_TRUE(CheckPattern(bufs[slot], msgSizes[msgId], batch, msgId))
|
|
<< "data mismatch at batch=" << batch
|
|
<< " slot=" << slot
|
|
<< " expected msgId=" << msgId
|
|
<< " tag=" << msgTags[msgId]
|
|
<< " size=" << msgSizes[msgId];
|
|
}
|
|
|
|
// cleanup guard fires here, deregistering MRs and freeing buffers
|
|
} else {
|
|
void* bufs[kWidth] = {};
|
|
void* mhs[kWidth] = {};
|
|
void* reqs[kWidth] = {};
|
|
|
|
auto cleanup = makeScopeGuard([&]() {
|
|
for (int i = 0; i < kWidth; i++) {
|
|
if (mhs[i]) { DeregisterMemory(sendComm, mhs[i]); mhs[i] = nullptr; }
|
|
if (bufs[i]) { free(bufs[i]); bufs[i] = nullptr; }
|
|
}
|
|
});
|
|
|
|
for (int msgId = 0; msgId < kWidth; msgId++) {
|
|
bufs[msgId] = malloc(msgSizes[msgId]);
|
|
ASSERT_NE(bufs[msgId], nullptr)
|
|
<< "malloc failed for send batch=" << batch << " msgId=" << msgId;
|
|
|
|
FillPattern(bufs[msgId], msgSizes[msgId], batch, msgId);
|
|
|
|
ASSERT_EQ(RegisterMemory(sendComm, bufs[msgId], msgSizes[msgId],
|
|
NCCL_PTR_HOST, &mhs[msgId]),
|
|
ncclSuccess)
|
|
<< "RegisterMemory failed for send batch=" << batch
|
|
<< " msgId=" << msgId;
|
|
ASSERT_NE(mhs[msgId], nullptr);
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
for (int oi = 0; oi < kWidth; oi++) {
|
|
int msgId = kSendOrder[oi];
|
|
reqs[msgId] = nullptr;
|
|
PostSendWithRetry(sendComm, bufs[msgId], msgSizes[msgId],
|
|
msgTags[msgId], mhs[msgId], &reqs[msgId]);
|
|
ASSERT_NE(reqs[msgId], nullptr)
|
|
<< "PostSend returned null request for batch=" << batch
|
|
<< " msgId=" << msgId;
|
|
}
|
|
|
|
for (int msgId = 0; msgId < kWidth; msgId++) {
|
|
int sentSize[1] = {0}; // send request yields one size entry
|
|
ASSERT_EQ(WaitForCompletion(reqs[msgId], sentSize, kLargeTransferTimeoutMs),
|
|
ncclSuccess)
|
|
<< "send completion failed for batch=" << batch
|
|
<< " msgId=" << msgId;
|
|
}
|
|
|
|
// cleanup guard fires here, deregistering MRs and freeing buffers
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
// connGuard closes comms on scope exit
|
|
}
|
|
|
|
// CTS cache overflow stress test. N P2P connections x D outstanding recvs.
|
|
// Env: CTS_NUM_CONNS (32), CTS_QP_DEPTH (256), CTS_SNAP_EVERY (1).
|
|
// With RCCL_IB_P2P_DISABLE_CTS=0 the AINIC CTS table overflows ~256 entries
|
|
// and the drain times out; with =1 it passes. Requires exactly 2 ranks.
|
|
TEST_F(NetIbMPITest, CtsDepthStress) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kExactTwoProcesses, kExactTwoProcesses,
|
|
false, kMinGpusPerNode, kNoNodeLimit))
|
|
<< "Test requires exactly " << kExactTwoProcesses << " MPI processes";
|
|
|
|
net_ = &netIbCast;
|
|
ASSERT_EQ(InitNetIb(), ncclSuccess);
|
|
|
|
int ndev = 0;
|
|
ASSERT_EQ(GetDeviceCount(&ndev), ncclSuccess);
|
|
ASSERT_GT(ndev, 0);
|
|
|
|
const int rank = MPIEnvironment::world_rank;
|
|
const int kMaxRetries = 10000000;
|
|
|
|
auto envInt = [](const char* name, int dflt) {
|
|
const char* e = getenv(name);
|
|
return e ? atoi(e) : dflt;
|
|
};
|
|
const int numConns = envInt("CTS_NUM_CONNS", 32);
|
|
const int qpDepth = envInt("CTS_QP_DEPTH", 256);
|
|
const int snapEvery = envInt("CTS_SNAP_EVERY", 1);
|
|
ASSERT_GT(numConns, 0) << "CTS_NUM_CONNS must be > 0";
|
|
ASSERT_GT(qpDepth, 0) << "CTS_QP_DEPTH must be > 0";
|
|
ASSERT_GT(snapEvery, 0) << "CTS_SNAP_EVERY must be > 0";
|
|
|
|
const int totalEntries = numConns * qpDepth;
|
|
const size_t bufSize = 4096;
|
|
const int baseTag = 100;
|
|
|
|
std::cout << "[Rank " << rank << "] CtsDepthStress:"
|
|
<< " numConns=" << numConns
|
|
<< " qpDepth=" << qpDepth
|
|
<< " totalCtsEntries=" << totalEntries
|
|
<< " snapEvery=" << snapEvery
|
|
<< " ndev=" << ndev
|
|
<< "\n" << std::flush;
|
|
|
|
// Self-cleaning so ASSERT_* mid-fill doesn't leak comms / MRs / bufs.
|
|
struct Conn {
|
|
ncclNet_t* net = nullptr;
|
|
void* sendComm = nullptr;
|
|
void* recvComm = nullptr;
|
|
void* listenComm = nullptr;
|
|
void* sendMhandle = nullptr;
|
|
std::vector<void*> recvMhandles;
|
|
std::vector<void*> recvBufs;
|
|
std::vector<void*> recvRequests;
|
|
|
|
Conn() = default;
|
|
Conn(const Conn&) = delete;
|
|
Conn& operator=(const Conn&) = delete;
|
|
|
|
~Conn() {
|
|
if (!net) return;
|
|
for (size_t d = 0; d < recvMhandles.size(); d++) {
|
|
if (recvMhandles[d] && recvComm) net->deregMr(recvComm, recvMhandles[d]);
|
|
}
|
|
for (auto* b : recvBufs) if (b) free(b);
|
|
if (sendMhandle && sendComm) net->deregMr(sendComm, sendMhandle);
|
|
if (recvComm) net->closeRecv(recvComm);
|
|
if (sendComm) net->closeSend(sendComm);
|
|
if (listenComm) net->closeListen(listenComm);
|
|
}
|
|
};
|
|
|
|
using namespace NetIbCts;
|
|
CounterMap snapInit = takeSnapshot();
|
|
std::cout << "[Rank " << rank << "] snapshot: Init\n" << std::flush;
|
|
|
|
std::vector<std::unique_ptr<Conn>> conns(numConns);
|
|
for (int i = 0; i < numConns; i++) {
|
|
conns[i] = std::make_unique<Conn>();
|
|
conns[i]->net = net_;
|
|
}
|
|
|
|
if (rank == 0) {
|
|
std::vector<ncclNetHandle_t> handles(numConns);
|
|
std::cout << "[Rank 0] listen() x " << numConns << "...\n" << std::flush;
|
|
for (int i = 0; i < numConns; i++) {
|
|
ASSERT_EQ(net_->listen(initCtx_, i % ndev, &handles[i],
|
|
&conns[i]->listenComm), ncclSuccess)
|
|
<< "listen failed conn=" << i;
|
|
ASSERT_EQ(rcclCastNetP2pPolicy(&handles[i], 1), ncclSuccess);
|
|
MPI_Send(&handles[i], sizeof(ncclNetHandle_t), MPI_BYTE,
|
|
1, i, MPI_COMM_WORLD);
|
|
}
|
|
|
|
std::cout << "[Rank 0] accept() x " << numConns << "...\n" << std::flush;
|
|
for (int i = 0; i < numConns; i++) {
|
|
int retries = 0;
|
|
while (!conns[i]->recvComm) {
|
|
ASSERT_EQ(net_->accept(conns[i]->listenComm,
|
|
&conns[i]->recvComm, nullptr),
|
|
ncclSuccess) << "accept failed conn=" << i;
|
|
++retries;
|
|
if (retries % 1000 == 0) usleep(100);
|
|
ASSERT_LT(retries, kMaxRetries) << "accept timeout conn=" << i;
|
|
}
|
|
conns[i]->recvBufs.resize(qpDepth);
|
|
conns[i]->recvMhandles.resize(qpDepth, nullptr);
|
|
conns[i]->recvRequests.resize(qpDepth, nullptr);
|
|
for (int d = 0; d < qpDepth; d++) {
|
|
conns[i]->recvBufs[d] = malloc(bufSize);
|
|
ASSERT_NE(conns[i]->recvBufs[d], nullptr);
|
|
ASSERT_EQ(RegisterMemory(conns[i]->recvComm,
|
|
conns[i]->recvBufs[d], bufSize,
|
|
NCCL_PTR_HOST,
|
|
&conns[i]->recvMhandles[d]), ncclSuccess)
|
|
<< "regMr failed conn=" << i << " depth=" << d;
|
|
}
|
|
}
|
|
std::cout << "[Rank 0] all " << numConns << " connections up\n" << std::flush;
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
// snapBeforePostRecv: fixed baseline for the final delta.
|
|
// snapStep: advances each sample so prints are per-window.
|
|
CounterMap snapBeforePostRecv = takeSnapshot();
|
|
CounterMap snapStep = snapBeforePostRecv;
|
|
std::cout << "[Rank 0] snapshot: BeforePostRecv\n"
|
|
<< "[Rank 0] filling " << numConns << " x " << qpDepth
|
|
<< " = " << totalEntries << " CTS entries...\n" << std::flush;
|
|
|
|
bool overflowDetected = false;
|
|
int overflowAtConn = -1;
|
|
|
|
for (int i = 0; i < numConns; i++) {
|
|
for (int d = 0; d < qpDepth; d++) {
|
|
void* rb[1] = {conns[i]->recvBufs[d]};
|
|
size_t rs[1] = {bufSize};
|
|
int rt[1] = {baseTag + i * qpDepth + d};
|
|
void* rh[1] = {conns[i]->recvMhandles[d]};
|
|
ASSERT_EQ(PostRecv(conns[i]->recvComm, 1, rb, rs, rt, rh,
|
|
&conns[i]->recvRequests[d]), ncclSuccess)
|
|
<< "PostRecv failed conn=" << i << " depth=" << d;
|
|
ASSERT_NE(conns[i]->recvRequests[d], nullptr);
|
|
}
|
|
|
|
if ((i + 1) % snapEvery == 0) {
|
|
CounterMap snapNow = takeSnapshot();
|
|
printSummary(rank, "filling step", i + 1, qpDepth, snapStep, snapNow);
|
|
auto s = calcSummary(snapStep, snapNow);
|
|
if ((s.retx_pkts > 0 || s.ack_timeout > 0) && !overflowDetected) {
|
|
overflowDetected = true;
|
|
overflowAtConn = i + 1;
|
|
printDelta(rank, "BeforePostRecv", "OverflowPoint",
|
|
snapBeforePostRecv, snapNow);
|
|
}
|
|
snapStep = std::move(snapNow);
|
|
}
|
|
}
|
|
|
|
CounterMap snapAfterRecv = takeSnapshot();
|
|
std::cout << "\n[Rank 0] snapshot: AfterPostRecv\n" << std::flush;
|
|
printDelta(rank, "BeforePostRecv", "AfterPostRecv",
|
|
snapBeforePostRecv, snapAfterRecv);
|
|
printSummary(rank, "FINAL after all PostRecv", numConns, qpDepth,
|
|
snapBeforePostRecv, snapAfterRecv);
|
|
|
|
if (!overflowDetected)
|
|
std::cout << "[Rank 0] No overflow at " << totalEntries
|
|
<< " entries. Try CTS_NUM_CONNS=" << numConns * 2 << "\n"
|
|
<< std::flush;
|
|
else
|
|
std::cout << "[Rank 0] Overflow confirmed at conn=" << overflowAtConn
|
|
<< " (~" << overflowAtConn * qpDepth << " entries)\n"
|
|
<< std::flush;
|
|
|
|
// On overflow, WaitForCompletion times out and we report the partial
|
|
// completion count -- that's the observable CTS-overflow signature.
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
std::cout << "[Rank 0] draining " << numConns << " connections"
|
|
<< (overflowDetected ? " (OVERFLOW - expect drain timeout)" : "")
|
|
<< "...\n" << std::flush;
|
|
|
|
int totalCompleted = 0;
|
|
for (int i = 0; i < numConns; i++) {
|
|
for (int d = 0; d < qpDepth; d++) {
|
|
int sizes[1] = {0};
|
|
if (WaitForCompletion(conns[i]->recvRequests[d], sizes,
|
|
kLargeTransferTimeoutMs) != ncclSuccess) {
|
|
std::cout << "[Rank 0] TIMEOUT conn=" << i
|
|
<< " depth=" << d
|
|
<< " total_completed=" << totalCompleted << "\n"
|
|
<< std::flush;
|
|
goto drain_done;
|
|
}
|
|
totalCompleted++;
|
|
}
|
|
if ((i + 1) % 8 == 0)
|
|
std::cout << "[Rank 0] drained " << (i+1) << "/" << numConns
|
|
<< " (" << totalCompleted << " completions)\n"
|
|
<< std::flush;
|
|
}
|
|
drain_done:
|
|
std::cout << "[Rank 0] " << totalCompleted << "/" << totalEntries
|
|
<< " completions done\n" << std::flush;
|
|
|
|
CounterMap snapAfterBurst = takeSnapshot();
|
|
std::cout << "[Rank 0] snapshot: AfterBurst\n" << std::flush;
|
|
printDelta(rank, "AfterPostRecv", "AfterBurst", snapAfterRecv, snapAfterBurst);
|
|
printSummary(rank, "AfterBurst delta", numConns, qpDepth,
|
|
snapAfterRecv, snapAfterBurst);
|
|
printDelta(rank, "Init", "Full run", snapInit, snapAfterBurst);
|
|
printSummary(rank, "Full run", numConns, qpDepth, snapInit, snapAfterBurst);
|
|
|
|
EXPECT_EQ(totalCompleted, totalEntries);
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
} else {
|
|
HostBufferAutoGuard sendBufGuard(malloc(bufSize));
|
|
void* const sendBuf = sendBufGuard.get();
|
|
ASSERT_NE(sendBuf, nullptr);
|
|
memset(sendBuf, 0xab, bufSize);
|
|
|
|
std::cout << "[Rank 1] connect() x " << numConns << "...\n" << std::flush;
|
|
for (int i = 0; i < numConns; i++) {
|
|
ncclNetHandle_t handle;
|
|
MPI_Recv(&handle, sizeof(ncclNetHandle_t), MPI_BYTE,
|
|
0, i, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
|
|
int retries = 0;
|
|
while (!conns[i]->sendComm) {
|
|
ASSERT_EQ(net_->connect(initCtx_, i % ndev, &handle,
|
|
&conns[i]->sendComm, nullptr),
|
|
ncclSuccess) << "connect failed conn=" << i;
|
|
++retries;
|
|
if (retries % 1000 == 0) usleep(100);
|
|
ASSERT_LT(retries, kMaxRetries) << "connect timeout conn=" << i;
|
|
}
|
|
ASSERT_EQ(RegisterMemory(conns[i]->sendComm, sendBuf, bufSize,
|
|
NCCL_PTR_HOST, &conns[i]->sendMhandle), ncclSuccess)
|
|
<< "regMr failed conn=" << i;
|
|
}
|
|
std::cout << "[Rank 1] all " << numConns << " connections up\n" << std::flush;
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
|
|
int totalCompleted = 0;
|
|
for (int i = 0; i < numConns; i++) {
|
|
std::vector<void*> sendRequests(qpDepth, nullptr);
|
|
for (int d = 0; d < qpDepth; d++) {
|
|
int sendTag = baseTag + i * qpDepth + d;
|
|
PostSendWithRetry(conns[i]->sendComm, sendBuf, bufSize,
|
|
sendTag, conns[i]->sendMhandle,
|
|
&sendRequests[d]);
|
|
ASSERT_NE(sendRequests[d], nullptr)
|
|
<< "null send request conn=" << i << " depth=" << d;
|
|
}
|
|
for (int d = 0; d < qpDepth; d++) {
|
|
int sizes[1] = {0};
|
|
if (WaitForCompletion(sendRequests[d], sizes,
|
|
kLargeTransferTimeoutMs) != ncclSuccess) {
|
|
std::cout << "[Rank 1] TIMEOUT conn=" << i
|
|
<< " depth=" << d
|
|
<< " total_completed=" << totalCompleted << "\n"
|
|
<< std::flush;
|
|
goto sender_drain_done;
|
|
}
|
|
totalCompleted++;
|
|
}
|
|
}
|
|
sender_drain_done:
|
|
std::cout << "[Rank 1] " << totalCompleted << "/" << totalEntries
|
|
<< " sends completed\n" << std::flush;
|
|
EXPECT_EQ(totalCompleted, totalEntries);
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
}
|
|
|
|
MPI_Barrier(MPI_COMM_WORLD);
|
|
}
|
|
|
|
#endif // MPI_TESTS_ENABLED
|