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

262 lines
9.3 KiB
C++

/*************************************************************************
* Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved.
*
* See LICENSE.txt for license information
************************************************************************/
// Topology unit tests proving send / recv / all-to-all honor
// NCCL_NETDEVS_POLICY on the P2P NET path.
//
// NCCL 2.29.2 made all2all, send, and recv obey NCCL_NETDEVS_POLICY when
// running over the network. The net transport (transport/net.cc) selects the
// NIC for each P2P send/recv channel through ncclTopoGetNetDev(), which for a
// remote peer resolves the device via the policy-aware ncclTopoGetLocalNet().
// These tests drive ncclTopoGetNetDev() directly -- the exact entry point the
// collectives use -- so the policy behavior is validated where send / recv /
// all-to-all actually consume it (rather than at the lower-level helpers).
//
// Setup is hardware-independent:
// * Loads tools/topo_expl/models/topo_8p6l_5nic.xml (GPU rank 0 reaches 3
// NICs) directly from XML -- no real GPUs, NICs, or MPI launch.
// * Builds a minimal MockComm (topo + rank + nRanks + zeroed peerInfo) so
// ncclTopoComputePaths()/ncclTopoGetNetDev() do not dereference a null comm.
// * Sets NCCL_P2P_PXN_LEVEL=0 so (with the default NCCL_CROSS_NIC=2)
// ncclTopoGetNetDev() skips the PXN/cross-NIC override and returns purely
// the policy-selected local NIC for (rank, channelId).
// * Runs each case in an isolated process (RUN_ISOLATED_TEST_WITH_ENV) so the
// std::call_once policy parse is fresh per case and env values do not leak.
//
// Target: rccl-UnitTestsFixturesDebug (internal symbols are hidden in Release).
#include <gtest/gtest.h>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <set>
#include <string>
#include <unistd.h>
#include "comm.h" // struct ncclComm, struct ncclPeerInfo
#include "graph.h" // ncclTopoGetNetDev(), ncclTopoComputePaths(), ncclTopoFree()
#include "graph/topo.h" // struct ncclTopoSystem, ncclTopoGetSystemFromXml(), GPU/NET
#include "graph/xml.h" // struct ncclXml, ncclTopoGetXmlFromFile()
#include "ProcessIsolatedTestRunner.hpp"
#ifndef RCCL_TEST_SOURCE_DIR
#define RCCL_TEST_SOURCE_DIR "."
#endif
namespace RcclUnitTesting
{
namespace
{
// GPU rank under test and the (remote) peer we resolve a NIC toward. With
// NCCL_P2P_PXN_LEVEL=0 and the default NCCL_CROSS_NIC=2, ncclTopoGetNetDev()
// returns the policy-selected local NIC of kRank for each channel, regardless
// of the peer, so kPeerRank only needs to differ from kRank.
constexpr int kRank = 0;
constexpr int kPeerRank = 1;
// 8 GPUs / 6 leaf / multi-NIC model. GPU rank 0 lives on NUMA 0 and reaches
// 3 NICs (mlx5_0/1/2), shared with the other 3 GPUs on that NUMA node.
std::string multiNicTopoPath()
{
return std::string(RCCL_TEST_SOURCE_DIR) +
"/../tools/topo_expl/models/topo_8p6l_5nic.xml";
}
struct ncclXml* allocateXml(int maxNodes)
{
size_t size =
offsetof(struct ncclXml, nodes) + sizeof(struct ncclXmlNode) * maxNodes;
struct ncclXml* xml = static_cast<struct ncclXml*>(malloc(size));
if(xml)
{
memset(xml, 0, size);
xml->maxNodes = maxNodes;
xml->maxIndex = 0;
}
return xml;
}
// Build an ncclTopoSystem directly from a topology XML file (no real hardware).
ncclResult_t loadTopoSystem(const char* path, struct ncclTopoSystem** system)
{
struct ncclXml* xml = allocateXml(NCCL_TOPO_XML_MAX_NODES);
if(xml == nullptr) return ncclSystemError;
ncclResult_t res = ncclTopoGetXmlFromFile(path, xml, /*warn=*/0);
if(res != ncclSuccess)
{
free(xml);
return res;
}
res = ncclTopoGetSystemFromXml(xml, system, /*localHostHash=*/0);
free(xml);
return res;
}
// Minimal stand-in communicator for topology-only path computation and NIC
// selection. ncclTopoComputePaths()/ncclTopoGetNetDev() dereference comm (PXN
// branch), so we supply just enough state and a zeroed peerInfo[] -- no real
// hardware. Mirrors the harness used by the NCCL_NETDEVS_POLICY parser tests.
struct MockComm
{
struct ncclComm* comm = nullptr;
struct ncclPeerInfo* peerInfo = nullptr;
explicit MockComm(struct ncclTopoSystem* system)
{
comm = new ncclComm();
memset(comm, 0, sizeof(*comm));
comm->topo = system;
comm->rank = kRank;
comm->nRanks = system->nodes[GPU].count;
comm->nNodes = 1;
peerInfo = new ncclPeerInfo[NCCL_TOPO_MAX_NODES];
memset(peerInfo, 0, NCCL_TOPO_MAX_NODES * sizeof(*peerInfo));
for(int r = 0; r < NCCL_TOPO_MAX_NODES; r++)
peerInfo[r].rank = r;
comm->peerInfo = peerInfo;
}
~MockComm()
{
delete[] peerInfo;
delete comm;
}
MockComm(const MockComm&) = delete;
MockComm& operator=(const MockComm&) = delete;
};
// Load the multi-NIC fixture, compute paths, run body(comm), then free.
void withTopoComm(const std::function<void(struct ncclComm*)>& body)
{
const std::string topoPath = multiNicTopoPath();
if(access(topoPath.c_str(), R_OK) != 0)
{
GTEST_SKIP() << "Topology fixture not found: " << topoPath;
return;
}
struct ncclTopoSystem* system = nullptr;
ASSERT_EQ(loadTopoSystem(topoPath.c_str(), &system), ncclSuccess);
ASSERT_NE(system, nullptr);
MockComm mock(system);
ASSERT_EQ(ncclTopoComputePaths(system, mock.comm), ncclSuccess);
body(mock.comm);
ncclTopoFree(system);
}
// NICs locally reachable from rank's GPU -- the pool the policy selects from.
int localNetCount(struct ncclComm* comm)
{
int gpu = -1;
EXPECT_EQ(ncclTopoRankToIndex(comm->topo, kRank, &gpu, /*showWarn=*/true), ncclSuccess);
int locals[NCCL_TOPO_MAX_NODES];
int count = 0;
EXPECT_EQ(ncclTopoGetLocal(comm->topo, GPU, gpu, NET, locals, &count, nullptr), ncclSuccess);
return count;
}
// Net devices that the send/recv NIC selector hands out as the channel rotates.
// This is the collective-facing path: transport/net.cc calls ncclTopoGetNetDev()
// per channel to pick the NIC for each P2P send/recv connection.
std::set<int> netDevsAcrossChannels(struct ncclComm* comm, int numChannels)
{
std::set<int> devs;
for(int c = 0; c < numChannels; c++)
{
int64_t netId = -1;
int netDev = -1;
int proxyRank = -1;
EXPECT_EQ(ncclTopoGetNetDev(comm, kRank, /*graph=*/nullptr, c, kPeerRank,
&netId, &netDev, &proxyRank),
ncclSuccess);
devs.insert(netDev);
}
return devs;
}
} // namespace
// NCCL_NETDEVS_POLICY=MAX:1 -- send/recv/all-to-all must funnel every channel
// through a single NIC: ncclTopoGetNetDev() returns the same device for all
// channels.
TEST(NetDevsPolicyP2pNetTests, Max1_SendRecvNetSelectionUsesSingleNic)
{
RUN_ISOLATED_TEST_WITH_ENV(
"Max1_SendRecvNetSelectionUsesSingleNic",
[]()
{
withTopoComm(
[](struct ncclComm* comm)
{
const int nets = localNetCount(comm);
ASSERT_GE(nets, 2) << "fixture must expose multiple NICs to rank 0";
std::set<int> devs = netDevsAcrossChannels(comm, /*numChannels=*/nets * 4);
EXPECT_EQ(devs.size(), 1u)
<< "MAX:1 must restrict send/recv to a single network device";
});
},
{{"NCCL_NETDEVS_POLICY", "MAX:1"}, {"NCCL_P2P_PXN_LEVEL", "0"}});
}
// NCCL_NETDEVS_POLICY=ALL -- send/recv/all-to-all must spread across every
// locally reachable NIC: ncclTopoGetNetDev() rotates over all of them.
TEST(NetDevsPolicyP2pNetTests, All_SendRecvNetSelectionRotatesAllNics)
{
RUN_ISOLATED_TEST_WITH_ENV(
"All_SendRecvNetSelectionRotatesAllNics",
[]()
{
withTopoComm(
[](struct ncclComm* comm)
{
const int nets = localNetCount(comm);
ASSERT_GE(nets, 2) << "fixture must expose multiple NICs to rank 0";
std::set<int> devs = netDevsAcrossChannels(comm, /*numChannels=*/nets * 4);
EXPECT_EQ(static_cast<int>(devs.size()), nets)
<< "ALL must use every locally reachable network device";
});
},
{{"NCCL_NETDEVS_POLICY", "ALL"}, {"NCCL_P2P_PXN_LEVEL", "0"}});
}
// NCCL_NETDEVS_POLICY=MAX:2 -- send/recv/all-to-all must use exactly two NICs
// (strictly between MAX:1 and the full 3-NIC pool).
TEST(NetDevsPolicyP2pNetTests, Max2_SendRecvNetSelectionUsesTwoNics)
{
RUN_ISOLATED_TEST_WITH_ENV(
"Max2_SendRecvNetSelectionUsesTwoNics",
[]()
{
withTopoComm(
[](struct ncclComm* comm)
{
const int nets = localNetCount(comm);
ASSERT_GE(nets, 3) << "fixture must expose at least 3 NICs to rank 0";
std::set<int> devs = netDevsAcrossChannels(comm, /*numChannels=*/nets * 4);
EXPECT_EQ(devs.size(), 2u)
<< "MAX:2 must restrict send/recv to two network devices";
});
},
{{"NCCL_NETDEVS_POLICY", "MAX:2"}, {"NCCL_P2P_PXN_LEVEL", "0"}});
}
} // namespace RcclUnitTesting