Fix NIC to GPU proximity detection on systems with multiple PCIe domains - #352
Open
paklui wants to merge 7 commits into
Open
Fix NIC to GPU proximity detection on systems with multiple PCIe domains#352paklui wants to merge 7 commits into
paklui wants to merge 7 commits into
Conversation
remove hasActivePort from PCIe tree insertion and ibvAddressList because proximity relates to hardware property, for inactive NICs that are still physically close to their GPU and should be included for correct topology mapping
rename ExtractBusNumber to ExtractDomainAndBus and update GetBusIdDistance to return 0 for same-domain pairs and abs(domain_diff)*256 for cross-domain pairs each GPU occupies its own PCIe domain the old bus-number-only metric produced wrong distances when GPUs and NICs share a domain but sit under different root complexes
Replace single-winner NIC selection with all equally-close NICs Previously, when multiple NICs tied as closest to a GPU, only the least-used NIC (tracked by assignedCount) was recorded on system that each GPU has 2 NICs at equal PCIe distance, so the old code reported only 1 NIC per GPU instead of 2 because closestNicsToGpu is a topology map, not a traffic assignment Reporting all equally-close NICs is the correct semantic remove the dead assignedCount load-balancing code apply the same fix to the reverse mapping: the bus-ID-distance fallback now collects all GPUs at minimum distance rather than stopping at the first one found also remove the hasActivePort guard from ibvAddressList and the NIC-to-GPU reverse mapping loop, because proximity relates to hardware property inactive NICs are still physically close to their nearest GPU
use GetClosestNicsToGpu (vector) instead of GetClosestNicToGpu (int) in both the NIC table and GPU table so all equally-close NICs are shown as a comma-separated list (e.g. "0,1") also fix the NIC table loop: the old code called GetClosestNicsToGpu inside a per-NIC outer loop, making numNics*numGpus times of calls change to build an inverse GPU-per-NIC map upfront in numGpus times and look up the result directly in the print loop
# Conflicts: # src/header/TransferBench.hpp
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes NIC↔GPU proximity detection on multi-PCIe-domain systems so that GPUs map to the correct physically-nearest NICs (including ties where multiple NICs are equally close), and updates the CLI topology display to reflect multi-NIC proximity.
Changes:
- Include NICs in the PCIe proximity tree regardless of link state, while still enforcing link state at transfer validation time.
- Make the proximity tie-breaker domain-aware to avoid “bus-number-only” false closeness across PCIe domains.
- Record and display all equally-close NICs per GPU (and GPUs per NIC), updating CLI output accordingly.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/header/TransferBench.hpp | Fixes PCIe-domain-aware proximity distance and records all equally-close NIC/GPU ties during topology discovery. |
| src/client/Topology.hpp | Updates printed topology to use vector-based “closest NICs” APIs and display multiple NICs per GPU / GPUs per NIC. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Reworks the earlier commits on this branch in response to review, keeping
the behaviour that fixes NIC-to-GPU proximity on multi-domain systems while
dropping the parts that were unnecessary or actively harmful.
Reverted: including inactive NICs in the PCIe proximity tree
Verified on a 4-GPU / 9-NIC multi-domain rack that this was a no-op for
the tree and a regression for executor selection:
- No-op: an LCA depends only on the two paths being compared, so adding a
NIC's nodes cannot change any other pair's result. On this hardware GPUs
and NICs never share a root complex (GPU 3 is under pci0004:00, its NICs
under pci0004:40), so every NIC ties at the same LCA depth and the domain
metric is what actually discriminates.
- Regression: it let a NIC with a down port be recorded as "closest".
NicIsActive() rejects such a Transfer with ERR_FATAL rather than falling
back to a usable NIC, so nic_rings (which builds a ring for every NIC
returned by GetClosestNicsToGpu) would abort on any tray with a down
link, and NIC_NEAREST could resolve to an unusable NIC.
Reverted: the minDistance/matches.clear() reordering in
GetNearestDevicesInTree, which was a no-op - matches.clear() does not read
minDistance, and minDistance is assigned before the loop advances, so both
orderings behave identically.
Kept and tightened:
- ExtractBusNumber -> ExtractDomain, GetBusIdDistance -> GetDomainDistance.
The metric no longer consults bus numbers at all, so the old names were
misleading. Returning a plain int also removes the unused structured
bindings flagged in review. Bus numbers are deliberately not used to
discriminate within a domain: doing so would break ties between NICs that
are genuinely equidistant from a GPU, which is the case this fix exists to
preserve. Dropped the *256 scaling, which became vestigial once
same-domain returns 0; ordering is unchanged.
- Record all equally-close NICs/GPUs instead of picking one via the
assignedCount round-robin.
- Topology display uses the vector-returning API so ties are visible, and
joins the NIC list with spaces so the field stays a single column when
OUTPUT_TO_CSV=1 makes the separator a comma.
Verified on ctheliosr-rck-g02-k19-2 (4x gfx1250, 8 BE + 1 FE NIC, one GPU
per PCIe domain). Before, all four GPUs mapped to NIC 0, the front-end NIC
in domain 0000. After: GPU0->1,2 GPU1->5,6 GPU2->3,4 GPU3->7 (ionic_8's
port is down), and the front-end NIC maps to no GPU. CSV rows hold a
constant 11 fields; build is warning-free.
Co-Authored-By: Claude <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
Suppressed comments (1)
src/client/Topology.hpp:77
PrintNicToGPUTopo()passesstd::to_string(...).c_str()directly toprintf. That pointer refers to a temporarystd::stringthat is destroyed before/during the call, so the%sargument is dangling (undefined behavior).
printf(" %-3d | %-11s | %-6s | %-12s | %-4d | %-14s | %-9s | %-20s\n",
i, ibvDeviceList[i].name.c_str(),
ibvDeviceList[i].hasActivePort ? "Yes" : "No",
ibvDeviceList[i].busId.c_str(),
ibvDeviceList[i].numaNode,
closestGpusStr.c_str(),
ibvDeviceList[i].isRoce && ibvDeviceList[i].hasActivePort ? std::to_string(ibvDeviceList[i].gidIndex).c_str() : "N/A",
ibvDeviceList[i].isRoce && ibvDeviceList[i].hasActivePort ? ibvDeviceList[i].gidDescriptor.c_str() : "N/A"
Three fixes, one per review comment. 1. Fallback path could map a GPU to a NIC with no active port (TransferBench.hpp:7519). The fallback guarded on ibvDeviceList[nicIndex].busId, the raw device list, which holds a valid BDF even when the port is down, while the PCIe-tree path above uses ibvAddressList, which is blanked out for inactive NICs. A GPU could therefore be mapped to a down NIC through the fallback, and NicIsActive() would later abort the Transfer with ERR_FATAL. Both paths now read ibvAddressList. This is pre-existing in develop, but recording every tie rather than a single NIC widens it, so it is fixed here. 2. An unparseable address won the distance tiebreak (GetNearestDevicesInTree). GetDomainDistance returns -1 on parse failure, and -1 compares smaller than every valid distance, so one malformed candidate outranked valid same-depth candidates. The develop NOTE said such a candidate "remains a valid closest candidate, so is included", but the code made it win rather than merely be included. Unknown distances are now clamped to INT_MAX: still eligible when nothing else matches at that depth, still tying with other unparseable candidates, never outranking a known distance. NOTE: this changes develop behaviour in the unparseable-address path. 3. ExtractDomain's comment overclaimed. It said the full address is parsed "so that a malformed address is rejected", but the stream parse consumes separators without checking them, so 0001-01-00-0 parses successfully. Reworded to describe what the parse actually guarantees. The parsing itself is unchanged from develop; every BDF here originates from sysfs or hipDeviceGetPCIBusId. Not changed: the CSV separator comment on Topology.hpp:209 was re-issued from the previous review and no longer applies. That line joins with a space, not a comma. Verified with OUTPUT_TO_CSV=1: every GPU row holds a constant 11 fields and a multi-NIC value stays a single field. Re-verified on ctheliosr-rck-g02-k19-2, warning-free build. A link flapped between runs (ionic_1 went down, ionic_8 came up), which independently exercised both behaviours: GPU0 correctly dropped to NIC 2 alone, and GPU3 correctly reported the 7,8 tie. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
On systems where each GPU sits in its own PCIe domain, TransferBench picked the wrong NIC as closest, thus every GPU mapped to a single front-end NIC that is not wired to any GPU, while the back-end NICs actually adjacent to each GPU mapped to nothing. The mapping decides which NIC a GPU uses for RDMA transfers (
NIC_NEARESTexecutor,nic_ringspreset). With incorrect mapping it would mean benchmarking ising a NIC that is not attached to that GPU's root complex, producing misleading numbers. The causes are:This PR fixes the topology detection so it works correctly on systems with multiple PCIe domains and multiple NICs per GPU (verified on a 4-GPU, 8 BE + 1 FE, or 2 BE NICs per GPU).
Technical Details
Two functions were renamed to match what they now do.
ExtractBusNumberbecameExtractDomain, andGetBusIdDistancebecameGetDomainDistance.The distance between two devices is now computed as follows:
Bus numbers are no longer used to compare devices within a domain. Firmware assigns them, and they do not reliably indicate which device is physically closer. Using them would also separate NICs that are genuinely the same distance from a GPU, and reporting those ties is the point of this fix.
Replaces the
assignedCountround-robin, which picked one NIC per GPU even when several were tied. Both directions now keep the full tie set, matching howGetClosestNicsToGpu()was already shaped.Trade-off: the removed round-robin was intended to spread multi-port NICs (ports sharing one busId) across GPUs. With ties recorded,
nic_ringsnow uses all tied ports, better, but a bareNIC_NEARESTresolves slot 0 for every GPU rather than spreading. Use the explicitN<gpu>.<slot>form to select a specific port.The display called the scalar
GetClosestNicToGpu(), so it would have shown one NIC even after (2). Switched to the vector API. The NIC list is space-separated so the field stays a single column underOUTPUT_TO_CSV=1, where the separator is a comma.ionic_8's port is down, so GPU 3 reports only NIC 7. NICs that cannot execute a Transfer are excluded from the mapping:NicIsActive()rejects such a Transfer withERR_FATALrather than falling back, so one would makenic_ringsabort on any tray with a down link.OUTPUT_TO_CSV=1rows hold a constant 11 fields regardless of NIC count.Test Plan
Compare both unmodified
develop(44dec25) and this branch merged withdevelopon the same hardware, and compared the topology output.Test Result
Current
developHEAD:Currently every one of the 4 GPUs is mapped to NIC 0 which is on PCIe domain 0000, a completely different domain from any GPU (
0001-0004). NIC 0 is a Front End NIC that is not physically wired to any GPU. Meanwhile the 8 Back End NICs that are actually adjacent to a GPU (ionic_1-ionic_8) show up as closest to blank. This is the domain-blind bus-number comparison from change (2) picking the numerically-smallest bus number regardless of domain, combined with (3)'s single-NIC-only recording.actual output from ./TransferBench after (this branch merged with
develop):Each GPU is now correctly mapped to the NIC(s) in its own PCIe domain:
0001)0002)0003)0004)Also confirmed a clean build with no new warnings, using the dynamically-loaded libibverbs path (
IsIbvSymbolsReady()) thatdevelopadded since this branch's base, e.g. this fix is verified against the currentdevelopHEADSubmission Checklist