mirror of
https://github.com/hanzo-ml/kserve.git
synced 2026-10-30 20:53:49 +00:00
227 lines
7.1 KiB
Go
227 lines
7.1 KiB
Go
/*
|
|
Copyright 2026 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 contains the controller logic for the LLMInferenceService.
|
|
//
|
|
// Note on Managed DRA:
|
|
// The Managed DRA feature implemented here via the serving.kserve.io/exp-dra-* annotations
|
|
// is an intentionally limited-scope convenience feature for basic DRA use cases.
|
|
// It provides a simplified mechanism for users to dynamically request accelerator
|
|
// devices (GPUs, TPUs, NICs, etc.) via DeviceClass, avoiding the need to manually
|
|
// create and manage ResourceClaimTemplate objects.
|
|
//
|
|
// Complex DRA topologies or advanced use cases should bypass this managed feature
|
|
// and use native Kubernetes ResourceClaimTemplate objects directly by attaching
|
|
// them to the PodSpec.
|
|
package llmisvc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
resourcev1 "k8s.io/api/resource/v1"
|
|
"k8s.io/apimachinery/pkg/api/equality"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"knative.dev/pkg/kmeta"
|
|
|
|
"github.com/kserve/kserve/pkg/apis/serving/v1alpha2"
|
|
"github.com/kserve/kserve/pkg/utils"
|
|
)
|
|
|
|
const (
|
|
managedDRASuffix = "-managed-dra"
|
|
managedDRAClaimName = "managed-device"
|
|
defaultManagedDRAName = "device"
|
|
)
|
|
|
|
// reconcileManagedDRA creates, updates, or deletes the ResourceClaimTemplate
|
|
// that backs managed DRA for this LLMInferenceService.
|
|
func (r *LLMISVCReconciler) reconcileManagedDRA(ctx context.Context, llmSvc *v1alpha2.LLMInferenceService) error {
|
|
if !llmSvc.HasManagedDRA() || utils.GetForceStopRuntime(llmSvc) {
|
|
return r.cleanupManagedDRA(ctx, llmSvc)
|
|
}
|
|
|
|
expected, err := expectedManagedDRATemplate(llmSvc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := Reconcile(ctx, r, llmSvc, &resourcev1.ResourceClaimTemplate{}, expected, semanticResourceClaimTemplateIsEqual); err != nil {
|
|
return fmt.Errorf("failed to reconcile Managed DRA ResourceClaimTemplate: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// cleanupManagedDRA removes any previously generated ResourceClaimTemplate
|
|
// owned by this LLMInferenceService.
|
|
func (r *LLMISVCReconciler) cleanupManagedDRA(ctx context.Context, llmSvc *v1alpha2.LLMInferenceService) error {
|
|
stale := &resourcev1.ResourceClaimTemplate{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: managedDRAResourceName(llmSvc),
|
|
Namespace: llmSvc.GetNamespace(),
|
|
},
|
|
}
|
|
if err := Delete(ctx, r, llmSvc, stale); err != nil {
|
|
return fmt.Errorf("failed to cleanup Managed DRA ResourceClaimTemplate: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func expectedManagedDRATemplate(llmSvc *v1alpha2.LLMInferenceService) (*resourcev1.ResourceClaimTemplate, error) {
|
|
deviceClass, _ := llmSvc.ManagedDRADeviceClass()
|
|
celSelectors := llmSvc.ManagedDRACelSelectors()
|
|
|
|
deviceCount, err := llmSvc.ManagedDRADeviceCount()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &resourcev1.ResourceClaimTemplate{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: managedDRAResourceName(llmSvc),
|
|
Namespace: llmSvc.GetNamespace(),
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
*metav1.NewControllerRef(llmSvc, v1alpha2.LLMInferenceServiceGVK),
|
|
},
|
|
},
|
|
Spec: resourcev1.ResourceClaimTemplateSpec{
|
|
Spec: resourcev1.ResourceClaimSpec{
|
|
Devices: resourcev1.DeviceClaim{
|
|
Requests: buildDeviceRequests(deviceClass, celSelectors, deviceCount),
|
|
},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// buildDeviceRequests creates the slice of DeviceRequest objects based on the requested count and class.
|
|
func buildDeviceRequests(deviceClass string, celSelectors []string, deviceCount int) []resourcev1.DeviceRequest {
|
|
req := resourcev1.DeviceRequest{
|
|
Name: defaultManagedDRAName,
|
|
Exactly: &resourcev1.ExactDeviceRequest{
|
|
DeviceClassName: deviceClass,
|
|
},
|
|
}
|
|
|
|
if deviceCount > 1 {
|
|
req.Exactly.Count = int64(deviceCount)
|
|
}
|
|
|
|
if len(celSelectors) > 0 {
|
|
selectors := make([]resourcev1.DeviceSelector, len(celSelectors))
|
|
for j, expr := range celSelectors {
|
|
selectors[j] = resourcev1.DeviceSelector{
|
|
CEL: &resourcev1.CELDeviceSelector{
|
|
Expression: expr,
|
|
},
|
|
}
|
|
}
|
|
req.Exactly.Selectors = selectors
|
|
}
|
|
|
|
return []resourcev1.DeviceRequest{req}
|
|
}
|
|
|
|
func semanticResourceClaimTemplateIsEqual(expected *resourcev1.ResourceClaimTemplate, curr *resourcev1.ResourceClaimTemplate) bool {
|
|
return equality.Semantic.DeepEqual(expected.Spec, curr.Spec)
|
|
}
|
|
|
|
// managedDRAResourceName returns the name of the ResourceClaimTemplate
|
|
func managedDRAResourceName(llmSvc *v1alpha2.LLMInferenceService) string {
|
|
return kmeta.ChildName(llmSvc.GetName(), managedDRASuffix)
|
|
}
|
|
|
|
// injectManagedDRAIntoConfig fans injectManagedDRA out to every workload
|
|
// PodSpec of the merged config (Template, Worker, Prefill.Template, Prefill.Worker).
|
|
func injectManagedDRAIntoConfig(llmSvc *v1alpha2.LLMInferenceService, cfg *v1alpha2.LLMInferenceServiceConfig) {
|
|
if cfg == nil || !llmSvc.HasManagedDRA() {
|
|
return
|
|
}
|
|
if cfg.Spec.Template != nil {
|
|
injectManagedDRA(llmSvc, cfg.Spec.Template)
|
|
}
|
|
if cfg.Spec.Worker != nil {
|
|
injectManagedDRA(llmSvc, cfg.Spec.Worker)
|
|
}
|
|
if cfg.Spec.Prefill != nil {
|
|
if cfg.Spec.Prefill.Template != nil {
|
|
injectManagedDRA(llmSvc, cfg.Spec.Prefill.Template)
|
|
}
|
|
if cfg.Spec.Prefill.Worker != nil {
|
|
injectManagedDRA(llmSvc, cfg.Spec.Prefill.Worker)
|
|
}
|
|
}
|
|
}
|
|
|
|
// injectManagedDRA adds the managed DRA claim to the PodSpec at both the
|
|
// pod level and the target container chosen by targetContainerForDRA.
|
|
func injectManagedDRA(llmSvc *v1alpha2.LLMInferenceService, podSpec *corev1.PodSpec) {
|
|
if !llmSvc.HasManagedDRA() {
|
|
return
|
|
}
|
|
|
|
resourceName := managedDRAResourceName(llmSvc)
|
|
|
|
hasPodClaim := false
|
|
for _, claim := range podSpec.ResourceClaims {
|
|
if claim.Name == managedDRAClaimName {
|
|
hasPodClaim = true
|
|
break
|
|
}
|
|
}
|
|
if !hasPodClaim {
|
|
podSpec.ResourceClaims = append(podSpec.ResourceClaims, corev1.PodResourceClaim{
|
|
Name: managedDRAClaimName,
|
|
ResourceClaimTemplateName: &resourceName,
|
|
})
|
|
}
|
|
|
|
idx := targetContainerForDRA(llmSvc, podSpec)
|
|
if idx < 0 {
|
|
return
|
|
}
|
|
for _, claim := range podSpec.Containers[idx].Resources.Claims {
|
|
if claim.Name == managedDRAClaimName {
|
|
return
|
|
}
|
|
}
|
|
podSpec.Containers[idx].Resources.Claims = append(
|
|
podSpec.Containers[idx].Resources.Claims,
|
|
corev1.ResourceClaim{Name: managedDRAClaimName},
|
|
)
|
|
}
|
|
|
|
// targetContainerForDRA returns the index of the container that should receive
|
|
// the DRA claim: the annotated container when set and present, otherwise the
|
|
// first container. Returns -1 if no suitable container exists.
|
|
func targetContainerForDRA(llmSvc *v1alpha2.LLMInferenceService, podSpec *corev1.PodSpec) int {
|
|
if len(podSpec.Containers) == 0 {
|
|
return -1
|
|
}
|
|
want, present := llmSvc.ManagedDRAContainerName()
|
|
if !present || want == "" {
|
|
return 0
|
|
}
|
|
for i := range podSpec.Containers {
|
|
if podSpec.Containers[i].Name == want {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|