Skip to content

Commit c570f0a

Browse files
authored
feat(xid/sxid): allow custom reboot thresholds (#1244)
Add per-XID and per-SXID reboot threshold maps for gpud run. ```bash gpud run \ --xid-thresholds '{"overrides":{"94":{"rebootThreshold":1000}}}' \ --sxid-thresholds '{"overrides":{"11004":{"rebootThreshold":7}}}' ```
1 parent 38cc3e7 commit c570f0a

21 files changed

Lines changed: 657 additions & 178 deletions

cmd/gpud/command/command.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,14 @@ sudo rm /etc/systemd/system/gpud.service
253253
Usage: fmt.Sprintf("set the allowed reboot attempts for XID errors before escalation (defaults to %d)", componentsxid.DefaultRebootThreshold),
254254
Value: componentsxid.DefaultRebootThreshold,
255255
},
256+
&cli.StringFlag{
257+
Name: "xid-thresholds",
258+
Usage: `set per-XID thresholds in JSON, e.g. '{"overrides":{"94":{"rebootThreshold":1000}}}'`,
259+
},
260+
&cli.StringFlag{
261+
Name: "sxid-thresholds",
262+
Usage: `set per-SXID thresholds in JSON, e.g. '{"overrides":{"11004":{"rebootThreshold":7}}}'`,
263+
},
256264
&cli.DurationFlag{
257265
Name: "xid-lookback-period",
258266
Usage: "set the lookback period for XID errors",

cmd/gpud/run/command.go

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ func Command(cliContext *cli.Context) error {
142142
nvlinkExpectedLinkStates := cliContext.String("nvlink-expected-link-states")
143143
nfsCheckerConfigs := cliContext.String("nfs-checker-configs")
144144
xidRebootThreshold := cliContext.Int("xid-reboot-threshold")
145+
xidThresholds := cliContext.String("xid-thresholds")
146+
sxidThresholds := cliContext.String("sxid-thresholds")
145147
temperatureMarginThresholdCelsius := cliContext.Int("threshold-celsius-slowdown-margin")
146148

147149
if len(infinibandExpectedPortStates) > 0 {
@@ -174,16 +176,42 @@ func Command(cliContext *cli.Context) error {
174176
log.Logger.Infow("set nfs checker group configs", "groupConfigs", groupConfigs)
175177
}
176178

179+
xidThresholdsChanged := false
177180
if cliContext.IsSet("xid-reboot-threshold") {
178181
if xidRebootThreshold > 0 {
179-
componentsxid.SetDefaultRebootThreshold(componentsxid.RebootThreshold{
180-
Threshold: xidRebootThreshold,
181-
})
182-
log.Logger.Infow("set xid reboot threshold", "xidRebootThreshold", xidRebootThreshold)
182+
componentsxid.SetDefaultRebootThreshold(xidRebootThreshold)
183+
xidThresholdsChanged = true
183184
} else {
184185
log.Logger.Warnw("ignoring xid reboot threshold override, value must be positive", "xidRebootThreshold", xidRebootThreshold)
185186
}
186187
}
188+
if strings.TrimSpace(xidThresholds) != "" {
189+
thresholds, err := parseXIDThresholds(xidThresholds)
190+
if err != nil {
191+
return err
192+
}
193+
componentsxid.SetDefaultThresholds(thresholds)
194+
xidThresholdsChanged = true
195+
}
196+
if xidThresholdsChanged {
197+
xidThresholdConfig := componentsxid.GetDefaultThresholds()
198+
log.Logger.Infow(
199+
"set xid thresholds",
200+
"xidDefaultRebootThreshold",
201+
componentsxid.GetDefaultRebootThreshold(),
202+
"xidOverrides",
203+
xidThresholdConfig.Overrides,
204+
)
205+
}
206+
207+
if strings.TrimSpace(sxidThresholds) != "" {
208+
thresholds, err := parseSXIDThresholds(sxidThresholds)
209+
if err != nil {
210+
return err
211+
}
212+
componentssxid.SetDefaultThresholds(thresholds)
213+
log.Logger.Infow("set sxid thresholds", "sxidOverrides", thresholds.Overrides)
214+
}
187215

188216
if eventsRetentionPeriod > 0 && !cliContext.IsSet("xid-lookback-period") {
189217
componentsxid.SetLookbackPeriod(eventsRetentionPeriod)
@@ -438,6 +466,79 @@ func parseRetentionPeriods(cliContext *cli.Context) (metricsRetentionPeriod, eve
438466
return metricsRetentionPeriod, eventsRetentionPeriod
439467
}
440468

469+
type thresholdOverrideJSON struct {
470+
RebootThreshold int `json:"rebootThreshold"`
471+
}
472+
473+
type thresholdsJSON struct {
474+
Overrides map[int]thresholdOverrideJSON `json:"overrides"`
475+
}
476+
477+
func parseXIDThresholds(raw string) (componentsxid.Thresholds, error) {
478+
thresholds, err := parseThresholds(raw, "xid thresholds")
479+
if err != nil {
480+
return componentsxid.Thresholds{}, err
481+
}
482+
483+
ret := make(map[int]componentsxid.ThresholdOverride, len(thresholds))
484+
for xid, threshold := range thresholds {
485+
ret[xid] = componentsxid.ThresholdOverride{
486+
RebootThreshold: threshold.RebootThreshold,
487+
}
488+
}
489+
return componentsxid.Thresholds{Overrides: ret}, nil
490+
}
491+
492+
func parseSXIDThresholds(raw string) (componentssxid.Thresholds, error) {
493+
thresholds, err := parseThresholds(raw, "sxid thresholds")
494+
if err != nil {
495+
return componentssxid.Thresholds{}, err
496+
}
497+
498+
ret := make(map[int]componentssxid.ThresholdOverride, len(thresholds))
499+
for sxid, threshold := range thresholds {
500+
ret[sxid] = componentssxid.ThresholdOverride{
501+
RebootThreshold: threshold.RebootThreshold,
502+
}
503+
}
504+
return componentssxid.Thresholds{Overrides: ret}, nil
505+
}
506+
507+
func parseThresholds(raw string, name string) (map[int]thresholdOverrideJSON, error) {
508+
raw = strings.TrimSpace(raw)
509+
if raw == "" {
510+
return nil, nil
511+
}
512+
513+
var fields map[string]json.RawMessage
514+
if err := json.Unmarshal([]byte(raw), &fields); err != nil {
515+
return nil, fmt.Errorf("invalid %s: %w", name, err)
516+
}
517+
if fields == nil {
518+
return nil, fmt.Errorf("invalid %s: expected JSON object", name)
519+
}
520+
for field := range fields {
521+
if field != "overrides" {
522+
return nil, fmt.Errorf("invalid %s: unknown field %q", name, field)
523+
}
524+
}
525+
526+
var thresholds thresholdsJSON
527+
if err := json.Unmarshal([]byte(raw), &thresholds); err != nil {
528+
return nil, fmt.Errorf("invalid %s: %w", name, err)
529+
}
530+
531+
for id, threshold := range thresholds.Overrides {
532+
if id < 0 {
533+
return nil, fmt.Errorf("invalid %s for %d: event ID must be non-negative", name, id)
534+
}
535+
if threshold.RebootThreshold <= 0 {
536+
return nil, fmt.Errorf("invalid %s for %d: rebootThreshold must be positive", name, id)
537+
}
538+
}
539+
return thresholds.Overrides, nil
540+
}
541+
441542
func parseInfinibandExcludeDevices(s string) []string {
442543
if s == "" {
443544
return nil

cmd/gpud/run/mock_command_exec_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ func newTestCLIContext(t *testing.T, values cliFlagValues) *cli.Context {
6363
set.String("nvlink-expected-link-states", "", "")
6464
set.String("nfs-checker-configs", "", "")
6565
set.Int("xid-reboot-threshold", 0, "")
66+
set.String("xid-thresholds", "", "")
67+
set.String("sxid-thresholds", "", "")
6668
set.Duration("xid-lookback-period", 0, "")
6769
set.Duration("sxid-lookback-period", 0, "")
6870
set.Int("threshold-celsius-slowdown-margin", 0, "")
@@ -275,6 +277,62 @@ func TestCommand_SetLookbackPeriods(t *testing.T) {
275277
})
276278
}
277279

280+
func TestCommand_SetThresholds(t *testing.T) {
281+
tmpDir := t.TempDir()
282+
283+
originalXidRebootThreshold := componentsxid.GetDefaultRebootThreshold()
284+
originalXidThresholds := componentsxid.GetDefaultThresholds()
285+
originalSxidThresholds := componentssxid.GetDefaultThresholds()
286+
t.Cleanup(func() {
287+
componentsxid.SetDefaultRebootThreshold(originalXidRebootThreshold)
288+
componentsxid.SetDefaultThresholds(originalXidThresholds)
289+
componentssxid.SetDefaultThresholds(originalSxidThresholds)
290+
})
291+
292+
ctx := newTestCLIContext(t, cliFlagValues{
293+
stringFlags: map[string]string{
294+
"log-level": "info",
295+
"data-dir": tmpDir,
296+
"xid-thresholds": `{"overrides":{"94":{"rebootThreshold":2000},"95":{"rebootThreshold":3}}}`,
297+
"sxid-thresholds": `{"overrides":{"11004":{"rebootThreshold":7}}}`,
298+
},
299+
intFlags: map[string]int{
300+
"xid-reboot-threshold": 6,
301+
},
302+
})
303+
304+
mockey.PatchConvey("Command sets xid and sxid thresholds", t, func() {
305+
mockey.Mock(gpudmanager.New).To(func(dataDir string) (*gpudmanager.Manager, error) {
306+
return &gpudmanager.Manager{}, nil
307+
}).Build()
308+
mockey.Mock((*gpudmanager.Manager).Start).To(func(_ *gpudmanager.Manager, _ context.Context) error {
309+
return nil
310+
}).Build()
311+
mockey.Mock(gpudserver.New).To(func(_ context.Context, _ log.AuditLogger, _ *config.Config, _ *gpudmanager.Manager) (*gpudserver.Server, error) {
312+
return &gpudserver.Server{}, nil
313+
}).Build()
314+
mockey.Mock(gpudserver.HandleSignals).To(func(_ context.Context, _ context.CancelFunc, _ chan os.Signal, _ chan gpudserver.ServerStopper, _ func(context.Context) error) chan struct{} {
315+
done := make(chan struct{})
316+
close(done)
317+
return done
318+
}).Build()
319+
mockey.Mock(pkgsystemd.SystemctlExists).To(func() bool {
320+
return false
321+
}).Build()
322+
323+
err := Command(ctx)
324+
require.NoError(t, err)
325+
326+
xidThresholdConfig := componentsxid.GetDefaultThresholds()
327+
assert.Equal(t, 6, componentsxid.GetDefaultRebootThreshold())
328+
assert.Equal(t, 2000, xidThresholdConfig.Overrides[94].RebootThreshold)
329+
assert.Equal(t, 3, xidThresholdConfig.Overrides[95].RebootThreshold)
330+
331+
sxidThresholds := componentssxid.GetDefaultThresholds()
332+
assert.Equal(t, 7, sxidThresholds.Overrides[11004].RebootThreshold)
333+
})
334+
}
335+
278336
func TestCommand_UsesEventsRetentionForXidAndSxidLookback(t *testing.T) {
279337
tmpDir := t.TempDir()
280338

cmd/gpud/run/thresholds_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package run
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestParseThresholds(t *testing.T) {
11+
xidThresholds, err := parseXIDThresholds(`{"overrides":{"94":{"rebootThreshold":1000}}}`)
12+
require.NoError(t, err)
13+
assert.Equal(t, 1000, xidThresholds.Overrides[94].RebootThreshold)
14+
15+
sxidThresholds, err := parseSXIDThresholds(`{"overrides":{"11004":{"rebootThreshold":7}}}`)
16+
require.NoError(t, err)
17+
assert.Equal(t, 7, sxidThresholds.Overrides[11004].RebootThreshold)
18+
19+
_, err = parseXIDThresholds(`{"overrides":{"94":{"rebootThreshold":0}}}`)
20+
require.Error(t, err)
21+
assert.Contains(t, err.Error(), "rebootThreshold must be positive")
22+
23+
_, err = parseXIDThresholds(`{"94":{"rebootThreshold":1000}}`)
24+
require.Error(t, err)
25+
assert.Contains(t, err.Error(), `unknown field "94"`)
26+
27+
_, err = parseSXIDThresholds(`{not-valid-json}`)
28+
require.Error(t, err)
29+
assert.Contains(t, err.Error(), "sxid thresholds")
30+
}

cmd/gpud/scan/command.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,7 @@ func cmdScan(
142142

143143
if xidRebootThresholdIsSet {
144144
if xidRebootThreshold > 0 {
145-
componentsxid.SetDefaultRebootThreshold(componentsxid.RebootThreshold{
146-
Threshold: xidRebootThreshold,
147-
})
145+
componentsxid.SetDefaultRebootThreshold(xidRebootThreshold)
148146
log.Logger.Infow("set xid reboot threshold", "xidRebootThreshold", xidRebootThreshold)
149147
} else {
150148
log.Logger.Warnw("ignoring xid reboot threshold override, value must be positive", "xidRebootThreshold", xidRebootThreshold)

components/accelerator/nvidia/sxid/health_state.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,17 @@ func translateToStateHealth(health int) apiv1.HealthStateType {
3232
}
3333
}
3434

35-
const rebootThreshold = 2
36-
3735
// evolveHealthyState resolves the state of the SXID error component.
3836
// note: assume events are sorted by time in descending order
3937
func evolveHealthyState(events eventstore.Events) (ret apiv1.HealthState) {
38+
return evolveHealthyStateWithThresholds(events, DefaultRebootThreshold, GetDefaultThresholds())
39+
}
40+
41+
func evolveHealthyStateWithThresholds(events eventstore.Events, defaultRebootThreshold int, thresholds Thresholds) (ret apiv1.HealthState) {
4042
defer func() {
4143
log.Logger.Debugf("EvolveHealthyState: %v", ret)
4244
}()
45+
thresholds = normalizeThresholds(thresholds)
4346
var lastSuggestedAction *apiv1.SuggestedActions
4447
var lastSXidErr *sxidErrorEventDetail
4548
lastHealth := healthStateHealthy
@@ -68,9 +71,10 @@ func evolveHealthyState(events eventstore.Events) (ret apiv1.HealthState) {
6871
lastSXidErr = &currSXidErr
6972
if currSXidErr.SuggestedActionsByGPUd != nil && len(currSXidErr.SuggestedActionsByGPUd.RepairActions) > 0 {
7073
if currSXidErr.SuggestedActionsByGPUd.RepairActions[0] == apiv1.RepairActionTypeRebootSystem {
74+
effectiveRebootThreshold := rebootThresholdForSXID(currSXidErr.SXid, defaultRebootThreshold, thresholds)
7175
if count, ok := sxidRebootMap[currSXidErr.SXid]; !ok {
7276
sxidRebootMap[currSXidErr.SXid] = 0
73-
} else if count >= rebootThreshold {
77+
} else if count >= effectiveRebootThreshold {
7478
currSXidErr.SuggestedActionsByGPUd.RepairActions[0] = apiv1.RepairActionTypeHardwareInspection
7579
}
7680
}

components/accelerator/nvidia/sxid/health_state_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,26 @@ func TestStateUpdateBasedOnEvents(t *testing.T) {
8282
assert.Equal(t, apiv1.RepairActionTypeHardwareInspection, state.SuggestedActions.RepairActions[0])
8383
})
8484

85+
t.Run("custom sxid reboot threshold override prevents hardware inspection", func(t *testing.T) {
86+
events := eventstore.Events{
87+
createSXidEvent(time.Time{}, 94, apiv1.EventTypeFatal, apiv1.RepairActionTypeRebootSystem),
88+
{Name: "reboot"},
89+
createSXidEvent(time.Time{}, 94, apiv1.EventTypeFatal, apiv1.RepairActionTypeRebootSystem),
90+
{Name: "reboot"},
91+
createSXidEvent(time.Time{}, 94, apiv1.EventTypeFatal, apiv1.RepairActionTypeRebootSystem),
92+
}
93+
94+
state := evolveHealthyStateWithThresholds(events, DefaultRebootThreshold, Thresholds{
95+
Overrides: map[int]ThresholdOverride{
96+
94: {RebootThreshold: 1000},
97+
},
98+
})
99+
assert.Equal(t, apiv1.HealthStateTypeUnhealthy, state.Health)
100+
require.NotNil(t, state.SuggestedActions)
101+
require.NotEmpty(t, state.SuggestedActions.RepairActions)
102+
assert.Equal(t, apiv1.RepairActionTypeRebootSystem, state.SuggestedActions.RepairActions[0])
103+
})
104+
85105
t.Run("EmptyEvents_ReturnsHealthy", func(t *testing.T) {
86106
events := eventstore.Events{}
87107
state := evolveHealthyState(events)

0 commit comments

Comments
 (0)