feat: Dual-level accelerated domain support - Design and Prototype - #416
feat: Dual-level accelerated domain support - Design and Prototype#416ravisoundar wants to merge 7 commits into
Conversation
|
/ok-to-test 24e80de |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #416 +/- ##
==========================================
+ Coverage 72.15% 74.51% +2.35%
==========================================
Files 89 94 +5
Lines 5689 6536 +847
==========================================
+ Hits 4105 4870 +765
- Misses 1382 1421 +39
- Partials 202 245 +43 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Greptile SummaryThis PR introduces dual-level accelerated domain support, allowing a cluster topology to express a two-tier accelerator hierarchy (domain → sub-domain, e.g., NVL domain → rack) alongside the existing single-level model. It also renames the
Confidence Score: 5/5Safe to merge; the dual-level tree construction and label rename are well-tested with unit and integration tests, and the legacy-label cleanup path handles in-place upgrades correctly. All core algorithm changes are backed by focused unit tests and a full integration test using the new dual-level.yaml model. The label rename includes a legacyXclrDomainLabel cleanup so nodes are migrated on first reconcile without operator intervention. Both flagged observations are about future maintainability rather than current runtime defects. Files Needing Attention: pkg/topology/domain.go — the unsafe.Pointer cast in asBlockVertex relies on an invariant that the exported Vertices field (inherited from embedded Vertex) cannot enforce at compile time; worth revisiting if BlockVertex usage is extended. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Provider: InstanceTopology\nXclrDomainID + XclrSubDomainID] --> B[ClusterTopology.ToGraph]
B --> C[DomainMap.AddHostInfo\nHostInfo with Domain + SubDomain]
C --> D[DomainMap.GetDomainTree\nblockSizes]
D --> E{Has SubDomain?}
E -- No --> F[Single-level\nDomain leaf BlockVertex\nwith all hosts]
E -- Yes --> G[Dual-level\nDomain interior BlockVertex\nSub-domain leaf BlockVertex]
F --> H[setDesiredCountByLevel BFS\nmax-driven per depth]
G --> H
H --> I[convert root BlockVertex\nrecursive aggregateBlockNode]
I --> J{Leaf node?\nhosts != nil}
J -- Yes --> K[splitIntoBaseBlocks\npad to groupSize base blocks]
J -- No --> L[recurse into children\npad to LCM desiredNodeCount]
K --> M[flat base block list]
L --> M
M --> N[Slurm block topology output]
C --> O[labeler.getDomainLabels]
O --> P[xclr domain + sub-domain node labels]
Reviews (27): Last reviewed commit: "incorporated review comments" | Re-trigger Greptile |
24e80de to
507afeb
Compare
|
/ok-to-test 507afeb |
|
🌿 Preview your docs: https://nvidia-preview-pull-request-416.docs.buildwithfern.com/topograph |
507afeb to
664c001
Compare
664c001 to
1411514
Compare
|
/ok-to-test 1411514 |
1411514 to
efa3725
Compare
|
/ok-to-test efa3725 |
efa3725 to
fae6059
Compare
|
/ok-to-test fae6059 |
fae6059 to
bdd65e8
Compare
|
/ok-to-test bdd65e8 |
bdd65e8 to
84b8b2c
Compare
|
/ok-to-test 84b8b2c |
dmitsh
left a comment
There was a problem hiding this comment.
Correctness Issues
1. SDD documents the wrong label name
docs/design/dual-level-topology-sdd.md (Data Model / Simulation model YAML section) states:
network.topology.nvidia.com/group → HostInfo.SubDomain (parent domain)
But pkg/models/model.go defines:
LabelTopologySubDomain = "network.topology.nvidia.com/sub-domain"The SDD name (/group) and the implementation name (/sub-domain) don't match. Providers reading the SDD to opt in will use the wrong label.
2. convert: nil return value not guarded (latent panic)
In the interior-node path of convert (pkg/translate/block_tree.go):
for _, name := range slices.Sorted(maps.Keys(src.Children)) {
converted := convert(src.Children[name], baseBlockSize)
target.children = append(target.children, converted)
target.nodeCount += converted.nodeCount // panics if converted == nilconvert returns nil when src == nil. src.Children[name] can't be nil today (all values are set by GetDomainTree), but this is a latent panic for any future caller. A guard if converted == nil { continue } eliminates the footgun at zero cost.
3. childCapacity can stay zero, silently suppressing padding
childCapacity := 0
for _, name := range slices.Sorted(maps.Keys(src.Children)) {
converted := convert(src.Children[name], baseBlockSize)
...
if childCapacity == 0 {
childCapacity = converted.nodeCount
}
}
for target.nodeCount < src.DesiredNodeCount && childCapacity > 0 {
...
}childCapacity is sampled only from the first child. If that child has nodeCount == 0 (e.g. its DesiredNodeCount was never set because blockSizes was empty, or it's a leaf domain with zero hosts), childCapacity stays 0 and the padding loop never fires. Absent base domains are silently omitted from the output rather than getting placeholder slots. The fix is to sample childCapacity from the first child with nodeCount > 0, not just the first child unconditionally.
4. domainsForBlocks interaction with SubDomain (pre-existing, but worth noting)
complementBlocks calls domainsForBlocks(nt.domains, blocks) which filters nt.domains by matching b.name (block name) against domain map keys. In the single-level case b.name is the accelerator domain name, so the lookup works. In the dual-level case b.name will be the parent domain name (domain-01, etc.) which IS the key in DomainMap, so it still works — but only because the test fixture uses the accelerator domain as the outer key.
If a future provider populates the block name as the sub-domain (base rack name) instead of the parent domain, all[b.name] will return nil and hosts will be silently dropped. The pairing between blockInfo.name and DomainMap keys is an undocumented contract worth making explicit (or enforcing in tests).
5. Placeholder positions diverge from alphabetical slot positions
rack-1-03 and rack-1-13 are alphabetically positions 3 and 13 within domain-01, but their placeholder blocks appear as block015 and block016 (the last two slots). The SDD documents this as a known limitation, but it means Slurm's position-based aggregate inference will be wrong for those specific slots — a real operational impact. The limitation callout in the SDD should be elevated to the PR description so reviewers and operators are not surprised.
1b32609 to
785bc8d
Compare
|
/ok-to-test 785bc8d |
e8209f9 to
cf7f184
Compare
cf7f184 to
43518f9
Compare
9f1a330 to
87bf5b5
Compare
Signed-off-by: Ravi Shankar <ravish@nvidia.com>
…6 domain Signed-off-by: Ravi Shankar <ravish@nvidia.com>
Signed-off-by: Ravi Shankar <ravish@nvidia.com>
Signed-off-by: Ravi Shankar <ravish@nvidia.com> Signed-off-by: Dmitry Shmulevich <dshmulevich@nvidia.com>
87bf5b5 to
b6bb7ee
Compare
|
/ok-to-test b6bb7ee |
b6bb7ee to
b2f0ef6
Compare
…rID is present Signed-off-by: Ravi Shankar <ravish@nvidia.com>
b2f0ef6 to
f5fd343
Compare
Signed-off-by: Dmitry Shmulevich <dshmulevich@nvidia.com>
2f8ccb7 to
654a715
Compare
|
/ok-to-test 654a715 |
ArangoGutierrez
left a comment
There was a problem hiding this comment.
Solid docs discipline here — every breaking change is in the CHANGELOG and AGENTS.md/.claude/CLAUDE.md are updated in the same PR. The main concern is the block-tree rewrite: running the old and new generators side by side on identical single-level DomainMaps, the output diverges in both directions, which the SDD says should stay identical.
- Single-level domains now pad to
desiredNodeCount / baseBlockSizebase blocks, wheredesiredNodeCountis the smallest configured BlockSize >= the largest domain. WheneverblockSizes[0] < maxDomainHosts < blockSizes[last], every domain gets padded out to a full top-tier block. A/B against the merge base on identical no-SubDomain DomainMaps: 8 domains x 18 hosts with BlockSizes=9,144 gives 16 blocks / 0 empty before and 128 / 112 after; 6 x 16 with 8,64 goes 16/4 to 48/36; 3 x 72 with 18,144 goes 16/4 to 24/12. The SDD goal states no-SubDomain domains must produce output identical to the pre-change behavior. (pkg/translate/block_tree.go:257) - The padding loop stops as soon as
target.nodeCountreachesdesiredNodeCountinstead of rounding up to a multiple of it, so an incomplete top-tier group is no longer complemented. Same A/B, opposite direction: 5 single-level domains of 3 hosts with BlockSizes=4,16 emits 8 blocks before (5 real + 3 placeholders completing the second 16-node aggregate) and 5 after; 9 domains goes 12 to 9. The removedpackAggregateNodes/newAggregateBlockpadded every aggregate to a full groupSize, which is what protects Slurm's position-based aggregate inference. (pkg/translate/block_tree.go:287) - A host with an empty SubDomain inside a domain where others carry one is skipped entirely, so it never reaches a block and vanishes from topology.conf with only a klog warning. Verified: 8 racked hosts plus 1 without a SubDomain emits
Nodes=node[0001-0008]and drops the ninth. OCI'sconvertComputeHostleavesrackempty wheneveradditionalData.locationDetails.rackis absent, so a partialGetComputeHostresponse silently removes those compute nodes. Before this change such a host landed in its own single-level domain and was always emitted. (pkg/topology/domain.go:161) isManagedLevelLabelnow matches only the new xclr keys, sonetwork.topology.nvidia.com/acceleratoris no longer treated as managed andremoveManagedTopologyLabelsnever strips it. Nodes labelled by a previous release keep that key with a frozen value forever, alongside the new one. That string now appears nowhere in the tree except this CHANGELOG entry, so there is no cleanup path and no migration note — and CLAUDE.md names KAI Scheduler, NVSentinel and Kueue as consumers of it. Keeping the legacy key in the managed set would let one reconcile clean it up. (pkg/engines/k8s/kubernetes.go:68)- The gpu.clique shadow check skips only
topologyTypeXclrDomain, so when a clique is present the domain is replaced by the clique value while the provider sub-domain is still published.TestBuildNFDObjectsGroupsXclrSubDomainsAndDoesNotGroupShadowedDomainsencodes the result: domaingpu-clique-atogether with sub-domainprovider-domain-a.rack-a, nested under a domain that was never published. The k8s engine'sskipXclrLabelsWhenGPUCliqueExistsdrops both levels, and node-labels.md says the sub-domain is written only when the domain is. AddingtopologyTypeXclrSubDomainto the samecontinuewould line them up. (pkg/engines/nfd/objects.go:95) GetDomainTree,setDesiredCountByLevelandBlockVertexadd roughly 120 lines of exported behavior to the load-bearing topology package with no direct unit test — no*_test.goin the tree references any of the three, anddomain_test.gostill holds onlyTestDomainMapAddHost. The BFS level sizing, the empty-SubDomain skip path and the documented "blockSizes need not be sorted" contract are reached only indirectly through translate's rendered-output assertion, and the SDD test plan lists them as covered by pkg/topology tests. (pkg/topology/domain.go:127)topology.KeyTopologyAcceleratorSubDomainnever existed —git log -Sfinds it only in this PR's own commit, and the base declares justKeyTopologyAccelerator.KeyTopologyXclrSubDomainis a new constant, so this reads better under Added than as a BREAKING rename. (CHANGELOG.md:29)- This says retaining a provider-supplied XCLR sub-domain when
nvidia.com/gpu.cliqueis present matches the k8s engine, butskipXclrLabelsWhenGPUCliqueExistsdeletes both the domain and the sub-domain. Worth correcting in whichever direction the NFD behavior gets resolved. (docs/design/nfd-engine-sdd.md:234) - This delete is redundant with
removeManagedTopologyLabels, which already treats the sub-domain key as managed. Removing the line keeps pkg/engines, pkg/topology and pkg/translate green, so nothing pins the intended behavior — "Case 8: remove stale accelerator sub-domain label" exercises the removeManagedTopologyLabels path instead. (pkg/engines/k8s/kubernetes.go:102)
| continue | ||
| } | ||
| if kind == topologyTypeAccelerator && gpuCliqueValue != "" { | ||
| if kind == topologyTypeXclrDomain && gpuCliqueValue != "" { |
There was a problem hiding this comment.
The gpu.clique shadow check skips only topologyTypeXclrDomain, so when a clique is present the domain is replaced by the clique value while the provider sub-domain is still published. TestBuildNFDObjectsGroupsXclrSubDomainsAndDoesNotGroupShadowedDomains encodes the result: domain gpu-clique-a together with sub-domain provider-domain-a.rack-a, nested under a domain that was never published. The k8s engine's skipXclrLabelsWhenGPUCliqueExists drops both levels, and node-labels.md says the sub-domain is written only when the domain is. Adding topologyTypeXclrSubDomain to the same continue would line them up.
There was a problem hiding this comment.
@dmitsh, not sure if this is the desired behavior. I assume sub-domain information may still be required.
| - Verify nodes with `nvidia.com/gpu.clique` follow the same accelerator behavior | ||
| as the `k8s` engine. | ||
| - Verify nodes with `nvidia.com/gpu.clique` override the XCLR domain while | ||
| retaining any provider-supplied XCLR sub-domain, matching the `k8s` engine. |
There was a problem hiding this comment.
This says retaining a provider-supplied XCLR sub-domain when nvidia.com/gpu.clique is present matches the k8s engine, but skipXclrLabelsWhenGPUCliqueExists deletes both the domain and the sub-domain. Worth correcting in whichever direction the NFD behavior gets resolved.
|
/ok-to-test 22de1b7 |
22de1b7 to
a778d23
Compare
|
/ok-to-test a778d23 |
a778d23 to
0941487
Compare
Signed-off-by: Ravi Shankar <ravish@nvidia.com>
0941487 to
902bbd6
Compare
Description
Design and Prototype for the dual level accelerated domain support.
Addresses #415
Checklist
git commit -s).