From c4c29f977f98d74bba8092854dbce4cea078d088 Mon Sep 17 00:00:00 2001 From: wangmin Date: Thu, 9 Jul 2026 21:47:01 +0800 Subject: [PATCH 1/3] fix(scheduler): treat empty use/nouse uuid annotation as no constraint CheckUUID split the annotation value on ',' and required a device id to equal one of the parts. An empty value yielded [""], which matches no real device, so an empty nvidia.com/use-gpuuuid (or the per-vendor equivalent) filtered out every device and made the node unschedulable with CardUuidMismatch. Skip the check when the value is empty/whitespace. Signed-off-by: wangmin --- pkg/device/devices.go | 5 +++-- pkg/device/devices_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/pkg/device/devices.go b/pkg/device/devices.go index 7817660aa5..1c43b36cb4 100644 --- a/pkg/device/devices.go +++ b/pkg/device/devices.go @@ -679,13 +679,14 @@ func CheckUUID(annos map[string]string, id, useKey, noUseKey, deviceType string) return strings.TrimSpace(u) == id }) } - if userUUID, ok := annos[useKey]; ok { + // An empty value means "no constraint" rather than "match nothing". + if userUUID, ok := annos[useKey]; ok && strings.TrimSpace(userUUID) != "" { klog.V(5).Infof("check uuid for %s user uuid [%s], device id is %s", deviceType, userUUID, id) if !match(userUUID) { return false } } - if noUserUUID, ok := annos[noUseKey]; ok { + if noUserUUID, ok := annos[noUseKey]; ok && strings.TrimSpace(noUserUUID) != "" { klog.V(5).Infof("check uuid for %s not user uuid [%s], device id is %s", deviceType, noUserUUID, id) if match(noUserUUID) { return false diff --git a/pkg/device/devices_test.go b/pkg/device/devices_test.go index a8c22cdd04..b089d44f3c 100644 --- a/pkg/device/devices_test.go +++ b/pkg/device/devices_test.go @@ -1454,6 +1454,30 @@ func TestCheckUUID(t *testing.T) { id: "abc", want: true, }, + { + name: "empty GPUUseUUID annotation should not filter out any device", + annos: map[string]string{ + GPUUseUUID: "", + }, + id: "abc", + want: true, + }, + { + name: "whitespace-only GPUUseUUID annotation should not filter out any device", + annos: map[string]string{ + GPUUseUUID: " ", + }, + id: "abc", + want: true, + }, + { + name: "empty GPUNoUseUUID annotation should not exclude any device", + annos: map[string]string{ + GPUNoUseUUID: "", + }, + id: "abc", + want: true, + }, } for _, test := range tests { From c0c2a088927499f781d715305dd992621377c0f3 Mon Sep 17 00:00:00 2001 From: wangmin Date: Fri, 10 Jul 2026 08:41:52 +0800 Subject: [PATCH 2/3] fix(scheduler): treat empty use/nouse gputype annotation as no constraint checkGPUtype (nvidia) and checkDCUtype (hygon) match a card type with strings.Contains, which treats an empty string as a substring of every type. An empty nouse-gputype / nouse-dcutype annotation therefore made Contains(cardtype, "") always true and excluded every device, leaving the node unschedulable. Skip the check when the value is empty or whitespace so an empty type allow/deny list means no constraint, mirroring the CheckUUID fix in this PR. Signed-off-by: wangmin --- pkg/device/hygon/device.go | 5 ++-- pkg/device/hygon/device_test.go | 39 ++++++++++++++++++++++++++++++++ pkg/device/nvidia/device.go | 5 ++-- pkg/device/nvidia/device_test.go | 10 ++++++++ 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/pkg/device/hygon/device.go b/pkg/device/hygon/device.go index af4c37fc65..51add845e3 100644 --- a/pkg/device/hygon/device.go +++ b/pkg/device/hygon/device.go @@ -96,7 +96,8 @@ func (dev *DCUDevices) MutateAdmission(ctr *corev1.Container, p *corev1.Pod) (bo } func checkDCUtype(annos map[string]string, cardtype string) bool { - if inuse, ok := annos[DCUInUse]; ok { + // Empty value means "no constraint"; otherwise strings.Contains("") matches every type. + if inuse, ok := annos[DCUInUse]; ok && strings.TrimSpace(inuse) != "" { if !strings.Contains(inuse, ",") { if strings.Contains(strings.ToUpper(cardtype), strings.ToUpper(inuse)) { return true @@ -110,7 +111,7 @@ func checkDCUtype(annos map[string]string, cardtype string) bool { } return false } - if nouse, ok := annos[DCUNoUse]; ok { + if nouse, ok := annos[DCUNoUse]; ok && strings.TrimSpace(nouse) != "" { if !strings.Contains(nouse, ",") { if strings.Contains(strings.ToUpper(cardtype), strings.ToUpper(nouse)) { return false diff --git a/pkg/device/hygon/device_test.go b/pkg/device/hygon/device_test.go index ac251e07a3..d3197a60a3 100644 --- a/pkg/device/hygon/device_test.go +++ b/pkg/device/hygon/device_test.go @@ -191,6 +191,45 @@ func Test_checkDCUtype(t *testing.T) { }, want: true, }, + { + name: "empty use type annotation is no constraint", + args: struct { + annos map[string]string + cardtype string + }{ + annos: map[string]string{ + "hygon.com/use-dcutype": "", + }, + cardtype: "dcu", + }, + want: true, + }, + { + name: "empty nouse type annotation excludes nothing", + args: struct { + annos map[string]string + cardtype string + }{ + annos: map[string]string{ + "hygon.com/nouse-dcutype": "", + }, + cardtype: "dcu", + }, + want: true, + }, + { + name: "whitespace-only nouse type annotation excludes nothing", + args: struct { + annos map[string]string + cardtype string + }{ + annos: map[string]string{ + "hygon.com/nouse-dcutype": " ", + }, + cardtype: "dcu", + }, + want: true, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/pkg/device/nvidia/device.go b/pkg/device/nvidia/device.go index 3fda035905..153be5534d 100644 --- a/pkg/device/nvidia/device.go +++ b/pkg/device/nvidia/device.go @@ -486,7 +486,8 @@ func resourcePresent(ctr *corev1.Container, name corev1.ResourceName) bool { func checkGPUtype(annos map[string]string, cardtype string) bool { cardtype = strings.ToUpper(cardtype) - if inuse, ok := annos[GPUInUse]; ok { + // Empty value means "no constraint"; otherwise strings.Contains("") matches every type. + if inuse, ok := annos[GPUInUse]; ok && strings.TrimSpace(inuse) != "" { useTypes := strings.Split(inuse, ",") if !slices.ContainsFunc(useTypes, func(useType string) bool { return strings.Contains(cardtype, strings.ToUpper(useType)) @@ -494,7 +495,7 @@ func checkGPUtype(annos map[string]string, cardtype string) bool { return false } } - if unuse, ok := annos[GPUNoUse]; ok { + if unuse, ok := annos[GPUNoUse]; ok && strings.TrimSpace(unuse) != "" { unuseTypes := strings.Split(unuse, ",") if slices.ContainsFunc(unuseTypes, func(unuseType string) bool { return strings.Contains(cardtype, strings.ToUpper(unuseType)) diff --git a/pkg/device/nvidia/device_test.go b/pkg/device/nvidia/device_test.go index a98f79f157..25f936e1d8 100644 --- a/pkg/device/nvidia/device_test.go +++ b/pkg/device/nvidia/device_test.go @@ -2420,6 +2420,16 @@ func TestCheckGPUtype_NoUse(t *testing.T) { assert.Equal(t, checkGPUtype(annos, "NVIDIA-V100"), true) } +func TestCheckGPUtype_EmptyAnnotation(t *testing.T) { + // An empty use/nouse type annotation means "no constraint": strings.Contains + // treats "" as a substring of every card type, so without a guard an empty + // nouse-gputype would wrongly exclude every device. + assert.Equal(t, checkGPUtype(map[string]string{GPUInUse: ""}, "NVIDIA-A100"), true) + assert.Equal(t, checkGPUtype(map[string]string{GPUInUse: " "}, "NVIDIA-A100"), true) + assert.Equal(t, checkGPUtype(map[string]string{GPUNoUse: ""}, "NVIDIA-A100"), true) + assert.Equal(t, checkGPUtype(map[string]string{GPUNoUse: " "}, "NVIDIA-A100"), true) +} + func TestCheckType_AllocateMode(t *testing.T) { dev := &NvidiaGPUDevices{} req := device.ContainerDeviceRequest{Type: NvidiaGPUDevice} From da9254ce58eb1f28c19c4a686be0e021736ce471 Mon Sep 17 00:00:00 2001 From: wangmin Date: Fri, 10 Jul 2026 12:07:39 +0800 Subject: [PATCH 3/3] refactor(device): extract CheckType shared helper for gpu/dcu type filtering checkGPUtype (nvidia) and checkDCUtype (hygon) duplicated the same use/nouse type-filter shape, including the empty-value guard added in the previous commit. Extract a CheckType helper in devices.go, parallel to CheckUUID, so both vendors and the empty-value guard live in one place. hygon previously returned early once the use annotation was set and never evaluated nouse; CheckType evaluates both, matching CheckUUID/nvidia. No existing test set both, and the behaviour only differs for the contradictory case where a card is in both the use and nouse list. Signed-off-by: wangmin --- pkg/device/devices.go | 24 ++++++++++++++ pkg/device/devices_test.go | 65 +++++++++++++++++++++++++++++++++++++ pkg/device/hygon/device.go | 31 +----------------- pkg/device/nvidia/device.go | 21 +----------- 4 files changed, 91 insertions(+), 50 deletions(-) diff --git a/pkg/device/devices.go b/pkg/device/devices.go index 1c43b36cb4..0efbe0d4d8 100644 --- a/pkg/device/devices.go +++ b/pkg/device/devices.go @@ -694,3 +694,27 @@ func CheckUUID(annos map[string]string, id, useKey, noUseKey, deviceType string) } return true } + +// CheckType reports whether a device model is allowed by the use/noUse type +// constraints in annos. It mirrors CheckUUID but matches the card model as a +// case-insensitive substring instead of an exact device id. An empty value +// means "no constraint" rather than "match nothing". +func CheckType(annos map[string]string, cardType, useKey, noUseKey string) bool { + cardType = strings.ToUpper(cardType) + match := func(list string) bool { + return slices.ContainsFunc(strings.Split(list, ","), func(t string) bool { + return strings.Contains(cardType, strings.ToUpper(t)) + }) + } + if inuse, ok := annos[useKey]; ok && strings.TrimSpace(inuse) != "" { + if !match(inuse) { + return false + } + } + if noUse, ok := annos[noUseKey]; ok && strings.TrimSpace(noUse) != "" { + if match(noUse) { + return false + } + } + return true +} diff --git a/pkg/device/devices_test.go b/pkg/device/devices_test.go index b089d44f3c..1d602442fc 100644 --- a/pkg/device/devices_test.go +++ b/pkg/device/devices_test.go @@ -1488,6 +1488,71 @@ func TestCheckUUID(t *testing.T) { } } +func TestCheckType(t *testing.T) { + useKey := "example.com/use-gputype" + noUseKey := "example.com/nouse-gputype" + tests := []struct { + name string + annos map[string]string + cardType string + want bool + }{ + { + name: "no annotation is no constraint", + annos: map[string]string{}, + cardType: "NVIDIA-A100", + want: true, + }, + { + name: "use list matches by substring", + annos: map[string]string{useKey: "A100,V100"}, + cardType: "NVIDIA-A100", + want: true, + }, + { + name: "use list does not match", + annos: map[string]string{useKey: "V100"}, + cardType: "NVIDIA-A100", + want: false, + }, + { + name: "nouse list matches excludes the device", + annos: map[string]string{noUseKey: "A100"}, + cardType: "NVIDIA-A100", + want: false, + }, + { + name: "nouse list does not match keeps the device", + annos: map[string]string{noUseKey: "V100"}, + cardType: "NVIDIA-A100", + want: true, + }, + { + name: "empty use value is no constraint", + annos: map[string]string{useKey: ""}, + cardType: "NVIDIA-A100", + want: true, + }, + { + name: "whitespace-only nouse value excludes nothing", + annos: map[string]string{noUseKey: " "}, + cardType: "NVIDIA-A100", + want: true, + }, + { + name: "use satisfied and nouse matches still excludes", + annos: map[string]string{useKey: "A100", noUseKey: "A100"}, + cardType: "NVIDIA-A100", + want: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.want, CheckType(test.annos, test.cardType, useKey, noUseKey)) + }) + } +} + func TestDeviceUsageDeepCopy(t *testing.T) { tests := []struct { name string diff --git a/pkg/device/hygon/device.go b/pkg/device/hygon/device.go index 51add845e3..a0b136137e 100644 --- a/pkg/device/hygon/device.go +++ b/pkg/device/hygon/device.go @@ -96,36 +96,7 @@ func (dev *DCUDevices) MutateAdmission(ctr *corev1.Container, p *corev1.Pod) (bo } func checkDCUtype(annos map[string]string, cardtype string) bool { - // Empty value means "no constraint"; otherwise strings.Contains("") matches every type. - if inuse, ok := annos[DCUInUse]; ok && strings.TrimSpace(inuse) != "" { - if !strings.Contains(inuse, ",") { - if strings.Contains(strings.ToUpper(cardtype), strings.ToUpper(inuse)) { - return true - } - } else { - for val := range strings.SplitSeq(inuse, ",") { - if strings.Contains(strings.ToUpper(cardtype), strings.ToUpper(val)) { - return true - } - } - } - return false - } - if nouse, ok := annos[DCUNoUse]; ok && strings.TrimSpace(nouse) != "" { - if !strings.Contains(nouse, ",") { - if strings.Contains(strings.ToUpper(cardtype), strings.ToUpper(nouse)) { - return false - } - } else { - for val := range strings.SplitSeq(nouse, ",") { - if strings.Contains(strings.ToUpper(cardtype), strings.ToUpper(val)) { - return false - } - } - } - return true - } - return true + return device.CheckType(annos, cardtype, DCUInUse, DCUNoUse) } func (dev *DCUDevices) LockNode(n *corev1.Node, p *corev1.Pod) error { diff --git a/pkg/device/nvidia/device.go b/pkg/device/nvidia/device.go index 153be5534d..dab398e67c 100644 --- a/pkg/device/nvidia/device.go +++ b/pkg/device/nvidia/device.go @@ -20,7 +20,6 @@ import ( "errors" "flag" "fmt" - "slices" "strconv" "strings" "sync" @@ -485,25 +484,7 @@ func resourcePresent(ctr *corev1.Container, name corev1.ResourceName) bool { } func checkGPUtype(annos map[string]string, cardtype string) bool { - cardtype = strings.ToUpper(cardtype) - // Empty value means "no constraint"; otherwise strings.Contains("") matches every type. - if inuse, ok := annos[GPUInUse]; ok && strings.TrimSpace(inuse) != "" { - useTypes := strings.Split(inuse, ",") - if !slices.ContainsFunc(useTypes, func(useType string) bool { - return strings.Contains(cardtype, strings.ToUpper(useType)) - }) { - return false - } - } - if unuse, ok := annos[GPUNoUse]; ok && strings.TrimSpace(unuse) != "" { - unuseTypes := strings.Split(unuse, ",") - if slices.ContainsFunc(unuseTypes, func(unuseType string) bool { - return strings.Contains(cardtype, strings.ToUpper(unuseType)) - }) { - return false - } - } - return true + return device.CheckType(annos, cardtype, GPUInUse, GPUNoUse) } func assertNuma(annos map[string]string) bool {