Skip to content

Commit 52d0078

Browse files
minsiimeta-codesync[bot]
authored andcommitted
Fix vnode/nolocal/vClique topology to preserve real pids (#2153)
Summary: Pull Request resolved: #2153 Fix IpcRegCache key collision caused by fake pids in virtual topology modes: - initRankTopologyVnode and initRankTopologyNolocal never set pid (default -1), causing all ranks on the same vnode to share the same gPid(). This made IpcRegCache deduplicate cuMem IPC imports incorrectly, corrupting AllGatherP data with ncclMemAlloc buffers. - initRankStatesTopology now always does bootstrap allgather when bootstrap is available (prod and dist test paths), falling back to fake topology only when bootstrap is nullptr (unit tests, benchmarks). - setRankStatesTopologies gains vClique host override alongside existing vnode/nolocal overrides, so the bootstrap path handles all virtual topology modes. - initRankTopologyVnode/initRankTopologyNolocal now set getpid() for the local rank, so even non-bootstrap callers get a real pid. - Test makeCtranComm (CtranDistTestUtils) simplified to always use initRankStatesTopology(bootstrap) — virtual topology overrides are handled internally. - Added VnodeTopologyHasRealPid unit test and fixed gPidTest to match host:pid format. - Enabled CreateVCliqueSizeFromNcclComm and added host/gPid validation with oobAllGather. Reviewed By: dsjohns2 Differential Revision: D101393262 fbshipit-source-id: dc7e3e717199ad9a9eb58079bec81e0e7ecd90cd
1 parent 351f0db commit 52d0078

7 files changed

Lines changed: 203 additions & 98 deletions

File tree

comms/ctran/commstate/CommStateX.cc

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ namespace {
3838
CHECK_VALID_RANK(rank, rankStates_.size()); \
3939
} while (0)
4040

41+
// Return vnode nLocalRanks from global debug CVAR, or 0 if not set.
42+
int getGlobalVNodeNLocalRanks() {
43+
if (NCCL_COMM_STATE_DEBUG_TOPO == NCCL_COMM_STATE_DEBUG_TOPO::vnode) {
44+
return static_cast<int>(NCCL_COMM_STATE_DEBUG_TOPO_VNODE_NLOCALRANKS);
45+
}
46+
return 0;
47+
}
48+
4149
} // namespace
4250

