Skip to content

Commit 50e472c

Browse files
authored
[LEP-4632] fix(nvidia/nvlink): add peer P2P health checks (#1221)
- implicit unhealthy on obvious system-wide failures: - zero GPUs report active NVLink links, or - complete peer probing finds zero OK NVLink P2P pairs - partial degradation still stays healthy by default without ExpectedLinkStates - with ExpectedLinkStates configured, partial-degradation policy still comes from the threshold, but a confirmed zero-OK peer matrix still fails health ``` nvidia-smi Tue Mar 17 07:40:34 2026 +-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 570.195.03 Driver Version: 570.195.03 CUDA Version: 12.8 | |-----------------------------------------+------------------------+----------------------+ | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+========================+======================| | 0 NVIDIA H100 80GB HBM3 On | 00000000:04:00.0 Off | 0 | | N/A 32C P0 69W / 700W | 0MiB / 81559MiB | 0% Default | | | | Disabled | +-----------------------------------------+------------------------+----------------------+ | 1 NVIDIA H100 80GB HBM3 On | 00000000:05:00.0 Off | 0 | | N/A 29C P0 69W / 700W | 0MiB / 81559MiB | 0% Default | | | | Disabled | +-----------------------------------------+------------------------+----------------------+ | 2 NVIDIA H100 80GB HBM3 On | 00000000:0B:00.0 Off | 0 | | N/A 31C P0 67W / 700W | 0MiB / 81559MiB | 0% Default | | | | Disabled | +-----------------------------------------+------------------------+----------------------+ | 3 NVIDIA H100 80GB HBM3 On | 00000000:0C:00.0 Off | 0 | | N/A 30C P0 71W / 700W | 0MiB / 81559MiB | 0% Default | | | | Disabled | +-----------------------------------------+------------------------+----------------------+ | 4 NVIDIA H100 80GB HBM3 On | 00000000:84:00.0 Off | 0 | | N/A 32C P0 69W / 700W | 0MiB / 81559MiB | 0% Default | | | | Disabled | +-----------------------------------------+------------------------+----------------------+ | 5 NVIDIA H100 80GB HBM3 On | 00000000:85:00.0 Off | 0 | | N/A 29C P0 67W / 700W | 0MiB / 81559MiB | 0% Default | | | | Disabled | +-----------------------------------------+------------------------+----------------------+ | 6 NVIDIA H100 80GB HBM3 On | 00000000:8B:00.0 Off | 0 | | N/A 31C P0 72W / 700W | 0MiB / 81559MiB | 0% Default | | | | Disabled | +-----------------------------------------+------------------------+----------------------+ | 7 NVIDIA H100 80GB HBM3 On | 00000000:8C:00.0 Off | 0 | | N/A 30C P0 70W / 700W | 0MiB / 81559MiB | 0% Default | | | | Disabled | +-----------------------------------------+------------------------+----------------------+ ``` ``` nvidia-smi topo -p2p n GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7 GPU0 X NS NS NS NS NS NS NS GPU1 NS X NS NS NS NS NS NS GPU2 NS NS X NS NS NS NS NS GPU3 NS NS NS X NS NS NS NS GPU4 NS NS NS NS X NS NS NS GPU5 NS NS NS NS NS X NS NS GPU6 NS NS NS NS NS NS X NS GPU7 NS NS NS NS NS NS NS X Legend: X = Self OK = Status Ok CNS = Chipset not supported GNS = GPU not supported TNS = Topology not supported NS = Not supported U = Unknown ``` ``` gpud scan {"level":"warn","ts":"2026-03-17T07:39:15.232Z","caller":"log/log.go:175","msg":"detected system-wide nvlink peer-to-peer failure","gpuCount":8,"expectedPairs":28,"probedPairs":28,"observedPeerStatuses":["NS"]} ✘ no GPU pairs report NVLink P2P connectivity on 8-GPU NVLink-capable system (peer nvlink p2p statuses=NS) +------------------------------------------+--------------+----------------+------------------+---------------+ | GPU UUID | GPU BUS ID | NVLINK ENABLED | NVLINK SUPPORTED | NVLINK P2P OK | +------------------------------------------+--------------+----------------+------------------+---------------+ | GPU-0def2989-c0d5-e572-0449-8d4acef9a62e | 0000:85:00.0 | true | true | false | | GPU-44a01294-5fd9-4531-2e25-1849e59af856 | 0000:8b:00.0 | true | true | false | | GPU-4536e60e-c227-c308-695f-325b63da2692 | 0000:0c:00.0 | true | true | false | | GPU-7a9080d1-246c-2ad7-488c-687e2d99d2b7 | 0000:05:00.0 | true | true | false | | GPU-b5a43906-e38d-d4be-4fb3-884212886518 | 0000:0b:00.0 | true | true | false | | GPU-d942483d-9051-b76e-436d-a35167ddba4f | 0000:8c:00.0 | true | true | false | | GPU-da0da07f-896e-2de6-ed66-94c987f4b639 | 0000:84:00.0 | true | true | false | | GPU-f507afa1-68ca-36cd-4df5-7de8b79593f3 | 0000:04:00.0 | true | true | false | +------------------------------------------+--------------+----------------+------------------+---------------+ ``` Signed-off-by: Gyuho Lee <gyuhol@nvidia.com>
1 parent 99a6269 commit 50e472c

6 files changed

Lines changed: 1048 additions & 137 deletions

File tree

components/accelerator/nvidia/nvlink/component.go

Lines changed: 183 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ type component struct {
3434

3535
getTimeNowFunc func() time.Time
3636

37-
nvmlInstance nvidianvml.Instance
38-
getNVLinkFunc func(uuid string, dev device.Device) (NVLink, error)
39-
getThresholdsFunc func() ExpectedLinkStates
37+
nvmlInstance nvidianvml.Instance
38+
getNVLinkFunc func(uuid string, dev device.Device) (NVLink, error)
39+
getPeerNVLinkP2PStatusFn func(dev device.Device, peer device.Device) (string, error)
40+
getThresholdsFunc func() ExpectedLinkStates
4041

4142
lastMu sync.RWMutex
4243
lastCheckResult *checkResult
@@ -52,9 +53,10 @@ func New(gpudInstance *components.GPUdInstance) (components.Component, error) {
5253
getTimeNowFunc: func() time.Time {
5354
return time.Now().UTC()
5455
},
55-
nvmlInstance: gpudInstance.NVMLInstance,
56-
getNVLinkFunc: GetNVLink,
57-
getThresholdsFunc: GetDefaultExpectedLinkStates,
56+
nvmlInstance: gpudInstance.NVMLInstance,
57+
getNVLinkFunc: GetNVLink,
58+
getPeerNVLinkP2PStatusFn: getPeerNVLinkP2PStatus,
59+
getThresholdsFunc: GetDefaultExpectedLinkStates,
5860
}
5961
return c, nil
6062
}
@@ -160,7 +162,79 @@ func (c *component) Check() components.CheckResult {
160162
}
161163

162164
devs := c.nvmlInstance.Devices()
163-
for uuid, dev := range devs {
165+
// Only expect NVLink by default on multi-GPU hosts that advertise NVIDIA
166+
// fabric support. This keeps 1-GPU machines safe: for a single GPU,
167+
// `nvidia-smi topo -p2p n` legitimately shows only the self entry:
168+
//
169+
// $ nvidia-smi topo -p2p n
170+
// GPU0
171+
// GPU0 X
172+
//
173+
// On a healthy 2-GPU NVLink-capable host, peer entries should be `OK`:
174+
//
175+
// $ nvidia-smi topo -p2p n
176+
// GPU0 GPU1
177+
// GPU0 X OK
178+
// GPU1 OK X
179+
//
180+
// This flag is intentionally conservative: it only enables the fallback for
181+
// the obvious "zero GPUs have active NVLink" case. If some GPUs are active
182+
// and some are not, operators must still configure ExpectedLinkStates to make
183+
// partial degradation fail health checks.
184+
cr.SystemExpectedNVLink = len(devs) > 1 && (c.nvmlInstance.FabricManagerSupported() || c.nvmlInstance.FabricStateSupported())
185+
sortedUUIDs := make([]string, 0, len(devs))
186+
for uuid := range devs {
187+
sortedUUIDs = append(sortedUUIDs, uuid)
188+
}
189+
sort.Strings(sortedUUIDs)
190+
191+
if c.getPeerNVLinkP2PStatusFn != nil && len(sortedUUIDs) > 1 {
192+
peerNVLinkOKGPUUUIDs := make(map[string]struct{})
193+
peerNVLinkObservedStatusCodes := make(map[string]struct{})
194+
cr.PeerNVLinkExpectedPairCount = len(sortedUUIDs) * (len(sortedUUIDs) - 1) / 2
195+
196+
// WHY: per-GPU NVLink port state can stay FEATURE_ENABLED even when the
197+
// fabric is unusable between GPU peers. In the field this showed up as
198+
// `nvidia-smi topo -p2p n` reporting only `NS` peer entries while GPUD
199+
// still rendered every GPU as NVLink enabled/supported. Probe pairwise
200+
// NVLink P2P status here so the component can catch topology-level
201+
// failures that per-port state alone misses.
202+
for i := 0; i < len(sortedUUIDs); i++ {
203+
for j := i + 1; j < len(sortedUUIDs); j++ {
204+
uuid := sortedUUIDs[i]
205+
peerUUID := sortedUUIDs[j]
206+
207+
statusCode, err := c.getPeerNVLinkP2PStatusFn(devs[uuid], devs[peerUUID])
208+
if err != nil {
209+
log.Logger.Debugw(
210+
"failed to get nvlink peer-to-peer status",
211+
"uuid", uuid,
212+
"peer_uuid", peerUUID,
213+
"error", err,
214+
)
215+
continue
216+
}
217+
218+
cr.PeerNVLinkProbePairCount++
219+
peerNVLinkObservedStatusCodes[statusCode] = struct{}{}
220+
if statusCode == p2pStatusOK {
221+
cr.PeerNVLinkOKPairCount++
222+
peerNVLinkOKGPUUUIDs[uuid] = struct{}{}
223+
peerNVLinkOKGPUUUIDs[peerUUID] = struct{}{}
224+
}
225+
}
226+
}
227+
228+
if len(peerNVLinkOKGPUUUIDs) > 0 {
229+
cr.PeerNVLinkOKGPUUUIDs = sortedKeys(peerNVLinkOKGPUUUIDs)
230+
}
231+
if len(peerNVLinkObservedStatusCodes) > 0 {
232+
cr.PeerNVLinkObservedStatusCodes = sortedKeys(peerNVLinkObservedStatusCodes)
233+
}
234+
}
235+
236+
for _, uuid := range sortedUUIDs {
237+
dev := devs[uuid]
164238
nvLink, err := c.getNVLinkFunc(uuid, dev)
165239
if err != nil {
166240
cr.err = err
@@ -198,6 +272,10 @@ func (c *component) Check() components.CheckResult {
198272
metricSupported.With(labels).Set(1.0)
199273
} else {
200274
metricSupported.With(labels).Set(0.0)
275+
metricFeatureEnabled.With(labels).Set(0.0)
276+
metricReplayErrors.With(labels).Set(0.0)
277+
metricRecoveryErrors.With(labels).Set(0.0)
278+
metricCRCErrors.With(labels).Set(0.0)
201279
cr.UnsupportedNVLinkUUIDs = append(cr.UnsupportedNVLinkUUIDs, uuid)
202280
continue
203281
}
@@ -256,6 +334,31 @@ type checkResult struct {
256334
// Used by evaluateHealthStateWithThresholds to determine if the system is healthy
257335
ExpectedLinkStates *ExpectedLinkStates `json:"expected_link_states,omitempty"`
258336

337+
// PeerNVLinkProbePairCount is the number of GPU peer pairs where NVML returned
338+
// a usable P2P-over-NVLink status. This mirrors `nvidia-smi topo -p2p n`.
339+
PeerNVLinkProbePairCount int `json:"peer_nvlink_probe_pair_count,omitempty"`
340+
341+
// PeerNVLinkExpectedPairCount is the total number of unique GPU peer pairs GPUD
342+
// tried to inspect. Comparing this with PeerNVLinkProbePairCount lets the
343+
// health evaluator distinguish a confirmed all-NS matrix from partial data.
344+
PeerNVLinkExpectedPairCount int `json:"peer_nvlink_expected_pair_count,omitempty"`
345+
346+
// PeerNVLinkOKPairCount is the number of peer pairs that report NVLink P2P OK.
347+
PeerNVLinkOKPairCount int `json:"peer_nvlink_ok_pair_count,omitempty"`
348+
349+
// PeerNVLinkOKGPUUUIDs lists GPUs that have at least one peer with NVLink P2P OK.
350+
PeerNVLinkOKGPUUUIDs []string `json:"peer_nvlink_ok_gpu_uuids,omitempty"`
351+
352+
// PeerNVLinkObservedStatusCodes lists the distinct peer NVLink P2P status codes
353+
// observed across probed GPU pairs (e.g. OK, NS, TNS).
354+
PeerNVLinkObservedStatusCodes []string `json:"peer_nvlink_observed_status_codes,omitempty"`
355+
356+
// SystemExpectedNVLink reports whether this node looks like a multi-GPU NVIDIA
357+
// host where GPUD expects an NVLink fabric to exist by default. This lets the
358+
// component catch obvious topology failures even when no explicit threshold is
359+
// configured.
360+
SystemExpectedNVLink bool `json:"system_expected_nvlink,omitempty"`
361+
259362
// timestamp of the last check
260363
ts time.Time
261364
// error from the last check
@@ -269,6 +372,49 @@ type checkResult struct {
269372
reason string
270373
}
271374

375+
func (cr *checkResult) hasCompletePeerNVLinkProbeCoverage() bool {
376+
if cr == nil || cr.PeerNVLinkExpectedPairCount == 0 {
377+
return false
378+
}
379+
return cr.PeerNVLinkProbePairCount == cr.PeerNVLinkExpectedPairCount
380+
}
381+
382+
func (cr *checkResult) missingPeerNVLinkProbePairCount() int {
383+
if cr == nil || cr.PeerNVLinkExpectedPairCount <= cr.PeerNVLinkProbePairCount {
384+
return 0
385+
}
386+
return cr.PeerNVLinkExpectedPairCount - cr.PeerNVLinkProbePairCount
387+
}
388+
389+
func (cr *checkResult) hasPeerNVLinkP2PFailure() bool {
390+
if cr == nil {
391+
return false
392+
}
393+
return cr.SystemExpectedNVLink &&
394+
len(cr.NVLinks) > 1 &&
395+
cr.PeerNVLinkProbePairCount > 0 &&
396+
cr.PeerNVLinkOKPairCount == 0
397+
}
398+
399+
func (cr *checkResult) peerNVLinkStatusesSuggestReboot() bool {
400+
if cr == nil || len(cr.PeerNVLinkObservedStatusCodes) == 0 {
401+
return false
402+
}
403+
for _, statusCode := range cr.PeerNVLinkObservedStatusCodes {
404+
switch statusCode {
405+
case p2pStatusChipsetNotSupported,
406+
p2pStatusGPUNotSupported,
407+
p2pStatusTopologyNotSupported,
408+
p2pStatusDisabledByRegkey,
409+
p2pStatusNotSupported:
410+
continue
411+
default:
412+
return true
413+
}
414+
}
415+
return false
416+
}
417+
272418
func (cr *checkResult) ComponentName() string {
273419
return Name
274420
}
@@ -284,10 +430,29 @@ func (cr *checkResult) String() string {
284430
buf := bytes.NewBuffer(nil)
285431
table := tablewriter.NewWriter(buf)
286432
table.SetAlignment(tablewriter.ALIGN_CENTER)
287-
table.SetHeader([]string{"GPU UUID", "GPU Bus ID", "NVLink Enabled", "NVLink Supported"})
433+
headers := []string{"GPU UUID", "GPU Bus ID", "NVLink Enabled", "NVLink Supported"}
434+
peerNVLinkOKGPUUUIDs := make(map[string]struct{}, len(cr.PeerNVLinkOKGPUUUIDs))
435+
for _, uuid := range cr.PeerNVLinkOKGPUUUIDs {
436+
peerNVLinkOKGPUUUIDs[uuid] = struct{}{}
437+
}
438+
includePeerColumn := cr.PeerNVLinkProbePairCount > 0
439+
if includePeerColumn {
440+
headers = append(headers, "NVLink P2P OK")
441+
}
442+
table.SetHeader(headers)
288443
for _, nvlink := range cr.NVLinks {
289444
featureEnabled := nvlink.Supported && len(nvlink.States) > 0 && nvlink.States.AllFeatureEnabled()
290-
table.Append([]string{nvlink.UUID, nvlink.BusID, fmt.Sprintf("%t", featureEnabled), fmt.Sprintf("%t", nvlink.Supported)})
445+
row := []string{
446+
nvlink.UUID,
447+
nvlink.BusID,
448+
fmt.Sprintf("%t", featureEnabled),
449+
fmt.Sprintf("%t", nvlink.Supported),
450+
}
451+
if includePeerColumn {
452+
_, ok := peerNVLinkOKGPUUUIDs[nvlink.UUID]
453+
row = append(row, fmt.Sprintf("%t", ok))
454+
}
455+
table.Append(row)
291456
}
292457
table.Render()
293458

@@ -356,3 +521,12 @@ func (cr *checkResult) HealthStates() apiv1.HealthStates {
356521
}
357522
return apiv1.HealthStates{state}
358523
}
524+
525+
func sortedKeys(values map[string]struct{}) []string {
526+
keys := make([]string, 0, len(values))
527+
for key := range values {
528+
keys = append(keys, key)
529+
}
530+
sort.Strings(keys)
531+
return keys
532+
}

0 commit comments

Comments
 (0)