mirror of
https://github.com/hanzo-ml/kserve.git
synced 2026-10-30 20:53:49 +00:00
384 lines
15 KiB
Go
384 lines
15 KiB
Go
/*
|
|
Copyright 2022 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.
|
|
*/
|
|
|
|
// +kubebuilder:rbac:groups=serving.kserve.io,resources=inferencegraphs;inferencegraphs/finalizers,verbs=get;list;watch;create;update;patch;delete
|
|
// +kubebuilder:rbac:groups=serving.kserve.io,resources=inferencegraphs/status,verbs=get;update;patch
|
|
// +kubebuilder:rbac:groups=serving.knative.dev,resources=services,verbs=get;list;watch;create;update;patch;delete
|
|
// +kubebuilder:rbac:groups=serving.knative.dev,resources=services/finalizers,verbs=get;list;watch;create;update;patch;delete
|
|
// +kubebuilder:rbac:groups=serving.knative.dev,resources=services/status,verbs=get;update;patch
|
|
package inferencegraph
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/go-logr/logr"
|
|
"github.com/pkg/errors"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/equality"
|
|
apierr "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/rest"
|
|
"k8s.io/client-go/tools/record"
|
|
"knative.dev/pkg/apis"
|
|
knservingv1 "knative.dev/serving/pkg/apis/serving/v1"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
|
|
"github.com/kserve/kserve/pkg/apis/serving/v1alpha1"
|
|
"github.com/kserve/kserve/pkg/apis/serving/v1beta1"
|
|
"github.com/kserve/kserve/pkg/constants"
|
|
knutils "github.com/kserve/kserve/pkg/controller/v1alpha1/utils"
|
|
isvcutils "github.com/kserve/kserve/pkg/controller/v1beta1/inferenceservice/utils"
|
|
"github.com/kserve/kserve/pkg/utils"
|
|
)
|
|
|
|
// InferenceGraphReconciler reconciles a InferenceGraph object
|
|
type InferenceGraphReconciler struct {
|
|
client.Client
|
|
ClientConfig *rest.Config
|
|
Clientset kubernetes.Interface
|
|
Log logr.Logger
|
|
Scheme *runtime.Scheme
|
|
Recorder record.EventRecorder
|
|
}
|
|
|
|
// InferenceGraphState describes the Readiness of the InferenceGraph
|
|
type InferenceGraphState string
|
|
|
|
const (
|
|
InferenceGraphNotReadyState InferenceGraphState = "InferenceGraphNotReady"
|
|
InferenceGraphReadyState InferenceGraphState = "InferenceGraphReady"
|
|
)
|
|
|
|
type RouterConfig struct {
|
|
Image string `json:"image"`
|
|
CpuRequest string `json:"cpuRequest"`
|
|
CpuLimit string `json:"cpuLimit"`
|
|
MemoryRequest string `json:"memoryRequest"`
|
|
MemoryLimit string `json:"memoryLimit"`
|
|
/*
|
|
Example of how to add headers in router config:
|
|
headers: {
|
|
"propagate": [
|
|
"Custom-Header1",
|
|
"Custom-Header2"
|
|
]
|
|
}
|
|
Note: Making Headers, a map of strings, gives the flexibility to extend it in the future to support adding more
|
|
operations on headers. For example: Similar to "propagate" operation, one can add "transform" operation if they
|
|
want to transform headers keys or values before passing down to nodes.
|
|
*/
|
|
Headers map[string][]string `json:"headers"`
|
|
ImagePullPolicy string `json:"imagePullPolicy"`
|
|
ImagePullSecrets []string `json:"imagePullSecrets"`
|
|
}
|
|
|
|
func (rc *RouterConfig) GetImagePullSecrets() []corev1.LocalObjectReference {
|
|
imagePullSecrets := make([]corev1.LocalObjectReference, 0, len(rc.ImagePullSecrets))
|
|
for _, secret := range rc.ImagePullSecrets {
|
|
imagePullSecrets = append(imagePullSecrets, corev1.LocalObjectReference{Name: secret})
|
|
}
|
|
return imagePullSecrets
|
|
}
|
|
|
|
func getRouterConfigs(configMap *corev1.ConfigMap) (*RouterConfig, error) {
|
|
routerConfig := &RouterConfig{}
|
|
if agentConfigValue, ok := configMap.Data["router"]; ok {
|
|
err := json.Unmarshal([]byte(agentConfigValue), &routerConfig)
|
|
if err != nil {
|
|
panic(fmt.Errorf("Unable to unmarshall agent json string due to %w ", err))
|
|
}
|
|
}
|
|
|
|
// Ensure that we set proper values for CPU/Memory Limit/Request
|
|
resourceDefaults := []string{
|
|
routerConfig.MemoryRequest,
|
|
routerConfig.MemoryLimit,
|
|
routerConfig.CpuRequest,
|
|
routerConfig.CpuLimit,
|
|
}
|
|
for _, key := range resourceDefaults {
|
|
_, err := resource.ParseQuantity(key)
|
|
if err != nil {
|
|
return routerConfig, fmt.Errorf("Failed to parse resource configuration for router: %q",
|
|
err.Error())
|
|
}
|
|
}
|
|
|
|
return routerConfig, nil
|
|
}
|
|
|
|
func (r *InferenceGraphReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
|
// Fetch the InferenceService instance
|
|
graph := &v1alpha1.InferenceGraph{}
|
|
if err := r.Get(ctx, req.NamespacedName, graph); err != nil {
|
|
if apierr.IsNotFound(err) {
|
|
// Object not found, return. Created objects are automatically garbage collected.
|
|
// For additional cleanup logic use finalizers.
|
|
return reconcile.Result{}, nil
|
|
}
|
|
return reconcile.Result{}, err
|
|
}
|
|
|
|
r.Log.Info("Reconciling inference graph", "apiVersion", graph.APIVersion, "graph", graph.Name)
|
|
|
|
forceStopRuntime := utils.GetForceStopRuntime(graph)
|
|
|
|
configMap, err := r.Clientset.CoreV1().ConfigMaps(constants.KServeNamespace).Get(ctx, constants.InferenceServiceConfigMapName, metav1.GetOptions{})
|
|
if err != nil {
|
|
r.Log.Error(err, "Failed to find config map", "name", constants.InferenceServiceConfigMapName)
|
|
return reconcile.Result{}, err
|
|
}
|
|
routerConfig, err := getRouterConfigs(configMap)
|
|
if err != nil {
|
|
return reconcile.Result{}, err
|
|
}
|
|
// resolve service urls
|
|
if !forceStopRuntime {
|
|
for node, router := range graph.Spec.Nodes {
|
|
for i, route := range router.Steps {
|
|
isvc := v1beta1.InferenceService{}
|
|
if route.ServiceName == "" {
|
|
continue
|
|
}
|
|
err := r.Get(ctx, types.NamespacedName{Namespace: graph.Namespace, Name: route.ServiceName}, &isvc)
|
|
if err == nil {
|
|
if graph.Spec.Nodes[node].Steps[i].ServiceURL == "" {
|
|
serviceUrl, err := isvcutils.GetPredictorEndpoint(ctx, r.Client, &isvc)
|
|
if err == nil {
|
|
graph.Spec.Nodes[node].Steps[i].ServiceURL = serviceUrl
|
|
} else {
|
|
r.Log.Info("inference service is not ready", "name", route.ServiceName)
|
|
return reconcile.Result{Requeue: true}, errors.Wrapf(err, "service %s is not ready", route.ServiceName)
|
|
}
|
|
}
|
|
} else {
|
|
r.Log.Info("inference service is not found", "name", route.ServiceName)
|
|
return reconcile.Result{Requeue: true}, errors.Wrapf(err, "Failed to find graph service %s", route.ServiceName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
isvcConfigMap, err := v1beta1.GetInferenceServiceConfigMap(ctx, r.Clientset)
|
|
if err != nil {
|
|
r.Log.Error(err, "unable to get configmap", "name", constants.InferenceServiceConfigMapName, "namespace", constants.KServeNamespace)
|
|
return reconcile.Result{}, err
|
|
}
|
|
deployConfig, err := v1beta1.NewDeployConfig(isvcConfigMap)
|
|
if err != nil {
|
|
return reconcile.Result{}, errors.Wrapf(err, "fails to create DeployConfig")
|
|
}
|
|
|
|
deploymentMode := isvcutils.GetDeploymentMode(graph.Status.DeploymentMode, graph.Annotations, deployConfig)
|
|
r.Log.Info("Inference graph deployment ", "deployment mode ", deploymentMode)
|
|
if deploymentMode == constants.Standard {
|
|
// Create inference graph resources such as deployment, service, hpa in raw deployment mode
|
|
deployment, url, err := handleInferenceGraphRawDeployment(ctx, r.Client, r.Clientset, r.Scheme, graph, routerConfig)
|
|
if err != nil {
|
|
return ctrl.Result{}, errors.Wrapf(err, "fails to reconcile inference graph raw deployment")
|
|
}
|
|
|
|
r.Log.Info("Inference graph raw", "deployment conditions", deployment.Status.Conditions)
|
|
if !forceStopRuntime {
|
|
// Check if the deployment is ready. If not, requeue
|
|
igAvailable := false
|
|
for _, con := range deployment.Status.Conditions {
|
|
if con.Type == appsv1.DeploymentAvailable {
|
|
igAvailable = true
|
|
break
|
|
}
|
|
}
|
|
if !igAvailable {
|
|
// If Deployment resource not yet available, IG is not available as well. Reconcile again.
|
|
return reconcile.Result{Requeue: true}, errors.Wrapf(err,
|
|
"Failed to find inference graph deployment %s", graph.Name)
|
|
}
|
|
}
|
|
|
|
logger.Info("Inference graph raw before propagate status")
|
|
PropagateRawStatus(&graph.Status, deployment, url)
|
|
} else {
|
|
// Abort if Knative Services are not available
|
|
ksvcAvailable, checkKsvcErr := utils.IsCrdAvailable(r.ClientConfig, knservingv1.SchemeGroupVersion.String(), constants.KnativeServiceKind)
|
|
if checkKsvcErr != nil {
|
|
return reconcile.Result{}, checkKsvcErr
|
|
}
|
|
|
|
if !ksvcAvailable {
|
|
r.Recorder.Event(graph, corev1.EventTypeWarning, "ServerlessModeRejected",
|
|
"It is not possible to use Knative deployment mode when Knative Services are not available")
|
|
return reconcile.Result{Requeue: false}, reconcile.TerminalError(fmt.Errorf("the resolved deployment mode of InferenceGraph '%s' is Knative, but Knative Serving is not available", graph.Name))
|
|
}
|
|
|
|
// Retrieve the allow-zero-initial-scale value from the knative autoscaler configuration.
|
|
allowZeroInitialScale, err := knutils.CheckZeroInitialScaleAllowed(ctx, r.Clientset)
|
|
if err != nil {
|
|
return ctrl.Result{}, errors.Wrapf(err, "failed to retrieve the knative autoscaler configuration")
|
|
}
|
|
|
|
knutils.ValidateInitialScaleAnnotation(graph.Annotations, allowZeroInitialScale, r.Log)
|
|
|
|
desired := createKnativeService(graph.ObjectMeta, graph, routerConfig)
|
|
|
|
err = controllerutil.SetControllerReference(graph, desired, r.Scheme)
|
|
if err != nil {
|
|
return reconcile.Result{}, err
|
|
}
|
|
knativeReconciler := NewGraphKnativeServiceReconciler(r.Client, r.Scheme, desired)
|
|
ksvcStatus, err := knativeReconciler.Reconcile(ctx)
|
|
if err != nil {
|
|
r.Log.Error(err, "failed to reconcile inference graph ksvc", "name", graph.GetName())
|
|
return reconcile.Result{}, errors.Wrapf(err, "fails to reconcile inference graph ksvc")
|
|
}
|
|
|
|
r.Log.Info("updating inference graph status", "status", ksvcStatus)
|
|
graph.Status.Conditions = ksvcStatus.Conditions
|
|
// @TODO Need to check the status of all the graph components, find the inference services from all the nodes and collect the status
|
|
for _, con := range ksvcStatus.Conditions {
|
|
if con.Type == apis.ConditionReady {
|
|
if con.Status == "True" {
|
|
graph.Status.URL = ksvcStatus.URL
|
|
} else {
|
|
graph.Status.URL = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle InferenceGraph status updates based on the force stop annotation.
|
|
// If true, transition the service to a stopped and unready state; otherwise, ensure it's not marked as stopped.
|
|
transition_time := apis.VolatileTime{Inner: metav1.Now()}
|
|
existingStoppedCondition := graph.Status.GetCondition(v1beta1.Stopped)
|
|
if existingStoppedCondition == nil {
|
|
defaultStoppedCondition := apis.Condition{
|
|
LastTransitionTime: transition_time,
|
|
Type: v1beta1.Stopped,
|
|
Status: corev1.ConditionFalse,
|
|
}
|
|
graph.Status.Conditions = append(graph.Status.Conditions, defaultStoppedCondition)
|
|
existingStoppedCondition = &defaultStoppedCondition
|
|
}
|
|
if forceStopRuntime {
|
|
// If the graph's stopped condition is not set or
|
|
// If the graph is currently running, update its status to signal that it should be stopped
|
|
if existingStoppedCondition.Status == corev1.ConditionFalse {
|
|
// Add the stopped condition
|
|
stoppedCondition := apis.Condition{
|
|
LastTransitionTime: transition_time,
|
|
Type: v1beta1.Stopped,
|
|
Status: corev1.ConditionTrue,
|
|
}
|
|
readyCondition := apis.Condition{
|
|
LastTransitionTime: transition_time,
|
|
Type: apis.ConditionReady,
|
|
Status: corev1.ConditionFalse,
|
|
Reason: v1beta1.StoppedISVCReason,
|
|
}
|
|
graph.Status.Conditions = []apis.Condition{stoppedCondition, readyCondition}
|
|
|
|
graph.Status.URL = nil
|
|
}
|
|
} else {
|
|
// If the graph's stopped condition is not set or
|
|
// If the graph is currently stopped, update its status to signal that it should resume
|
|
if existingStoppedCondition.Status == corev1.ConditionTrue {
|
|
resumeCondition := apis.Condition{
|
|
LastTransitionTime: transition_time,
|
|
Type: v1beta1.Stopped,
|
|
Status: corev1.ConditionFalse,
|
|
}
|
|
graph.Status.Conditions = append(graph.Status.Conditions, resumeCondition)
|
|
}
|
|
}
|
|
|
|
if err := r.updateStatus(ctx, graph); err != nil {
|
|
r.Recorder.Eventf(graph, corev1.EventTypeWarning, "InternalError", err.Error())
|
|
return reconcile.Result{}, err
|
|
}
|
|
|
|
return ctrl.Result{}, nil
|
|
}
|
|
|
|
func (r *InferenceGraphReconciler) updateStatus(ctx context.Context, desiredGraph *v1alpha1.InferenceGraph) error {
|
|
graph := &v1alpha1.InferenceGraph{}
|
|
namespacedName := types.NamespacedName{Name: desiredGraph.Name, Namespace: desiredGraph.Namespace}
|
|
if err := r.Get(ctx, namespacedName, graph); err != nil {
|
|
return client.IgnoreNotFound(err)
|
|
}
|
|
|
|
wasReady := inferenceGraphReadiness(graph.Status)
|
|
if equality.Semantic.DeepEqual(graph.Status, desiredGraph.Status) {
|
|
// If we didn't change anything then don't call updateStatus.
|
|
// This is important because the copy we loaded from the informer's
|
|
// cache may be stale and we don't want to overwrite a prior update
|
|
// to status with this stale state.
|
|
} else if err := r.Status().Update(ctx, desiredGraph); err != nil {
|
|
r.Log.Error(err, "Failed to update InferenceGraph status", "InferenceGraph", desiredGraph.Name)
|
|
r.Recorder.Eventf(desiredGraph, corev1.EventTypeWarning, "UpdateFailed",
|
|
"Failed to update status for InferenceGraph %q: %v", desiredGraph.Name, err)
|
|
return errors.Wrapf(err, "fails to update InferenceGraph status")
|
|
} else {
|
|
r.Log.Info("updated InferenceGraph status", "InferenceGraph", desiredGraph.Name)
|
|
// If there was a difference and there was no error.
|
|
isReady := inferenceGraphReadiness(desiredGraph.Status)
|
|
if wasReady && !isReady { // Moved to NotReady State
|
|
r.Recorder.Eventf(desiredGraph, corev1.EventTypeWarning, string(InferenceGraphNotReadyState),
|
|
fmt.Sprintf("InferenceGraph [%v] is no longer Ready", desiredGraph.GetName()))
|
|
} else if !wasReady && isReady { // Moved to Ready State
|
|
r.Recorder.Eventf(desiredGraph, corev1.EventTypeNormal, string(InferenceGraphReadyState),
|
|
fmt.Sprintf("InferenceGraph [%v] is Ready", desiredGraph.GetName()))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func inferenceGraphReadiness(status v1alpha1.InferenceGraphStatus) bool {
|
|
return status.Conditions != nil &&
|
|
status.GetCondition(apis.ConditionReady) != nil &&
|
|
status.GetCondition(apis.ConditionReady).Status == corev1.ConditionTrue
|
|
}
|
|
|
|
func (r *InferenceGraphReconciler) SetupWithManager(mgr ctrl.Manager, deployConfig *v1beta1.DeployConfig) error {
|
|
r.ClientConfig = mgr.GetConfig()
|
|
|
|
ksvcFound, err := utils.IsCrdAvailable(r.ClientConfig, knservingv1.SchemeGroupVersion.String(), constants.KnativeServiceKind)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctrlBuilder := ctrl.NewControllerManagedBy(mgr).
|
|
For(&v1alpha1.InferenceGraph{}).
|
|
Owns(&appsv1.Deployment{})
|
|
|
|
if ksvcFound {
|
|
ctrlBuilder = ctrlBuilder.Owns(&knservingv1.Service{})
|
|
} else {
|
|
r.Log.Info("The InferenceGraph controller won't watch serving.knative.dev/v1/Service resources because the CRD is not available.")
|
|
}
|
|
|
|
return ctrlBuilder.Complete(r)
|
|
}
|