Skip to content

Commit fe32560

Browse files
committed
add taints on health events
Signed-off-by: Swati Gupta <swatig@nvidia.com>
1 parent 476b478 commit fe32560

6 files changed

Lines changed: 170 additions & 116 deletions

File tree

cmd/gpu-kubelet-plugin/allocatable.go

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ type AllocatableDevice struct {
4141
MigDynamic *MigSpec
4242
MigStatic *MigDeviceInfo
4343
Vfio *VfioDeviceInfo
44+
45+
// taints holds DRA device taints set by the health monitor. Published
46+
// as part of the device's ResourceSlice entry so the scheduler and
47+
// kubelet honour them per KEP-5055.
48+
taints []resourceapi.DeviceTaint
4449
}
4550

4651
type AllocatableDeviceList []*AllocatableDevice
@@ -272,18 +277,44 @@ func (d AllocatableDevices) RemoveSiblingDevices(device *AllocatableDevice) {
272277
}
273278
}
274279

275-
func (d *AllocatableDevice) IsHealthy() bool {
276-
switch d.Type() {
277-
case GpuDeviceType:
278-
return d.Gpu.health == Healthy
279-
case MigStaticDeviceType:
280-
// TODO: review -- what about the parent?
281-
return d.MigStatic.health == Healthy
282-
case MigDynamicDeviceType:
283-
// TODOMIG: For now, pretend health -- this device maybe hasn't
284-
// manifested yet. Or has it? We could adopt the health status of the
285-
// parent, but that's also not meaningful I think.
286-
return true
280+
func (d *AllocatableDevice) Taints() []resourceapi.DeviceTaint {
281+
return d.taints
282+
}
283+
284+
// AddOrUpdateTaint adds a new taint or updates an existing one with the same
285+
// key. Within the same key, the effect can only escalate
286+
// (None < NoSchedule < NoExecute), and the value is always updated to the
287+
// latest. Returns true if the taint set was modified.
288+
func (d *AllocatableDevice) AddOrUpdateTaint(taint resourceapi.DeviceTaint) bool {
289+
for i, existing := range d.taints {
290+
if existing.Key != taint.Key {
291+
continue
292+
}
293+
changed := false
294+
if taintEffectSeverity(taint.Effect) > taintEffectSeverity(existing.Effect) {
295+
d.taints[i].Effect = taint.Effect
296+
d.taints[i].TimeAdded = nil // reset so the API server sets a fresh timestamp
297+
changed = true
298+
}
299+
if d.taints[i].Value != taint.Value {
300+
d.taints[i].Value = taint.Value
301+
changed = true
302+
}
303+
return changed
304+
}
305+
d.taints = append(d.taints, taint)
306+
return true
307+
}
308+
309+
func taintEffectSeverity(effect resourceapi.DeviceTaintEffect) int {
310+
switch effect {
311+
case DeviceTaintEffectNone:
312+
return 0
313+
case resourceapi.DeviceTaintEffectNoSchedule:
314+
return 1
315+
case resourceapi.DeviceTaintEffectNoExecute:
316+
return 2
317+
default:
318+
return -1
287319
}
288-
panic("unexpected type for AllocatableDevice")
289320
}

cmd/gpu-kubelet-plugin/device_health.go

Lines changed: 97 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,85 @@ import (
2424
"sync"
2525

2626
"github.com/NVIDIA/go-nvml/pkg/nvml"
27+
resourceapi "k8s.io/api/resource/v1"
2728
"k8s.io/klog/v2"
2829
)
2930

3031
const (
3132
FullGPUInstanceID uint32 = 0xFFFFFFFF
3233
)
3334

35+
const (
36+
TaintKeyXID = DriverName + "/xid"
37+
TaintKeyGPULost = DriverName + "/gpu-lost"
38+
TaintKeyUnmonitored = DriverName + "/unmonitored"
39+
)
40+
41+
// TODO: remove later, as its not in vendored api
42+
// DeviceTaintEffectNone is an informational effect that does not affect
43+
// scheduling or eviction. Available in k8s 1.36+ (KEP-5055 beta).
44+
const DeviceTaintEffectNone resourceapi.DeviceTaintEffect = "None"
45+
46+
// DeviceHealthEventType classifies the category of health event detected by
47+
// the NVML health monitor.
48+
type DeviceHealthEventType string
49+
50+
const (
51+
HealthEventXID DeviceHealthEventType = "xid"
52+
HealthEventGPULost DeviceHealthEventType = "gpu-lost"
53+
HealthEventUnmonitored DeviceHealthEventType = "unmonitored"
54+
)
55+
56+
// DeviceHealthEvent carries a typed health notification from the NVML health
57+
// monitor to the driver's event handler, enabling the driver to set the
58+
// appropriate DRA device taint per the Option A schema (KEP-5055).
59+
type DeviceHealthEvent struct {
60+
Device *AllocatableDevice
61+
EventType DeviceHealthEventType
62+
// inspired by NVML Event type and only meaningful for xid errors.
63+
// may have to create a custom type based on future device-api
64+
EventData uint64
65+
}
66+
67+
// healthEventToTaint maps a DeviceHealthEvent to the corresponding DRA
68+
// DeviceTaint using the Option A taint key schema: one key per health
69+
// dimension under the gpu.nvidia.com domain.
70+
// TODO: revisit this to double check on the desired effects
71+
func healthEventToTaint(event *DeviceHealthEvent) resourceapi.DeviceTaint {
72+
switch event.EventType {
73+
case HealthEventXID:
74+
return resourceapi.DeviceTaint{
75+
Key: TaintKeyXID,
76+
Value: strconv.FormatUint(event.EventData, 10),
77+
Effect: DeviceTaintEffectNone,
78+
}
79+
case HealthEventGPULost:
80+
return resourceapi.DeviceTaint{
81+
Key: TaintKeyGPULost,
82+
Effect: resourceapi.DeviceTaintEffectNoExecute,
83+
}
84+
case HealthEventUnmonitored:
85+
return resourceapi.DeviceTaint{
86+
Key: TaintKeyUnmonitored,
87+
Effect: DeviceTaintEffectNone,
88+
}
89+
default:
90+
klog.Errorf("Unknown health event type %q, defaulting to unmonitored taint", event.EventType)
91+
return resourceapi.DeviceTaint{
92+
Key: TaintKeyUnmonitored,
93+
Effect: DeviceTaintEffectNone,
94+
}
95+
}
96+
}
97+
3498
// For a MIG device the placement is defined by the 3-tuple <parent UUID, GI, CI>.
3599
// For a full device the returned 3-tuple is the device's uuid and (FullGPUInstanceID) 0xFFFFFFFF for the other two elements.
36100
type devicePlacementMap map[string]map[uint32]map[uint32]*AllocatableDevice
37101

38102
type nvmlDeviceHealthMonitor struct {
39103
nvmllib nvml.Interface
40104
eventSet nvml.EventSet
41-
unhealthy chan *AllocatableDevice
105+
unhealthy chan *DeviceHealthEvent
42106
deviceByPlacement devicePlacementMap
43107
skippedXids map[uint64]bool
44108
wg sync.WaitGroup
@@ -57,7 +121,7 @@ func newNvmlDeviceHealthMonitor(config *Config, allocatable AllocatableDevices,
57121

58122
m := &nvmlDeviceHealthMonitor{
59123
nvmllib: nvdevlib.nvmllib,
60-
unhealthy: make(chan *AllocatableDevice, len(allocatable)),
124+
unhealthy: make(chan *DeviceHealthEvent, len(allocatable)),
61125
deviceByPlacement: getDevicePlacementMap(allocatable),
62126
skippedXids: xidsToSkip(config.flags.additionalXidsToIgnore),
63127
}
@@ -102,15 +166,15 @@ func (m *nvmlDeviceHealthMonitor) registerEventsForDevices() {
102166
for parentUUID, giMap := range m.deviceByPlacement {
103167
gpu, ret := m.nvmllib.DeviceGetHandleByUUID(parentUUID)
104168
if ret != nvml.SUCCESS {
105-
klog.Warningf("Unable to get device handle from UUID[%s]: %v; marking it as unhealthy", parentUUID, ret)
106-
m.markAllMigDevicesUnhealthy(giMap)
169+
klog.Warningf("Unable to get device handle from UUID[%s]: %v; marking devices as unmonitored", parentUUID, ret)
170+
m.sendHealthEventsForDevices(giMap, HealthEventUnmonitored)
107171
continue
108172
}
109173

110174
supportedEvents, ret := gpu.GetSupportedEventTypes()
111175
if ret != nvml.SUCCESS {
112-
klog.Warningf("unable to determine the supported events for %s: %v; marking it as unhealthy", parentUUID, ret)
113-
m.markAllMigDevicesUnhealthy(giMap)
176+
klog.Warningf("unable to determine the supported events for %s: %v; marking devices as unmonitored", parentUUID, ret)
177+
m.sendHealthEventsForDevices(giMap, HealthEventUnmonitored)
114178
continue
115179
}
116180

@@ -119,8 +183,8 @@ func (m *nvmlDeviceHealthMonitor) registerEventsForDevices() {
119183
klog.Warningf("Device %v is too old to support healthchecking.", parentUUID)
120184
}
121185
if ret != nvml.SUCCESS {
122-
klog.Warningf("unable to register events for %s: %v; marking it as unhealthy", parentUUID, ret)
123-
m.markAllMigDevicesUnhealthy(giMap)
186+
klog.Warningf("unable to register events for %s: %v; marking devices as unmonitored", parentUUID, ret)
187+
m.sendHealthEventsForDevices(giMap, HealthEventUnmonitored)
124188
}
125189
}
126190
}
@@ -158,8 +222,8 @@ func (m *nvmlDeviceHealthMonitor) run(ctx context.Context) {
158222
// Ref doc: [https://docs.nvidia.com/deploy/nvml-api/group__nvmlEvents.html#group__nvmlEvents_1g9714b0ca9a34c7a7780f87fee16b205c].
159223
if ret != nvml.SUCCESS {
160224
if ret == nvml.ERROR_GPU_IS_LOST {
161-
klog.Warningf("GPU is lost error: %v; Marking all devices as unhealthy", ret)
162-
m.markAllDevicesUnhealthy()
225+
klog.Warningf("GPU is lost error: %v; Tainting all devices with %s", ret, TaintKeyGPULost)
226+
m.sendHealthEventsForAllDevices(HealthEventGPULost)
163227
continue
164228
}
165229
klog.V(6).Infof("Error waiting for NVML event: %v. Retrying...", ret)
@@ -187,8 +251,8 @@ func (m *nvmlDeviceHealthMonitor) run(ctx context.Context) {
187251
// TODO: look into how to properly handle this error.
188252
eventUUID, ret := event.Device.GetUUID()
189253
if ret != nvml.SUCCESS {
190-
klog.Warningf("Failed to determine uuid for event %v: %v; Marking all devices as unhealthy.", event, ret)
191-
m.markAllDevicesUnhealthy()
254+
klog.Warningf("Failed to determine uuid for event %v: %v; Tainting all devices with %s", event, ret, TaintKeyGPULost)
255+
m.sendHealthEventsForAllDevices(HealthEventGPULost)
192256
continue
193257
}
194258
affectedDevice := m.deviceByPlacement.get(eventUUID, gi, ci)
@@ -197,38 +261,41 @@ func (m *nvmlDeviceHealthMonitor) run(ctx context.Context) {
197261
continue
198262
}
199263

200-
klog.V(4).Infof("Sending unhealthy notification for device %s due to event type:%v and event data:%d", affectedDevice.UUID(), eType, xid)
201-
m.unhealthy <- affectedDevice
264+
klog.V(4).Infof("Sending XID=%d health event for device %s", xid, affectedDevice.UUID())
265+
m.unhealthy <- &DeviceHealthEvent{
266+
Device: affectedDevice,
267+
EventType: HealthEventXID,
268+
EventData: xid,
269+
}
202270
}
203271
}
204272
}
205273

206-
func (m *nvmlDeviceHealthMonitor) Unhealthy() <-chan *AllocatableDevice {
274+
func (m *nvmlDeviceHealthMonitor) Unhealthy() <-chan *DeviceHealthEvent {
207275
return m.unhealthy
208276
}
209277

210-
func (m *nvmlDeviceHealthMonitor) markAllDevicesUnhealthy() {
278+
func (m *nvmlDeviceHealthMonitor) sendHealthEventsForAllDevices(eventType DeviceHealthEventType) {
211279
for _, giMap := range m.deviceByPlacement {
212-
m.markAllMigDevicesUnhealthy(giMap)
280+
m.sendHealthEventsForDevices(giMap, eventType)
213281
}
214282
}
215283

216-
// markAllMigDevicesUnhealthy is a helper function to mark every mig device under a parent as unhealthy.
217-
func (m *nvmlDeviceHealthMonitor) markAllMigDevicesUnhealthy(giMap map[uint32]map[uint32]*AllocatableDevice) {
284+
// sendHealthEventsForDevices sends a DeviceHealthEvent for every device under
285+
// the given parent-level map. Uses non-blocking sends to protect the monitor
286+
// goroutine from deadlocks when the channel is full.
287+
func (m *nvmlDeviceHealthMonitor) sendHealthEventsForDevices(giMap map[uint32]map[uint32]*AllocatableDevice, eventType DeviceHealthEventType) {
218288
for _, ciMap := range giMap {
219289
for _, dev := range ciMap {
220-
// Non-blocking send to avoid deadlocks if channel is full.
290+
event := &DeviceHealthEvent{
291+
Device: dev,
292+
EventType: eventType,
293+
}
221294
select {
222-
case m.unhealthy <- dev:
223-
klog.V(6).Infof("Marked device %s as unhealthy", dev.UUID())
224-
// TODO: The non-blocking send protects the health-monitor goroutine from deadlocks,
225-
// but dropping an unhealthy notification means the device's health transition may
226-
// never reach the consumer. Consider follow-up improvements:
227-
// - increase the channel buffer beyond len(allocatable) to reduce backpressure;
228-
// - introduce a special "all devices unhealthy" message when bulk updates occur;
229-
// - or revisit whether blocking briefly here is acceptable.
295+
case m.unhealthy <- event:
296+
klog.V(6).Infof("Sent %s health event for device %s", eventType, dev.UUID())
230297
default:
231-
klog.Errorf("Unhealthy channel full. Dropping unhealthy notification for device %s", dev.UUID())
298+
klog.Errorf("Health event channel full; dropping %s event for device %s", eventType, dev.UUID())
232299
}
233300
}
234301
}
@@ -326,6 +393,7 @@ func getAdditionalXids(input string) []uint64 {
326393
return additionalXids
327394
}
328395

396+
// TODO: this can be simplified now with "EffectNone"
329397
func xidsToSkip(additionalXids string) map[uint64]bool {
330398
// Add the list of hardcoded disabled (ignored) XIDs:
331399
// http://docs.nvidia.com/deploy/xid-errors/index.html#topic_4

cmd/gpu-kubelet-plugin/device_state.go

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -638,12 +638,6 @@ func (s *DeviceState) prepareDevices(ctx context.Context, claim *resourceapi.Res
638638
if !exists {
639639
return nil, fmt.Errorf("requested device is not allocatable: %v", result.Device)
640640
}
641-
// only proceed with config mapping if device is healthy.
642-
if featuregates.Enabled(featuregates.NVMLDeviceHealthCheck) {
643-
if !device.IsHealthy() {
644-
return nil, fmt.Errorf("requested device is not healthy: %v", result.Device)
645-
}
646-
}
647641
for _, c := range slices.Backward(configs) {
648642
if slices.Contains(c.Requests, result.Request) {
649643
if _, ok := c.Config.(*configapi.GpuConfig); ok && device.Type() != GpuDeviceType {
@@ -1071,31 +1065,6 @@ func GetOpaqueDeviceConfigs(
10711065
return resultConfigs, nil
10721066
}
10731067

1074-
// TODO: this needs a code comment.
1075-
func (s *DeviceState) UpdateDeviceHealthStatus(d *AllocatableDevice, hs HealthStatus) {
1076-
s.Lock()
1077-
defer s.Unlock()
1078-
1079-
switch d.Type() {
1080-
case GpuDeviceType:
1081-
d.Gpu.health = hs
1082-
case MigDynamicDeviceType:
1083-
// Here we do not have access to a concrete MIG device. The
1084-
// 'allocatable' MIG device is an abstract representation to a specific
1085-
// MIG device.
1086-
klog.Warningf("UpdateDeviceHealthStatus() called for abstract, dynamic MIG device %s: %s", d.MigDynamic.CanonicalName(), hs)
1087-
case MigStaticDeviceType:
1088-
// Does it make sense to update the health for a MIG device? Do we
1089-
// receive health events that are specific to individual MIG devices? If
1090-
// yes: does this allow for making conclusions about the parent device?
1091-
d.MigStatic.health = hs
1092-
default:
1093-
klog.V(6).Infof("Cannot update health status for unknown device type: %s", d.Type())
1094-
return
1095-
}
1096-
klog.V(4).Infof("Updated device: %s health status to %s", d.UUID(), hs)
1097-
}
1098-
10991068
// requestedNonAdminDevices returns the set of device names requested by the claim,
11001069
// excluding admin-access allocations.
11011070
func (s *DeviceState) requestedNonAdminDevices(claim *resourceapi.ResourceClaim) map[string]struct{} {
@@ -1182,3 +1151,11 @@ func (s *DeviceState) deleteMigDevIfExistsAndNotUsedByCompletedClaim(ms *MigSpec
11821151

11831152
return nil
11841153
}
1154+
1155+
// AddDeviceTaint adds or updates a DRA device taint on the given device under
1156+
// the DeviceState lock. Returns true if the taint set was actually modified.
1157+
func (s *DeviceState) AddDeviceTaint(d *AllocatableDevice, taint resourceapi.DeviceTaint) bool {
1158+
s.Lock()
1159+
defer s.Unlock()
1160+
return d.AddOrUpdateTaint(taint)
1161+
}

cmd/gpu-kubelet-plugin/deviceinfo.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,6 @@ import (
2727
"k8s.io/utils/ptr"
2828
)
2929

30-
// Defined similarly as https://pkg.go.dev/k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1#Healthy.
31-
type HealthStatus string
32-
33-
const (
34-
Healthy HealthStatus = "Healthy"
35-
// With NVMLDeviceHealthCheck, Unhealthy means that there are critcal xid errors on the device.
36-
Unhealthy HealthStatus = "Unhealthy"
37-
)
38-
3930
// Represents a specific, full, physical GPU device.
4031
type GpuInfo struct {
4132
UUID string `json:"uuid"`
@@ -53,7 +44,6 @@ type GpuInfo struct {
5344
pcieRootAttr *deviceattribute.DeviceAttribute
5445
migProfiles []*MigProfileInfo
5546
addressingMode *string
56-
health HealthStatus
5747

5848
// The following properties that can only be known after inspecting MIG
5949
// profiles.
@@ -89,7 +79,6 @@ type MigDeviceInfo struct {
8979
ciProfileInfo *nvml.ComputeInstanceProfileInfo
9080
pcieBusID string
9181
pcieRootAttr *deviceattribute.DeviceAttribute
92-
health HealthStatus
9382
}
9483

9584
type VfioDeviceInfo struct {

0 commit comments

Comments
 (0)