4351
CommStateX::CommStateX(
@@ -62,7 +70,8 @@ CommStateX::CommStateX(
6270
noLocal_(
6371
noLocal ||
6472
NCCL_COMM_STATE_DEBUG_TOPO == NCCL_COMM_STATE_DEBUG_TOPO::nolocal),
65-
vCliqueSize_(vCliqueSize) {
73+
vCliqueSize_(
74+
vCliqueSize > 0 ? vCliqueSize : getGlobalVNodeNLocalRanks()) {
6675
setRankStatesTopologies(std::move(rankTopologies));
6776
setCommRankToWorldRanks(std::move(commRanksToWorldRanks));
6877
}
@@ -75,24 +84,26 @@ void CommStateX::initRankTopologyNolocal() {
7584
for (int r = 0; r < nRanks_; ++r) {
7685
auto& rankState = rankStates_.at(r);
7786
rankState.rank = r;
87+
rankState.pid = (r == rank_) ? getpid() : -1;
7888
rankState.nodeId = r;
7989
rankState.localRank = 0;
8090
rankState.nLocalRanks = 1;
8191
rankState.localRankToRanks.assign(1, r);
8292
const std::string nolocalHost("nolocal_node_" + std::to_string(r));
8393
rankState.host = nolocalHost;
84-
hostToRanks_[nolocalHost].emplace_back(r);
8594
nodeRanks_[rankState.nodeId].emplace_back(rankState.rank);
8695
}
8796
}
8897

8998
void CommStateX::initRankTopologyVnode(const int nLocalRanks) {
9099
rankStates_.resize(nRanks_);
91-
nodeRanks_.resize(nRanks_);
100+
const int nNodes = (nRanks_ + nLocalRanks - 1) / nLocalRanks;
101+
nodeRanks_.resize(nNodes);
92102
for (int r = 0; r < nRanks_; ++r) {
93103
auto& rankState = rankStates_.at(r);
94104
rankState.nLocalRanks = nLocalRanks;
95105
rankState.rank = r;
106+
rankState.pid = (r == rank_) ? getpid() : -1;
96107
rankState.nodeId = r / rankState.nLocalRanks;
97108
rankState.localRank = r % rankState.nLocalRanks;
98109
rankState.localRankToRanks.assign(
@@ -103,20 +114,21 @@ void CommStateX::initRankTopologyVnode(const int nLocalRanks) {
103114
const std::string vNodeHost(
104115
"vnode_node_" + std::to_string(rankState.nodeId));
105116
rankState.host = vNodeHost;
106-
hostToRanks_[vNodeHost].emplace_back(r);
107117
nodeRanks_[rankState.nodeId].emplace_back(r);
108118
}
109119
}
110120

111121
void CommStateX::initRankStatesTopology(meta::comms::IBootstrap* bootstrap) {
112-
if (noLocal_) {
122+
if (bootstrap == nullptr) {
123+
FB_CHECKTHROW_EX(
124+
nRanks_ == 1,
125+
rank_,
126+
commHash_,
127+
commDesc_,
128+
"bootstrap is required for multi-rank topology initialization");
113129
initRankTopologyNolocal();
114130
return;
115131
}
116-
if (vCliqueSize_ > 0) {
117-
initRankTopologyVnode(vCliqueSize_);
118-
return;
119-
}
120132
auto myTopo = ctran::commstate::loadTopology(rank_, NCCL_TOPO_FILE_PATH);
121133
if (!myTopo) {
122134
FB_CHECKTHROW_EX(
@@ -349,27 +361,32 @@ void CommStateX::setRankStatesTopologies(
349361
std::vector<RankTopology> rankTopologies) {
350362
rankStates_.clear();
351363
nodeRanks_.clear();
352-
hostToRanks_.clear();
353-
354364
rankTopologies_ = rankTopologies;
355365

366+
// nodeGroupMap uses a virtual grouping key for node assignment.
367+
// Virtual topology overrides (nolocal, vnode, vClique) change node grouping
368+
// without altering the real hostname stored in rankState.host.
369+
std::unordered_map<std::string, std::vector<int>> nodeGroupMap;
370+
356371
for (const auto& rankTopology : rankTopologies_) {
357-
std::string host(rankTopology.host);
372+
const std::string host(rankTopology.host);
358373
const std::string rtsw(rankTopology.rtsw);
359374
const std::string su(rankTopology.su);
360375
const std::string dc(rankTopology.dc);
361376
const std::string zone(rankTopology.zone);
362377

363-
if (NCCL_COMM_STATE_DEBUG_TOPO == NCCL_COMM_STATE_DEBUG_TOPO::nolocal) {
364-
host = "nolocal_node_" + std::to_string(rankTopology.rank);
365-
} else if (
366-
NCCL_COMM_STATE_DEBUG_TOPO == NCCL_COMM_STATE_DEBUG_TOPO::vnode) {
367-
host =
368-
"vnode_node_" +
369-
std::to_string(
370-
rankTopology.rank / NCCL_COMM_STATE_DEBUG_TOPO_VNODE_NLOCALRANKS);
378+
// Determine node grouping key — may differ from real host for virtual
379+
// topos. noLocal_ treats each rank as its own node; vCliqueSize_ (set from
380+
// either explicit vClique param or NCCL_COMM_STATE_DEBUG_TOPO=vnode) groups
381+
// ranks into virtual nodes of the given size.
382+
std::string nodeGroupKey = host;
383+
if (noLocal_) {
384+
nodeGroupKey = "nolocal_node_" + std::to_string(rankTopology.rank);
385+
} else if (vCliqueSize_ > 0) {
386+
nodeGroupKey =
387+
"vnode_" + std::to_string(rankTopology.rank / vCliqueSize_);
371388
}
372-
hostToRanks_[host].emplace_back(rankTopology.rank);
389+
nodeGroupMap[nodeGroupKey].emplace_back(rankTopology.rank);
373390

374391
RankState state;
375392
state.rank = rankTopology.rank;
@@ -393,21 +410,21 @@ void CommStateX::setRankStatesTopologies(
393410
}
394411
}
395412

396-
state.nodeId = hostToRanks_.size() - 1;
397-
state.localRank = hostToRanks_.at(host).size() - 1;
413+
state.nodeId = nodeGroupMap.size() - 1;
414+
state.localRank = nodeGroupMap.at(nodeGroupKey).size() - 1;
398415

399416
rankStates_.push_back(std::move(state));
400417
}
401418

402-
// Populate nodeRanks_ after setup hostToRanks_ so we know how many nodes
419+
// Populate nodeRanks_ after the first pass so we know how many nodes
403420
// there are to resize nodeRanks_
404-
nodeRanks_.resize(hostToRanks_.size());
421+
nodeRanks_.resize(nodeGroupMap.size());
405422
for (const auto& state : rankStates_) {
406423
nodeRanks_[state.nodeId].emplace_back(state.rank);
407424
}
408425

409426
for (auto& state : rankStates_) {
410-
state.localRankToRanks = hostToRanks_.at(state.host);
427+
state.localRankToRanks = nodeRanks_.at(state.nodeId);
411428
state.nLocalRanks = state.localRankToRanks.size();
412429
}
413430

@@ -489,7 +506,7 @@ int CommStateX::nNodes() const {
489506
}
490507
return nvlDomainRanks_.size();
491508
} else {
492-
return hostToRanks_.size();
509+
return nodeRanks_.size();
493510
}
494511
}
495512

comms/ctran/commstate/CommStateX.h

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -266,12 +266,9 @@ class CommStateX {
266266
// and an associated communicator.
267267
std::vector<int> commRanksToWorldRanks_{};
268268

269-
// e.g map<host-name, localRankToRank>
270-
// e.g map<host1: [0, 1, 2, 3], host2: [4, 5, 6, 7]>
271-
std::unordered_map<std::string, std::vector<int>> hostToRanks_{};
272-
273-
// similar to hostToRanks_ but access by nodeId
274-
// e.g vector<0: [0, 1, 2, 3], 1: [0, 1, 2, 3]>
269+
// Node grouping: nodeRanks_[nodeId] = [rank0, rank1, ...]
270+
// For virtual topologies (nolocal, vnode, vClique), node grouping may differ
271+
// from physical host grouping.
275272
std::vector<std::vector<int>> nodeRanks_{};
276273

277274
// similar to nodeRanks, but at nvlDomain level. e.g vector<0: [0, 1, 2, 3],

comms/ctran/commstate/tests/CommStateXTest.cc

Lines changed: 104 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <gtest/gtest.h>
1010

1111
#include "comms/ctran/commstate/CommStateX.h"
12+
#include "comms/ctran/tests/VerifyCommStateXUtil.h"
1213
#include "comms/testinfra/TestXPlatUtils.h"
1314

1415
namespace ncclx {
@@ -552,49 +553,114 @@ TEST(CommStateXTest, CommRankToWorldRanks) {
552553
EXPECT_EQ(commState->gRank(3), 7);
553554
}
554555

555-
TEST(CommStateXTest, gPidTest) {
556-
const int nRanks = 4;
557-
const int cudaDev = 0;
558-
const int cudaArch = 90;
559-
const int64_t busId = 25;
560-
const uint64_t commHash = 0;
561-
const std::string kSu;
556+
// Parameterized test: verify host()/gPid() preserve real values and node
557+
// grouping is correct across all virtual topology modes.
558+
enum class TopoMode { kSystem, kNolocal, kVnode, kVClique };
562559

563-
std::vector<RankTopology> rankTopologies{};
564-
rankTopologies.emplace_back(
565-
createRankTopology(0, kDc, kZone, kSu, kRtsw0, kHost0, -1, 1000));
566-
rankTopologies.emplace_back(
567-
createRankTopology(1, kDc, kZone, kSu, kRtsw0, kHost0, -1, 1001));
568-
rankTopologies.emplace_back(
569-
createRankTopology(2, kDc, kZone, kSu, kRtsw0, kHost1, -1, 2000));
570-
rankTopologies.emplace_back(
571-
createRankTopology(3, kDc, kZone, kSu, kRtsw0, kHost1, -1, 2001));
560+
struct TopoTestParam {
561+
TopoMode mode;
562+
int vCliqueSize; // only used for kVClique
563+
int expectedNNodes;
564+
int expectedNLocalRanks; // for rank 0
565+
};
572566

573-
for (int rank = 0; rank < nRanks; ++rank) {
574-
auto commState = std::make_unique<CommStateX>(
575-
rank,
576-
nRanks,
577-
cudaDev,
578-
cudaArch,
579-
busId,
580-
commHash,
581-
rankTopologies,
582-
std::vector<int>{});
583-
584-
// Test gPid() for default (current) rank
585-
std::string expectedGPid = std::string(rankTopologies[rank].host) + ":" +
586-
std::to_string(rankTopologies[rank].pid);
587-
EXPECT_EQ(commState->gPid(), expectedGPid);
588-
589-
// Test gPid(rank) for all ranks
590-
for (int r = 0; r < nRanks; ++r) {
591-
std::string expected = std::string(rankTopologies[r].host) + ":" +
592-
std::to_string(rankTopologies[r].pid);
593-
EXPECT_EQ(commState->gPid(r), expected);
567+
std::string topoTestName(const testing::TestParamInfo<TopoTestParam>& info) {
568+
switch (info.param.mode) {
569+
case TopoMode::kSystem:
570+
return "system";
571+
case TopoMode::kNolocal:
572+
return "nolocal";
573+
case TopoMode::kVnode:
574+
return "vnode4";
575+
case TopoMode::kVClique:
576+
return "vclique" + std::to_string(info.param.vCliqueSize);
577+
}
578+
return "unknown";
579+
}
580+
581+
class GpidTopoTest : public ::testing::TestWithParam<TopoTestParam> {
582+
protected:
583+
static constexpr int kRanksPerHost = 4;
584+
static constexpr int kNumHosts = 2;
585+
static constexpr int kNRanks = kRanksPerHost * kNumHosts;
586+
static constexpr int kCudaDev = 0;
587+
static constexpr int kCudaArch = 90;
588+
static constexpr int64_t kBusId = 25;
589+
static constexpr uint64_t kCommHash = 0;
590+
591+
std::vector<RankTopology> makeRankTopologies() {
592+
const std::string kSu;
593+
const std::array<std::pair<const char*, const char*>, kNumHosts> hostInfo =
594+
{{{kHost0, kRtsw0}, {kHost1, kRtsw1}}};
595+
std::vector<RankTopology> topos;
596+
topos.reserve(kNRanks);
597+
for (int r = 0; r < kNRanks; r++) {
598+
const auto& [host, rtsw] = hostInfo[r / kRanksPerHost];
599+
const int pid = 1000 * (r / kRanksPerHost + 1) + r % kRanksPerHost;
600+
topos.emplace_back(
601+
createRankTopology(r, kDc, kZone, kSu, rtsw, host, -1, pid));
594602
}
603+
return topos;
604+
}
605+
};
606+
607+
TEST_P(GpidTopoTest, HostAndGpidPreserved) {
608+
const auto& [mode, vCliqueSize, expectedNNodes, expectedNLocalRanks] =
609+
GetParam();
610+
auto rankTopologies = makeRankTopologies();
611+
612+
// Set CVAR for vnode mode (needs ncclx EnvRAII, not SysEnvRAII)
613+
// nolocal and vClique are passed via constructor params instead.
614+
std::unique_ptr<SysEnvRAII> envTopo, envPpn;
615+
if (mode == TopoMode::kVnode) {
616+
envTopo =
617+
std::make_unique<SysEnvRAII>("NCCL_COMM_STATE_DEBUG_TOPO", "vnode");
618+
envPpn = std::make_unique<SysEnvRAII>(
619+
"NCCL_COMM_STATE_DEBUG_TOPO_VNODE_NLOCALRANKS", "4");
595620
}
621+
622+
const bool noLocal = (mode == TopoMode::kNolocal);
623+
auto commState = std::make_unique<CommStateX>(
624+
0,
625+
kNRanks,
626+
kCudaDev,
627+
kCudaArch,
628+
kBusId,
629+
kCommHash,
630+
rankTopologies,
631+
std::vector<int>{},
632+
"" /* commDesc */,
633+
noLocal,
634+
vCliqueSize);
635+
636+
// Node grouping matches expected virtual topology
637+
EXPECT_EQ(commState->nNodes(), expectedNNodes);
638+
EXPECT_EQ(commState->nLocalRanks(0), expectedNLocalRanks);
639+
640+
// Real hostname and gPid preserved for all ranks
641+
using Helper = ctran::testing::VerifyCommStateXHelper;
642+
for (int r = 0; r < kNRanks; ++r) {
643+
Helper::verifyHost(commState.get(), r, rankTopologies[r].host);
644+
Helper::verifyGPid(
645+
commState.get(), r, rankTopologies[r].host, rankTopologies[r].pid);
646+
}
647+
Helper::verifyGPidUniqueness(commState.get(), kNRanks);
596648
}
597649

650+
INSTANTIATE_TEST_SUITE_P(
651+
CommStateXTest,
652+
GpidTopoTest,
653+
::testing::Values(
654+
// system: 2 real hosts × 4 ranks each
655+
TopoTestParam{TopoMode::kSystem, 0, 2, 4},
656+
// nolocal: each rank is its own node
657+
TopoTestParam{TopoMode::kNolocal, 0, 8, 1},
658+
// vnode with nLocalRanks=4: 2 virtual nodes
659+
TopoTestParam{TopoMode::kVnode, 0, 2, 4},
660+
// vClique size=2: 4 virtual nodes of 2 ranks each
661+
TopoTestParam{TopoMode::kVClique, 2, 4, 2}),
662+
topoTestName);
663+
598664
TEST(CommStateXTest, TopologySetInvalidNvlFabricTopos) {
599665
const int rank = 0;
600666
const int nRanks = 4;
@@ -663,9 +729,7 @@ TEST(CommStateXTest, nvlFabricWithNoLocal) {
663729
"" /* commDesc */,
664730
true /* noLocal */);
665731

666-
// noLocal is set at construction; initRankStatesTopology delegates to
667-
// initRankTopologyNolocal
668-
commState->initRankStatesTopology(nullptr);
732+
commState->initRankTopologyNolocal();
669733

670734
// Set up NVL fabric with 2 clusters of 4 ranks each (e.g. GB200 2-GPU trays)
671735
std::vector<NvlFabricTopology> nvlFabricTopologies{};

comms/ctran/tests/CtranDistTestUtils.cc

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,9 @@ std::unique_ptr<CtranComm> CtranDistTestFixture::makeCtranComm() {
141141
std::unique_ptr<meta::comms::IBootstrap> commBootstrap(
142142
meta::comms::createBootstrap("ctrancomm"));
143143

144-
// Initialize topology
145-
if (NCCL_COMM_STATE_DEBUG_TOPO == NCCL_COMM_STATE_DEBUG_TOPO::nolocal) {
146-
comm->statex_->initRankTopologyNolocal();
147-
} else if (NCCL_COMM_STATE_DEBUG_TOPO == NCCL_COMM_STATE_DEBUG_TOPO::vnode) {
148-
comm->statex_->initRankTopologyVnode(
149-
NCCL_COMM_STATE_DEBUG_TOPO_VNODE_NLOCALRANKS);
150-
} else {
151-
comm->statex_->initRankStatesTopology(commBootstrap.get());
152-
}
144+
// Always use bootstrap to get real pids. Virtual topology overrides
145+
// (nolocal, vnode, vClique) are applied inside setRankStatesTopologies.
146+
comm->statex_->initRankStatesTopology(commBootstrap.get());
153147

154148
comm->bootstrap_ = std::make_unique<ctran::testing::CtranTestBootstrap>(
155149
std::move(commBootstrap));

0 commit comments

Comments
 (0)