Files

849 lines
29 KiB
Go

/*
Copyright 2023 The KServe Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package llmisvc
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
resourcev1 "k8s.io/api/resource/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/yaml"
"github.com/kserve/kserve/pkg/apis/serving/v1alpha2"
"github.com/kserve/kserve/pkg/constants"
)
func newManagedDRATestLLMISVC(name string, annotations map[string]string) *v1alpha2.LLMInferenceService {
return &v1alpha2.LLMInferenceService{
TypeMeta: metav1.TypeMeta{
APIVersion: "serving.kserve.io/v1alpha2",
Kind: "LLMInferenceService",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
UID: "test-uid-dra",
Annotations: annotations,
},
}
}
// TestYAMLBlockScalarRoundTripsToSelectors verifies that the documented YAML `|`
// block scalar correctly parses into newline-separated CEL selectors.
func TestYAMLBlockScalarRoundTripsToSelectors(t *testing.T) {
manifest := `
apiVersion: serving.kserve.io/v1alpha2
kind: LLMInferenceService
metadata:
name: my-llm
namespace: default
annotations:
serving.kserve.io/exp-dra-device-class: gpu.example.com
serving.kserve.io/exp-dra-device-count: "1"
serving.kserve.io/exp-dra-cel-selector: |
device.attributes['gpu.example.com'].model == 'LATEST-GPU-MODEL'
device.capacity['gpu.example.com'].memory.compareTo(quantity('4Gi')) >= 0
`
llmSvc := &v1alpha2.LLMInferenceService{}
require.NoError(t, yaml.Unmarshal([]byte(manifest), llmSvc))
// Verify basic annotations are parsed as strings.
assert.Equal(t, "1", llmSvc.Annotations[constants.ManagedDRADeviceCountAnnotationKey])
assert.Equal(t, "gpu.example.com", llmSvc.Annotations[constants.ManagedDRADeviceClassAnnotationKey])
// Verify the block scalar preserves newlines.
raw := llmSvc.Annotations[constants.ManagedDRACelSelectorAnnotationKey]
assert.Contains(t, raw, "\n")
selectors := llmSvc.ManagedDRACelSelectors()
require.Len(t, selectors, 2)
assert.Equal(t, "device.attributes['gpu.example.com'].model == 'LATEST-GPU-MODEL'", selectors[0])
assert.Equal(t, "device.capacity['gpu.example.com'].memory.compareTo(quantity('4Gi')) >= 0", selectors[1])
// Verify the assembled template applies all selectors.
tmpl, err := expectedManagedDRATemplate(llmSvc)
require.NoError(t, err)
require.Len(t, tmpl.Spec.Spec.Devices.Requests, 1)
require.Len(t, tmpl.Spec.Spec.Devices.Requests[0].Exactly.Selectors, 2)
}
func TestBuildDeviceRequests(t *testing.T) {
t.Run("single device without CEL", func(t *testing.T) {
reqs := buildDeviceRequests("gpu.nvidia.com", nil, 1)
require.Len(t, reqs, 1)
assert.Equal(t, "device", reqs[0].Name)
assert.Equal(t, "gpu.nvidia.com", reqs[0].Exactly.DeviceClassName)
assert.Empty(t, reqs[0].Exactly.Selectors)
})
t.Run("single device with single CEL", func(t *testing.T) {
cel := "device.capacity['gpu.nvidia.com']['memory'].compareTo(quantity('40Gi')) > 0"
reqs := buildDeviceRequests("gpu.nvidia.com", []string{cel}, 1)
require.Len(t, reqs, 1)
assert.Equal(t, "device", reqs[0].Name)
require.Len(t, reqs[0].Exactly.Selectors, 1)
assert.Equal(t, cel, reqs[0].Exactly.Selectors[0].CEL.Expression)
})
t.Run("single device with multiple CEL selectors", func(t *testing.T) {
cels := []string{
"device.attributes['gpu.nvidia.com']['type'] == 'A100'",
"device.capacity['gpu.nvidia.com']['memory'].compareTo(quantity('40Gi')) > 0",
}
reqs := buildDeviceRequests("gpu.nvidia.com", cels, 1)
require.Len(t, reqs, 1)
require.Len(t, reqs[0].Exactly.Selectors, 2)
assert.Equal(t, cels[0], reqs[0].Exactly.Selectors[0].CEL.Expression)
assert.Equal(t, cels[1], reqs[0].Exactly.Selectors[1].CEL.Expression)
})
t.Run("multiple devices", func(t *testing.T) {
reqs := buildDeviceRequests("gpu.nvidia.com", nil, 3)
require.Len(t, reqs, 1)
assert.Equal(t, "device", reqs[0].Name)
assert.Equal(t, "gpu.nvidia.com", reqs[0].Exactly.DeviceClassName)
assert.Equal(t, int64(3), reqs[0].Exactly.Count)
})
t.Run("multiple devices with multiple CEL selectors", func(t *testing.T) {
cels := []string{
"device.attributes['gpu.nvidia.com']['type'] == 'mig'",
"device.attributes['gpu.nvidia.com']['profile'] == '3g.40gb'",
}
reqs := buildDeviceRequests("mig-3g.40gb", cels, 2)
require.Len(t, reqs, 1)
assert.Equal(t, "device", reqs[0].Name)
assert.Equal(t, "mig-3g.40gb", reqs[0].Exactly.DeviceClassName)
assert.Equal(t, int64(2), reqs[0].Exactly.Count)
require.Len(t, reqs[0].Exactly.Selectors, 2)
assert.Equal(t, cels[0], reqs[0].Exactly.Selectors[0].CEL.Expression)
assert.Equal(t, cels[1], reqs[0].Exactly.Selectors[1].CEL.Expression)
})
}
func TestExpectedManagedDRATemplate(t *testing.T) {
t.Run("device class only", func(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("my-model", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.nvidia.com",
})
tmpl, err := expectedManagedDRATemplate(llmSvc)
require.NoError(t, err)
assert.Equal(t, "my-model-managed-dra", tmpl.Name)
assert.Equal(t, "default", tmpl.Namespace)
require.Len(t, tmpl.Spec.Spec.Devices.Requests, 1)
assert.Equal(t, "device", tmpl.Spec.Spec.Devices.Requests[0].Name)
assert.Equal(t, "gpu.nvidia.com", tmpl.Spec.Spec.Devices.Requests[0].Exactly.DeviceClassName)
require.Len(t, tmpl.OwnerReferences, 1)
assert.True(t, *tmpl.OwnerReferences[0].Controller)
})
t.Run("device class with whitespace is trimmed", func(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("my-model", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: " gpu.nvidia.com ",
})
tmpl, err := expectedManagedDRATemplate(llmSvc)
require.NoError(t, err)
assert.Equal(t, "gpu.nvidia.com", tmpl.Spec.Spec.Devices.Requests[0].Exactly.DeviceClassName)
})
t.Run("invalid device count surfaces a parse error", func(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("my-model", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.nvidia.com",
constants.ManagedDRADeviceCountAnnotationKey: "abc",
})
_, err := expectedManagedDRATemplate(llmSvc)
require.Error(t, err)
})
}
func TestInjectManagedDRA_NoAnnotation(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test", nil)
podSpec := &corev1.PodSpec{
Containers: []corev1.Container{{Name: "main"}},
}
injectManagedDRA(llmSvc, podSpec)
assert.Empty(t, podSpec.ResourceClaims)
assert.Empty(t, podSpec.Containers[0].Resources.Claims)
}
func TestTargetContainerForDRA(t *testing.T) {
annKey := constants.ManagedDRAContainerNameAnnotationKey
tests := []struct {
name string
containers []corev1.Container
// nil means the annotation key is absent.
annotation *string
expected int
}{
{
name: "no annotation, no containers -> -1",
containers: nil,
annotation: nil,
expected: -1,
},
{
name: "no annotation, single container -> first",
containers: []corev1.Container{{Name: "main"}},
annotation: nil,
expected: 0,
},
{
name: "no annotation, multiple containers -> first",
containers: []corev1.Container{{Name: "main"}, {Name: "sidecar"}, {Name: "monitor"}},
annotation: nil,
expected: 0,
},
{
name: "empty annotation value falls back to first container",
containers: []corev1.Container{{Name: "main"}, {Name: "sidecar"}},
annotation: ptr.To(""),
expected: 0,
},
{
name: "whitespace-only annotation falls back to first container",
containers: []corev1.Container{{Name: "main"}, {Name: "sidecar"}},
annotation: ptr.To(" \t\n "),
expected: 0,
},
{
name: "annotation matches first container",
containers: []corev1.Container{{Name: "main"}, {Name: "sidecar"}},
annotation: ptr.To("main"),
expected: 0,
},
{
name: "annotation matches middle container",
containers: []corev1.Container{{Name: "init"}, {Name: "worker"}, {Name: "sidecar"}},
annotation: ptr.To("worker"),
expected: 1,
},
{
name: "annotation matches last container",
containers: []corev1.Container{{Name: "init"}, {Name: "worker"}, {Name: "sidecar"}},
annotation: ptr.To("sidecar"),
expected: 2,
},
{
name: "annotation with surrounding whitespace is trimmed before matching",
containers: []corev1.Container{{Name: "main"}, {Name: "sidecar"}},
annotation: ptr.To(" sidecar "),
expected: 1,
},
{
name: "annotation names a container that does not exist -> -1",
containers: []corev1.Container{{Name: "main"}, {Name: "sidecar"}},
annotation: ptr.To("ghost"),
expected: -1,
},
{
name: "annotation set but PodSpec has no containers -> -1",
containers: nil,
annotation: ptr.To("main"),
expected: -1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
annotations := map[string]string{}
if tt.annotation != nil {
annotations[annKey] = *tt.annotation
}
llmSvc := newManagedDRATestLLMISVC("t", annotations)
podSpec := &corev1.PodSpec{Containers: tt.containers}
assert.Equal(t, tt.expected, targetContainerForDRA(llmSvc, podSpec))
})
}
}
func TestInjectManagedDRA_SingleContainer(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.example.com",
})
podSpec := &corev1.PodSpec{
Containers: []corev1.Container{{Name: "main"}},
}
injectManagedDRA(llmSvc, podSpec)
require.Len(t, podSpec.ResourceClaims, 1)
assert.Equal(t, managedDRAClaimName, podSpec.ResourceClaims[0].Name)
assert.NotNil(t, podSpec.ResourceClaims[0].ResourceClaimTemplateName)
assert.Equal(t, "test-managed-dra", *podSpec.ResourceClaims[0].ResourceClaimTemplateName)
assert.Nil(t, podSpec.ResourceClaims[0].ResourceClaimName)
require.Len(t, podSpec.Containers[0].Resources.Claims, 1)
assert.Equal(t, managedDRAClaimName, podSpec.Containers[0].Resources.Claims[0].Name)
}
func TestInjectManagedDRA_FirstContainerByDefault(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.example.com",
})
podSpec := &corev1.PodSpec{
Containers: []corev1.Container{
{Name: "primary"},
{Name: "sidecar"},
{Name: "monitor"},
},
}
injectManagedDRA(llmSvc, podSpec)
require.Len(t, podSpec.ResourceClaims, 1)
require.Len(t, podSpec.Containers[0].Resources.Claims, 1, "first container should have the device claim")
assert.Equal(t, managedDRAClaimName, podSpec.Containers[0].Resources.Claims[0].Name)
for _, ctr := range podSpec.Containers[1:] {
assert.Empty(t, ctr.Resources.Claims, "container %q must not receive the device claim", ctr.Name)
}
}
func TestInjectManagedDRA_ExplicitContainerName(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.example.com",
constants.ManagedDRAContainerNameAnnotationKey: "worker",
})
podSpec := &corev1.PodSpec{
Containers: []corev1.Container{
{Name: "init"},
{Name: "worker"},
{Name: "monitor"},
},
}
injectManagedDRA(llmSvc, podSpec)
require.Len(t, podSpec.ResourceClaims, 1)
assert.Empty(t, podSpec.Containers[0].Resources.Claims, "container %q must not receive the device claim", podSpec.Containers[0].Name)
require.Len(t, podSpec.Containers[1].Resources.Claims, 1, "annotated container should have the device claim")
assert.Equal(t, managedDRAClaimName, podSpec.Containers[1].Resources.Claims[0].Name)
assert.Empty(t, podSpec.Containers[2].Resources.Claims, "container %q must not receive the device claim", podSpec.Containers[2].Name)
}
func TestInjectManagedDRA_AnnotatedContainerMissing(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.example.com",
constants.ManagedDRAContainerNameAnnotationKey: "ghost",
})
podSpec := &corev1.PodSpec{
Containers: []corev1.Container{
{Name: "sidecar"},
{Name: "monitor"},
},
}
injectManagedDRA(llmSvc, podSpec)
require.Len(t, podSpec.ResourceClaims, 1)
for _, ctr := range podSpec.Containers {
assert.Empty(t, ctr.Resources.Claims, "container %q must not receive the device claim", ctr.Name)
}
}
func TestInjectManagedDRA_NoContainers(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.example.com",
})
podSpec := &corev1.PodSpec{}
injectManagedDRA(llmSvc, podSpec)
require.Len(t, podSpec.ResourceClaims, 1)
assert.Empty(t, podSpec.Containers)
}
func TestInjectManagedDRA_Idempotent(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.example.com",
})
podSpec := &corev1.PodSpec{
Containers: []corev1.Container{{Name: "main"}, {Name: "sidecar"}},
}
injectManagedDRA(llmSvc, podSpec)
injectManagedDRA(llmSvc, podSpec)
assert.Len(t, podSpec.ResourceClaims, 1)
assert.Len(t, podSpec.Containers[0].Resources.Claims, 1)
assert.Empty(t, podSpec.Containers[1].Resources.Claims)
}
func TestInjectManagedDRA_PreservesExistingClaims(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.example.com",
})
podSpec := &corev1.PodSpec{
ResourceClaims: []corev1.PodResourceClaim{
{Name: "user-claim"},
},
Containers: []corev1.Container{
{
Name: "main",
Resources: corev1.ResourceRequirements{
Claims: []corev1.ResourceClaim{{Name: "user-claim"}},
},
},
},
}
injectManagedDRA(llmSvc, podSpec)
assert.Len(t, podSpec.ResourceClaims, 2)
assert.Len(t, podSpec.Containers[0].Resources.Claims, 2)
}
func TestInjectManagedDRAIntoConfig(t *testing.T) {
const deviceClass = "gpu.example.com"
mainPod := func() *corev1.PodSpec {
return &corev1.PodSpec{Containers: []corev1.Container{{Name: "main"}}}
}
tests := []struct {
name string
annotated bool
cfg *v1alpha2.LLMInferenceServiceConfig
wantTemplateInj bool
wantWorkerInj bool
wantPrefTmplInj bool
wantPrefWrkrInj bool
}{
{
name: "no annotation: nothing injected even if all slots set",
annotated: false,
cfg: &v1alpha2.LLMInferenceServiceConfig{
Spec: v1alpha2.LLMInferenceServiceSpec{
WorkloadSpec: v1alpha2.WorkloadSpec{Template: mainPod(), Worker: mainPod()},
Prefill: &v1alpha2.WorkloadSpec{Template: mainPod(), Worker: mainPod()},
},
},
},
{
name: "annotated: all four slots populated -> all four injected",
annotated: true,
cfg: &v1alpha2.LLMInferenceServiceConfig{
Spec: v1alpha2.LLMInferenceServiceSpec{
WorkloadSpec: v1alpha2.WorkloadSpec{Template: mainPod(), Worker: mainPod()},
Prefill: &v1alpha2.WorkloadSpec{Template: mainPod(), Worker: mainPod()},
},
},
wantTemplateInj: true,
wantWorkerInj: true,
wantPrefTmplInj: true,
wantPrefWrkrInj: true,
},
{
name: "annotated: only Template set (single-node main)",
annotated: true,
cfg: &v1alpha2.LLMInferenceServiceConfig{
Spec: v1alpha2.LLMInferenceServiceSpec{
WorkloadSpec: v1alpha2.WorkloadSpec{Template: mainPod()},
},
},
wantTemplateInj: true,
},
{
name: "annotated: only Worker set (multi-node worker, no leader)",
annotated: true,
cfg: &v1alpha2.LLMInferenceServiceConfig{
Spec: v1alpha2.LLMInferenceServiceSpec{
WorkloadSpec: v1alpha2.WorkloadSpec{Worker: mainPod()},
},
},
wantWorkerInj: true,
},
{
name: "annotated: only Prefill.Template set (single-node prefill)",
annotated: true,
cfg: &v1alpha2.LLMInferenceServiceConfig{
Spec: v1alpha2.LLMInferenceServiceSpec{
Prefill: &v1alpha2.WorkloadSpec{Template: mainPod()},
},
},
wantPrefTmplInj: true,
},
{
name: "annotated: only Prefill.Worker set (multi-node prefill worker)",
annotated: true,
cfg: &v1alpha2.LLMInferenceServiceConfig{
Spec: v1alpha2.LLMInferenceServiceSpec{
Prefill: &v1alpha2.WorkloadSpec{Worker: mainPod()},
},
},
wantPrefWrkrInj: true,
},
{
name: "annotated: Prefill is nil -> only decode slots considered, no nil deref",
annotated: true,
cfg: &v1alpha2.LLMInferenceServiceConfig{
Spec: v1alpha2.LLMInferenceServiceSpec{
WorkloadSpec: v1alpha2.WorkloadSpec{Template: mainPod(), Worker: mainPod()},
},
},
wantTemplateInj: true,
wantWorkerInj: true,
},
{
name: "annotated: all slots nil -> no-op",
annotated: true,
cfg: &v1alpha2.LLMInferenceServiceConfig{},
},
}
hasClaim := func(t *testing.T, pod *corev1.PodSpec) bool {
t.Helper()
if pod == nil {
return false
}
for _, c := range pod.ResourceClaims {
if c.Name == managedDRAClaimName {
return true
}
}
return false
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var ann map[string]string
if tt.annotated {
ann = map[string]string{constants.ManagedDRADeviceClassAnnotationKey: deviceClass}
}
llmSvc := newManagedDRATestLLMISVC("test", ann)
injectManagedDRAIntoConfig(llmSvc, tt.cfg)
assert.Equal(t, tt.wantTemplateInj, hasClaim(t, tt.cfg.Spec.Template), "Template")
assert.Equal(t, tt.wantWorkerInj, hasClaim(t, tt.cfg.Spec.Worker), "Worker")
if tt.cfg.Spec.Prefill != nil {
assert.Equal(t, tt.wantPrefTmplInj, hasClaim(t, tt.cfg.Spec.Prefill.Template), "Prefill.Template")
assert.Equal(t, tt.wantPrefWrkrInj, hasClaim(t, tt.cfg.Spec.Prefill.Worker), "Prefill.Worker")
}
})
}
}
func TestInjectManagedDRAIntoConfig_AnnotatedContainerAcrossSlots(t *testing.T) {
mkPod := func() *corev1.PodSpec {
return &corev1.PodSpec{Containers: []corev1.Container{
{Name: "init"},
{Name: "worker"},
{Name: "sidecar"},
}}
}
llmSvc := newManagedDRATestLLMISVC("test", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.example.com",
constants.ManagedDRAContainerNameAnnotationKey: "worker",
})
cfg := &v1alpha2.LLMInferenceServiceConfig{
Spec: v1alpha2.LLMInferenceServiceSpec{
WorkloadSpec: v1alpha2.WorkloadSpec{Template: mkPod(), Worker: mkPod()},
Prefill: &v1alpha2.WorkloadSpec{Template: mkPod(), Worker: mkPod()},
},
}
injectManagedDRAIntoConfig(llmSvc, cfg)
for name, pod := range map[string]*corev1.PodSpec{
"Template": cfg.Spec.Template,
"Worker": cfg.Spec.Worker,
"Prefill.Template": cfg.Spec.Prefill.Template,
"Prefill.Worker": cfg.Spec.Prefill.Worker,
} {
require.Len(t, pod.ResourceClaims, 1, "%s pod-level claim", name)
assert.Empty(t, pod.Containers[0].Resources.Claims, "%s init must not receive the claim", name)
require.Len(t, pod.Containers[1].Resources.Claims, 1, "%s worker should receive the claim", name)
assert.Equal(t, managedDRAClaimName, pod.Containers[1].Resources.Claims[0].Name, "%s worker claim name", name)
assert.Empty(t, pod.Containers[2].Resources.Claims, "%s sidecar must not receive the claim", name)
}
}
func TestInjectManagedDRAIntoConfig_NilCfg(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.example.com",
})
assert.NotPanics(t, func() {
injectManagedDRAIntoConfig(llmSvc, nil)
})
}
func TestInjectManagedDRAIntoConfig_Idempotent(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.example.com",
})
mkPod := func() *corev1.PodSpec {
return &corev1.PodSpec{Containers: []corev1.Container{{Name: "main"}}}
}
cfg := &v1alpha2.LLMInferenceServiceConfig{
Spec: v1alpha2.LLMInferenceServiceSpec{
WorkloadSpec: v1alpha2.WorkloadSpec{Template: mkPod(), Worker: mkPod()},
Prefill: &v1alpha2.WorkloadSpec{Template: mkPod(), Worker: mkPod()},
},
}
injectManagedDRAIntoConfig(llmSvc, cfg)
injectManagedDRAIntoConfig(llmSvc, cfg)
for name, pod := range map[string]*corev1.PodSpec{
"Template": cfg.Spec.Template,
"Worker": cfg.Spec.Worker,
"Prefill.Template": cfg.Spec.Prefill.Template,
"Prefill.Worker": cfg.Spec.Prefill.Worker,
} {
assert.Len(t, pod.ResourceClaims, 1, "%s pod-level claim should not be duplicated", name)
assert.Len(t, pod.Containers[0].Resources.Claims, 1, "%s main-container claim should not be duplicated", name)
}
}
func TestSemanticResourceClaimTemplateIsEqual(t *testing.T) {
makeTemplate := func(className string) *resourcev1.ResourceClaimTemplate {
return &resourcev1.ResourceClaimTemplate{
Spec: resourcev1.ResourceClaimTemplateSpec{
Spec: resourcev1.ResourceClaimSpec{
Devices: resourcev1.DeviceClaim{
Requests: []resourcev1.DeviceRequest{
{
Name: "gpu",
Exactly: &resourcev1.ExactDeviceRequest{DeviceClassName: className},
},
},
},
},
},
}
}
assert.True(t, semanticResourceClaimTemplateIsEqual(makeTemplate("gpu.nvidia.com"), makeTemplate("gpu.nvidia.com")))
assert.False(t, semanticResourceClaimTemplateIsEqual(makeTemplate("gpu.nvidia.com"), makeTemplate("gpu.amd.com")))
}
func TestReconcileManagedDRA(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()
require.NoError(t, corev1.AddToScheme(scheme))
require.NoError(t, resourcev1.AddToScheme(scheme))
require.NoError(t, v1alpha2.AddToScheme(scheme))
t.Run("no managed DRA, nothing to clean up", func(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test-svc", nil)
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
r := &LLMISVCReconciler{
Client: fakeClient,
EventRecorder: record.NewFakeRecorder(10),
}
err := r.reconcileManagedDRA(ctx, llmSvc)
require.NoError(t, err)
})
t.Run("invalid device count", func(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test-svc", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.nvidia.com",
constants.ManagedDRADeviceCountAnnotationKey: "invalid",
})
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
r := &LLMISVCReconciler{
Client: fakeClient,
EventRecorder: record.NewFakeRecorder(10),
}
err := r.reconcileManagedDRA(ctx, llmSvc)
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid syntax")
})
t.Run("template claim is created", func(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test-svc", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.nvidia.com",
})
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
r := &LLMISVCReconciler{
Client: fakeClient,
EventRecorder: record.NewFakeRecorder(10),
}
err := r.reconcileManagedDRA(ctx, llmSvc)
require.NoError(t, err)
tmpl := &resourcev1.ResourceClaimTemplate{}
err = fakeClient.Get(ctx, types.NamespacedName{Name: "test-svc-managed-dra", Namespace: "default"}, tmpl)
require.NoError(t, err)
assert.Equal(t, "device", tmpl.Spec.Spec.Devices.Requests[0].Name)
})
t.Run("template claim with multiple CEL selectors", func(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test-svc-cel", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.nvidia.com",
constants.ManagedDRACelSelectorAnnotationKey: "device.attributes['gpu.nvidia.com']['type'] == 'A100'\n" +
"device.capacity['gpu.nvidia.com']['memory'].compareTo(quantity('40Gi')) > 0",
})
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
r := &LLMISVCReconciler{
Client: fakeClient,
EventRecorder: record.NewFakeRecorder(10),
}
require.NoError(t, r.reconcileManagedDRA(ctx, llmSvc))
tmpl := &resourcev1.ResourceClaimTemplate{}
require.NoError(t, fakeClient.Get(ctx, types.NamespacedName{Name: "test-svc-cel-managed-dra", Namespace: "default"}, tmpl))
require.Len(t, tmpl.Spec.Spec.Devices.Requests, 1)
require.Len(t, tmpl.Spec.Spec.Devices.Requests[0].Exactly.Selectors, 2)
})
t.Run("cleanup deletes orphaned template after DRA disabled", func(t *testing.T) {
objNamespacedName := types.NamespacedName{Name: "test-svc-managed-dra", Namespace: "default"}
llmSvcWithDRA := newManagedDRATestLLMISVC("test-svc", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.nvidia.com",
})
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
r := &LLMISVCReconciler{
Client: fakeClient,
EventRecorder: record.NewFakeRecorder(10),
}
require.NoError(t, r.reconcileManagedDRA(ctx, llmSvcWithDRA))
require.NoError(t, fakeClient.Get(ctx, objNamespacedName, &resourcev1.ResourceClaimTemplate{}),
"template should exist after first reconcile")
llmSvcWithoutDRA := newManagedDRATestLLMISVC("test-svc", nil)
require.NoError(t, r.reconcileManagedDRA(ctx, llmSvcWithoutDRA))
err := fakeClient.Get(ctx, objNamespacedName, &resourcev1.ResourceClaimTemplate{})
require.Error(t, err, "orphaned template should be cleaned up after DRA is disabled")
assert.True(t, apierrors.IsNotFound(err), "expected NotFound, got %v", err)
})
t.Run("cleanup is a no-op when there is no orphaned template", func(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("never-had-dra", nil)
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
r := &LLMISVCReconciler{
Client: fakeClient,
EventRecorder: record.NewFakeRecorder(10),
}
require.NoError(t, r.reconcileManagedDRA(ctx, llmSvc))
})
t.Run("cleanup deletes orphaned template when service is force-stopped", func(t *testing.T) {
objNamespacedName := types.NamespacedName{Name: "test-svc-stopped-managed-dra", Namespace: "default"}
llmSvcWithDRA := newManagedDRATestLLMISVC("test-svc-stopped", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.nvidia.com",
})
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
r := &LLMISVCReconciler{
Client: fakeClient,
EventRecorder: record.NewFakeRecorder(10),
}
require.NoError(t, r.reconcileManagedDRA(ctx, llmSvcWithDRA))
require.NoError(t, fakeClient.Get(ctx, objNamespacedName, &resourcev1.ResourceClaimTemplate{}),
"template should exist after first reconcile")
llmSvcWithDRA.Annotations[constants.StopAnnotationKey] = "true"
require.NoError(t, r.reconcileManagedDRA(ctx, llmSvcWithDRA))
err := fakeClient.Get(ctx, objNamespacedName, &resourcev1.ResourceClaimTemplate{})
require.Error(t, err, "orphaned template should be cleaned up when service is stopped")
assert.True(t, apierrors.IsNotFound(err), "expected NotFound, got %v", err)
})
}
func TestReconcileManagedDRA_CreateError(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()
require.NoError(t, corev1.AddToScheme(scheme))
require.NoError(t, resourcev1.AddToScheme(scheme))
require.NoError(t, v1alpha2.AddToScheme(scheme))
t.Run("template claim error", func(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test-svc", map[string]string{
constants.ManagedDRADeviceClassAnnotationKey: "gpu.nvidia.com",
})
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithInterceptorFuncs(interceptor.Funcs{
Create: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.CreateOption) error {
return assert.AnError
},
}).
Build()
r := &LLMISVCReconciler{
Client: fakeClient,
EventRecorder: record.NewFakeRecorder(10),
}
err := r.reconcileManagedDRA(ctx, llmSvc)
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to reconcile Managed DRA ResourceClaimTemplate")
})
t.Run("cleanup error", func(t *testing.T) {
llmSvc := newManagedDRATestLLMISVC("test-svc", nil)
// Create the existing object so Delete doesn't return nil early
existingTemplate := &resourcev1.ResourceClaimTemplate{
ObjectMeta: metav1.ObjectMeta{
Name: managedDRAResourceName(llmSvc),
Namespace: llmSvc.GetNamespace(),
},
}
require.NoError(t, controllerutil.SetControllerReference(llmSvc, existingTemplate, scheme))
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(existingTemplate).
WithInterceptorFuncs(interceptor.Funcs{
Delete: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.DeleteOption) error {
return errors.New("mock delete error")
},
}).
Build()
r := &LLMISVCReconciler{
Client: fakeClient,
EventRecorder: record.NewFakeRecorder(10),
}
err := r.reconcileManagedDRA(ctx, llmSvc)
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to cleanup Managed DRA ResourceClaimTemplate")
})
}