mirror of
https://github.com/hanzo-ml/kserve.git
synced 2026-10-30 20:53:49 +00:00
Signed-off-by: Vedant Mahabaleshwarkar <[email protected]>
775 lines
29 KiB
Go
775 lines
29 KiB
Go
/*
|
|
Copyright 2024 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 reconcilers
|
|
|
|
import (
|
|
"context"
|
|
"maps"
|
|
"slices"
|
|
|
|
"github.com/go-logr/logr"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierr "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/kubernetes"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
|
|
|
"github.com/kserve/kserve/pkg/apis/serving/v1alpha1"
|
|
"github.com/kserve/kserve/pkg/apis/serving/v1alpha2"
|
|
"github.com/kserve/kserve/pkg/apis/serving/v1beta1"
|
|
"github.com/kserve/kserve/pkg/constants"
|
|
controllerutils "github.com/kserve/kserve/pkg/controller/v1alpha1/utils"
|
|
"github.com/kserve/kserve/pkg/utils"
|
|
)
|
|
|
|
var (
|
|
OwnerKey = ".metadata.controller"
|
|
LocalModelKey = ".localmodel"
|
|
LocalModelNamespaceKey = ".localmodelnamespace"
|
|
APIGVStr = v1alpha1.SchemeGroupVersion.String()
|
|
ModelCacheCRName = "LocalModelCache"
|
|
ModelNamespaceCacheCRName = "LocalModelNamespaceCache"
|
|
FinalizerName = "localmodel.kserve.io/finalizer"
|
|
NamespaceCacheFinalizerName = "localmodelnamespacecache.kserve.io/finalizer"
|
|
)
|
|
|
|
func hasLLMInferenceServiceResource(restMapper meta.RESTMapper) (bool, error) {
|
|
gk := schema.GroupKind{
|
|
Group: v1alpha2.SchemeGroupVersion.Group,
|
|
Kind: "LLMInferenceService",
|
|
}
|
|
|
|
_, err := restMapper.RESTMapping(gk, v1alpha2.SchemeGroupVersion.Version)
|
|
if err != nil {
|
|
if meta.IsNoMatchError(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func hasLLMInferenceServiceCRD(mgr ctrl.Manager) (bool, error) {
|
|
return hasLLMInferenceServiceResource(mgr.GetRESTMapper())
|
|
}
|
|
|
|
// LocalModelParams holds common parameters extracted from either LocalModelCache or LocalModelNamespaceCache
|
|
type LocalModelParams struct {
|
|
Name string
|
|
Namespace string // Empty for cluster-scoped LocalModelCache
|
|
SourceModelUri string
|
|
NodeGroups []string
|
|
ServiceAccountName string
|
|
Storage *v1alpha1.LocalModelStorageSpec
|
|
Finalizers []string
|
|
FinalizerName string
|
|
IsNamespaceScoped bool
|
|
}
|
|
|
|
// ExtractLocalModelParams extracts common parameters from either LocalModelCache or LocalModelNamespaceCache
|
|
// Only one of the parameters should be non-nil
|
|
func ExtractLocalModelParams(localModelCache *v1alpha1.LocalModelCache, localModelNamespaceCache *v1alpha1.LocalModelNamespaceCache) LocalModelParams {
|
|
if localModelCache != nil {
|
|
return LocalModelParams{
|
|
Name: localModelCache.Name,
|
|
Namespace: "", // Cluster-scoped
|
|
SourceModelUri: localModelCache.Spec.SourceModelUri,
|
|
NodeGroups: localModelCache.Spec.NodeGroups,
|
|
ServiceAccountName: localModelCache.Spec.ServiceAccountName,
|
|
Storage: localModelCache.Spec.Storage,
|
|
Finalizers: localModelCache.Finalizers,
|
|
FinalizerName: FinalizerName,
|
|
IsNamespaceScoped: false,
|
|
}
|
|
}
|
|
if localModelNamespaceCache != nil {
|
|
return LocalModelParams{
|
|
Name: localModelNamespaceCache.Name,
|
|
Namespace: localModelNamespaceCache.Namespace,
|
|
SourceModelUri: localModelNamespaceCache.Spec.SourceModelUri,
|
|
NodeGroups: localModelNamespaceCache.Spec.NodeGroups,
|
|
ServiceAccountName: localModelNamespaceCache.Spec.ServiceAccountName,
|
|
Storage: localModelNamespaceCache.Spec.Storage,
|
|
Finalizers: localModelNamespaceCache.Finalizers,
|
|
FinalizerName: NamespaceCacheFinalizerName,
|
|
IsNamespaceScoped: true,
|
|
}
|
|
}
|
|
return LocalModelParams{}
|
|
}
|
|
|
|
// CreateLocalModelInfo creates a LocalModelInfo from either LocalModelCache or LocalModelNamespaceCache
|
|
// nodeGroupName specifies which LocalModelNodeGroup this model belongs to, used by the agent
|
|
// to construct the correct PVC name when multiple nodegroups have overlapping node affinity.
|
|
func CreateLocalModelInfo(localModelCache *v1alpha1.LocalModelCache, localModelNamespaceCache *v1alpha1.LocalModelNamespaceCache, nodeGroupName string) v1alpha1.LocalModelInfo {
|
|
params := ExtractLocalModelParams(localModelCache, localModelNamespaceCache)
|
|
return v1alpha1.LocalModelInfo{
|
|
ModelName: params.Name,
|
|
SourceModelUri: params.SourceModelUri,
|
|
Namespace: params.Namespace,
|
|
NodeGroup: nodeGroupName,
|
|
ServiceAccountName: params.ServiceAccountName,
|
|
Storage: params.Storage,
|
|
}
|
|
}
|
|
|
|
// IsNodeReady checks if a node is in ready state
|
|
func IsNodeReady(node corev1.Node) bool {
|
|
for _, condition := range node.Status.Conditions {
|
|
if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetNodesFromNodeGroup returns a list of ready nodes, and not ready nodes that matches the node selector in the node group
|
|
func GetNodesFromNodeGroup(ctx context.Context, nodeGroup *v1alpha1.LocalModelNodeGroup, c client.Client) (*corev1.NodeList, *corev1.NodeList, error) {
|
|
nodes := &corev1.NodeList{}
|
|
readyNodes := &corev1.NodeList{}
|
|
notReadyNodes := &corev1.NodeList{}
|
|
if err := c.List(ctx, nodes); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, node := range nodes.Items {
|
|
matches, err := controllerutils.CheckNodeAffinity(&nodeGroup.Spec.PersistentVolumeSpec, node)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if matches {
|
|
if IsNodeReady(node) {
|
|
readyNodes.Items = append(readyNodes.Items, node)
|
|
} else {
|
|
notReadyNodes.Items = append(notReadyNodes.Items, node)
|
|
}
|
|
}
|
|
}
|
|
return readyNodes, notReadyNodes, nil
|
|
}
|
|
|
|
// StorageSpecEqual compares two LocalModelStorageSpec for equality
|
|
func StorageSpecEqual(a, b *v1alpha1.LocalModelStorageSpec) bool {
|
|
if a == nil && b == nil {
|
|
return true
|
|
}
|
|
if a == nil || b == nil {
|
|
return false
|
|
}
|
|
if (a.StorageKey == nil) != (b.StorageKey == nil) {
|
|
return false
|
|
}
|
|
if a.StorageKey != nil && *a.StorageKey != *b.StorageKey {
|
|
return false
|
|
}
|
|
if (a.Parameters == nil) != (b.Parameters == nil) {
|
|
return false
|
|
}
|
|
if a.Parameters != nil && !maps.Equal(*a.Parameters, *b.Parameters) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// NodeStatusFromLocalModelStatus converts a ModelStatus to NodeStatus
|
|
func NodeStatusFromLocalModelStatus(modelStatus v1alpha1.ModelStatus) v1alpha1.NodeStatus {
|
|
switch modelStatus {
|
|
case v1alpha1.ModelDownloadPending:
|
|
return v1alpha1.NodeDownloadPending
|
|
case v1alpha1.ModelDownloading:
|
|
return v1alpha1.NodeDownloading
|
|
case v1alpha1.ModelDownloadError:
|
|
return v1alpha1.NodeDownloadError
|
|
case v1alpha1.ModelDownloaded:
|
|
return v1alpha1.NodeDownloaded
|
|
}
|
|
return v1alpha1.NodeDownloadPending
|
|
}
|
|
|
|
// DeleteModelFromNodes deletes the model from all nodes in the node groups
|
|
// Only one of localModelCache or localModelNamespaceCache should be non-nil
|
|
// For namespace-scoped models, also explicitly deletes PVs since they cannot have owner references
|
|
func DeleteModelFromNodes(
|
|
ctx context.Context,
|
|
c client.Client,
|
|
clientset *kubernetes.Clientset,
|
|
log logr.Logger,
|
|
localModelCache *v1alpha1.LocalModelCache,
|
|
localModelNamespaceCache *v1alpha1.LocalModelNamespaceCache,
|
|
nodeGroups map[string]*v1alpha1.LocalModelNodeGroup,
|
|
) (ctrl.Result, error) {
|
|
params := ExtractLocalModelParams(localModelCache, localModelNamespaceCache)
|
|
|
|
// finalizer does not exist, nothing to do here!
|
|
if !utils.Includes(params.Finalizers, params.FinalizerName) {
|
|
return ctrl.Result{}, nil
|
|
}
|
|
log.Info("deleting model", "name", params.Name, "namespace", params.Namespace)
|
|
|
|
for nodeGroupName, nodeGroup := range nodeGroups {
|
|
readyNodes, notReadyNodes, err := GetNodesFromNodeGroup(ctx, nodeGroup, c)
|
|
if err != nil {
|
|
log.Error(err, "getNodesFromNodeGroup node error")
|
|
return ctrl.Result{}, err
|
|
}
|
|
for _, node := range append(readyNodes.Items, notReadyNodes.Items...) {
|
|
localModelNode := &v1alpha1.LocalModelNode{}
|
|
err := c.Get(ctx, types.NamespacedName{Name: node.Name}, localModelNode)
|
|
if err != nil {
|
|
if apierr.IsNotFound(err) {
|
|
log.Info("localmodelNode not found", "node", node.Name)
|
|
continue
|
|
} else {
|
|
log.Error(err, "Failed to get localmodelnode", "name", node.Name)
|
|
return ctrl.Result{}, err
|
|
}
|
|
}
|
|
|
|
if err := DeleteModelFromNode(ctx, c, log, localModelNode, params.Name, params.Namespace); err != nil {
|
|
log.Error(err, "failed to delete model from localModelNode", "localModelNode", localModelNode.Name)
|
|
return ctrl.Result{}, err
|
|
}
|
|
}
|
|
|
|
// For namespace-scoped LocalModelNamespaceCache, PVs cannot have owner references
|
|
// (Kubernetes limitation: cluster-scoped resources cannot be owned by namespace-scoped resources)
|
|
// So we must explicitly delete them here
|
|
if params.IsNamespaceScoped {
|
|
// Delete download PV: {modelName}-{nodeGroup}-{namespace}-download
|
|
downloadPVName := params.Name + "-" + nodeGroupName + "-" + params.Namespace + "-download"
|
|
if err := DeletePV(ctx, clientset, log, downloadPVName); err != nil {
|
|
log.Error(err, "failed to delete download PV", "name", downloadPVName)
|
|
}
|
|
|
|
// Delete download PVC from jobNamespace (where download jobs run)
|
|
downloadPVCName := downloadPVName
|
|
isvcConfigMap, cfgErr := v1beta1.GetInferenceServiceConfigMap(ctx, clientset)
|
|
if cfgErr == nil {
|
|
localModelConfig, cfgErr := v1beta1.NewLocalModelConfig(isvcConfigMap)
|
|
if cfgErr == nil {
|
|
if err := DeletePVC(ctx, clientset, log, downloadPVCName, localModelConfig.JobNamespace); err != nil {
|
|
log.Error(err, "failed to delete download PVC from jobNamespace", "name", downloadPVCName, "namespace", localModelConfig.JobNamespace)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete serving PV: {modelName}-{nodeGroup}-{namespace}
|
|
servingPVName := params.Name + "-" + nodeGroupName + "-" + params.Namespace
|
|
if err := DeletePV(ctx, clientset, log, servingPVName); err != nil {
|
|
log.Error(err, "failed to delete serving PV", "name", servingPVName)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove finalizer
|
|
if localModelCache != nil {
|
|
patch := client.MergeFrom(localModelCache.DeepCopy())
|
|
localModelCache.Finalizers = utils.RemoveString(localModelCache.Finalizers, params.FinalizerName)
|
|
if err := c.Patch(ctx, localModelCache, patch); err != nil {
|
|
log.Error(err, "Cannot remove finalizer", "model name", params.Name)
|
|
return ctrl.Result{}, err
|
|
}
|
|
} else if localModelNamespaceCache != nil {
|
|
patch := client.MergeFrom(localModelNamespaceCache.DeepCopy())
|
|
localModelNamespaceCache.Finalizers = utils.RemoveString(localModelNamespaceCache.Finalizers, params.FinalizerName)
|
|
if err := c.Patch(ctx, localModelNamespaceCache, patch); err != nil {
|
|
log.Error(err, "Cannot remove finalizer", "model name", params.Name)
|
|
return ctrl.Result{}, err
|
|
}
|
|
}
|
|
|
|
// Stop reconciliation as the item is being deleted
|
|
return ctrl.Result{}, nil
|
|
}
|
|
|
|
// DeleteModelFromNode deletes a model entry from a LocalModelNode
|
|
func DeleteModelFromNode(ctx context.Context, c client.Client, log logr.Logger, localModelNode *v1alpha1.LocalModelNode, modelName, namespace string) error {
|
|
var patch client.Patch
|
|
for i, modelInfo := range localModelNode.Spec.LocalModels {
|
|
if modelInfo.ModelName == modelName && modelInfo.Namespace == namespace {
|
|
patch = client.MergeFrom(localModelNode.DeepCopy())
|
|
localModelNode.Spec.LocalModels = append(localModelNode.Spec.LocalModels[:i], localModelNode.Spec.LocalModels[i+1:]...)
|
|
if err := c.Patch(ctx, localModelNode, patch); err != nil {
|
|
log.Error(err, "Update localmodelnode", "name", localModelNode.Name)
|
|
return err
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeletePV deletes a PersistentVolume by name
|
|
func DeletePV(ctx context.Context, clientset *kubernetes.Clientset, log logr.Logger, name string) error {
|
|
persistentVolumes := clientset.CoreV1().PersistentVolumes()
|
|
if _, err := persistentVolumes.Get(ctx, name, metav1.GetOptions{}); err != nil {
|
|
if apierr.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
log.Error(err, "Failed to get PV for deletion", "name", name)
|
|
return err
|
|
}
|
|
log.Info("Deleting PV", "name", name)
|
|
if err := persistentVolumes.Delete(ctx, name, metav1.DeleteOptions{}); err != nil {
|
|
if apierr.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
log.Error(err, "Failed to delete PV", "name", name)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeletePVC deletes a PersistentVolumeClaim by name and namespace
|
|
func DeletePVC(ctx context.Context, clientset *kubernetes.Clientset, log logr.Logger, name, namespace string) error {
|
|
persistentVolumeClaims := clientset.CoreV1().PersistentVolumeClaims(namespace)
|
|
if _, err := persistentVolumeClaims.Get(ctx, name, metav1.GetOptions{}); err != nil {
|
|
if apierr.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
log.Error(err, "Failed to get PVC for deletion", "name", name, "namespace", namespace)
|
|
return err
|
|
}
|
|
log.Info("Deleting PVC", "name", name, "namespace", namespace)
|
|
if err := persistentVolumeClaims.Delete(ctx, name, metav1.DeleteOptions{}); err != nil {
|
|
if apierr.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
log.Error(err, "Failed to delete PVC", "name", name, "namespace", namespace)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CreatePV creates a PersistentVolume and sets the owner reference
|
|
// Only one of localModelCache or localModelNamespaceCache should be non-nil
|
|
// Note: For namespace-scoped LocalModelNamespaceCache, owner reference is NOT set because
|
|
// PVs are cluster-scoped and cannot be owned by namespace-scoped resources
|
|
func CreatePV(
|
|
ctx context.Context,
|
|
clientset *kubernetes.Clientset,
|
|
scheme *runtime.Scheme,
|
|
log logr.Logger,
|
|
spec corev1.PersistentVolume,
|
|
localModelCache *v1alpha1.LocalModelCache,
|
|
localModelNamespaceCache *v1alpha1.LocalModelNamespaceCache,
|
|
) error {
|
|
persistentVolumes := clientset.CoreV1().PersistentVolumes()
|
|
if _, err := persistentVolumes.Get(ctx, spec.Name, metav1.GetOptions{}); err != nil {
|
|
if !apierr.IsNotFound(err) {
|
|
log.Error(err, "Failed to get PV")
|
|
return err
|
|
}
|
|
log.Info("Create PV", "name", spec.Name)
|
|
|
|
// Set controller reference only for cluster-scoped LocalModelCache
|
|
if localModelCache != nil {
|
|
if err := controllerutil.SetControllerReference(localModelCache, &spec, scheme); err != nil {
|
|
log.Error(err, "Failed to set controller reference")
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err := persistentVolumes.Create(ctx, &spec, metav1.CreateOptions{}); err != nil {
|
|
log.Error(err, "Failed to create PV", "name", spec.Name)
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CreatePVC creates a PersistentVolumeClaim and sets the owner reference
|
|
// Only one of localModelCache or localModelNamespaceCache should be non-nil
|
|
func CreatePVC(
|
|
ctx context.Context,
|
|
clientset *kubernetes.Clientset,
|
|
scheme *runtime.Scheme,
|
|
log logr.Logger,
|
|
spec corev1.PersistentVolumeClaim,
|
|
namespace string,
|
|
localModelCache *v1alpha1.LocalModelCache,
|
|
localModelNamespaceCache *v1alpha1.LocalModelNamespaceCache,
|
|
) error {
|
|
persistentVolumeClaims := clientset.CoreV1().PersistentVolumeClaims(namespace)
|
|
if _, err := persistentVolumeClaims.Get(ctx, spec.Name, metav1.GetOptions{}); err != nil {
|
|
if !apierr.IsNotFound(err) {
|
|
log.Error(err, "Failed to get PVC")
|
|
return err
|
|
}
|
|
log.Info("Create PVC", "name", spec.Name, "namespace", namespace)
|
|
|
|
spec.Namespace = namespace
|
|
|
|
if localModelCache != nil {
|
|
if err := controllerutil.SetControllerReference(localModelCache, &spec, scheme); err != nil {
|
|
log.Error(err, "Set controller reference")
|
|
return err
|
|
}
|
|
} else if localModelNamespaceCache != nil {
|
|
// Only set owner reference if PVC is in the same namespace as the cache
|
|
if namespace == localModelNamespaceCache.Namespace {
|
|
if err := controllerutil.SetControllerReference(localModelNamespaceCache, &spec, scheme); err != nil {
|
|
log.Error(err, "Set controller reference")
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if _, err := persistentVolumeClaims.Create(ctx, &spec, metav1.CreateOptions{}); err != nil {
|
|
log.Error(err, "Failed to create PVC", "name", spec.Name)
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReconcileForIsvcs reconciles PVs and PVCs for InferenceServices using this cached model
|
|
// Only one of localModelCache or localModelNamespaceCache should be non-nil
|
|
// For LocalModelCache: lists all ISVCs matching the model globally
|
|
// For LocalModelNamespaceCache: lists ISVCs only in the same namespace
|
|
// Also cleans up unused serving PVs/PVCs when ISVCs are removed
|
|
func ReconcileForIsvcs(
|
|
ctx context.Context,
|
|
c client.Client,
|
|
clientset *kubernetes.Clientset,
|
|
scheme *runtime.Scheme,
|
|
log logr.Logger,
|
|
localModelCache *v1alpha1.LocalModelCache,
|
|
localModelNamespaceCache *v1alpha1.LocalModelNamespaceCache,
|
|
localModelNodeGroups map[string]*v1alpha1.LocalModelNodeGroup,
|
|
defaultNodeGroup *v1alpha1.LocalModelNodeGroup,
|
|
llmInferenceServiceEnabled bool,
|
|
) error {
|
|
params := ExtractLocalModelParams(localModelCache, localModelNamespaceCache)
|
|
|
|
isvcs := &v1beta1.InferenceServiceList{}
|
|
|
|
if params.IsNamespaceScoped {
|
|
if err := c.List(ctx, isvcs,
|
|
client.InNamespace(params.Namespace),
|
|
client.MatchingFields{LocalModelNamespaceKey: params.Name}); err != nil {
|
|
log.Error(err, "List isvc error")
|
|
return err
|
|
}
|
|
} else {
|
|
if err := c.List(ctx, isvcs, client.MatchingFields{LocalModelKey: params.Name}); err != nil {
|
|
log.Error(err, "List isvc error")
|
|
return err
|
|
}
|
|
}
|
|
|
|
isvcNames := []v1alpha1.NamespacedName{}
|
|
namespaceToNodeGroups := make(map[string]map[string]*v1alpha1.LocalModelNodeGroup)
|
|
for _, isvc := range isvcs.Items {
|
|
isvcNames = append(isvcNames, v1alpha1.NamespacedName{Name: isvc.Name, Namespace: isvc.Namespace})
|
|
if isvcNodeGroup, ok := isvc.Annotations[constants.NodeGroupAnnotationKey]; ok {
|
|
if nodeGroup, ok := localModelNodeGroups[isvcNodeGroup]; ok {
|
|
if _, ok := namespaceToNodeGroups[isvc.Namespace]; !ok {
|
|
namespaceToNodeGroups[isvc.Namespace] = map[string]*v1alpha1.LocalModelNodeGroup{}
|
|
}
|
|
namespaceToNodeGroups[isvc.Namespace][nodeGroup.Name] = nodeGroup
|
|
} else {
|
|
log.Info("Didn't find isvc node group in model cache node groups", "isvc name", isvc.Name, "isvc node group", isvcNodeGroup, "model cache node groups", slices.Collect(maps.Keys(localModelNodeGroups)))
|
|
}
|
|
// isvc does not have nodegroup annotation. Use default nodegroup
|
|
} else if _, ok := namespaceToNodeGroups[isvc.Namespace]; !ok {
|
|
log.Info("Isvc does not have node group annotation", "isvc name", isvc.Name, "nodegroup annotation", constants.NodeGroupAnnotationKey)
|
|
namespaceToNodeGroups[isvc.Namespace] = map[string]*v1alpha1.LocalModelNodeGroup{defaultNodeGroup.Name: defaultNodeGroup}
|
|
} else {
|
|
namespaceToNodeGroups[isvc.Namespace][defaultNodeGroup.Name] = defaultNodeGroup
|
|
}
|
|
}
|
|
|
|
llmSvcNames := []v1alpha1.NamespacedName{}
|
|
if llmInferenceServiceEnabled {
|
|
// List LLMInferenceServices using this cached model
|
|
llmSvcs := &v1alpha2.LLMInferenceServiceList{}
|
|
if params.IsNamespaceScoped {
|
|
if err := c.List(ctx, llmSvcs,
|
|
client.InNamespace(params.Namespace),
|
|
client.MatchingFields{LocalModelNamespaceKey: params.Name}); err != nil {
|
|
log.Error(err, "List llm inference service error")
|
|
return err
|
|
}
|
|
} else {
|
|
if err := c.List(ctx, llmSvcs, client.MatchingFields{LocalModelKey: params.Name}); err != nil {
|
|
log.Error(err, "List llm inference service error")
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, llmSvc := range llmSvcs.Items {
|
|
llmSvcNames = append(llmSvcNames, v1alpha1.NamespacedName{Name: llmSvc.Name, Namespace: llmSvc.Namespace})
|
|
if llmSvcNodeGroup, ok := llmSvc.Annotations[constants.NodeGroupAnnotationKey]; ok {
|
|
if nodeGroup, ok := localModelNodeGroups[llmSvcNodeGroup]; ok {
|
|
if _, ok := namespaceToNodeGroups[llmSvc.Namespace]; !ok {
|
|
namespaceToNodeGroups[llmSvc.Namespace] = map[string]*v1alpha1.LocalModelNodeGroup{}
|
|
}
|
|
namespaceToNodeGroups[llmSvc.Namespace][nodeGroup.Name] = nodeGroup
|
|
} else {
|
|
log.Info("Didn't find llmisvc node group in model cache node groups", "llmisvc name", llmSvc.Name, "llmisvc node group", llmSvcNodeGroup, "model cache node groups", slices.Collect(maps.Keys(localModelNodeGroups)))
|
|
}
|
|
} else if _, ok := namespaceToNodeGroups[llmSvc.Namespace]; !ok {
|
|
log.Info("LLMIsvc does not have node group annotation", "llmisvc name", llmSvc.Name, "nodegroup annotation", constants.NodeGroupAnnotationKey)
|
|
namespaceToNodeGroups[llmSvc.Namespace] = map[string]*v1alpha1.LocalModelNodeGroup{defaultNodeGroup.Name: defaultNodeGroup}
|
|
} else {
|
|
namespaceToNodeGroups[llmSvc.Namespace][defaultNodeGroup.Name] = defaultNodeGroup
|
|
}
|
|
}
|
|
} else {
|
|
log.V(1).Info("LLMInferenceService CRD not installed; skipping LLMInferenceService reconcile path", "name", params.Name, "namespace", params.Namespace)
|
|
}
|
|
|
|
// Get the previous list of namespaces from status to detect removed ISVCs/LLMIsvcs
|
|
var previousNamespaces map[string]bool
|
|
if localModelCache != nil {
|
|
previousNamespaces = make(map[string]bool)
|
|
for _, isvc := range localModelCache.Status.InferenceServices {
|
|
previousNamespaces[isvc.Namespace] = true
|
|
}
|
|
for _, llmSvc := range localModelCache.Status.LLMInferenceServices {
|
|
previousNamespaces[llmSvc.Namespace] = true
|
|
}
|
|
} else if localModelNamespaceCache != nil {
|
|
previousNamespaces = make(map[string]bool)
|
|
for _, isvc := range localModelNamespaceCache.Status.InferenceServices {
|
|
previousNamespaces[isvc.Namespace] = true
|
|
}
|
|
for _, llmSvc := range localModelNamespaceCache.Status.LLMInferenceServices {
|
|
previousNamespaces[llmSvc.Namespace] = true
|
|
}
|
|
}
|
|
|
|
if localModelCache != nil {
|
|
localModelCache.Status.InferenceServices = isvcNames
|
|
localModelCache.Status.LLMInferenceServices = llmSvcNames
|
|
if err := c.Status().Update(ctx, localModelCache); err != nil {
|
|
log.Error(err, "cannot update status", "name", params.Name)
|
|
}
|
|
} else if localModelNamespaceCache != nil {
|
|
localModelNamespaceCache.Status.InferenceServices = isvcNames
|
|
localModelNamespaceCache.Status.LLMInferenceServices = llmSvcNames
|
|
if err := c.Status().Update(ctx, localModelNamespaceCache); err != nil {
|
|
log.Error(err, "cannot update status", "name", params.Name)
|
|
}
|
|
}
|
|
|
|
currentNamespaces := make(map[string]bool)
|
|
for namespace := range namespaceToNodeGroups {
|
|
currentNamespaces[namespace] = true
|
|
}
|
|
|
|
// Clean up serving PVs/PVCs for namespaces that no longer have ISVCs using this model
|
|
for namespace := range previousNamespaces {
|
|
if !currentNamespaces[namespace] {
|
|
for nodeGroupName := range localModelNodeGroups {
|
|
pvcName := params.Name + "-" + nodeGroupName
|
|
pvName := pvcName + "-" + namespace
|
|
|
|
if params.IsNamespaceScoped {
|
|
if err := DeletePV(ctx, clientset, log, pvName); err != nil {
|
|
log.Error(err, "failed to delete serving PV for removed ISVC namespace", "name", pvName)
|
|
}
|
|
}
|
|
|
|
if err := DeletePVC(ctx, clientset, log, pvcName, namespace); err != nil {
|
|
log.Error(err, "failed to delete serving PVC for removed ISVC namespace", "name", pvcName, "namespace", namespace)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create PVs and PVCs for each namespace
|
|
for namespace, nodeGroups := range namespaceToNodeGroups {
|
|
for nodeGroupName, nodeGroup := range nodeGroups {
|
|
pvcName := params.Name + "-" + nodeGroupName
|
|
pv := corev1.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: pvcName + "-" + namespace,
|
|
},
|
|
Spec: nodeGroup.Spec.PersistentVolumeSpec,
|
|
}
|
|
if err := CreatePV(ctx, clientset, scheme, log, pv, localModelCache, localModelNamespaceCache); err != nil {
|
|
log.Error(err, "Create PV err", "name", pv.Name)
|
|
}
|
|
|
|
pvc := corev1.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: pvcName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: nodeGroup.Spec.PersistentVolumeClaimSpec,
|
|
}
|
|
pvc.Spec.VolumeName = pv.Name
|
|
if err := CreatePVC(ctx, clientset, scheme, log, pvc, namespace, localModelCache, localModelNamespaceCache); err != nil {
|
|
log.Error(err, "Create PVC err", "name", pvc.Name)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateLocalModelNode updates or adds a model to a LocalModelNode
|
|
// Only one of localModelCache or localModelNamespaceCache should be non-nil
|
|
// nodeGroupName specifies which LocalModelNodeGroup this model belongs to
|
|
func UpdateLocalModelNode(
|
|
ctx context.Context,
|
|
c client.Client,
|
|
log logr.Logger,
|
|
localModelNode *v1alpha1.LocalModelNode,
|
|
localModelCache *v1alpha1.LocalModelCache,
|
|
localModelNamespaceCache *v1alpha1.LocalModelNamespaceCache,
|
|
nodeGroupName string,
|
|
) error {
|
|
params := ExtractLocalModelParams(localModelCache, localModelNamespaceCache)
|
|
newModelInfo := CreateLocalModelInfo(localModelCache, localModelNamespaceCache, nodeGroupName)
|
|
|
|
var patch client.Patch
|
|
updated := false
|
|
for i, modelInfo := range localModelNode.Spec.LocalModels {
|
|
if modelInfo.ModelName == params.Name && modelInfo.Namespace == params.Namespace {
|
|
needsUpdate := modelInfo.SourceModelUri != params.SourceModelUri ||
|
|
modelInfo.ServiceAccountName != params.ServiceAccountName ||
|
|
modelInfo.NodeGroup != nodeGroupName ||
|
|
!StorageSpecEqual(modelInfo.Storage, params.Storage)
|
|
if !needsUpdate {
|
|
return nil
|
|
}
|
|
log.Info("Updating localModelInfo", "node", localModelNode.Name, "model", params.Name, "namespace", params.Namespace)
|
|
updated = true
|
|
patch = client.MergeFrom(localModelNode.DeepCopy())
|
|
localModelNode.Spec.LocalModels[i] = newModelInfo
|
|
break
|
|
}
|
|
}
|
|
if !updated {
|
|
patch = client.MergeFrom(localModelNode.DeepCopy())
|
|
localModelNode.Spec.LocalModels = append(localModelNode.Spec.LocalModels, newModelInfo)
|
|
}
|
|
if err := c.Patch(ctx, localModelNode, patch); err != nil {
|
|
log.Error(err, "Update localmodelnode", "name", localModelNode.Name)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReconcileLocalModelNode creates/updates LocalModelNode for each node in the node groups
|
|
// Only one of localModelCache or localModelNamespaceCache should be non-nil
|
|
func ReconcileLocalModelNode(
|
|
ctx context.Context,
|
|
c client.Client,
|
|
log logr.Logger,
|
|
localModelCache *v1alpha1.LocalModelCache,
|
|
localModelNamespaceCache *v1alpha1.LocalModelNamespaceCache,
|
|
nodeGroups map[string]*v1alpha1.LocalModelNodeGroup,
|
|
) error {
|
|
params := ExtractLocalModelParams(localModelCache, localModelNamespaceCache)
|
|
|
|
var nodeStatus map[string]v1alpha1.NodeStatus
|
|
if localModelCache != nil {
|
|
if localModelCache.Status.NodeStatus == nil {
|
|
localModelCache.Status.NodeStatus = make(map[string]v1alpha1.NodeStatus)
|
|
}
|
|
nodeStatus = localModelCache.Status.NodeStatus
|
|
} else if localModelNamespaceCache != nil {
|
|
if localModelNamespaceCache.Status.NodeStatus == nil {
|
|
localModelNamespaceCache.Status.NodeStatus = make(map[string]v1alpha1.NodeStatus)
|
|
}
|
|
nodeStatus = localModelNamespaceCache.Status.NodeStatus
|
|
}
|
|
|
|
for nodeGroupName, nodeGroup := range nodeGroups {
|
|
modelInfo := CreateLocalModelInfo(localModelCache, localModelNamespaceCache, nodeGroupName)
|
|
statusKey := modelInfo.GetStatusKey()
|
|
readyNodes, notReadyNodes, err := GetNodesFromNodeGroup(ctx, nodeGroup, c)
|
|
if err != nil {
|
|
log.Error(err, "getNodesFromNodeGroup node error")
|
|
return err
|
|
}
|
|
|
|
for _, node := range notReadyNodes.Items {
|
|
if _, ok := nodeStatus[node.Name]; !ok {
|
|
nodeStatus[node.Name] = v1alpha1.NodeNotReady
|
|
}
|
|
}
|
|
|
|
for _, node := range readyNodes.Items {
|
|
localModelNode := &v1alpha1.LocalModelNode{}
|
|
err := c.Get(ctx, types.NamespacedName{Name: node.Name}, localModelNode)
|
|
found := true
|
|
if err != nil {
|
|
if apierr.IsNotFound(err) {
|
|
found = false
|
|
log.Info("localmodelNode not found")
|
|
} else {
|
|
log.Error(err, "Failed to get localmodelnode", "name", node.Name)
|
|
return err
|
|
}
|
|
}
|
|
if !found {
|
|
localModelNode = &v1alpha1.LocalModelNode{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: node.Name,
|
|
},
|
|
Spec: v1alpha1.LocalModelNodeSpec{LocalModels: []v1alpha1.LocalModelInfo{modelInfo}},
|
|
}
|
|
if err := c.Create(ctx, localModelNode); err != nil {
|
|
log.Error(err, "Create localmodelnode", "name", node.Name)
|
|
return err
|
|
}
|
|
} else {
|
|
if err := UpdateLocalModelNode(ctx, c, log, localModelNode, localModelCache, localModelNamespaceCache, nodeGroupName); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
modelStatus := localModelNode.Status.ModelStatus[statusKey]
|
|
nodeStatus[node.Name] = NodeStatusFromLocalModelStatus(modelStatus)
|
|
}
|
|
|
|
successfulNodes := 0
|
|
failedNodes := 0
|
|
for _, status := range nodeStatus {
|
|
switch status {
|
|
case v1alpha1.NodeDownloaded:
|
|
successfulNodes += 1
|
|
case v1alpha1.NodeDownloadError:
|
|
failedNodes += 1
|
|
}
|
|
}
|
|
|
|
modelCopies := &v1alpha1.ModelCopies{Total: len(nodeStatus), Available: successfulNodes, Failed: failedNodes}
|
|
if localModelCache != nil {
|
|
localModelCache.Status.ModelCopies = modelCopies
|
|
if err := c.Status().Update(ctx, localModelCache); err != nil {
|
|
log.Error(err, "cannot update model status from node", "name", params.Name)
|
|
}
|
|
} else if localModelNamespaceCache != nil {
|
|
localModelNamespaceCache.Status.ModelCopies = modelCopies
|
|
if err := c.Status().Update(ctx, localModelNamespaceCache); err != nil {
|
|
log.Error(err, "cannot update model status from node", "name", params.Name, "namespace", params.Namespace)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|