Skip to content

Commit a275c5c

Browse files
committed
[LEP-3777] feat(nvidia): add --nvml-device-get-devices-error flag for testing NVML init failures
This adds a hidden testing flag to simulate Device().GetDevices() failures, enabling integration testing of the erroredInstance path introduced in: #1180 The flag simulates the "Unable to determine the device handle for GPU: Unknown Error" scenario that occurs when NVML loads but device enumeration fails (e.g., Xid 79). When enabled, gpud continues running but all nvidia components report unhealthy. Signed-off-by: Gyuho Lee <gyuhol@nvidia.com>
1 parent befab47 commit a275c5c

9 files changed

Lines changed: 162 additions & 2 deletions

File tree

cmd/gpud/command/command.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,11 @@ sudo rm /etc/systemd/system/gpud.service
300300
Usage: "(testing purposes) set the comma-separated gpu uuids to return GPU fabric health summary unhealthy (nvml.GPU_FABRIC_HEALTH_SUMMARY_UNHEALTHY). NOTE: Only works on multi-GPU NVSwitch systems (H100-SXM, H200-SXM, GB200). Ignored on PCIe variants and single-GPU systems.",
301301
Hidden: true, // only for testing
302302
},
303+
cli.BoolFlag{
304+
Name: "nvml-device-get-devices-error",
305+
Usage: "(testing purposes) simulate NVML Device().GetDevices() failure returning 'Unable to determine the device handle for GPU: Unknown Error'. Use to test gpud behavior when NVML loads but device enumeration fails (e.g., Xid 79).",
306+
Hidden: true, // only for testing
307+
},
303308
},
304309
},
305310
{

cmd/gpud/run/command.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,10 @@ func Command(cliContext *cli.Context) error {
221221
// (e.g., set "H100-SXM" on H100-PCIe to enable fabric state failure injection testing)
222222
gpuProductNameOverride := cliContext.String("gpu-product-name")
223223

224+
// NVML device enumeration error injection for testing
225+
// When enabled, Device().GetDevices() returns an error simulating Xid 79 or similar failures
226+
nvmlDeviceGetDevicesError := cliContext.Bool("nvml-device-get-devices-error")
227+
224228
ibExcludedDevices := parseInfinibandExcludeDevices(ibExcludeDevicesStr)
225229
if len(ibExcludedDevices) > 0 {
226230
log.Logger.Infow("excluding infiniband devices from monitoring", "devices", ibExcludedDevices)
@@ -241,6 +245,7 @@ func Command(cliContext *cli.Context) error {
241245
GPUUUIDsWithGPURequiresReset: gpuUUIDsWithGPURequiresReset,
242246
GPUUUIDsWithFabricStateHealthSummaryUnhealthy: gpuUUIDsWithFabricStateHealthSummaryUnhealthy,
243247
GPUProductNameOverride: gpuProductNameOverride,
248+
NVMLDeviceGetDevicesError: nvmlDeviceGetDevicesError,
244249
}),
245250
}
246251

components/registry.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ type FailureInjector struct {
6060
// (e.g., H100-PCIe) doesn't support fabric state monitoring.
6161
// Set to "H100-SXM" or "H200-SXM" to simulate a fabric-capable system.
6262
GPUProductNameOverride string
63+
64+
// NVMLDeviceGetDevicesError when true simulates Device().GetDevices() failure.
65+
// This is useful for testing the "Unable to determine the device handle for GPU: Unknown Error"
66+
// scenario that occurs when NVML library loads but device enumeration fails (e.g., Xid 79).
67+
// ref. https://github.com/leptonai/gpud/pull/1180
68+
NVMLDeviceGetDevicesError bool
6369
}
6470

6571
// InitFunc is the function that initializes a component.

pkg/nvidia-query/nvml/instance.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ type FailureInjectorConfig struct {
2828
// When set, this affects FabricStateSupported(), FabricManagerSupported(),
2929
// and memory management capabilities detection.
3030
GPUProductNameOverride string
31+
32+
// NVMLDeviceGetDevicesError when true simulates Device().GetDevices() failure.
33+
// This is useful for testing the "Unable to determine the device handle for GPU: Unknown Error"
34+
// scenario that occurs when NVML library loads but device enumeration fails (e.g., Xid 79).
35+
// When enabled, gpud continues running but all nvidia components report unhealthy.
36+
// ref. https://github.com/leptonai/gpud/pull/1180
37+
NVMLDeviceGetDevicesError bool
3138
}
3239

3340
var _ Instance = &instance{}
@@ -133,11 +140,22 @@ func refreshNVMLAndExit(ctx context.Context) {
133140
}
134141
}
135142

