Skip to content

Commit e0efb32

Browse files
authored
[LEP-4139] feat(nvlink): dedup link failures, report one-shot kmsg scans (#1272)
## Summary - detect both post-RX failure signatures from `knvlinkUpdatePostRxDetectLinkMask_*` and `knvlinkDiscoverPostRxDetLinks_*` - normalize paired and repeated messages into one event per 5-minute bucket for daemon and one-shot scan paths, consistent with the other NVIDIA kmsg components - report one-shot kmsg scan results and return an unhealthy state with a reboot recommendation when the failure is found
1 parent e5ce846 commit e0efb32

6 files changed

Lines changed: 106 additions & 15 deletions

File tree

components/accelerator/nvidia/nvlink/component.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ import (
3232
const Name = "accelerator-nvidia-nvlink"
3333

3434
const (
35-
defaultCheckInterval = time.Minute
36-
defaultCheckStaleAfter = 2 * defaultCheckInterval
37-
defaultStateUpdateInterval = 30 * time.Second
35+
defaultCheckInterval = time.Minute
36+
defaultCheckStaleAfter = 2 * defaultCheckInterval
37+
defaultStateUpdateInterval = 30 * time.Second
38+
defaultKmsgEventDedupWindow = 5 * time.Minute
3839
)
3940

4041
var _ components.Component = &component{}
@@ -102,6 +103,7 @@ func New(gpudInstance *components.GPUdInstance) (components.Component, error) {
102103
cctx,
103104
func(line string) (string, string) { return matchWithBootID(line, bootID) },
104105
c.eventBucket,
106+
kmsg.WithCacheKeyTruncateSeconds(int(defaultKmsgEventDedupWindow.Seconds())),
105107
)
106108
if err != nil {
107109
c.eventBucket.Close()
@@ -274,10 +276,29 @@ func (c *component) Check() components.CheckResult {
274276
log.Logger.Warnw(cr.reason, "error", cr.err)
275277
return cr
276278
}
279+
cr.KmsgScanned = true
280+
type kmsgEventWindow struct {
281+
name, message string
282+
window int64
283+
}
284+
seen := make(map[kmsgEventWindow]struct{})
277285
for _, message := range kmsgs {
278-
if !HasPostRxDetectFailure(message.Message) {
286+
eventName, eventMessage := Match(message.Message)
287+
if eventName == "" {
279288
continue
280289
}
290+
key := kmsgEventWindow{
291+
name: eventName,
292+
message: eventMessage,
293+
window: message.Timestamp.Unix() / int64(defaultKmsgEventDedupWindow/time.Second),
294+
}
295+
if _, ok := seen[key]; ok {
296+
continue
297+
}
298+
seen[key] = struct{}{}
299+
cr.MatchedKmsgs = append(cr.MatchedKmsgs, message)
300+
}
301+
if len(cr.MatchedKmsgs) > 0 {
281302
state := unhealthyRebootState(cr.ts, postRxDetectFailureMessage)
282303
cr.health = state.Health
283304
cr.reason = state.Reason
@@ -439,6 +460,9 @@ func (c *component) Check() components.CheckResult {
439460
var _ components.CheckResult = &checkResult{}
440461

441462
type checkResult struct {
463+
KmsgScanned bool `json:"-"`
464+
MatchedKmsgs []kmsg.Message `json:"-"`
465+
442466
// NVLinks contains detailed NVLink information for all GPUs checked
443467
NVLinks []NVLink `json:"nvlinks,omitempty"`
444468

@@ -548,7 +572,14 @@ func (cr *checkResult) String() string {
548572
if cr == nil {
549573
return ""
550574
}
575+
kmsgSummary := ""
576+
if cr.KmsgScanned {
577+
kmsgSummary = fmt.Sprintf("matched %d kmsg(s)", len(cr.MatchedKmsgs))
578+
}
551579
if len(cr.NVLinks) == 0 {
580+
if kmsgSummary != "" {
581+
return kmsgSummary
582+
}
552583
return "no data"
553584
}
554585

@@ -581,13 +612,19 @@ func (cr *checkResult) String() string {
581612
}
582613
table.Render()
583614

615+
if kmsgSummary != "" {
616+
return kmsgSummary + "\n\n" + buf.String()
617+
}
584618
return buf.String()
585619
}
586620

587621
func (cr *checkResult) Summary() string {
588622
if cr == nil {
589623
return ""
590624
}
625+
if cr.KmsgScanned {
626+
return "scanned kmsg(s); " + cr.reason
627+
}
591628
return cr.reason
592629
}
593630

components/accelerator/nvidia/nvlink/component_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,15 @@ func TestData_String(t *testing.T) {
613613
}
614614
})
615615
}
616+
617+
t.Run("with kmsg summary", func(t *testing.T) {
618+
got := (&checkResult{
619+
KmsgScanned: true,
620+
NVLinks: []NVLink{{UUID: "gpu-uuid-123"}},
621+
}).String()
622+
assert.Contains(t, got, "matched 0 kmsg(s)")
623+
assert.Contains(t, got, "gpu-uuid-123")
624+
})
616625
}
617626

618627
func TestData_Summary(t *testing.T) {

components/accelerator/nvidia/nvlink/kmsg.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@ const (
77
// update an NVLink post-RX-detection link mask.
88
EventNamePostRxDetectFailure = "nvlink_post_rx_detect_failure"
99

10-
// NVIDIA's published GH100 kernel source describes
11-
// knvlinkDiscoverPostRxDetLinks_GH100 as discovering links that are training
12-
// or trained on both GPUs. It emits this exact LEVEL_ERROR and returns
13-
// NV_ERR_INVALID_STATE when knvlinkUpdatePostRxDetectLinkMask fails. The
14-
// source does not classify this error as a driver hang.
10+
// NVIDIA's published GH100 discovery path calls
11+
// knvlinkUpdatePostRxDetectLinkMask and reports the peer-mask failure when
12+
// that update fails. Some driver builds also emit the paired update failure.
13+
// The source does not classify this error as a driver hang.
1514
// ref. https://github.com/NVIDIA/open-gpu-kernel-modules/blob/452cec62d827034798072827d3866d1881662b77/src/nvidia/src/kernel/gpu/nvlink/arch/hopper/kernel_nvlink_gh100.c#L173-L249
16-
RegexPostRxDetectFailureKMessage = `NVRM: knvlinkDiscoverPostRxDetLinks_[A-Za-z0-9_]+: Getting peer[0-9]+(?:'s)? postRxDetLinkMask failed!`
15+
RegexPostRxDetectFailureKMessage = `NVRM: (?:knvlinkUpdatePostRxDetectLinkMask_[A-Za-z0-9_]+: Failed to update Rx Detect Link mask!|knvlinkDiscoverPostRxDetLinks_[A-Za-z0-9_]+: Getting peer[0-9]+(?:'s)? postRxDetLinkMask failed!)`
1716

1817
postRxDetectFailureMessage = "NVIDIA driver failed to update an NVLink post-RX-detection link mask"
1918
)

components/accelerator/nvidia/nvlink/kmsg_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ func TestMatch(t *testing.T) {
1717
line: "NVRM: knvlinkDiscoverPostRxDetLinks_GH100: Getting peer0's postRxDetLinkMask failed!",
1818
match: true,
1919
},
20+
{
21+
name: "incident link-mask update signature",
22+
line: "NVRM: knvlinkUpdatePostRxDetectLinkMask_IMPL: Failed to update Rx Detect Link mask!",
23+
match: true,
24+
},
2025
{
2126
name: "other GPU architecture and peer",
2227
line: "kernel: NVRM: knvlinkDiscoverPostRxDetLinks_GB100_REV2: Getting peer17 postRxDetLinkMask failed!",

components/accelerator/nvidia/nvlink/mock_component_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"os"
7+
"reflect"
78
"runtime"
89
"testing"
910
"time"
@@ -27,13 +28,15 @@ func TestNew_WithEventStoreAsRoot_KmsgSyncer_WithMockey(t *testing.T) {
2728

2829
bucket := &memoryEventBucket{}
2930
var match kmsg.MatchFunc
31+
var syncerOpts []kmsg.OpOption
3032
mockey.Mock(kmsg.NewSyncer).To(func(
3133
_ context.Context,
3234
matchFunc kmsg.MatchFunc,
3335
eventBucket eventstore.Bucket,
34-
_ ...kmsg.OpOption,
36+
opts ...kmsg.OpOption,
3537
) (*kmsg.Syncer, error) {
3638
match = matchFunc
39+
syncerOpts = opts
3740
assert.Same(t, bucket, eventBucket)
3841
return &kmsg.Syncer{}, nil
3942
}).Build()
@@ -48,6 +51,8 @@ func TestNew_WithEventStoreAsRoot_KmsgSyncer_WithMockey(t *testing.T) {
4851
require.NotNil(t, c.kmsgSyncer)
4952
assert.Nil(t, c.readAllKmsg, "daemon components should rely on the kmsg syncer")
5053
require.NotNil(t, match)
54+
require.Len(t, syncerOpts, 1)
55+
assert.Equal(t, int(defaultKmsgEventDedupWindow.Seconds()), getKmsgCacheKeyTruncateSeconds(t, syncerOpts[0]))
5156

5257
name, message := match("NVRM: knvlinkDiscoverPostRxDetLinks_GH100: Getting peer0's postRxDetLinkMask failed!")
5358
assert.Equal(t, EventNamePostRxDetectFailure, name)
@@ -63,6 +68,17 @@ func TestNew_WithEventStoreAsRoot_KmsgSyncer_WithMockey(t *testing.T) {
6368
})
6469
}
6570

71+
func getKmsgCacheKeyTruncateSeconds(t *testing.T, opt kmsg.OpOption) int {
72+
t.Helper()
73+
op := &kmsg.Op{}
74+
opt(op)
75+
76+
v := reflect.ValueOf(op).Elem().FieldByName("cacheKeyTruncateSeconds")
77+
require.True(t, v.IsValid(), "cacheKeyTruncateSeconds field must exist")
78+
require.Equal(t, reflect.Int, v.Kind(), "cacheKeyTruncateSeconds must be int")
79+
return int(v.Int())
80+
}
81+
6682
func TestNew_WithEventStoreAsRoot_KmsgSyncerError_WithMockey(t *testing.T) {
6783
mockey.PatchConvey("New cleans up when the kmsg syncer cannot be created", t, func() {
6884
mockey.Mock(os.Geteuid).Return(0).Build()

components/accelerator/nvidia/nvlink/post_rx_detect_failure_test.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1314

1415
apiv1 "github.com/leptonai/gpud/api/v1"
1516
"github.com/leptonai/gpud/pkg/eventstore"
@@ -192,7 +193,7 @@ func TestHealthStateFailsWhenNoCheckCompletesAfterMonitoringStarts(t *testing.T)
192193
assert.Contains(t, state.Reason, "has not refreshed")
193194
}
194195

195-
func TestCheckDetectsPostRxDetectFailureForOneShotScan(t *testing.T) {
196+
func TestCheckDetectsAndDeduplicatesPostRxDetectFailureForOneShotScan(t *testing.T) {
196197
getNVLinkCalled := false
197198
c := mustComponent(t, MockNVLinkComponent(
198199
context.Background(),
@@ -202,19 +203,39 @@ func TestCheckDetectsPostRxDetectFailureForOneShotScan(t *testing.T) {
202203
return NVLink{}, nil
203204
},
204205
))
206+
baseTime := time.Date(2026, 2, 3, 3, 0, 0, 0, time.UTC)
207+
reportedKmsgs := []kmsg.Message{
208+
{Timestamp: metav1.NewTime(baseTime), Message: "[6619715.535712] NVRM: knvlinkUpdatePostRxDetectLinkMask_IMPL: Failed to update Rx Detect Link mask!"},
209+
{Timestamp: metav1.NewTime(baseTime.Add(18 * time.Microsecond)), Message: "[6619715.535730] NVRM: knvlinkDiscoverPostRxDetLinks_GH100: Getting peer0's postRxDetLinkMask failed!"},
210+
{Timestamp: metav1.NewTime(baseTime.Add(4*time.Second + 2771*time.Microsecond)), Message: "[6619719.538483] NVRM: knvlinkUpdatePostRxDetectLinkMask_IMPL: Failed to update Rx Detect Link mask!"},
211+
{Timestamp: metav1.NewTime(baseTime.Add(4*time.Second + 2787*time.Microsecond)), Message: "[6619719.538499] NVRM: knvlinkDiscoverPostRxDetLinks_GH100: Getting peer0's postRxDetLinkMask failed!"},
212+
{Timestamp: metav1.NewTime(baseTime.Add(8*time.Second + 3808*time.Microsecond)), Message: "[6619723.539520] NVRM: knvlinkUpdatePostRxDetectLinkMask_IMPL: Failed to update Rx Detect Link mask!"},
213+
{Timestamp: metav1.NewTime(baseTime.Add(8*time.Second + 3827*time.Microsecond)), Message: "[6619723.539539] NVRM: knvlinkDiscoverPostRxDetLinks_GH100: Getting peer0's postRxDetLinkMask failed!"},
214+
}
215+
for _, message := range reportedKmsgs {
216+
eventName, eventMessage := Match(message.Message)
217+
assert.Equal(t, EventNamePostRxDetectFailure, eventName, message.Message)
218+
assert.Equal(t, postRxDetectFailureMessage, eventMessage, message.Message)
219+
}
205220
c.readAllKmsg = func(context.Context) ([]kmsg.Message, error) {
206-
return []kmsg.Message{{
207-
Message: "NVRM: knvlinkDiscoverPostRxDetLinks_GH100: Getting peer0's postRxDetLinkMask failed!",
208-
}}, nil
221+
return reportedKmsgs, nil
209222
}
210223

211224
result := c.Check()
212225
assert.Equal(t, apiv1.HealthStateTypeUnhealthy, result.HealthStateType())
226+
assert.Contains(t, result.Summary(), "scanned kmsg(s)")
213227
assert.Contains(t, result.Summary(), postRxDetectFailureMessage)
228+
assert.Equal(t, "matched 1 kmsg(s)", result.String())
214229
assert.False(t, getNVLinkCalled, "kmsg failure should short-circuit NVML probing")
215230
state := result.HealthStates()[0]
216231
require.NotNil(t, state.SuggestedActions)
217232
assert.Equal(t, []apiv1.RepairActionType{apiv1.RepairActionTypeRebootSystem}, state.SuggestedActions.RepairActions)
233+
234+
reportedKmsgs = append(reportedKmsgs, kmsg.Message{
235+
Timestamp: metav1.NewTime(baseTime.Add(defaultKmsgEventDedupWindow)),
236+
Message: "NVRM: knvlinkUpdatePostRxDetectLinkMask_IMPL: Failed to update Rx Detect Link mask!",
237+
})
238+
assert.Equal(t, "matched 2 kmsg(s)", c.Check().String(), "a later dedup window should report the failure again")
218239
}
219240

220241
func TestCheckHandlesOneShotKmsgReadResult(t *testing.T) {
@@ -223,6 +244,7 @@ func TestCheckHandlesOneShotKmsgReadResult(t *testing.T) {
223244
readAll func(context.Context) ([]kmsg.Message, error)
224245
wantHealth apiv1.HealthStateType
225246
wantReason string
247+
wantString string
226248
}{
227249
{
228250
name: "read failure",
@@ -231,6 +253,7 @@ func TestCheckHandlesOneShotKmsgReadResult(t *testing.T) {
231253
},
232254
wantHealth: apiv1.HealthStateTypeUnhealthy,
233255
wantReason: "failed to read kmsg",
256+
wantString: "no data",
234257
},
235258
{
236259
name: "no matching message",
@@ -239,6 +262,7 @@ func TestCheckHandlesOneShotKmsgReadResult(t *testing.T) {
239262
},
240263
wantHealth: apiv1.HealthStateTypeHealthy,
241264
wantReason: "all 0 GPU(s) were checked, no nvlink issue found",
265+
wantString: "matched 0 kmsg(s)",
242266
},
243267
}
244268

@@ -254,6 +278,7 @@ func TestCheckHandlesOneShotKmsgReadResult(t *testing.T) {
254278
result := c.Check()
255279
assert.Equal(t, tt.wantHealth, result.HealthStateType())
256280
assert.Contains(t, result.Summary(), tt.wantReason)
281+
assert.Equal(t, tt.wantString, result.String())
257282
})
258283
}
259284
}

0 commit comments

Comments
 (0)