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

1054 lines
37 KiB
C++

/*************************************************************************
* Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved.
*
* See LICENSE.txt for license information
************************************************************************/
#include "DeviceBufferHelpers.hpp"
#include "MPITestBase.hpp"
#include "ResourceGuards.hpp"
#include "TestChecks.hpp"
#include "nccl.h"
#include <sched.h>
#include <vector>
#ifdef MPI_TESTS_ENABLED
using namespace MPITestConstants;
using namespace RCCLTestGuards;
using namespace RCCLTestHelpers;
class RevokeMPITest : public MPITestBase {};
static void computeSymmetricExclude(int worldRank, int worldSize,
std::vector<int>& excludeList, bool& isExcluded)
{
MPI_Comm nodeComm;
MPI_Comm_split_type(MPI_COMM_WORLD, MPI_COMM_TYPE_SHARED, worldRank, MPI_INFO_NULL, &nodeComm);
int localSize, localRank;
MPI_Comm_size(nodeComm, &localSize);
MPI_Comm_rank(nodeComm, &localRank);
MPI_Comm_free(&nodeComm);
int numNodes = worldSize / localSize;
isExcluded = (localRank == localSize - 1);
excludeList.clear();
for (int n = 0; n < numNodes; n++)
excludeList.push_back((n + 1) * localSize - 1);
}
/**
* After revoke, collective operations must return ncclInvalidUsage on all ranks.
*/
TEST_F(RevokeMPITest, Revoke_RejectsCollectives)
{
ASSERT_TRUE(validateTestPrerequisites(2,
kNoProcessLimit,
kNoPowerOfTwoRequired,
1,
kNoNodeLimit))
<< "Test requires at least 2 MPI processes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
ASSERT_MPI_EQ(ncclSuccess, ncclCommRevoke(comm, NCCL_REVOKE_DEFAULT));
MPI_Barrier(MPI_COMM_WORLD);
void* buf = nullptr;
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&buf, sizeof(float)));
auto buf_guard = makeScopeGuard([&]() { if(buf) (void)hipFree(buf); });
ncclResult_t result = ncclAllReduce(buf, buf, 1, ncclFloat, ncclSum, comm, stream);
ASSERT_MPI_EQ(ncclInvalidUsage, result);
}
/**
* Revoke parent communicator, split into child comm, run AllReduce on the
* child and assert success. Validates that revoke leaves the parent valid as
* a source for ncclCommSplit.
*/
TEST_F(RevokeMPITest, Revoke_ThenSplit_ChildWorks)
{
ASSERT_TRUE(validateTestPrerequisites(2,
kNoProcessLimit,
kNoPowerOfTwoRequired,
1,
kNoNodeLimit))
<< "Test requires at least 2 MPI processes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t parent = getActiveCommunicator();
hipStream_t stream = getActiveStream();
int rank = MPIEnvironment::world_rank;
ASSERT_MPI_EQ(ncclSuccess, ncclCommRevoke(parent, NCCL_REVOKE_DEFAULT));
MPI_Barrier(MPI_COMM_WORLD);
ncclComm_t child = nullptr;
ASSERT_MPI_EQ(ncclSuccess, ncclGroupStart());
ASSERT_MPI_EQ(ncclSuccess, ncclCommSplit(parent, 0, rank, &child, nullptr));
ASSERT_MPI_EQ(ncclSuccess, ncclGroupEnd());
ASSERT_NE(child, nullptr);
auto child_guard = makeCommAutoGuard(child);
void* send_buf = nullptr;
void* recv_buf = nullptr;
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&send_buf, sizeof(float)));
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&recv_buf, sizeof(float)));
auto send_guard = makeScopeGuard([&]() { if(send_buf) (void)hipFree(send_buf); });
auto recv_guard = makeScopeGuard([&]() { if(recv_buf) (void)hipFree(recv_buf); });
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(send_buf, 1));
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(recv_buf, 1));
ASSERT_MPI_EQ(ncclSuccess,
ncclAllReduce(send_buf, recv_buf, 1, ncclFloat, ncclSum, child, stream));
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
}
/**
* Revoke parent communicator, then shrink (excluding the last rank), and run
* AllReduce on the resulting child. Validates that revoke leaves the parent
* valid as a source for ncclCommShrink and that the child operates with its
* own resources independent of the revoked parent.
*/
TEST_F(RevokeMPITest, RevokeThenShrink_ChildWorks)
{
ASSERT_TRUE(validateTestPrerequisites(2,
kNoProcessLimit,
kNoPowerOfTwoRequired,
1,
kNoNodeLimit))
<< "Test requires at least 2 MPI processes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t parent = getActiveCommunicator();
hipStream_t stream = getActiveStream();
int rank = MPIEnvironment::world_rank;
ASSERT_MPI_EQ(ncclSuccess, ncclCommRevoke(parent, NCCL_REVOKE_DEFAULT));
MPI_Barrier(MPI_COMM_WORLD);
std::vector<int> excludeList;
bool isExcluded;
computeSymmetricExclude(rank, MPIEnvironment::world_size, excludeList, isExcluded);
ncclComm_t child = NCCL_COMM_NULL;
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess,
ncclCommShrink(parent, excludeList.data(), excludeList.size(),
&child, nullptr, NCCL_SHRINK_DEFAULT));
ASSERT_NE(child, nullptr);
void* send_buf = nullptr;
void* recv_buf = nullptr;
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&send_buf, sizeof(float)));
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&recv_buf, sizeof(float)));
auto send_guard = makeScopeGuard([&]() { if(send_buf) (void)hipFree(send_buf); });
auto recv_guard = makeScopeGuard([&]() { if(recv_buf) (void)hipFree(recv_buf); });
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(send_buf, 1));
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(recv_buf, 1));
ASSERT_EQ(ncclSuccess,
ncclAllReduce(send_buf, recv_buf, 1, ncclFloat, ncclSum, child, stream));
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
}
// Barrier before child destruction ensures all participating ranks
// complete their operations before any rank starts destroying
MPI_Barrier(MPI_COMM_WORLD);
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess, ncclCommDestroy(child));
}
MPI_Barrier(MPI_COMM_WORLD);
}
/**
* Explicit lifecycle: revoke the parent on every rank, then call
* ncclCommDestroy and assert ncclSuccess on every rank. Validates the
* documented "revoke leaves comm safe for destroy" contract (other tests
* rely on TearDown to call destroy implicitly).
*/
TEST_F(RevokeMPITest, Revoke_ThenDestroy_CleanLifecycle)
{
ASSERT_TRUE(validateTestPrerequisites(2,
kNoProcessLimit,
kNoPowerOfTwoRequired,
1,
kNoNodeLimit))
<< "Test requires at least 2 MPI processes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t parent = getActiveCommunicator();
ASSERT_MPI_EQ(ncclSuccess, ncclCommRevoke(parent, NCCL_REVOKE_DEFAULT));
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_EQ(ncclSuccess, ncclCommDestroy(parent));
test_comm_ = nullptr;
MPI_Barrier(MPI_COMM_WORLD);
}
/**
* In-flight collective recovery cycle: enqueue a large AllReduce on the
* parent, revoke without waiting for completion, drain the stream, then
* shrink and run another AllReduce on the child. Mirrors the Meta
* fault-tolerance recovery flow: collective -> revoke -> shrink -> collective.
*/
TEST_F(RevokeMPITest, Collective_Revoke_Shrink_Collective)
{
ASSERT_TRUE(validateTestPrerequisites(2,
kNoProcessLimit,
kNoPowerOfTwoRequired,
1,
kNoNodeLimit))
<< "Test requires at least 2 MPI processes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t parent = getActiveCommunicator();
hipStream_t stream = getActiveStream();
int rank = MPIEnvironment::world_rank;
constexpr size_t kCount = 1024 * 1024;
void* send_buf = nullptr;
void* recv_buf = nullptr;
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&send_buf, kCount * sizeof(float)));
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&recv_buf, kCount * sizeof(float)));
auto send_guard = makeScopeGuard([&]() { if(send_buf) (void)hipFree(send_buf); });
auto recv_guard = makeScopeGuard([&]() { if(recv_buf) (void)hipFree(recv_buf); });
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(send_buf, kCount));
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(recv_buf, kCount));
ASSERT_MPI_EQ(ncclSuccess,
ncclAllReduce(send_buf, recv_buf, kCount, ncclFloat, ncclSum, parent, stream));
ASSERT_MPI_EQ(ncclSuccess, ncclCommRevoke(parent, NCCL_REVOKE_DEFAULT));
MPI_Barrier(MPI_COMM_WORLD);
std::vector<int> excludeList;
bool isExcluded;
computeSymmetricExclude(rank, MPIEnvironment::world_size, excludeList, isExcluded);
ncclComm_t child = NCCL_COMM_NULL;
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess,
ncclCommShrink(parent, excludeList.data(), excludeList.size(),
&child, nullptr, NCCL_SHRINK_DEFAULT));
ASSERT_NE(child, nullptr);
}
MPI_Barrier(MPI_COMM_WORLD);
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess,
ncclAllReduce(send_buf, recv_buf, kCount, ncclFloat, ncclSum, child, stream));
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
}
// Barrier before child destruction ensures all participating ranks
// complete their operations before any rank starts destroying
MPI_Barrier(MPI_COMM_WORLD);
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess, ncclCommDestroy(child));
}
MPI_Barrier(MPI_COMM_WORLD);
}
/**
* P2P recovery cycle with in-flight ops: launch ncclSend/ncclRecv between
* pairs, revoke the parent before draining, then shrink and verify a fresh
* P2P exchange on the child succeeds. Validates that in-flight P2P is
* halted gracefully and the child's resources are clean.
*/
TEST_F(RevokeMPITest, P2P_Revoke_Shrink_P2P)
{
ASSERT_TRUE(validateTestPrerequisites(2,
kNoProcessLimit,
kNoPowerOfTwoRequired,
1,
kNoNodeLimit))
<< "Test requires at least 2 MPI processes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t parent = getActiveCommunicator();
hipStream_t stream = getActiveStream();
int rank = MPIEnvironment::world_rank;
int world_size = MPIEnvironment::world_size;
constexpr size_t kCount = 64 * 1024;
void* send_buf = nullptr;
void* recv_buf = nullptr;
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&send_buf, kCount * sizeof(float)));
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&recv_buf, kCount * sizeof(float)));
auto send_guard = makeScopeGuard([&]() { if(send_buf) (void)hipFree(send_buf); });
auto recv_guard = makeScopeGuard([&]() { if(recv_buf) (void)hipFree(recv_buf); });
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(send_buf, kCount));
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(recv_buf, kCount));
ASSERT_MPI_EQ(ncclSuccess, ncclGroupStart());
if(rank % 2 == 0 && rank + 1 < world_size)
{
ASSERT_MPI_EQ(ncclSuccess,
ncclSend(send_buf, kCount, ncclFloat, rank + 1, parent, stream));
}
else if(rank % 2 == 1)
{
ASSERT_MPI_EQ(ncclSuccess,
ncclRecv(recv_buf, kCount, ncclFloat, rank - 1, parent, stream));
}
ASSERT_MPI_EQ(ncclSuccess, ncclGroupEnd());
ASSERT_MPI_EQ(ncclSuccess, ncclCommRevoke(parent, NCCL_REVOKE_DEFAULT));
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
MPI_Barrier(MPI_COMM_WORLD);
std::vector<int> excludeList;
bool isExcluded;
computeSymmetricExclude(rank, world_size, excludeList, isExcluded);
ncclComm_t child = NCCL_COMM_NULL;
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess,
ncclCommShrink(parent, excludeList.data(), excludeList.size(),
&child, nullptr, NCCL_SHRINK_DEFAULT));
ASSERT_NE(child, nullptr);
}
MPI_Barrier(MPI_COMM_WORLD);
if(!isExcluded)
{
int child_rank = -1;
int child_size = 0;
ASSERT_EQ(ncclSuccess, ncclCommUserRank(child, &child_rank));
ASSERT_EQ(ncclSuccess, ncclCommCount(child, &child_size));
ASSERT_EQ(ncclSuccess, ncclGroupStart());
if(child_rank % 2 == 0 && child_rank + 1 < child_size)
{
ASSERT_EQ(ncclSuccess,
ncclSend(send_buf, kCount, ncclFloat, child_rank + 1, child, stream));
}
else if(child_rank % 2 == 1)
{
ASSERT_EQ(ncclSuccess,
ncclRecv(recv_buf, kCount, ncclFloat, child_rank - 1, child, stream));
}
ASSERT_EQ(ncclSuccess, ncclGroupEnd());
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
}
// Barrier before child destruction ensures all participating ranks
// complete their operations before any rank starts destroying
MPI_Barrier(MPI_COMM_WORLD);
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess, ncclCommDestroy(child));
}
MPI_Barrier(MPI_COMM_WORLD);
}
// Revoking the same communicator twice must be rejected with ncclInvalidArgument.
TEST_F(RevokeMPITest, Revoke_DoubleRevoke_Rejected)
{
ASSERT_TRUE(validateTestPrerequisites(2,
kNoProcessLimit,
kNoPowerOfTwoRequired,
1,
kNoNodeLimit))
<< "Test requires at least 2 MPI processes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t comm = getActiveCommunicator();
ASSERT_MPI_EQ(ncclSuccess, ncclCommRevoke(comm, NCCL_REVOKE_DEFAULT));
MPI_Barrier(MPI_COMM_WORLD);
ncclResult_t result = ncclCommRevoke(comm, NCCL_REVOKE_DEFAULT);
ASSERT_MPI_EQ(ncclInvalidArgument, result);
}
/**
* Calling ncclCommRevoke with a null handle must be rejected with ncclInvalidArgument.
*/
TEST_F(RevokeMPITest, Revoke_NullComm_Rejected)
{
ASSERT_MPI_EQ(ncclInvalidArgument, ncclCommRevoke(nullptr, NCCL_REVOKE_DEFAULT));
}
/**
* Calling ncclCommRevoke with unsupported revokeFlags must be rejected
* with ncclInvalidArgument.
*/
TEST_F(RevokeMPITest, Revoke_BadFlags_Rejected)
{
ASSERT_MPI_EQ(ncclInvalidArgument, ncclCommRevoke(nullptr, /*revokeFlags=*/0x1));
}
/**
* ncclCommFinalize on a revoked communicator must be rejected with
* ncclInvalidUsage. After revoke, the user should call ncclCommDestroy
* instead — finalize would double-drain streams already handled by
* commRevokeAsync.
*/
TEST_F(RevokeMPITest, Revoke_ThenFinalize_Rejected)
{
ASSERT_TRUE(validateTestPrerequisites(2,
kNoProcessLimit,
kNoPowerOfTwoRequired,
1,
kNoNodeLimit))
<< "Test requires at least 2 MPI processes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t comm = getActiveCommunicator();
ASSERT_MPI_EQ(ncclSuccess, ncclCommRevoke(comm, NCCL_REVOKE_DEFAULT));
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_EQ(ncclInvalidUsage, ncclCommFinalize(comm));
}
/**
* Incomplete collective variant: rank np-1 does NOT call AllReduce, so the
* collective is guaranteed to be stuck in-flight when revoke fires. This
* verifies that revoke can recover from a truly incomplete operation, not
* just one that might have raced to completion.
*
* Relies on ncclCommRevoke setting abortFlag=1 synchronously before launching
* commRevokeAsync, so that the device-side
* kernel waiting for the missing peer exits and the host stream sync inside
* commRevokeAsync can proceed.
*
*/
TEST_F(RevokeMPITest, IncompleteCollective_Revoke_Shrink_Collective)
{
ASSERT_TRUE(validateTestPrerequisites(4,
kNoProcessLimit,
kNoPowerOfTwoRequired,
1,
kNoNodeLimit))
<< "Test requires at least 4 MPI processes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t parent = getActiveCommunicator();
hipStream_t stream = getActiveStream();
int rank = MPIEnvironment::world_rank;
int world_size = MPIEnvironment::world_size;
constexpr size_t kCount = 1024 * 1024;
void* send_buf = nullptr;
void* recv_buf = nullptr;
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&send_buf, kCount * sizeof(float)));
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&recv_buf, kCount * sizeof(float)));
auto send_guard = makeScopeGuard([&]() { if(send_buf) (void)hipFree(send_buf); });
auto recv_guard = makeScopeGuard([&]() { if(recv_buf) (void)hipFree(recv_buf); });
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(send_buf, kCount));
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(recv_buf, kCount));
if (rank != world_size - 1) {
ASSERT_EQ(ncclSuccess,
ncclAllReduce(send_buf, recv_buf, kCount, ncclFloat, ncclSum, parent, stream));
}
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_EQ(ncclSuccess, ncclCommRevoke(parent, NCCL_REVOKE_DEFAULT));
if (rank != world_size - 1) {
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
}
MPI_Barrier(MPI_COMM_WORLD);
std::vector<int> excludeList;
bool isExcluded;
computeSymmetricExclude(rank, world_size, excludeList, isExcluded);
ncclComm_t child = NCCL_COMM_NULL;
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess,
ncclCommShrink(parent, excludeList.data(), excludeList.size(),
&child, nullptr, NCCL_SHRINK_DEFAULT));
ASSERT_NE(child, nullptr);
}
MPI_Barrier(MPI_COMM_WORLD);
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess,
ncclAllReduce(send_buf, recv_buf, kCount, ncclFloat, ncclSum, child, stream));
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
}
// Barrier before child destruction ensures all participating ranks
// complete their operations before any rank starts destroying
MPI_Barrier(MPI_COMM_WORLD);
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess, ncclCommDestroy(child));
}
MPI_Barrier(MPI_COMM_WORLD);
}
static void computeAsymmetricExclude(int worldRank, int worldSize,
std::vector<int>& excludeList, bool& isExcluded)
{
MPI_Comm nodeComm;
MPI_Comm_split_type(MPI_COMM_WORLD, MPI_COMM_TYPE_SHARED, worldRank, MPI_INFO_NULL, &nodeComm);
int localSize, localRank;
MPI_Comm_size(nodeComm, &localSize);
MPI_Comm_rank(nodeComm, &localRank);
int nodeId = 0;
if (localSize > 0) nodeId = worldRank / localSize;
MPI_Comm_free(&nodeComm);
int nNodes = (worldSize + localSize - 1) / localSize;
int nodesToExclude = std::max(1, nNodes / 2);
excludeList.clear();
isExcluded = false;
for (int n = 0; n < nodesToExclude; n++) {
int excludedRank = n * localSize + (localSize - 1);
if (excludedRank < worldSize) {
excludeList.push_back(excludedRank);
if (worldRank == excludedRank) isExcluded = true;
}
}
}
/**
* Asymmetric shrink: exclude one rank from each of the first half of nodes,
* creating unequal per-node GPU counts (e.g. 7+7+8+8 on 4 nodes).
* Exercises the asymmetric topology path where tree connections are
* skipped and only ring algorithm is used.
*/
TEST_F(RevokeMPITest, Collective_Revoke_AsymmetricShrink_Collective)
{
ASSERT_TRUE(validateTestPrerequisites(4,
kNoProcessLimit,
kNoPowerOfTwoRequired,
2,
kNoNodeLimit))
<< "Test requires at least 4 MPI processes across 2 nodes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t parent = getActiveCommunicator();
hipStream_t stream = getActiveStream();
int rank = MPIEnvironment::world_rank;
constexpr size_t kCount = 1024 * 1024;
void* send_buf = nullptr;
void* recv_buf = nullptr;
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&send_buf, kCount * sizeof(float)));
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&recv_buf, kCount * sizeof(float)));
auto send_guard = makeScopeGuard([&]() { if(send_buf) (void)hipFree(send_buf); });
auto recv_guard = makeScopeGuard([&]() { if(recv_buf) (void)hipFree(recv_buf); });
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(send_buf, kCount));
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(recv_buf, kCount));
ASSERT_MPI_EQ(ncclSuccess,
ncclAllReduce(send_buf, recv_buf, kCount, ncclFloat, ncclSum, parent, stream));
ASSERT_MPI_EQ(ncclSuccess, ncclCommRevoke(parent, NCCL_REVOKE_DEFAULT));
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
MPI_Barrier(MPI_COMM_WORLD);
std::vector<int> excludeList;
bool isExcluded;
computeAsymmetricExclude(rank, MPIEnvironment::world_size, excludeList, isExcluded);
ncclComm_t child = NCCL_COMM_NULL;
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess,
ncclCommShrink(parent, excludeList.data(), excludeList.size(),
&child, nullptr, NCCL_SHRINK_DEFAULT));
ASSERT_NE(child, nullptr);
}
MPI_Barrier(MPI_COMM_WORLD);
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess,
ncclAllReduce(send_buf, recv_buf, kCount, ncclFloat, ncclSum, child, stream));
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
}
// Barrier before child destruction ensures all participating ranks
// complete their operations before any rank starts destroying
MPI_Barrier(MPI_COMM_WORLD);
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess, ncclCommDestroy(child));
}
MPI_Barrier(MPI_COMM_WORLD);
}
/**
* NCCL_SHRINK_ABORT: enqueue a large collective on parent, then shrink with
* the ABORT flag (terminates in-flight ops before creating child).
* Verifies child collective works and rank renumbering is correct.
* Covers Meta TorchComms requirement 3: ncclCommShrink with NCCL_SHRINK_ABORT.
*/
TEST_F(RevokeMPITest, ShrinkAbort_InFlight_ChildWorks_RankRenumbering)
{
ASSERT_TRUE(validateTestPrerequisites(4,
kNoProcessLimit,
kNoPowerOfTwoRequired,
1,
kNoNodeLimit))
<< "Test requires at least 4 MPI processes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t parent = getActiveCommunicator();
hipStream_t stream = getActiveStream();
int rank = MPIEnvironment::world_rank;
constexpr size_t kCount = 1024 * 1024;
void* send_buf = nullptr;
void* recv_buf = nullptr;
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&send_buf, kCount * sizeof(float)));
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&recv_buf, kCount * sizeof(float)));
auto send_guard = makeScopeGuard([&]() { if(send_buf) (void)hipFree(send_buf); });
auto recv_guard = makeScopeGuard([&]() { if(recv_buf) (void)hipFree(recv_buf); });
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(send_buf, kCount));
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(recv_buf, kCount));
ASSERT_MPI_EQ(ncclSuccess,
ncclAllReduce(send_buf, recv_buf, kCount, ncclFloat, ncclSum, parent, stream));
MPI_Barrier(MPI_COMM_WORLD);
std::vector<int> excludeList;
bool isExcluded;
computeSymmetricExclude(rank, MPIEnvironment::world_size, excludeList, isExcluded);
ncclComm_t child = NCCL_COMM_NULL;
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess,
ncclCommShrink(parent, excludeList.data(), excludeList.size(),
&child, nullptr, NCCL_SHRINK_ABORT));
ASSERT_NE(child, nullptr);
int childRank = -1, childSize = 0;
ASSERT_EQ(ncclSuccess, ncclCommUserRank(child, &childRank));
ASSERT_EQ(ncclSuccess, ncclCommCount(child, &childSize));
int expectedSize = MPIEnvironment::world_size - static_cast<int>(excludeList.size());
ASSERT_EQ(childSize, expectedSize);
ASSERT_GE(childRank, 0);
ASSERT_LT(childRank, childSize);
}
MPI_Barrier(MPI_COMM_WORLD);
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess,
ncclAllReduce(send_buf, recv_buf, kCount, ncclFloat, ncclSum, child, stream));
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
}
// Barrier before child destruction ensures all participating ranks
// complete their operations before any rank starts destroying
MPI_Barrier(MPI_COMM_WORLD);
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess, ncclCommDestroy(child));
}
MPI_Barrier(MPI_COMM_WORLD);
}
/**
* In-flight collective, revoke, then explicit destroy on all ranks.
* Verifies ncclCommDestroy returns ncclSuccess after revoke — no SIGABRT.
* Covers Meta TorchComms requirement 1: revoke ≠ abort.
*/
TEST_F(RevokeMPITest, InFlightCollective_Revoke_Destroy_Clean)
{
ASSERT_TRUE(validateTestPrerequisites(2,
kNoProcessLimit,
kNoPowerOfTwoRequired,
1,
kNoNodeLimit))
<< "Test requires at least 2 MPI processes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t parent = getActiveCommunicator();
hipStream_t stream = getActiveStream();
constexpr size_t kCount = 1024 * 1024;
void* send_buf = nullptr;
void* recv_buf = nullptr;
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&send_buf, kCount * sizeof(float)));
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&recv_buf, kCount * sizeof(float)));
auto send_guard = makeScopeGuard([&]() { if(send_buf) (void)hipFree(send_buf); });
auto recv_guard = makeScopeGuard([&]() { if(recv_buf) (void)hipFree(recv_buf); });
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(send_buf, kCount));
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(recv_buf, kCount));
ASSERT_MPI_EQ(ncclSuccess,
ncclAllReduce(send_buf, recv_buf, kCount, ncclFloat, ncclSum, parent, stream));
ASSERT_MPI_EQ(ncclSuccess, ncclCommRevoke(parent, NCCL_REVOKE_DEFAULT));
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
MPI_Barrier(MPI_COMM_WORLD);
ASSERT_MPI_EQ(ncclSuccess, ncclCommDestroy(parent));
test_comm_ = nullptr;
MPI_Barrier(MPI_COMM_WORLD);
}
/**
* Repeated revoke+shrink cycle: revoke parent -> shrink -> child collective ->
* revoke child -> shrink child -> grandchild collective -> destroy all.
* Verifies no resource leaks or crashes across multiple generations.
* Covers Meta TorchComms requirement 4: no leaked resources.
*/
TEST_F(RevokeMPITest, RepeatedRevokeShrinkCycles_ResourceCleanup)
{
ASSERT_TRUE(validateTestPrerequisites(4,
kNoProcessLimit,
kNoPowerOfTwoRequired,
2,
kNoNodeLimit))
<< "Test requires at least 4 MPI processes across 2 nodes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t parent = getActiveCommunicator();
hipStream_t stream = getActiveStream();
int rank = MPIEnvironment::world_rank;
constexpr size_t kCount = 64 * 1024;
void* send_buf = nullptr;
void* recv_buf = nullptr;
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&send_buf, kCount * sizeof(float)));
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&recv_buf, kCount * sizeof(float)));
auto send_guard = makeScopeGuard([&]() { if(send_buf) (void)hipFree(send_buf); });
auto recv_guard = makeScopeGuard([&]() { if(recv_buf) (void)hipFree(recv_buf); });
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(send_buf, kCount));
HIP_TEST_CHECK_GTEST_FAIL(zeroInitializeBuffer<float>(recv_buf, kCount));
ASSERT_MPI_EQ(ncclSuccess,
ncclAllReduce(send_buf, recv_buf, kCount, ncclFloat, ncclSum, parent, stream));
ASSERT_MPI_EQ(ncclSuccess, ncclCommRevoke(parent, NCCL_REVOKE_DEFAULT));
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
MPI_Barrier(MPI_COMM_WORLD);
std::vector<int> excludeList;
bool isExcluded;
computeSymmetricExclude(rank, MPIEnvironment::world_size, excludeList, isExcluded);
ncclComm_t child = NCCL_COMM_NULL;
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess,
ncclCommShrink(parent, excludeList.data(), excludeList.size(),
&child, nullptr, NCCL_SHRINK_DEFAULT));
ASSERT_NE(child, nullptr);
}
MPI_Barrier(MPI_COMM_WORLD);
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess,
ncclAllReduce(send_buf, recv_buf, kCount, ncclFloat, ncclSum, child, stream));
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
ASSERT_EQ(ncclSuccess, ncclCommRevoke(child, NCCL_REVOKE_DEFAULT));
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(stream));
}
MPI_Barrier(MPI_COMM_WORLD);
if(!isExcluded)
{
ASSERT_EQ(ncclSuccess, ncclCommDestroy(child));
}
MPI_Barrier(MPI_COMM_WORLD);
}
// Revoke fixture using a non-blocking (config.blocking = 0) parent comm.
// Exercises the worker-thread async-job path that RevokeMPITest skips.
class RevokeNonBlockingMPITest : public MPITestBase
{
protected:
// ncclCommDestroy on a non-blocking comm can return ncclInProgress;
// use Abort to guarantee synchronous teardown between chained tests.
void TearDown() override
{
if(test_comm_)
{
(void)ncclCommAbort(test_comm_);
test_comm_ = nullptr;
}
MPITestBase::TearDown();
}
// Poll ncclCommGetAsyncError until the comm leaves ncclInProgress,
// yielding the CPU between checks. Returns the terminal state.
static ncclResult_t waitForAsyncResult(ncclComm_t comm)
{
ncclResult_t state = ncclInProgress;
while(state == ncclInProgress)
{
ncclResult_t r = ncclCommGetAsyncError(comm, &state);
if(r != ncclSuccess) return r;
if(state == ncclInProgress) sched_yield();
}
return state;
}
ncclResult_t createTestCommunicator() override
{
int world_rank = MPIEnvironment::world_rank;
int world_size = MPIEnvironment::world_size;
if(world_rank == 0)
{
RCCL_TEST_CHECK(ncclGetUniqueId(&nccl_id_));
}
MPI_Bcast(&nccl_id_, sizeof(ncclUniqueId), MPI_BYTE, 0, MPI_COMM_WORLD);
ncclConfig_t config = NCCL_CONFIG_INITIALIZER;
config.blocking = 0;
RCCL_TEST_CHECK(ncclGroupStart());
auto group_guard = makeScopeGuard([]() { (void)ncclGroupEnd(); });
ncclResult_t res = ncclCommInitRankConfig(
&test_comm_, world_size, nccl_id_, world_rank, &config);
if(res != ncclSuccess && res != ncclInProgress) return res;
auto comm_guard = makeScopeGuard(
[this]()
{
if(test_comm_)
{
(void)ncclCommAbort(test_comm_);
test_comm_ = nullptr;
}
});
res = ncclGroupEnd();
group_guard.dismiss();
if(res != ncclSuccess && res != ncclInProgress) return res;
// Drain ncclInProgress from the non-blocking init.
RCCL_TEST_CHECK(waitForAsyncResult(test_comm_));
HIP_TEST_CHECK(hipStreamCreate(&test_stream_));
auto stream_guard = makeScopeGuard(
[this]()
{
if(test_stream_)
{
(void)hipStreamDestroy(test_stream_);
test_stream_ = nullptr;
}
});
MPI_Barrier(MPI_COMM_WORLD);
comm_guard.dismiss();
stream_guard.dismiss();
return ncclSuccess;
}
};
// Non-blocking revoke must return ncclSuccess/ncclInProgress, then drain
// to ncclSuccess via ncclCommGetAsyncError; later collectives must reject.
TEST_F(RevokeNonBlockingMPITest, Revoke_NonBlocking_ReturnsInProgress)
{
ASSERT_TRUE(validateTestPrerequisites(2,
kNoProcessLimit,
kNoPowerOfTwoRequired,
1,
kNoNodeLimit))
<< "Test requires at least 2 MPI processes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t comm = getActiveCommunicator();
hipStream_t stream = getActiveStream();
ncclResult_t res = ncclCommRevoke(comm, NCCL_REVOKE_DEFAULT);
ASSERT_MPI_TRUE(res == ncclSuccess || res == ncclInProgress);
ASSERT_MPI_EQ(ncclSuccess, waitForAsyncResult(comm));
MPI_Barrier(MPI_COMM_WORLD);
void* buf = nullptr;
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&buf, sizeof(float)));
auto buf_guard = makeScopeGuard([&]() { if(buf) (void)hipFree(buf); });
ncclResult_t collRes =
ncclAllReduce(buf, buf, 1, ncclFloat, ncclSum, comm, stream);
ASSERT_MPI_EQ(ncclInvalidUsage, collRes);
}
// Race commRevokeAsync (worker thread) against an immediate ncclCommShrink
// on the same parent; shrink must still succeed across kIterations runs.
TEST_F(RevokeNonBlockingMPITest, Revoke_NonBlocking_ThenShrink_NoRace)
{
ASSERT_TRUE(validateTestPrerequisites(2,
kNoProcessLimit,
kNoPowerOfTwoRequired,
1,
kNoNodeLimit))
<< "Test requires at least 2 MPI processes";
constexpr int kIterations = 10;
int rank = MPIEnvironment::world_rank;
int world_size = MPIEnvironment::world_size;
for(int iter = 0; iter < kIterations; ++iter)
{
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t parent = getActiveCommunicator();
// Revoke parent and DO NOT poll before invoking shrink. This is
// the exact pattern that triggers the worker-thread race.
ncclResult_t res = ncclCommRevoke(parent, NCCL_REVOKE_DEFAULT);
ASSERT_MPI_TRUE(res == ncclSuccess || res == ncclInProgress);
MPI_Barrier(MPI_COMM_WORLD);
std::vector<int> excludeList;
bool isExcluded = false;
computeSymmetricExclude(rank, world_size, excludeList, isExcluded);
ncclComm_t child = NCCL_COMM_NULL;
if(!isExcluded)
{
res = ncclCommShrink(parent,
excludeList.data(),
excludeList.size(),
&child,
nullptr,
NCCL_SHRINK_DEFAULT);
ASSERT_TRUE(res == ncclSuccess || res == ncclInProgress)
<< "iter=" << iter << " shrink returned " << res;
ASSERT_NE(child, nullptr);
ASSERT_EQ(ncclSuccess, waitForAsyncResult(child));
}
MPI_Barrier(MPI_COMM_WORLD);
if(!isExcluded && child)
{
ASSERT_EQ(ncclSuccess, ncclCommDestroy(child));
}
MPI_Barrier(MPI_COMM_WORLD);
// Tear down parent so the next iteration starts from a clean state.
(void)cleanupTestCommunicator();
MPI_Barrier(MPI_COMM_WORLD);
}
}
// ncclCommAbort issued mid-flight on a non-blocking revoke must wind down
// the async-job worker via its abortFlag and return without hanging.
TEST_F(RevokeNonBlockingMPITest, Revoke_NonBlocking_AbortedMidFlight)
{
ASSERT_TRUE(validateTestPrerequisites(2,
kNoProcessLimit,
kNoPowerOfTwoRequired,
1,
kNoNodeLimit))
<< "Test requires at least 2 MPI processes";
ASSERT_MPI_EQ(ncclSuccess, createTestCommunicator());
ncclComm_t comm = getActiveCommunicator();
ncclResult_t res = ncclCommRevoke(comm, NCCL_REVOKE_DEFAULT);
ASSERT_MPI_TRUE(res == ncclSuccess || res == ncclInProgress);
// Abort immediately; do not poll the revoke result first.
ASSERT_MPI_EQ(ncclSuccess, ncclCommAbort(comm));
// Comm is now destroyed by Abort. Prevent the framework's TearDown
// from calling ncclCommDestroy on a dangling handle.
test_comm_ = nullptr;
MPI_Barrier(MPI_COMM_WORLD);
}
#endif // MPI_TESTS_ENABLED