143+
// ErrDeviceGetDevicesInjected is the error returned when NVMLDeviceGetDevicesError is enabled.
144+
// This simulates the "Unable to determine the device handle for GPU: Unknown Error" scenario.
145+
var ErrDeviceGetDevicesInjected = errors.New("error getting device handle for index '0': Unknown Error (injected for testing)")
146+
136147
// newInstance creates a new instance of the NVML library.
137148
// If NVML is not installed, it returns no-op nvml instance.
138149
// The "refreshNVML" function is only called when the NVML library is not found.
139150
func newInstance(refreshCtx context.Context, refreshNVML func(context.Context), failureInjector *FailureInjectorConfig) (Instance, error) {
140-
nvmlLib, err := nvmllib.New()
151+
// Build library options for failure injection
152+
var libOpts []nvmllib.OpOption
153+
if failureInjector != nil && failureInjector.NVMLDeviceGetDevicesError {
154+
log.Logger.Warnw("NVML Device().GetDevices() error injection enabled for testing")
155+
libOpts = append(libOpts, nvmllib.WithDeviceGetDevicesError(ErrDeviceGetDevicesInjected))
156+
}
157+
158+
nvmlLib, err := nvmllib.New(libOpts...)
141159
if err != nil {
142160
if errors.Is(err, nvmllib.ErrNVMLNotFound) {
143161
if refreshNVML != nil {

pkg/nvidia-query/nvml/instance_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,90 @@ func TestInstanceV2(t *testing.T) {
1717
}
1818
t.Logf("instance mem cap %+v", inst.GetMemoryErrorManagementCapabilities())
1919
}
20+
21+
func TestNewWithFailureInjector_NVMLDeviceGetDevicesError(t *testing.T) {
22+
// Test that enabling NVMLDeviceGetDevicesError returns an erroredInstance
23+
inst, err := NewWithFailureInjector(&FailureInjectorConfig{
24+
NVMLDeviceGetDevicesError: true,
25+
})
26+
27+
if errors.Is(err, nvmllib.ErrNVMLNotFound) {
28+
t.Skipf("nvml not installed, skipping")
29+
}
30+
31+
// Should not return an error from NewWithFailureInjector - instead, it returns an erroredInstance
32+
if err != nil {
33+
t.Fatalf("failed to create instance: %v", err)
34+
}
35+
36+
// If NVML is not installed, the function returns a noOpInstance (NVMLExists=false)
37+
// which is expected - the error injection only works when NVML is actually loaded
38+
if !inst.NVMLExists() {
39+
t.Skipf("nvml not installed (noOpInstance returned), skipping")
40+
}
41+
42+
// InitError() should return our injected error
43+
initErr := inst.InitError()
44+
if initErr == nil {
45+
t.Fatal("expected InitError() to return the injected error, got nil")
46+
}
47+
48+
// Verify it's our injected error
49+
if !errors.Is(initErr, ErrDeviceGetDevicesInjected) {
50+
t.Fatalf("expected InitError() to return ErrDeviceGetDevicesInjected, got: %v", initErr)
51+
}
52+
53+
// Devices should be nil for erroredInstance
54+
if inst.Devices() != nil {
55+
t.Fatalf("expected Devices() to return nil for erroredInstance, got: %v", inst.Devices())
56+
}
57+
58+
t.Logf("successfully tested NVMLDeviceGetDevicesError injection: %v", initErr)
59+
}
60+
61+
func TestErroredInstance(t *testing.T) {
62+
// Test the erroredInstance directly
63+
testErr := errors.New("test error")
64+
inst := NewErrored(testErr)
65+
66+
// NVMLExists returns true because the library loaded
67+
if !inst.NVMLExists() {
68+
t.Error("expected NVMLExists() to return true for erroredInstance")
69+
}
70+
71+
// InitError returns the error
72+
if inst.InitError() != testErr {
73+
t.Errorf("expected InitError() to return %v, got %v", testErr, inst.InitError())
74+
}
75+
76+
// All data methods return nil/zero values
77+
if inst.Devices() != nil {
78+
t.Error("expected Devices() to return nil for erroredInstance")
79+
}
80+
if inst.Library() != nil {
81+
t.Error("expected Library() to return nil for erroredInstance")
82+
}
83+
if inst.ProductName() != "" {
84+
t.Error("expected ProductName() to return empty string for erroredInstance")
85+
}
86+
if inst.DriverVersion() != "" {
87+
t.Error("expected DriverVersion() to return empty string for erroredInstance")
88+
}
89+
if inst.DriverMajor() != 0 {
90+
t.Error("expected DriverMajor() to return 0 for erroredInstance")
91+
}
92+
if inst.CUDAVersion() != "" {
93+
t.Error("expected CUDAVersion() to return empty string for erroredInstance")
94+
}
95+
if inst.FabricManagerSupported() {
96+
t.Error("expected FabricManagerSupported() to return false for erroredInstance")
97+
}
98+
if inst.FabricStateSupported() {
99+
t.Error("expected FabricStateSupported() to return false for erroredInstance")
100+
}
101+
102+
// Shutdown should not error
103+
if err := inst.Shutdown(); err != nil {
104+
t.Errorf("expected Shutdown() to return nil, got %v", err)
105+
}
106+
}

pkg/nvidia-query/nvml/lib/lib.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func createLibrary(opts ...OpOption) Library {
6262
devices: options.devicesToReturn,
6363
getRemappedRowsForAllDevs: options.devGetRemappedRowsForAllDevs,
6464
getCurrentClocksEventReasonsForAllDevs: options.devGetCurrentClocksEventReasonsForAllDevs,
65+
getDevicesError: options.devGetDevicesError,
6566
}
6667

6768
infoOpts := []nvinfo.Option{
@@ -90,9 +91,15 @@ type devInterface struct {
9091
devices []nvlibdevice.Device
9192
getRemappedRowsForAllDevs func() (int, int, bool, bool, nvml.Return)
9293
getCurrentClocksEventReasonsForAllDevs func() (uint64, nvml.Return)
94+
getDevicesError error
9395
}
9496

9597
func (d *devInterface) GetDevices() ([]nvlibdevice.Device, error) {
98+
// Check for injected error first (for testing device enumeration failures)
99+
if d.getDevicesError != nil {
100+
return nil, d.getDevicesError
101+
}
102+
96103
devs := d.devices
97104

98105
var err error

pkg/nvidia-query/nvml/lib/lib_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package lib
22

33
import (
4+
"errors"
45
"testing"
56

67
"github.com/NVIDIA/go-nvml/pkg/nvml"
@@ -13,3 +14,19 @@ func Test_createLibrary(t *testing.T) {
1314
)
1415
assert.Equal(t, nv.NVML().Init(), nvml.SUCCESS)
1516
}
17+
18+
func Test_WithDeviceGetDevicesError(t *testing.T) {
19+
testErr := errors.New("error getting device handle for index '0': Unknown Error (injected for testing)")
20+
21+
nv := createLibrary(
22+
WithInitReturn(nvml.SUCCESS),
23+
WithDeviceGetDevicesError(testErr),
24+
)
25+
26+
// GetDevices should return the injected error
27+
devices, err := nv.Device().GetDevices()
28+
assert.Nil(t, devices)
29+
assert.Equal(t, testErr, err)
30+
assert.ErrorIs(t, err, testErr)
31+
}
32+

pkg/nvidia-query/nvml/lib/options.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ type Op struct {
1818

1919
// ref. https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g7e505374454a0d4fc7339b6c885656d6
2020
devGetCurrentClocksEventReasonsForAllDevs func() (uint64, nvml.Return)
21+
22+
// devGetDevicesError is the error to return from Device().GetDevices().
23+
// Used for testing NVML device enumeration failure scenarios.
24+
devGetDevicesError error
2125
}
2226

2327
type OpOption func(*Op)
@@ -77,3 +81,12 @@ func WithDeviceGetCurrentClocksEventReasonsForAllDevs(f func() (uint64, nvml.Ret
7781
op.devGetCurrentClocksEventReasonsForAllDevs = f
7882
}
7983
}
84+
85+
// WithDeviceGetDevicesError specifies the error to return from Device().GetDevices().
86+
// This is used for testing NVML device enumeration failure scenarios, such as when
87+
// nvidia-smi shows "Unable to determine the device handle for GPU: Unknown Error".
88+
func WithDeviceGetDevicesError(err error) OpOption {
89+
return func(op *Op) {
90+
op.devGetDevicesError = err
91+
}
92+
}

pkg/server/server.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,13 +255,15 @@ func New(ctx context.Context, auditLogger log.AuditLogger, config *lepconfig.Con
255255
if config.FailureInjector != nil && (len(config.FailureInjector.GPUUUIDsWithGPULost) > 0 ||
256256
len(config.FailureInjector.GPUUUIDsWithGPURequiresReset) > 0 ||
257257
len(config.FailureInjector.GPUUUIDsWithFabricStateHealthSummaryUnhealthy) > 0 ||
258-
config.FailureInjector.GPUProductNameOverride != "") {
258+
config.FailureInjector.GPUProductNameOverride != "" ||
259+
config.FailureInjector.NVMLDeviceGetDevicesError) {
259260
// If failure injector is configured for NVML-level errors or product name override, use it
260261
nvmlInstance, err = nvidianvml.NewWithFailureInjector(&nvidianvml.FailureInjectorConfig{
261262
GPUUUIDsWithGPULost: config.FailureInjector.GPUUUIDsWithGPULost,
262263
GPUUUIDsWithGPURequiresReset: config.FailureInjector.GPUUUIDsWithGPURequiresReset,
263264
GPUUUIDsWithFabricStateHealthSummaryUnhealthy: config.FailureInjector.GPUUUIDsWithFabricStateHealthSummaryUnhealthy,
264265
GPUProductNameOverride: config.FailureInjector.GPUProductNameOverride,
266+
NVMLDeviceGetDevicesError: config.FailureInjector.NVMLDeviceGetDevicesError,
265267
})
266268
} else {
267269
nvmlInstance, err = nvidianvml.NewWithExitOnSuccessfulLoad(ctx)

0 commit comments

Comments
 (0)