Files
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

1910 lines
70 KiB
C++

/*************************************************************************
* Copyright (c) 2023 Advanced Micro Devices, Inc. All rights reserved.
*
* See LICENSE.txt for license information
************************************************************************/
#include <gtest/gtest.h>
#include <rccl/rccl.h>
#include <cstdlib>
#include <cstring>
#include "comm.h"
#include "common/ErrCode.hpp"
#include "common/ProcessIsolatedTestRunner.hpp"
#include "debug.h"
#include "graph/topo.h"
#include "net.h"
#include "rccl_common.h"
#include "rocmwrap.h"
namespace RcclUnitTesting
{
// Helper function to determine if P2P test should be skipped due to static
// variable state
static bool ShouldSkipP2pTest(const char* requiredEnvValue = nullptr)
{
const char* envValue = getenv("NCCL_P2P_NET_CHUNKSIZE");
// If a specific environment value is required, check for it
if(requiredEnvValue != nullptr)
{
if(!envValue || strcmp(envValue, requiredEnvValue) != 0)
{
return true; // Skip if env var is not set to required value
}
return false; // Don't skip if env var matches required value
}
// For architecture logic tests, skip only if environment variable is set
// (which would override the static variable behavior)
// Note: We cannot directly check if static variable is RCCL_VALUE_UNSET
// from test code, so we rely on clean environment for proper testing
if(envValue != nullptr)
{
return true; // Skip if env var is set (prevents testing architecture logic)
}
// Environment is clean - proceed with test
// Warning: Static variable might still be initialized from previous tests
// For guaranteed clean state, run tests individually or restart binary
return false; // Don't skip
}
// Helper function to determine if PXN test should be skipped due to static
// variable state
static bool ShouldSkipPxnTest(const char* requiredEnvValue = nullptr)
{
const char* envValue = getenv("NCCL_PXN_DISABLE");
// If a specific environment value is required, check for it
if(requiredEnvValue != nullptr)
{
if(!envValue || strcmp(envValue, requiredEnvValue) != 0)
{
return true; // Skip if env var is not set to required value
}
return false; // Don't skip if env var matches required value
}
// For architecture logic tests, skip only if environment variable is set
// (which would override the static variable behavior)
if(envValue != nullptr)
{
return true; // Skip if env var is set (prevents testing architecture logic)
}
// Environment is clean - proceed with test
return false; // Don't skip
}
// Helper function to test the static expose check
ncclResult_t testStaticExposeCheck()
{
RCCL_STATIC_EXPOSE_CHECK();
return ncclSuccess;
}
// Helper function to create and initialize mock communicator
static void CreateMockComm(
ncclComm_t& mockComm,
struct ncclTopoSystem& mockTopo,
struct ncclTopoNode& mockGpuNode,
const char* arch,
int nRanks
)
{
// Allocate memory for the communicator
mockComm = new ncclComm();
memset(mockComm, 0, sizeof(ncclComm));
// Initialize basic communicator fields
mockComm->nRanks = nRanks;
mockComm->nNodes = 1; // Default to single node for P2P tests
mockComm->rank = 0; // Default rank
mockComm->pxnDisable = RCCL_VALUE_UNSET;
mockComm->p2pNetChunkSize = RCCL_VALUE_UNSET;
// Initialize topology
memset(&mockTopo, 0, sizeof(mockTopo));
mockComm->topo = &mockTopo;
// Initialize GPU node
mockTopo.nodes[GPU].count = 1;
memset(&mockGpuNode, 0, sizeof(mockGpuNode));
// Set GPU architecture
strncpy(mockGpuNode.gpu.gcn, arch, sizeof(mockGpuNode.gpu.gcn) - 1);
mockGpuNode.gpu.gcn[sizeof(mockGpuNode.gpu.gcn) - 1] = '\0';
// Copy the node into the topology array
mockTopo.nodes[GPU].nodes[0] = mockGpuNode;
// Initialize other required fields for tests
memset(mockComm->minMaxLLRange, 0, sizeof(mockComm->minMaxLLRange));
}
// Helper function to cleanup mock communicator
static void CleanupMockComm(ncclComm_t& mockComm)
{
if(mockComm)
{
delete mockComm;
mockComm = nullptr;
}
}
// Helper function to determine if rcclSetPipelining test should be skipped
static bool ShouldSkipRcclSetPipeliningTests()
{
const char* disable = getenv("RCCL_DISABLE_REDUCE_COPY_PIPELINING");
// Skip the test if RCCL_DISABLE_REDUCE_COPY_PIPELINING is set
if(disable && strcmp(disable, "0") != 0)
{
return true;
}
return false;
}
// Helper function to validate protocol string against known valid protocols
static bool isProtoStrValid(const char* envStr)
{
if(!envStr)
return false;
for(int i = 0; i < NCCL_NUM_PROTOCOLS; ++i)
{
if(strcasecmp(envStr, ncclProtoStr[i]) == 0)
{
return true; // Match found
}
}
return false; // No match found
}
// Helper that exercises CUCHECK with a guaranteed-to-fail HIP call
static ncclResult_t triggerCucheckFailure()
{
CUCHECK(hipPointerGetAttribute(nullptr, HIP_POINTER_ATTRIBUTE_CONTEXT,
(hipDeviceptr_t)0x1));
return ncclSuccess;
}
// Helper function to validate algorithm string against known valid algorithms
static bool isAlgoStrValid(const char* envStr)
{
if(!envStr)
return false;
for(int i = 0; i < NCCL_NUM_ALGORITHMS; ++i)
{
if(strcasecmp(envStr, ncclAlgoStr[i]) == 0)
{
return true; // Match found
}
}
return false; // No match found
}
TEST(Rcclwrap, RcclFuncMaxSendRecvCount)
{
ncclResult_t staticCheckResult = testStaticExposeCheck();
#ifdef RCCL_EXPOSE_STATIC
EXPECT_EQ(staticCheckResult, ncclSuccess);
#else
EXPECT_EQ(staticCheckResult, ncclInvalidUsage);
#endif
size_t maxCount = 0;
ncclResult_t result = rcclFuncMaxSendRecvCount(ncclFuncAllReduce, 4, 1024, maxCount);
EXPECT_EQ(maxCount, 1024);
EXPECT_EQ(result, ncclSuccess);
}
TEST(Rcclwrap, RcclUpdateCollectiveProtocol_UsesLL128WhenInRange)
{
setenv("NCCL_PROTO", "", 1); // Trigger auto selection mode
unsetenv("NCCL_PROTO");
ncclComm_t comm = new ncclComm();
// Manually populate minimal fields for comm
comm->nRanks = 1;
comm->nNodes = 2; // triggers inter-node logic
comm->rank = 0;
comm->topo = new ncclTopoSystem();
*comm->topo = {};
comm->topo->ll128Enabled = true;
comm->topo->nodes[GPU].nodes[0] = {};
comm->topo->nodes[GPU].count = 1;
strncpy(
comm->topo->nodes[GPU].nodes[0].gpu.gcn,
"gfx942",
sizeof(comm->topo->nodes[GPU].nodes[0].gpu.gcn)
);
int idx = rcclGetTunableIndex(ncclFuncAllReduce);
comm->minMaxLLRange[idx][NCCL_PROTO_LL][RCCL_PROTOCOL_MIN_IDX] = 512;
comm->minMaxLLRange[idx][NCCL_PROTO_LL][RCCL_PROTOCOL_MAX_IDX] = 1024;
comm->minMaxLLRange[idx][NCCL_PROTO_LL128][RCCL_PROTOCOL_MIN_IDX] = 256;
comm->minMaxLLRange[idx][NCCL_PROTO_LL128][RCCL_PROTOCOL_MAX_IDX] = 2048;
comm->minMaxLLRange[idx][NCCL_PROTO_LL128][RCCL_PROTOCOL_FACTOR_IDX] = 1;
ncclTaskColl info = {};
// Manually populate minimal fields for info
info.func = ncclFuncAllReduce;
info.protocol = NCCL_PROTO_UNDEF;
size_t nBytes = 1024;
rcclUpdateCollectiveProtocol(comm, nBytes, &info);
EXPECT_TRUE(info.protocol == NCCL_PROTO_LL128 || info.protocol == NCCL_PROTO_LL);
delete comm->topo;
delete comm;
}
TEST(Rcclwrap, RcclUpdateCollectiveProtocol_WarnsOnGfx942Arch)
{
setenv("NCCL_PROTO", "", 1);
unsetenv("NCCL_PROTO");
ncclComm_t comm = new ncclComm();
// Manually populate minimal fields for comm
comm->nRanks = 1;
comm->nNodes = 2; // triggers inter-node logic
comm->rank = 0;
comm->topo = new ncclTopoSystem();
comm->topo->ll128Enabled = true;
comm->topo->nodes[GPU].nodes[0] = {};
strncpy(
comm->topo->nodes[GPU].nodes[0].gpu.gcn,
"gfx942",
sizeof(comm->topo->nodes[GPU].nodes[0].gpu.gcn)
);
int idx = rcclGetTunableIndex(ncclFuncAllReduce);
comm->minMaxLLRange[idx][NCCL_PROTO_LL][RCCL_PROTOCOL_MIN_IDX] = RCCL_LL_LIMITS_UNDEFINED;
comm->minMaxLLRange[idx][NCCL_PROTO_LL][RCCL_PROTOCOL_MAX_IDX] = RCCL_LL_LIMITS_UNDEFINED;
comm->minMaxLLRange[idx][NCCL_PROTO_LL128][RCCL_PROTOCOL_MIN_IDX] = RCCL_LL_LIMITS_UNDEFINED;
comm->minMaxLLRange[idx][NCCL_PROTO_LL128][RCCL_PROTOCOL_MAX_IDX] = RCCL_LL_LIMITS_UNDEFINED;
comm->minMaxLLRange[idx][NCCL_PROTO_LL128][RCCL_PROTOCOL_FACTOR_IDX] = RCCL_LL_LIMITS_UNDEFINED;
ncclTaskColl info = {};
// Manually populate minimal fields for info
info.func = ncclFuncAllReduce;
info.protocol = NCCL_PROTO_UNDEF;
size_t nBytes = 1024; // 1024 per rank for 4 ranks
rcclUpdateCollectiveProtocol(comm, nBytes, &info);
EXPECT_EQ(info.protocol, NCCL_PROTO_UNDEF);
delete comm->topo;
delete comm;
}
TEST(Rcclwrap, RcclUpdateCollectiveProtocol_HonorsUserProtocolEnv)
{ // Why does this pass
// if it does not
// enter the else if
// block
setenv("NCCL_PROTO", "1", 1); // Simulate manual override
ncclComm_t comm = new ncclComm();
// Manually populate minimal fields for comm
comm->nRanks = 1;
comm->nNodes = 2; // triggers inter-node logic
comm->rank = 0;
comm->topo = new ncclTopoSystem(); //(struct ncclTopoSystem*)calloc(1,
// sizeof(struct ncclTopoSystem));
*comm->topo = {};
comm->topo->ll128Enabled = true;
comm->topo->nodes[GPU].nodes[0] = {};
strncpy(
comm->topo->nodes[GPU].nodes[0].gpu.gcn,
"gfx942",
sizeof(comm->topo->nodes[GPU].nodes[0].gpu.gcn)
);
ncclTaskColl info = {};
// Manually populate minimal fields for info
info.func = ncclFuncAllReduce;
info.protocol = NCCL_PROTO_UNDEF;
size_t nBytes = 1024; // 1024 per rank for 4 ranks
rcclUpdateCollectiveProtocol(comm, nBytes, &info);
EXPECT_EQ(info.protocol, NCCL_PROTO_UNDEF);
delete comm->topo;
delete comm;
}
TEST(Rcclwrap, RcclUpdateCollectiveProtocol_SimpleFallbackWhenNoRanges)
{
setenv("NCCL_PROTO", "", 1); // Trigger auto selection mode
unsetenv("NCCL_PROTO");
ncclComm_t comm = new ncclComm();
// Manually populate minimal fields for comm
comm->nRanks = 1;
comm->nNodes = 2; // triggers inter-node logic
comm->rank = 0;
comm->topo = new ncclTopoSystem(); //(struct ncclTopoSystem*)calloc(1,
// sizeof(struct ncclTopoSystem));
*comm->topo = {};
comm->topo->ll128Enabled = true;
comm->topo->nodes[GPU].nodes[0] = {};
comm->topo->nodes[GPU].count = 1;
strncpy(
comm->topo->nodes[GPU].nodes[0].gpu.gcn,
"gfx942",
sizeof(comm->topo->nodes[GPU].nodes[0].gpu.gcn)
);
int idx = rcclGetTunableIndex(ncclFuncAllReduce);
comm->minMaxLLRange[idx][NCCL_PROTO_LL][RCCL_PROTOCOL_MIN_IDX] = 512;
comm->minMaxLLRange[idx][NCCL_PROTO_LL][RCCL_PROTOCOL_MAX_IDX] = 1024;
// Manually populate minimal fields for info
ncclTaskColl info = {};
info.func = ncclFuncAllReduce;
info.protocol = NCCL_PROTO_UNDEF;
size_t nBytes = 2048; // 1024 per rank for 4 ranks
rcclUpdateCollectiveProtocol(comm, nBytes, &info);
EXPECT_EQ(info.protocol, NCCL_PROTO_SIMPLE);
delete comm->topo;
delete comm;
}
TEST(Rcclwrap, validHsaScratchEnvSettingTest)
{
// When HSA_NO_SCRATCH_RECLAIM is set, it is always valid
EXPECT_TRUE(validHsaScratchEnvSetting("1", 0, 0, "gfx950"));
EXPECT_TRUE(validHsaScratchEnvSetting("1", 0, 0, "gfx942"));
// When HSA_NO_SCRATCH_RECLAIM is not set, looking at hip version and firmware
// version
EXPECT_TRUE(validHsaScratchEnvSetting(nullptr, 60443484, 24, "gfx950"));
EXPECT_FALSE(validHsaScratchEnvSetting(nullptr, 60443483, 24, "gfx950"));
EXPECT_FALSE(validHsaScratchEnvSetting(nullptr, 60443484, 23, "gfx950"));
EXPECT_TRUE(validHsaScratchEnvSetting(nullptr, 60443484, 177, "gfx942"));
EXPECT_FALSE(validHsaScratchEnvSetting(nullptr, 60443484, 176, "gfx942"));
EXPECT_FALSE(validHsaScratchEnvSetting(nullptr, 60443483, 177, "gfx942"));
EXPECT_TRUE(validHsaScratchEnvSetting(nullptr, 60443483, 0, "gfx000"));
EXPECT_TRUE(validHsaScratchEnvSetting(nullptr, 60300000, 0, "gfx000"));
}
TEST(Rcclwrap, RcclUpdateThreadThreshold_UserEnvSet)
{
RUN_ISOLATED_TEST_WITH_ENV(
"RcclUpdateThreadThreshold_UserEnvSet",
[]()
{
const char* value = getenv("NCCL_THREAD_THRESHOLDS");
if(!value)
{
TEST_INFO(
"[Rcclwrap] Test skipped. Set environment variable "
"NCCL_THREAD_THRESHOLD"
);
GTEST_SKIP() << "[Rcclwrap] Test skipped. Set environment variable "
"NCCL_THREAD_THRESHOLD\n";
}
else
{
ncclComm comm;
comm.nRanks = 8;
comm.nNodes = 4;
memset(comm.minMaxLLRange, 0, sizeof(comm.minMaxLLRange));
ncclTaskColl info;
info.func = ncclFuncReduceScatter;
info.protocol = 0;
int threadThreshold = 5; // Any number should do, we should make
// sure this number does not change
rcclUpdateThreadThreshold(&comm, 0, &info, threadThreshold);
EXPECT_EQ(threadThreshold, 5);
}
},
{{"NCCL_THREAD_THRESHOLDS", "1"}}
);
}
TEST(Rcclwrap, RcclUpdateThreadThreshold_MinNChannelsSet)
{
RUN_ISOLATED_TEST_WITH_ENV(
"RcclUpdateThreadThreshold_MinNChannelsSet",
[]()
{
const char* value = getenv("NCCL_MIN_NCHANNELS");
if(!value)
{
TEST_INFO(
"[Rcclwrap] Test skipped. Set environment "
"variable NCCL_MIN_NCHANNELS"
);
GTEST_SKIP() << "[Rcclwrap] Test skipped. Set environment variable "
"NCCL_MIN_NCHANNELS\n";
}
else
{
ncclComm comm{};
ncclTaskColl info{};
int threadThreshold = 5;
comm.nRanks = 4;
comm.nNodes = 4;
info.func = ncclFuncAllGather;
info.protocol = 0;
memset(comm.minMaxLLRange, 0, sizeof(comm.minMaxLLRange));
rcclUpdateThreadThreshold(&comm, 0, &info, threadThreshold);
EXPECT_EQ(threadThreshold, 5);
}
},
{{"NCCL_MIN_NCHANNELS", "1"}}
);
}
TEST(Rcclwrap, RcclUpdateThreadThreshold_MaxChannelsSet)
{
RUN_ISOLATED_TEST_WITH_ENV(
"RcclUpdateThreadThreshold_MaxChannelsSet",
[]()
{
const char* value = getenv("NCCL_MAX_NCHANNELS");
if(!value)
{
TEST_INFO(
"[Rcclwrap] Test skipped. Set environment "
"variable NCCL_MAX_NCHANNELS"
);
GTEST_SKIP() << "[Rcclwrap] Test skipped. Set environment variable "
"NCCL_MAX_NCHANNELS\n";
}
else
{
ncclComm comm{};
ncclTaskColl info{};
int threadThreshold = 5;
comm.nRanks = 4;
comm.nNodes = 4;
info.func = ncclFuncAllGather;
info.protocol = 0;
memset(comm.minMaxLLRange, 0, sizeof(comm.minMaxLLRange));
rcclUpdateThreadThreshold(&comm, 0, &info, threadThreshold);
EXPECT_EQ(threadThreshold, 5);
}
},
{{"NCCL_MAX_NCHANNELS", "1"}}
);
}
TEST(Rcclwrap, RcclUpdateThreadThreshold_NoEnv_nNodesLessThan2)
{
ncclComm comm{};
ncclTaskColl info{};
int threadThreshold = 5;
comm.nRanks = 4;
comm.nNodes = 1; // less than 2
info.func = ncclFuncReduceScatter;
info.protocol = 0;
memset(comm.minMaxLLRange, 0, sizeof(comm.minMaxLLRange));
rcclUpdateThreadThreshold(&comm, 0, &info, threadThreshold);
EXPECT_EQ(threadThreshold, 5); // no change
}
TEST(Rcclwrap, RcclUpdateThreadThreshold_NoEnv_FuncUnsupported)
{
ncclComm comm{};
ncclTaskColl info{};
int threadThreshold = 5;
comm.nRanks = 4;
comm.nNodes = 2;
info.func = ncclFuncAllReduce; // unsupported func
info.protocol = 0;
memset(comm.minMaxLLRange, 0, sizeof(comm.minMaxLLRange));
rcclUpdateThreadThreshold(&comm, 0, &info, threadThreshold);
EXPECT_EQ(threadThreshold, 5);
}
TEST(Rcclwrap, RcclUpdateThreadThreshold_NoEnv_UpdateOccurs)
{
ncclComm comm{};
ncclTaskColl info{};
int threadThreshold = 5;
comm.nRanks = 4;
comm.nNodes = 2;
info.func = ncclFuncReduceScatter;
info.protocol = 0;
memset(comm.minMaxLLRange, 0, sizeof(comm.minMaxLLRange));
int idx = rcclGetTunableIndex(info.func);
comm.minMaxLLRange[idx][info.protocol][RCCL_PROTOCOL_THREAD_THRESHOLD_IDX] = 10;
rcclUpdateThreadThreshold(&comm, 0, &info, threadThreshold);
EXPECT_EQ(threadThreshold, 40); // 10 * 4
}
TEST(Rcclwrap, RcclUpdateThreadThreshold_NoEnv_ThresholdUndefined)
{
ncclComm comm{};
ncclTaskColl info{};
int threadThreshold = 5;
comm.nRanks = 4;
comm.nNodes = 3;
info.func = ncclFuncAllGather;
info.protocol = 0;
memset(comm.minMaxLLRange, 0, sizeof(comm.minMaxLLRange));
int idx = rcclGetTunableIndex(info.func);
comm.minMaxLLRange[idx][info.protocol][RCCL_PROTOCOL_THREAD_THRESHOLD_IDX]
= RCCL_LL_LIMITS_UNDEFINED;
rcclUpdateThreadThreshold(&comm, 0, &info, threadThreshold);
EXPECT_EQ(threadThreshold, 5);
}
TEST(Rcclwrap, RcclSetPipelining_Invalid_DType)
{
// Skip the test if pipelining has been disabled
// (RCCL_DISABLE_REDUCE_COPY_PIPELINING=1)
if(ShouldSkipRcclSetPipeliningTests())
{
GTEST_SKIP() << "Skipping test: RCCL_DISABLE_REDUCE_COPY_PIPELINING environment "
"variable is set. Unset this variable to enable pipelining.";
}
// Skip the test if pipelining has been enabled for all data types
// (RCCL_PIPELINE_ALL_DATA_TYPES=1)
const char* allowAllDTypes = getenv("RCCL_PIPELINE_ALL_DATA_TYPES");
if(allowAllDTypes && strcmp(allowAllDTypes, "0") != 0)
{
GTEST_SKIP() << "Skipping test: RCCL_PIPELINE_ALL_DATA_TYPES environment "
"variable is set. Unset this variable to enable pipelining "
"only for bf16 data type.";
}
// Pipeline should not be set for non-bf16 datatypes, unless
// rcclParamPipelineAllDTypes() returns true
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", 8);
comm->nNodes = 2; // Multi node
ncclTaskColl info = {};
info.func = ncclFuncAllReduce;
info.datatype = ncclFloat32;
size_t nBytes = 16 * 1024 * 1024; // 16MB
rcclSetPipelining(comm, nBytes, &info);
EXPECT_EQ(info.pipeline, 0) << "Non-bf16 should not set pipeline by default";
CleanupMockComm(comm);
}
TEST(Rcclwrap, RcclSetPipelining_GFX950_SingleNode_Disable)
{
// Skip the test if pipelining has been disabled
// (RCCL_DISABLE_REDUCE_COPY_PIPELINING=1)
if(ShouldSkipRcclSetPipeliningTests())
{
GTEST_SKIP() << "Skipping test: RCCL_DISABLE_REDUCE_COPY_PIPELINING environment "
"variable is set. Unset this variable to enable pipelining.";
}
// For single-node, pipeline remains 0
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", 8);
comm->nNodes = 1; // Single node
ncclTaskColl info = {};
// In rcclSetPipelining(), ncclFuncAllReduce, ncclFuncReduceScatter, and
// ncclFuncReduce share the same case body. Testing any one of them is
// sufficient to validate that code path.
info.func = ncclFuncAllReduce;
info.datatype = ncclBfloat16;
size_t nBytes = 16 * 1024 * 1024; // 16MB
rcclSetPipelining(comm, nBytes, &info);
EXPECT_EQ(info.pipeline, 0) << "gfx950 single-node should not enable pipelining";
CleanupMockComm(comm);
}
TEST(Rcclwrap, RcclSetPipelining_GFX942_SingleNode_AllReduce_Enable)
{
// Skip the test if pipelining has been disabled
// (RCCL_DISABLE_REDUCE_COPY_PIPELINING=1)
if(ShouldSkipRcclSetPipeliningTests())
{
GTEST_SKIP() << "Skipping test: RCCL_DISABLE_REDUCE_COPY_PIPELINING environment "
"variable is set. Unset this variable to enable pipelining.";
}
// For single-node, pipeline is set to 1 for AllReduce with bf16
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx942", 8);
comm->nNodes = 1; // Single node
ncclTaskColl info = {};
info.func = ncclFuncAllReduce;
info.datatype = ncclBfloat16;
size_t nBytes = 16 * 1024 * 1024; // 16MB
rcclSetPipelining(comm, nBytes, &info);
EXPECT_EQ(info.pipeline, 1) << "gfx942 single-node AllReduce bf16 should enable pipelining";
CleanupMockComm(comm);
}
TEST(Rcclwrap, RcclSetPipelining_GFX942_MultiNode_AllReduce_Enable)
{
// Skip the test if pipelining has been disabled
// (RCCL_DISABLE_REDUCE_COPY_PIPELINING=1)
if(ShouldSkipRcclSetPipeliningTests())
{
GTEST_SKIP() << "Skipping test: RCCL_DISABLE_REDUCE_COPY_PIPELINING environment "
"variable is set. Unset this variable to enable pipelining.";
}
// For multi-node AllReduce with bf16, pipelining is enabled if
// nBytes <= 512MB * 2^(log2(nNodes)-1)
// Testing with nNodes = 4 => threshold = 512MB * 2^(2-1) = 1GB
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx942", 8);
comm->nNodes = 4;
ncclTaskColl info = {};
info.func = ncclFuncAllReduce;
info.datatype = ncclBfloat16;
size_t nBytes = (1ULL << 30); // 1GB, exactly at threshold
rcclSetPipelining(comm, nBytes, &info);
EXPECT_EQ(info.pipeline, 1) << "gfx942 4-node AllReduce at threshold should enable pipelining";
CleanupMockComm(comm);
}
TEST(Rcclwrap, RcclSetPipelining_GFX942_MultiNode_AllReduce_Disable)
{
// Skip the test if pipelining has been disabled
// (RCCL_DISABLE_REDUCE_COPY_PIPELINING=1)
if(ShouldSkipRcclSetPipeliningTests())
{
GTEST_SKIP() << "Skipping test: RCCL_DISABLE_REDUCE_COPY_PIPELINING environment "
"variable is set. Unset this variable to enable pipelining.";
}
// When nBytes is just above the threshold, pipelining should be disabled
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx942", 8);
comm->nNodes = 4;
ncclTaskColl info = {};
info.func = ncclFuncAllReduce;
info.datatype = ncclBfloat16;
size_t nBytes = (1ULL << 30) + 1024; // 1GB + 1KB, just above threshold
rcclSetPipelining(comm, nBytes, &info);
EXPECT_EQ(info.pipeline, 0)
<< "gfx942 4-node AllReduce above threshold should disable pipelining";
CleanupMockComm(comm);
}
TEST(Rcclwrap, RcclSetPipelining_GFX942_Enable)
{
// Skip the test if pipelining has been disabled
// (RCCL_DISABLE_REDUCE_COPY_PIPELINING=1)
if(ShouldSkipRcclSetPipeliningTests())
{
GTEST_SKIP() << "Skipping test: RCCL_DISABLE_REDUCE_COPY_PIPELINING environment "
"variable is set. Unset this variable to enable pipelining.";
}
// ReduceScatter & Reduce should enable pipelining regardless of no. of nodes
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx942", 8);
comm->nNodes = 8;
ncclTaskColl info = {};
// In rcclSetPipelining(), ncclFuncReduceScatter, and
// ncclFuncReduce share the same case body. Testing any one of them is
// sufficient to validate that code path.
info.func = ncclFuncReduceScatter;
info.datatype = ncclBfloat16;
size_t nBytes = 16 * 1024 * 1024; // 16MB
rcclSetPipelining(comm, nBytes, &info);
EXPECT_EQ(info.pipeline, 1) << "gfx942 ReduceScatter and Reduce should enable "
"pipelining with single or multi-node";
CleanupMockComm(comm);
}
TEST(Rcclwrap, RcclOverrideProtocol_NoOverride)
{
RUN_ISOLATED_TEST_WITH_ENV("RcclOverrideProtocol_NoOverride",
[]() {
float table[NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
ncclTaskColl info;
ncclResult_t result = rcclOverrideProtocol(ncclProtoStr, table, &info);
EXPECT_EQ(result, ncclSuccess)
<< "Expected ncclSuccess when RCCL_OVERRIDE_PROTO is unset, indicating "
"no override should be applied.";
},
{}
);
}
TEST(Rcclwrap, RcclOverrideProtocol_UnsupportedOverride)
{
RUN_ISOLATED_TEST_WITH_ENV("RcclOverrideProtocol_UnsupportedOverride",
[]() {
// Mark all combinations as unsupported for the purpose of this test.
float table[NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
for(int a = 0; a < NCCL_NUM_ALGORITHMS; ++a)
for(int p = 0; p < NCCL_NUM_PROTOCOLS; ++p)
table[a][p] = NCCL_ALGO_PROTO_IGNORE;
ncclTaskColl info;
info.func = ncclFuncReduceScatter;
info.datatype = ncclBfloat16;
info.algorithm = NCCL_ALGO_RING;
ncclResult_t result = rcclOverrideProtocol(ncclProtoStr, table, &info);
EXPECT_EQ(result, ncclInternalError)
<< "Expected ncclInternalError when the override protocol is valid, but "
"not enabled for the selected algorithm.";
},
{{"RCCL_OVERRIDE_PROTO", "Simple"}}
);
}
TEST(Rcclwrap, RcclOverrideProtocol_ValidOverride)
{
RUN_ISOLATED_TEST_WITH_ENV("RcclOverrideProtocol_ValidOverride",
[]() {
const char* protoOverrideEnv = getenv("RCCL_OVERRIDE_PROTO");
ASSERT_NE(protoOverrideEnv, nullptr) << "RCCL_OVERRIDE_PROTO should be set";
// Get the index of the protocol from the string for later comparison
int protoIndex = NCCL_PROTO_UNDEF;
ncclResult_t idxResult
= rcclGetAlgoProtoIndex(protoOverrideEnv, ncclProtoStr, NCCL_NUM_PROTOCOLS, protoIndex);
ASSERT_EQ(idxResult, ncclSuccess) << "Failed to get protocol index from string";
// Mark all combinations as valid for the purpose of this test.
float table[NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
for(int a = 0; a < NCCL_NUM_ALGORITHMS; ++a)
for(int p = 0; p < NCCL_NUM_PROTOCOLS; ++p)
table[a][p] = 0.0;
ncclTaskColl info;
info.func = ncclFuncAllReduce;
info.datatype = ncclBfloat16;
info.algorithm = NCCL_ALGO_RING;
info.protocol = NCCL_PROTO_UNDEF;
ncclResult_t result = rcclOverrideProtocol(ncclProtoStr, table, &info);
EXPECT_EQ(result, ncclSuccess) << "Expected ncclSuccess when override is applied successfully.";
EXPECT_EQ(info.protocol, protoIndex) << "Protocol index should match the "
"override value from environment.";
},
{{"RCCL_OVERRIDE_PROTO", "Simple"}}
);
}
TEST(Rcclwrap, RcclOverrideProtocol_ValidOverridePersists)
{
RUN_ISOLATED_TEST_WITH_ENV("RcclOverrideProtocol_ValidOverridePersists",
[]() {
const char* protoOverrideEnv = getenv("RCCL_OVERRIDE_PROTO");
ASSERT_NE(protoOverrideEnv, nullptr) << "RCCL_OVERRIDE_PROTO should be set";
// Get the index of the protocol from the string for later comparison
int protoIndex = NCCL_PROTO_UNDEF;
ncclResult_t idxResult
= rcclGetAlgoProtoIndex(protoOverrideEnv, ncclProtoStr, NCCL_NUM_PROTOCOLS, protoIndex);
ASSERT_EQ(idxResult, ncclSuccess) << "Failed to get protocol index from string";
// Mark all combinations as valid for the purpose of this test.
float table[NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
for(int a = 0; a < NCCL_NUM_ALGORITHMS; ++a)
for(int p = 0; p < NCCL_NUM_PROTOCOLS; ++p)
table[a][p] = 0.0;
ncclTaskColl info;
info.func = ncclFuncAllReduce;
info.datatype = ncclFloat16;
info.algorithm = NCCL_ALGO_RING;
info.protocol = NCCL_PROTO_UNDEF;
// First call
ncclResult_t result1 = rcclOverrideProtocol(ncclProtoStr, table, &info);
EXPECT_EQ(result1, ncclSuccess)
<< "Expected rcclOverrideProtocol to succeed with valid override";
EXPECT_EQ(info.protocol, protoIndex) << "Expected protocol to match override after first call";
// Second call
ncclResult_t result2 = rcclOverrideProtocol(ncclProtoStr, table, &info);
EXPECT_EQ(result2, ncclSuccess)
<< "Expected rcclOverrideProtocol to succeed again on second call";
EXPECT_EQ(info.protocol, protoIndex) << "Expected protocol to match override after second call";
},
{{"RCCL_OVERRIDE_PROTO", "Simple"}}
);
}
TEST(Rcclwrap, RcclOverrideProtocol_InvalidProtocol)
{
RUN_ISOLATED_TEST_WITH_ENV("RcclOverrideProtocol_InvalidProtocol",
[]() {
float table[NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
ncclTaskColl info;
ncclResult_t result = rcclOverrideProtocol(ncclProtoStr, table, &info);
EXPECT_EQ(result, ncclInvalidUsage) << "Expected ncclInvalidUsage when the "
"override protocol is invalid.";
},
{{"RCCL_OVERRIDE_PROTO", "InvalidProtocol"}}
);
}
TEST(Rcclwrap, RcclOverrideProtocol_InvalidOverridePersists)
{
RUN_ISOLATED_TEST_WITH_ENV("RcclOverrideProtocol_InvalidOverridePersists",
[]() {
float table[NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
ncclTaskColl info;
// First call should fail due to invalid proto string
ncclResult_t result1 = rcclOverrideProtocol(ncclProtoStr, table, &info);
EXPECT_EQ(result1, ncclInvalidUsage) << "Expected rcclOverrideProtocol to fail with invalid "
"RCCL_OVERRIDE_PROTO.";
// Second call should still fail because the static variable disables further
// overrides
ncclResult_t result2 = rcclOverrideProtocol(ncclProtoStr, table, &info);
EXPECT_EQ(result2, ncclInvalidUsage)
<< "Expected rcclOverrideProtocol to continue returning failure after "
"invalid proto was set.";
},
{{"RCCL_OVERRIDE_PROTO", "InvalidProtocol"}}
);
}
TEST(Rcclwrap, RcclOverrideAlgorithm_NoOverride)
{
RUN_ISOLATED_TEST_WITH_ENV("RcclOverrideAlgorithm_NoOverride",
[]() {
float table[NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
ncclTaskColl info;
ncclResult_t result = rcclOverrideAlgorithm(ncclAlgoStr, table, &info);
// Since no override is set, it should return success and do nothing
EXPECT_EQ(result, ncclSuccess)
<< "Expected ncclSuccess when RCCL_OVERRIDE_ALGO is unset, indicating no "
"override should be applied.";
},
{}
);
}
TEST(Rcclwrap, RcclOverrideAlgorithm_UnsupportedOverride)
{
RUN_ISOLATED_TEST_WITH_ENV("RcclOverrideAlgorithm_UnsupportedOverride",
[]() {
float table[NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
for(int a = 0; a < NCCL_NUM_ALGORITHMS; ++a)
for(int p = 0; p < NCCL_NUM_PROTOCOLS; ++p)
table[a][p] = NCCL_ALGO_PROTO_IGNORE;
ncclTaskColl info;
info.func = ncclFuncReduceScatter;
info.datatype = ncclBfloat16;
info.protocol = NCCL_PROTO_SIMPLE;
ncclResult_t result = rcclOverrideAlgorithm(ncclAlgoStr, table, &info);
EXPECT_EQ(result, ncclInternalError)
<< "Expected ncclInternalError when the override algorithm is valid, but "
"not enabled for the selected protocol.";
},
{{"RCCL_OVERRIDE_ALGO", "Ring"}}
);
}
TEST(Rcclwrap, RcclOverrideAlgorithm_ValidOverride)
{
RUN_ISOLATED_TEST_WITH_ENV("RcclOverrideAlgorithm_ValidOverride",
[]() {
const char* algoOverrideEnv = getenv("RCCL_OVERRIDE_ALGO");
ASSERT_NE(algoOverrideEnv, nullptr) << "RCCL_OVERRIDE_ALGO should be set";
// Get the index of the algorithm from the string for later comparison
int algoIndex = NCCL_ALGO_UNDEF;
ncclResult_t idxResult
= rcclGetAlgoProtoIndex(algoOverrideEnv, ncclAlgoStr, NCCL_NUM_ALGORITHMS, algoIndex);
ASSERT_EQ(idxResult, ncclSuccess) << "Failed to get algorithm index from string";
float table[NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
// Mark all combinations as valid for the purpose of this test.
for(int a = 0; a < NCCL_NUM_ALGORITHMS; ++a)
for(int p = 0; p < NCCL_NUM_PROTOCOLS; ++p)
table[a][p] = 0.0;
ncclTaskColl info;
info.func = ncclFuncAllReduce;
info.datatype = ncclBfloat16;
info.protocol = NCCL_PROTO_SIMPLE;
info.algorithm = NCCL_ALGO_UNDEF;
ncclResult_t result = rcclOverrideAlgorithm(ncclAlgoStr, table, &info);
EXPECT_EQ(result, ncclSuccess) << "Expected ncclSuccess when override is applied successfully.";
EXPECT_EQ(info.algorithm, algoIndex)
<< "Algorithm index should match the override value from environment.";
},
{{"RCCL_OVERRIDE_ALGO", "Ring"}}
);
}
TEST(Rcclwrap, RcclOverrideAlgorithm_ValidOverridePersists)
{
RUN_ISOLATED_TEST_WITH_ENV("RcclOverrideAlgorithm_ValidOverridePersists",
[]() {
const char* algoOverrideEnv = getenv("RCCL_OVERRIDE_ALGO");
ASSERT_NE(algoOverrideEnv, nullptr) << "RCCL_OVERRIDE_ALGO should be set";
// Get the index of the algorithm from the string for later comparison
int algoIndex = NCCL_ALGO_UNDEF;
ncclResult_t idxResult
= rcclGetAlgoProtoIndex(algoOverrideEnv, ncclAlgoStr, NCCL_NUM_ALGORITHMS, algoIndex);
ASSERT_EQ(idxResult, ncclSuccess) << "Failed to get algorithm index from string";
// Mark all combinations as valid for the purpose of this test.
float table[NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
for(int a = 0; a < NCCL_NUM_ALGORITHMS; ++a)
for(int p = 0; p < NCCL_NUM_PROTOCOLS; ++p)
table[a][p] = 0.0;
ncclTaskColl info;
info.func = ncclFuncAllReduce;
info.datatype = ncclFloat16;
info.protocol = NCCL_PROTO_SIMPLE;
info.algorithm = NCCL_ALGO_UNDEF;
// First call
ncclResult_t result1 = rcclOverrideAlgorithm(ncclAlgoStr, table, &info);
EXPECT_EQ(result1, ncclSuccess)
<< "Expected rcclOverrideAlgorithm to succeed with valid override.";
EXPECT_EQ(info.algorithm, algoIndex)
<< "Expected algorithm to match override after first call.";
// Second call
ncclResult_t result2 = rcclOverrideAlgorithm(ncclAlgoStr, table, &info);
EXPECT_EQ(result2, ncclSuccess)
<< "Expected rcclOverrideAlgorithm to succeed again on second call.";
EXPECT_EQ(info.algorithm, algoIndex)
<< "Expected algorithm to match override after second call.";
},
{{"RCCL_OVERRIDE_ALGO", "Ring"}}
);
}
TEST(Rcclwrap, RcclOverrideAlgorithm_InvalidAlgorithm)
{
RUN_ISOLATED_TEST_WITH_ENV("RcclOverrideAlgorithm_InvalidAlgorithm",
[]() {
float table[NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
ncclTaskColl info;
ncclResult_t result = rcclOverrideAlgorithm(ncclAlgoStr, table, &info);
EXPECT_EQ(result, ncclInvalidUsage)
<< "Expected ncclInvalidUsage when the override algorithm is invalid.";
},
{{"RCCL_OVERRIDE_ALGO", "InvalidAlgorithm"}}
);
}
TEST(Rcclwrap, RcclOverrideAlgorithm_InvalidOverridePersists)
{
RUN_ISOLATED_TEST_WITH_ENV("RcclOverrideAlgorithm_InvalidOverridePersists",
[]() {
float table[NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
ncclTaskColl info;
// First call should fail due to invalid algo string (and set the static flag)
ncclResult_t result1 = rcclOverrideAlgorithm(ncclAlgoStr, table, &info);
EXPECT_EQ(result1, ncclInvalidUsage) << "Expected rcclOverrideAlgorithm to fail with invalid "
"RCCL_OVERRIDE_ALGO.";
// Second call should also fail due to static validInput=false
ncclResult_t result2 = rcclOverrideAlgorithm(ncclAlgoStr, table, &info);
EXPECT_EQ(result2, ncclInvalidUsage)
<< "Expected rcclOverrideAlgorithm to continue returning failure after "
"invalid algo was set.";
},
{{"RCCL_OVERRIDE_ALGO", "InvalidAlgorithm"}}
);
}
TEST(Rcclwrap, AllrcclSetP2pNetChunkSizeTests)
{
TEST_INFO(
"=== Starting Process-Isolated rcclSetP2pNetChunkSize "
"Tests Execution ==="
);
// Define test case structure
struct P2PChunkSizeTestCase
{
std::string name;
std::string arch;
int ranks;
int expectedChunkSize;
std::unordered_map<std::string, std::string> extraEnv;
};
// Define all test cases
std::vector<P2PChunkSizeTestCase> testCases = {
// GFX942 tests
{ "GFX942_LargeRanks_Isolated","gfx942", 128,1 << 19, {} },
{ "GFX942_BoundaryRank64_Isolated", "gfx942", 64, 1 << 19, {}},
{ "GFX942_BoundaryRank63_Isolated", "gfx942", 63, 1 << 17, {}},
// GFX950 tests
{ "GFX950_SmallRanks_Isolated", "gfx950", 8, 1 << 17, {}},
{ "GFX950_MediumRanks_Isolated", "gfx950", 24, 1 << 18, {}},
{ "GFX950_LargeRanks_Isolated", "gfx950", 64, 1 << 19, {}},
{ "GFX950_BoundaryRank16_Isolated", "gfx950", 16, 1 << 18, {}},
{ "GFX950_BoundaryRank15_Isolated", "gfx950", 15, 1 << 17, {}},
{ "GFX950_BoundaryRank32_Isolated", "gfx950", 32, 1 << 19, {}},
{ "GFX950_BoundaryRank31_Isolated", "gfx950", 31, 1 << 18, {}},
// Unsupported architectures
{ "UnsupportedArch_GFX908_Isolated", "gfx908", 32, RCCL_VALUE_INVALID, {}},
{ "UnsupportedArch_GFX90A_Isolated", "gfx90a", 32, RCCL_VALUE_INVALID, {}},
// Edge cases
{ "EmptyArchString_Isolated", "", 32, RCCL_VALUE_INVALID, {}},
{ "PartialArchMatch_Isolated", "gfx94", 32, RCCL_VALUE_INVALID, {}},
{ "ZeroRanks_GFX942_Isolated", "gfx942", 0, 1 << 17, {}},
{ "ZeroRanks_GFX950_Isolated", "gfx950", 0, 1 << 17, {}},
{ "LargeRankValues_GFX950_Isolated", "gfx950", 1000, 1 << 19, {}},
{ "CaseInsensitiveArch_Isolated", "GFX942", 32, RCCL_VALUE_INVALID, {}},
// Environment variable test
{"WithEnvironmentVariable_Isolated",
"gfx942", 32,
RCCL_VALUE_UNSET, {{"NCCL_P2P_NET_CHUNKSIZE", "123456"}, {"NCCL_MAX_NCHANNELS", "1"}} }
};
// Base environment for all tests
std::unordered_map<std::string, std::string> baseEnv = {
{ "NCCL_DEBUG", "TRACE"},
{"NCCL_DEBUG_SUBSYS", "ALL"}
};
// Register all tests using a loop
for(const auto& tc : testCases)
{
ProcessIsolatedTestRunner::registerTest(
ProcessIsolatedTestRunner::TestConfig(
tc.name,
[tc]()
{
ncclComm_t mockComm = nullptr;
struct ncclTopoSystem mockTopo;
struct ncclTopoNode mockGpuNode;
CreateMockComm(mockComm, mockTopo, mockGpuNode, tc.arch.c_str(), tc.ranks);
int chunkSize = RCCL_VALUE_UNSET;
rcclSetP2pNetChunkSize(mockComm, chunkSize);
// Special handling for environment variable test
if(tc.name == "WithEnvironmentVariable_Isolated")
{
const char* envValue = getenv("NCCL_P2P_NET_CHUNKSIZE");
EXPECT_STREQ(envValue, "123456")
<< "Environment variable should be set to 123456";
EXPECT_NE(chunkSize, RCCL_VALUE_UNSET)
<< "Environment variable should override default logic";
}
else
{
EXPECT_EQ(chunkSize, tc.expectedChunkSize)
<< "Failed for " << tc.arch << " with " << tc.ranks << " ranks";
}
CleanupMockComm(mockComm);
}
)
.withEnvironment(
[&tc, &baseEnv]()
{
auto env = baseEnv;
env.insert(tc.extraEnv.begin(), tc.extraEnv.end());
return env;
}()
)
.withTimeout(std::chrono::seconds(60))
.withNumGpus(0)
);
}
// Configure execution options
ProcessIsolatedTestRunner::ExecutionOptions options;
options.stopOnFirstFailure = false; // Continue running all tests
options.verboseLogging = true;
// Execute all tests
bool allTestsPassed = ProcessIsolatedTestRunner::executeAllTests(options);
// Verify that all tests passed
EXPECT_TRUE(allTestsPassed) << "One or more process-isolated GFX tests failed";
TEST_INFO(
"=== Process-Isolated rcclSetP2pNetChunkSize Tests "
"Execution Completed ==="
);
}
TEST(Rcclwrap, AllPxnTests)
{
// Define test case structure
struct PxnTestCase
{
std::string name;
std::string arch;
int ranks;
int expectedPxnDisable;
std::unordered_map<std::string, std::string> extraEnv;
bool shouldSkipCheck; // For tests with environment variable set
};
// Define all test cases
std::vector<PxnTestCase> testCases = {
// GFX942 tests
{ "PXN_GFX942_SmallRanks_Isolated","gfx942", 32, 1, {},true },
{ "PXN_GFX942_LargeRanks_Isolated", "gfx942", 128, 0, {}, true},
{ "PXN_GFX942_BoundaryRank64_Isolated", "gfx942", 64, 0, {}, true},
{ "PXN_GFX942_BoundaryRank63_Isolated", "gfx942", 63, 1, {}, true},
// GFX950 tests
{ "PXN_GFX950_SmallRanks_Isolated", "gfx950", 8, 1, {}, true},
{ "PXN_GFX950_LargeRanks_Isolated", "gfx950", 64, 0, {}, true},
{ "PXN_GFX950_BoundaryRank32_Isolated", "gfx950", 32, 0, {}, true},
{ "PXN_GFX950_BoundaryRank31_Isolated", "gfx950", 31, 1, {}, true},
// Unsupported architecture
{ "PXN_UnsupportedArch_GFX908_Isolated", "gfx908", 32, RCCL_VALUE_INVALID, {}, true},
// Environment variable test (no skip check needed)
{"PXN_WithEnvironmentVariable_Isolated",
"gfx942", 32,
RCCL_VALUE_INVALID, {{"NCCL_PXN_DISABLE", "1"}},
false }
};
// Base environment for all tests
std::unordered_map<std::string, std::string> baseEnv = {
{ "NCCL_DEBUG", "TRACE"},
{"NCCL_DEBUG_SUBSYS", "ALL"}
};
// Register all tests using a loop
for(const auto& tc : testCases)
{
ProcessIsolatedTestRunner::registerTest(
ProcessIsolatedTestRunner::TestConfig(
tc.name,
[tc]()
{
// Check if we should skip this test due to environment variable being
// set
if(tc.shouldSkipCheck && ShouldSkipPxnTest())
{
GTEST_SKIP()
<< "Skipping " << tc.name << " due to environment variable being set";
return;
}
TEST_INFO(
"Testing rcclSetPxn for %s with %d ranks",
tc.arch.c_str(),
tc.ranks
);
ncclComm_t mockComm = nullptr;
struct ncclTopoSystem mockTopo;
struct ncclTopoNode mockGpuNode;
CreateMockComm(mockComm, mockTopo, mockGpuNode, tc.arch.c_str(), tc.ranks);
int pxnDisable = RCCL_VALUE_UNSET;
rcclSetPxn(mockComm, pxnDisable);
EXPECT_EQ(pxnDisable, tc.expectedPxnDisable)
<< "Failed for " << tc.arch << " with " << tc.ranks << " ranks";
TEST_INFO(
"%s test completed - pxnDisable: %d",
tc.name.c_str(),
pxnDisable
);
CleanupMockComm(mockComm);
}
)
.withEnvironment(
[&tc, &baseEnv]()
{
auto env = baseEnv;
env.insert(tc.extraEnv.begin(), tc.extraEnv.end());
return env;
}()
)
.withNumGpus(0)
);
}
// Configure execution options for sequential execution with stop on first
// failure
ProcessIsolatedTestRunner::ExecutionOptions options;
options.stopOnFirstFailure = true;
options.verboseLogging = true;
// Execute all registered tests
bool allTestsPassed = ProcessIsolatedTestRunner::executeAllTests(options);
EXPECT_TRUE(allTestsPassed) << "One or more PXN process-isolated tests failed";
}
TEST(Rcclwrap, CucheckMacro_CheckStickyHipErrorOnFailure)
{
hipError_t hipErr = hipSetDevice(0);
if(hipErr != hipSuccess)
{
GTEST_SKIP() << "No GPU available";
}
// Clear any pre-existing sticky error so we start clean
(void)hipGetLastError();
// Force a HIP failure through CUCHECK using an invalid device pointer (0x1)
ncclResult_t ret = triggerCucheckFailure();
EXPECT_EQ(ncclUnhandledCudaError, ret)
<< "CUCHECK should return ncclUnhandledCudaError on failure";
EXPECT_EQ(hipSuccess, hipGetLastError())
<< "CUCHECK must clear sticky HIP error after failure";
}
TEST(Rcclwrap, CucheckgotoMacro_CheckStickyHipErrorOnFailure)
{
hipError_t hipErr = hipSetDevice(0);
if(hipErr != hipSuccess)
{
GTEST_SKIP() << "No GPU available";
}
// Clear any pre-existing sticky error so we start clean
(void)hipGetLastError();
// Force a HIP failure through CUCHECKGOTO using an invalid device pointer (0x1)
ncclResult_t ret = ncclSuccess;
CUCHECKGOTO(hipPointerGetAttribute(nullptr, HIP_POINTER_ATTRIBUTE_CONTEXT,
(hipDeviceptr_t)0x1),
ret, check_sticky);
check_sticky:
EXPECT_EQ(ncclUnhandledCudaError, ret)
<< "CUCHECKGOTO should set result to ncclUnhandledCudaError on failure";
EXPECT_EQ(hipSuccess, hipGetLastError())
<< "CUCHECKGOTO must clear sticky HIP error after failure";
}
TEST(Rcclwrap, RcclUseAllGatherDirectNodeCountTests)
{
TEST_INFO("=== Starting Process-Isolated rcclUseAllGatherDirect Node Count Tests ===");
// Test case structure for AllGather Direct node-count gating tests
struct AGDirectNodeCountTestCase
{
std::string name;
std::string arch;
int nRanks;
int nNodes;
bool requiresAinic; // Skip if AINIC not present
std::unordered_map<std::string, std::string> extraEnv;
};
std::vector<AGDirectNodeCountTestCase> testCases = {
// nNodes > 32: must return false for any NIC type (hardware-independent)
{
"AGDirect_Disabled_nNodes33",
"gfx942",
128, // >= 64 ranks so PXN is enabled for gfx942
33, // > 32 nodes
false,
{}
},
// nNodes > 16 with AINIC: must return false (skipped when AINIC hardware absent)
{
"AGDirect_Disabled_AINIC_nNodes17",
"gfx942",
128, // >= 64 ranks so PXN is enabled for gfx942
17, // > 16 nodes
true,
{}
},
};
// Base environment: clean state, no env vars that would short-circuit earlier checks
std::unordered_map<std::string, std::string> baseEnv = {
{ "NCCL_DEBUG", "TRACE"},
{"NCCL_DEBUG_SUBSYS", "ALL"}
};
for(const auto& tc : testCases)
{
ProcessIsolatedTestRunner::registerTest(
ProcessIsolatedTestRunner::TestConfig(
tc.name,
[tc]()
{
// Skip AINIC tests when AINIC hardware is not present, since
// rcclUseAinic() relies on hardware detection and cannot be
// forced in a test environment without actual AINIC NICs.
if(tc.requiresAinic && !rcclUseAinic())
{
GTEST_SKIP() << "Skipping " << tc.name
<< ": AINIC hardware not present";
return;
}
ncclComm_t mockComm = nullptr;
struct ncclTopoSystem mockTopo;
struct ncclTopoNode mockGpuNode;
CreateMockComm(mockComm, mockTopo, mockGpuNode, tc.arch.c_str(), tc.nRanks);
mockComm->nNodes = tc.nNodes;
// Use a message size that passes the threshold check so only
// the node-count guards are responsible for any false return.
size_t msgSize = 8388608; // 8 MiB
bool result = rcclUseAllGatherDirect(mockComm, msgSize);
EXPECT_FALSE(result)
<< "Expected rcclUseAllGatherDirect to return false for "
<< tc.arch << " with nNodes=" << tc.nNodes;
CleanupMockComm(mockComm);
}
)
.withEnvironment(
[&tc, &baseEnv]()
{
auto env = baseEnv;
env.insert(tc.extraEnv.begin(), tc.extraEnv.end());
return env;
}()
)
.withTimeout(std::chrono::seconds(60))
.withNumGpus(0)
);
}
ProcessIsolatedTestRunner::ExecutionOptions options;
options.stopOnFirstFailure = false;
options.verboseLogging = true;
bool allTestsPassed = ProcessIsolatedTestRunner::executeAllTests(options);
EXPECT_TRUE(allTestsPassed) << "One or more AllGather Direct node count tests failed";
TEST_INFO("=== Process-Isolated rcclUseAllGatherDirect Node Count Tests Completed ===");
}
TEST(Rcclwrap, RcclUseHierarchicalAllGatherTests)
{
TEST_INFO("=== Starting Process-Isolated rcclUseHierarchicalAllGather Tests ===");
struct HierAGCase
{
std::string name;
int nNodes;
bool hierCommsInit;
size_t msgSize;
bool expected;
std::unordered_map<std::string, std::string> extraEnv;
};
const size_t HALF = HIERARCHICAL_AG_TEMP_BUFFER_SIZE / 2;
const size_t QUARTER = HIERARCHICAL_AG_TEMP_BUFFER_SIZE / 4;
const size_t FULL = HIERARCHICAL_AG_TEMP_BUFFER_SIZE;
std::vector<HierAGCase> testCases = {
// nNodes < 8 --> disabled
{"LessThan8Nodes", 4, true, 1ULL << 20, false, {}},
// sub-comms not initialized --> disabled
{"CommsNotInitialized", 16, false, 1ULL << 20, false, {}},
// 8-15 nodes --> limit is 32MB
{"Enabled_8Nodes_AtQuarter", 8, true, QUARTER, true, {}},
{"Disabled_8Nodes_AboveQuarter", 8, true, QUARTER + 1, false, {}},
{"Enabled_15Nodes_AtQuarter", 15, true, QUARTER, true, {}},
{"Disabled_15Nodes_AtHalf", 15, true, HALF, false, {}},
// 16-31 nodes --> limit is 64MB
{"Enabled_16Nodes_AtHalf", 16, true, HALF, true, {}},
{"Disabled_16Nodes_AboveHalf", 16, true, HALF + 1, false, {}},
{"Enabled_31Nodes_AtHalf", 31, true, HALF, true, {}},
{"Disabled_31Nodes_AtFull", 31, true, FULL, false, {}},
// 32+ nodes --> limit is 128MB
{"Enabled_32Nodes_AtHalf", 32, true, HALF, true, {}},
{"Enabled_32Nodes_AtFull", 32, true, FULL, true, {}},
{"Disabled_32Nodes_AboveFull", 32, true, FULL + 1, false, {}},
{"Enabled_64Nodes_AtFull", 64, true, FULL, true, {}},
// env var forces off --> disabled
{"DisabledByEnvVar", 16, true, 1ULL << 20, false, {{"RCCL_HIERARCHICAL_ALLGATHER", "0"}}},
};
// Base environment shared by every case
std::unordered_map<std::string, std::string> baseEnv = {
{ "NCCL_DEBUG", "TRACE"},
{"NCCL_DEBUG_SUBSYS", "ALL"}
};
for(const auto& tc : testCases)
{
ProcessIsolatedTestRunner::registerTest(
ProcessIsolatedTestRunner::TestConfig(
tc.name,
[tc]()
{
ncclComm_t mockComm = nullptr;
struct ncclTopoSystem mockTopo;
struct ncclTopoNode mockGpu;
CreateMockComm(mockComm,
mockTopo,
mockGpu,
"gfx942",
/*nRanks=*/8 * tc.nNodes);
mockComm->nNodes = tc.nNodes;
mockComm->hierarchicalCommsInitialized = tc.hierCommsInit;
EXPECT_EQ(rcclUseHierarchicalAllGather(mockComm, tc.msgSize),
tc.expected)
<< "Case: " << tc.name
<< " (nNodes=" << tc.nNodes
<< ", hierCommsInit=" << tc.hierCommsInit
<< ", msgSize=" << tc.msgSize << ")";
CleanupMockComm(mockComm);
}
)
.withEnvironment(
[&tc, &baseEnv]()
{
auto env = baseEnv;
env.insert(tc.extraEnv.begin(), tc.extraEnv.end());
return env;
}()
)
.withTimeout(std::chrono::seconds(60))
);
}
ProcessIsolatedTestRunner::ExecutionOptions options;
options.stopOnFirstFailure = false;
options.verboseLogging = true;
bool allTestsPassed = ProcessIsolatedTestRunner::executeAllTests(options);
EXPECT_TRUE(allTestsPassed)
<< "One or more rcclUseHierarchicalAllGather tests failed";
TEST_INFO("=== Process-Isolated rcclUseHierarchicalAllGather Tests Completed ===");
}
#ifdef ENABLE_WARP_SPEED
// ---------------------------------------------------------------------------
// WarpSpeed enablement / channel-math helpers (rccl_wrap.cc)
//
// These exercise the pure decision/tuning helpers that gate WarpSpeed:
// - rcclCanUseWarpSpeedAuto (arch/node/env eligibility)
// - rcclGetMaxWarpsPerBlock (warps-per-block multiplier)
// - rcclWarpSpeedComputeNChannels (connect.cc channel math)
// - rcclWarpSpeedAdjustChannels (enqueue.cc per-collective channel adj.)
// ---------------------------------------------------------------------------
// rcclCanUseWarpSpeedAuto: gfx950 single-node with auto mode on -> eligible.
TEST(Rcclwrap, CanUseWarpSpeedAuto_Gfx950SingleNode_True)
{
// Auto mode defaults to 1; if the environment forces it off, the eligibility
// result would legitimately be false, so skip to keep the test deterministic.
if(rcclParamWarpSpeedAutoMode() == 0)
{
GTEST_SKIP() << "RCCL_WARP_SPEED_AUTO=0 in environment";
}
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", /*nRanks=*/8);
comm->cuCount = 256; // SPX mode (256 CU on gfx950); auto mode requires cuCount > 128
EXPECT_TRUE(rcclCanUseWarpSpeedAuto(comm, /*nNodes=*/1));
comm->cuCount = 128; // Not SPX mode (128 CU on gfx950); auto mode requires cuCount > 128
EXPECT_FALSE(rcclCanUseWarpSpeedAuto(comm, /*nNodes=*/1));
CleanupMockComm(comm);
}
// rcclCanUseWarpSpeedAuto: non-gfx950 arch is never eligible.
TEST(Rcclwrap, CanUseWarpSpeedAuto_NonGfx950_False)
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx942", /*nRanks=*/8);
EXPECT_FALSE(rcclCanUseWarpSpeedAuto(comm, /*nNodes=*/1));
CleanupMockComm(comm);
}
// rcclCanUseWarpSpeedAuto: multi-node is never eligible (auto mode is single-node).
TEST(Rcclwrap, CanUseWarpSpeedAuto_MultiNode_False)
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", /*nRanks=*/16);
EXPECT_FALSE(rcclCanUseWarpSpeedAuto(comm, /*nNodes=*/2));
CleanupMockComm(comm);
}
// rcclCanUseWarpSpeedAuto: RCCL_WARP_SPEED_AUTO=0 disables eligibility even on
// gfx950 single-node. Isolated so the cached RCCL_PARAM value doesn't leak.
TEST(Rcclwrap, CanUseWarpSpeedAuto_AutoModeDisabled_False)
{
RUN_ISOLATED_TEST_WITH_ENV(
"CanUseWarpSpeedAuto_AutoModeDisabled_False",
[]()
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", /*nRanks=*/8);
comm->cuCount = 256; // ensure only RCCL_WARP_SPEED_AUTO=0 makes this ineligible
EXPECT_FALSE(rcclCanUseWarpSpeedAuto(comm, /*nNodes=*/1));
CleanupMockComm(comm);
},
{{"RCCL_WARP_SPEED_AUTO", "0"}}
);
}
// rcclGetMaxWarpsPerBlock: single node -> RCCL_SINGLE_NODE_MAX_NTHREADS / WarpSize.
TEST(Rcclwrap, GetMaxWarpsPerBlock_SingleNode)
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", /*nRanks=*/8);
comm->nNodes = 1;
comm->WarpSize = 64;
EXPECT_EQ(rcclGetMaxWarpsPerBlock(comm), RCCL_SINGLE_NODE_MAX_NTHREADS / 64);
CleanupMockComm(comm);
}
// rcclGetMaxWarpsPerBlock: multi-node gfx950 -> RCCL_GFX950_MAX_NTHREADS / WarpSize.
TEST(Rcclwrap, GetMaxWarpsPerBlock_MultiNodeGfx950)
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", /*nRanks=*/16);
comm->nNodes = 2;
comm->WarpSize = 64;
EXPECT_EQ(rcclGetMaxWarpsPerBlock(comm), RCCL_GFX950_MAX_NTHREADS / 64);
CleanupMockComm(comm);
}
// rcclGetMaxWarpsPerBlock: multi-node non-gfx950 -> RCCL_DEFAULT_MAX_NTHREADS / WarpSize.
TEST(Rcclwrap, GetMaxWarpsPerBlock_MultiNodeOtherArch)
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx942", /*nRanks=*/16);
comm->nNodes = 2;
comm->WarpSize = 64;
EXPECT_EQ(rcclGetMaxWarpsPerBlock(comm), RCCL_DEFAULT_MAX_NTHREADS / 64);
CleanupMockComm(comm);
}
// Documents a latent issue: rcclGetMaxWarpsPerBlock branches on single-node vs
// multi-node (and arch), and the single-node comment claims "half the number of
// threads", but RCCL_SINGLE_NODE_MAX_NTHREADS == RCCL_GFX950_MAX_NTHREADS ==
// RCCL_DEFAULT_MAX_NTHREADS (all 256), so every branch returns the same value and
// the intended single-node halving is currently a no-op. If the constants are ever
// differentiated (as the comment intends), this test should be updated.
TEST(Rcclwrap, GetMaxWarpsPerBlock_BranchesCurrentlyEquivalent)
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", /*nRanks=*/8);
comm->WarpSize = 64;
comm->nNodes = 1;
const int single = rcclGetMaxWarpsPerBlock(comm);
comm->nNodes = 2;
const int multi = rcclGetMaxWarpsPerBlock(comm);
EXPECT_EQ(single, multi)
<< "Single-node is documented to use half the threads, but the MAX_NTHREADS "
"constants are identical so the branch has no effect.";
CleanupMockComm(comm);
}
// rcclWarpSpeedComputeNChannels: single-node, no user override, gfx950 8-rank ->
// nc*nChannels*mult, then halved for the gfx950 single-node 8-rank special case.
TEST(Rcclwrap, ComputeNChannels_SingleNode_Gfx950_8Ranks_Halved)
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", /*nRanks=*/8);
comm->nNodes = 1;
comm->nChannels = 2;
// maxNchannels = nc(2) * nChannels(2) * mult(4) = 16; single-node keeps 16;
// gfx950 && single-node && nRanks==8 -> nc /= 2 -> 8.
const int nc = rcclWarpSpeedComputeNChannels(comm, /*nc=*/2, /*channelMultiplier=*/4,
/*maxChannels=*/64, /*adjustedMaxNchannels=*/64,
/*userUpdatedMaxChannels=*/false);
EXPECT_EQ(nc, 8);
CleanupMockComm(comm);
}
// rcclWarpSpeedComputeNChannels: single-node, non-8-rank -> no halving.
TEST(Rcclwrap, ComputeNChannels_SingleNode_4Ranks_NoHalving)
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", /*nRanks=*/4);
comm->nNodes = 1;
comm->nChannels = 2;
const int nc = rcclWarpSpeedComputeNChannels(comm, /*nc=*/2, /*channelMultiplier=*/4,
/*maxChannels=*/64, /*adjustedMaxNchannels=*/64,
/*userUpdatedMaxChannels=*/false);
EXPECT_EQ(nc, 16); // 2*2*4, no halving
CleanupMockComm(comm);
}
// rcclWarpSpeedComputeNChannels: multi-node, no user override -> capped by maxChannels,
// no halving (halving is single-node only).
TEST(Rcclwrap, ComputeNChannels_MultiNode_CappedByMaxChannels)
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", /*nRanks=*/8);
comm->nNodes = 2;
comm->nChannels = 2;
// maxNchannels = 2*2*4 = 16; multi-node -> min(16, maxChannels=8) = 8; no halving.
const int nc = rcclWarpSpeedComputeNChannels(comm, /*nc=*/2, /*channelMultiplier=*/4,
/*maxChannels=*/8, /*adjustedMaxNchannels=*/64,
/*userUpdatedMaxChannels=*/false);
EXPECT_EQ(nc, 8);
CleanupMockComm(comm);
}
// rcclWarpSpeedComputeNChannels: user override path uses adjustedMaxNchannels*mult
// (never halved), below the MAXCHANNELS clamp.
TEST(Rcclwrap, ComputeNChannels_UserOverride_NoClamp)
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", /*nRanks=*/8);
comm->nNodes = 1;
comm->nChannels = 2;
// userUpdated -> nc = min(adjusted(10) * mult(4), MAXCHANNELS) = 40; no halving.
const int nc = rcclWarpSpeedComputeNChannels(comm, /*nc=*/2, /*channelMultiplier=*/4,
/*maxChannels=*/64, /*adjustedMaxNchannels=*/10,
/*userUpdatedMaxChannels=*/true);
EXPECT_EQ(nc, 40);
CleanupMockComm(comm);
}
// rcclWarpSpeedComputeNChannels: user override clamps to MAXCHANNELS (512 with WS).
TEST(Rcclwrap, ComputeNChannels_UserOverride_ClampedToMaxChannels)
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", /*nRanks=*/8);
comm->nNodes = 1;
comm->nChannels = 2;
// adjusted(200) * mult(4) = 800, clamped to MAXCHANNELS.
const int nc = rcclWarpSpeedComputeNChannels(comm, /*nc=*/2, /*channelMultiplier=*/4,
/*maxChannels=*/64, /*adjustedMaxNchannels=*/200,
/*userUpdatedMaxChannels=*/true);
EXPECT_EQ(nc, MAXCHANNELS);
CleanupMockComm(comm);
}
// rcclWarpSpeedAdjustChannels: disabled -> nc unchanged.
TEST(Rcclwrap, AdjustChannels_Disabled_NoChange)
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", /*nRanks=*/8);
comm->topo->warpSpeedEnabled = false;
comm->warpSpeedChannelMultiplier = 4;
ncclTaskColl info = {};
info.func = ncclFuncAllReduce;
EXPECT_EQ(rcclWarpSpeedAdjustChannels(comm, &info, /*nc=*/32), 32);
CleanupMockComm(comm);
}
// rcclWarpSpeedAdjustChannels: enabled -> nc divided by the channel multiplier.
// Non-gfx950 arch avoids the single-node 8-rank doubling special case.
TEST(Rcclwrap, AdjustChannels_Enabled_DividesByMultiplier)
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx942", /*nRanks=*/8);
comm->topo->warpSpeedEnabled = true;
comm->warpSpeedChannelMultiplier = 4;
ncclTaskColl info = {};
info.func = ncclFuncAllReduce;
EXPECT_EQ(rcclWarpSpeedAdjustChannels(comm, &info, /*nc=*/32), 8); // 32/4
CleanupMockComm(comm);
}
// rcclWarpSpeedAdjustChannels: gfx950 single-node 8-rank, non-(AR/AG/RS) collective
// -> divided then doubled (the "reduced CU usage" special case).
TEST(Rcclwrap, AdjustChannels_Gfx950SingleNode8Ranks_NonMainColl_Doubles)
{
// Relies on default RCCL_MAX_NCHANNELS (-2, i.e. < 0). Isolate so the cached
// ncclParamMaxNchannels() value is deterministic regardless of test ordering.
RUN_ISOLATED_TEST_WITH_ENV(
"AdjustChannels_Gfx950SingleNode8Ranks_NonMainColl_Doubles",
[]()
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", /*nRanks=*/8);
comm->nNodes = 1;
comm->topo->warpSpeedEnabled = true;
comm->warpSpeedChannelMultiplier = 4;
ncclTaskColl info = {};
info.func = ncclFuncBroadcast; // not AR/AG/RS
EXPECT_EQ(rcclWarpSpeedAdjustChannels(comm, &info, /*nc=*/32), 16); // 32/4=8, *2=16
CleanupMockComm(comm);
},
{}
);
}
// rcclWarpSpeedAdjustChannels: gfx950 single-node 8-rank, main collective
// (AllReduce) -> divided only, not doubled.
TEST(Rcclwrap, AdjustChannels_Gfx950SingleNode8Ranks_MainColl_NoDouble)
{
RUN_ISOLATED_TEST_WITH_ENV(
"AdjustChannels_Gfx950SingleNode8Ranks_MainColl_NoDouble",
[]()
{
ncclComm_t comm = nullptr;
struct ncclTopoSystem topo;
struct ncclTopoNode gpu;
CreateMockComm(comm, topo, gpu, "gfx950", /*nRanks=*/8);
comm->nNodes = 1;
comm->topo->warpSpeedEnabled = true;
comm->warpSpeedChannelMultiplier = 4;
ncclTaskColl info = {};
info.func = ncclFuncAllReduce; // main collective, excluded from doubling
EXPECT_EQ(rcclWarpSpeedAdjustChannels(comm, &info, /*nc=*/32), 8); // 32/4, no doubling
CleanupMockComm(comm);
},
{}
);
}
#endif // ENABLE_WARP_SPEED
} // namespace RcclUnitTesting