mirror of
https://github.com/hanzo-ml/kserve.git
synced 2026-10-30 20:53:49 +00:00
241 lines
10 KiB
Go
241 lines
10 KiB
Go
/*
|
|
Copyright 2025 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"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
k8stypes "k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
"github.com/kserve/kserve/pkg/apis/serving/v1beta1"
|
|
"github.com/kserve/kserve/pkg/constants"
|
|
"github.com/kserve/kserve/pkg/credentials"
|
|
"github.com/kserve/kserve/pkg/types"
|
|
)
|
|
|
|
const (
|
|
// schedulerConfigMapKey is the key in the inferenceservice-config ConfigMap
|
|
// that holds scheduler-specific configuration (annotation keys, etc.).
|
|
schedulerConfigMapKey = "scheduler"
|
|
)
|
|
|
|
// DefaultExpirationAnnotations is the default list of annotation keys
|
|
// checked for certificate secret expiration. The first entry is the write key.
|
|
var DefaultExpirationAnnotations = []string{"certificates.kserve.io/expiration"}
|
|
|
|
// SchedulerConfig holds configurable settings for the scheduler component,
|
|
// parsed from the "scheduler" key in the inferenceservice-config ConfigMap.
|
|
//
|
|
// Certificate annotation keys control the self-signed TLS certificate lifecycle:
|
|
//
|
|
// - ExpirationAnnotations: first entry is the write key (set on new secrets),
|
|
// all entries are read keys (checked for expiration, first match wins).
|
|
//
|
|
// Defaults (upstream - no ConfigMap override needed, can default to values shown below):
|
|
//
|
|
// {"expirationAnnotations": ["certificates.kserve.io/expiration"]}
|
|
//
|
|
// Override example (reads both old and new keys during upgrade):
|
|
//
|
|
// {"expirationAnnotations": ["certificates.kserve.io/expiration-v2", "certificates.kserve.io/expiration"]}
|
|
//
|
|
// During a rolling upgrade, existing secrets carrying the old annotation key are
|
|
// recognized via the read list; no unnecessary cert regeneration or secret updates.
|
|
// A one-time scheduler restart on upgrade is expected (Recreate strategy).
|
|
type SchedulerConfig struct {
|
|
// ExpirationAnnotations is the ordered list of annotation keys checked when
|
|
// determining whether a certificate secret has expired. The first entry is the
|
|
// write key (set on newly created secrets); all entries are read keys.
|
|
ExpirationAnnotations []string `json:"expirationAnnotations,omitempty"`
|
|
}
|
|
|
|
// NewSchedulerConfig parses the "scheduler" key from the inferenceservice-config
|
|
// ConfigMap, applying defaults for any missing fields.
|
|
func NewSchedulerConfig(isvcConfigMap *corev1.ConfigMap) (*SchedulerConfig, error) {
|
|
cfg := &SchedulerConfig{}
|
|
if raw, ok := isvcConfigMap.Data[schedulerConfigMapKey]; ok {
|
|
if err := json.Unmarshal([]byte(raw), cfg); err != nil {
|
|
return nil, fmt.Errorf("unable to parse scheduler config json: %w", err)
|
|
}
|
|
}
|
|
|
|
if len(cfg.ExpirationAnnotations) == 0 {
|
|
cfg.ExpirationAnnotations = DefaultExpirationAnnotations
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// Config holds configuration needed for LLM inference services.
|
|
// It aggregates ingress, storage, credential, and autoscaling settings from the KServe configmap.
|
|
type Config struct {
|
|
SystemNamespace string `json:"systemNamespace,omitempty"`
|
|
IngressGatewayName string `json:"ingressGatewayName,omitempty"`
|
|
IngressGatewayNamespace string `json:"ingressGatewayNamespace,omitempty"`
|
|
UrlScheme string `json:"urlScheme,omitempty"`
|
|
EnableTLS bool `json:"enableTLS,omitempty"`
|
|
|
|
ModelBasedRoutingHeaderName string `json:"modelBasedRoutingHeaderName,omitempty"`
|
|
ModelBasedRoutingMode ModelBasedRoutingMode `json:"modelBasedRoutingMode,omitempty"`
|
|
|
|
// WVAAutoscalingConfig holds Prometheus and monitoring settings for WVA autoscaling.
|
|
// nil when the "autoscaling-wva-controller-config" key is not present in inferenceservice-config.
|
|
WVAAutoscalingConfig *WVAAutoscalingConfig `json:"-"`
|
|
|
|
// Storage and credential configs are excluded from JSON serialization
|
|
// as they contain sensitive information
|
|
StorageConfig *types.StorageInitializerConfig `json:"-"`
|
|
CredentialConfig *credentials.CredentialConfig `json:"-"`
|
|
SchedulerConfig *SchedulerConfig `json:"-"`
|
|
|
|
// ResolvedLoRAAdapters holds the resolved LoRA adapter list derived from the final merged spec.
|
|
// Populated inside combineBaseRefsConfig (after all spec overlays and variable substitution)
|
|
// so that resolution is tied to the config-merge step and all downstream workload functions
|
|
// share a single consistent result.
|
|
ResolvedLoRAAdapters []resolvedLoRAAdapter `json:"-"`
|
|
}
|
|
|
|
// PrometheusConfig holds Prometheus connection and authentication settings used by KEDA
|
|
// to query the wva_desired_replicas metric.
|
|
type PrometheusConfig struct {
|
|
// URL is the URL of the Prometheus server (used by KEDA to query wva_desired_replicas).
|
|
URL string `json:"url"`
|
|
// TLSInsecureSkipVerify disables TLS certificate verification for the Prometheus connection.
|
|
TLSInsecureSkipVerify bool `json:"tlsInsecureSkipVerify"`
|
|
// AuthModes is the KEDA authModes value for the Prometheus trigger
|
|
// (e.g. "bearer", "basic", "tls"). Empty means no authentication.
|
|
// See: https://keda.sh/docs/latest/scalers/prometheus/#authentication-parameters
|
|
// +optional
|
|
AuthModes string `json:"authModes,omitempty"`
|
|
// TriggerAuthName is the name of a pre-existing TriggerAuthentication or
|
|
// ClusterTriggerAuthentication CR that KEDA should use when querying Prometheus.
|
|
// The CR must be created by the cluster admin before enabling KEDA autoscaling.
|
|
// +optional
|
|
TriggerAuthName string `json:"triggerAuthName,omitempty"`
|
|
// TriggerAuthKind specifies the kind of the authentication CR referenced by
|
|
// TriggerAuthName. Accepted values are "TriggerAuthentication" (namespaced)
|
|
// and "ClusterTriggerAuthentication" (cluster-scoped). Defaults to "TriggerAuthentication"
|
|
// when empty. ClusterTriggerAuthentication is recommended for multi-namespace deployments.
|
|
// +optional
|
|
TriggerAuthKind string `json:"triggerAuthKind,omitempty"`
|
|
}
|
|
|
|
// WVAAutoscalingConfig holds cluster-wide WVA autoscaling settings loaded from the
|
|
// "autoscaling-wva-controller-config" key in the inferenceservice-config ConfigMap.
|
|
// These are shared across all LLMISVC instances.
|
|
type WVAAutoscalingConfig struct {
|
|
// Prometheus holds Prometheus connection and authentication settings.
|
|
Prometheus PrometheusConfig `json:"prometheus"`
|
|
}
|
|
|
|
// autoscalingConfigName is the key in the inferenceservice-config ConfigMap
|
|
// that holds WVA-specific autoscaling controller configuration.
|
|
const autoscalingConfigName = "autoscaling-wva-controller-config"
|
|
|
|
// NewConfig creates an instance of llm-specific config based on predefined values
|
|
// in IngressConfig struct
|
|
func NewConfig(ingressConfig *v1beta1.IngressConfig, storageConfig *types.StorageInitializerConfig, credentialConfig *credentials.CredentialConfig, schedulerConfig *SchedulerConfig) *Config {
|
|
igwNs := constants.KServeNamespace
|
|
igwName := ingressConfig.KserveIngressGateway
|
|
// Parse gateway name to extract namespace and name components
|
|
// Format can be either "gateway-name" or "namespace/gateway-name"
|
|
igw := strings.Split(igwName, "/")
|
|
if len(igw) == 2 {
|
|
igwNs = igw[0]
|
|
igwName = igw[1]
|
|
}
|
|
|
|
return &Config{
|
|
SystemNamespace: constants.KServeNamespace,
|
|
IngressGatewayNamespace: igwNs,
|
|
IngressGatewayName: igwName,
|
|
UrlScheme: ingressConfig.UrlScheme,
|
|
EnableTLS: ingressConfig.EnableLLMInferenceServiceTLS,
|
|
ModelBasedRoutingHeaderName: ingressConfig.ModelBasedRoutingHeaderName,
|
|
ModelBasedRoutingMode: parseModelBasedRoutingMode(ingressConfig.ModelBasedRoutingMode),
|
|
StorageConfig: storageConfig,
|
|
CredentialConfig: credentialConfig,
|
|
SchedulerConfig: schedulerConfig,
|
|
}
|
|
}
|
|
|
|
// LoadConfig loads configuration from the supplied Kubernetes object reader.
|
|
func LoadConfig(ctx context.Context, reader client.Reader) (*Config, error) {
|
|
isvcConfigMap := &corev1.ConfigMap{}
|
|
if err := reader.Get(ctx, k8stypes.NamespacedName{
|
|
Namespace: constants.KServeNamespace,
|
|
Name: constants.InferenceServiceConfigMapName,
|
|
}, isvcConfigMap); err != nil {
|
|
return nil, fmt.Errorf("failed to load InferenceServiceConfigMap: %w", err)
|
|
}
|
|
|
|
return toConfig(isvcConfigMap)
|
|
}
|
|
|
|
func (r *LLMISVCReconciler) loadConfig(ctx context.Context) (*Config, error) {
|
|
configMap, fallbackErr := Get(
|
|
ctx,
|
|
r.Client,
|
|
client.ObjectKey{Namespace: constants.KServeNamespace, Name: constants.InferenceServiceConfigMapName},
|
|
&corev1.ConfigMap{},
|
|
WithGetFallbackAPIServerConfigMap(r.Clientset),
|
|
)
|
|
if fallbackErr != nil {
|
|
return nil, fmt.Errorf("failed to load InferenceServiceConfigMap: %w", fallbackErr)
|
|
}
|
|
return toConfig(configMap)
|
|
}
|
|
|
|
func toConfig(isvcConfigMap *corev1.ConfigMap) (*Config, error) {
|
|
ingressConfig, errConvert := v1beta1.NewIngressConfig(isvcConfigMap)
|
|
if errConvert != nil {
|
|
return nil, fmt.Errorf("failed to convert InferenceServiceConfigMap to IngressConfig: %w", errConvert)
|
|
}
|
|
|
|
storageInitializerConfig, errConvert := v1beta1.GetStorageInitializerConfigs(isvcConfigMap)
|
|
if errConvert != nil {
|
|
return nil, fmt.Errorf("failed to convert InferenceServiceConfigMap to StorageInitializerConfig: %w", errConvert)
|
|
}
|
|
|
|
credentialConfig, errConvert := credentials.GetCredentialConfig(isvcConfigMap)
|
|
if errConvert != nil {
|
|
return nil, fmt.Errorf("failed to convert InferenceServiceConfigMap to CredentialConfig: %w", errConvert)
|
|
}
|
|
|
|
schedulerConfig, errConvert := NewSchedulerConfig(isvcConfigMap)
|
|
if errConvert != nil {
|
|
return nil, fmt.Errorf("failed to parse scheduler config: %w", errConvert)
|
|
}
|
|
|
|
config := NewConfig(ingressConfig, storageInitializerConfig, &credentialConfig, schedulerConfig)
|
|
|
|
if autoscalingData, ok := isvcConfigMap.Data[autoscalingConfigName]; ok {
|
|
asCfg := &WVAAutoscalingConfig{}
|
|
if err := json.Unmarshal([]byte(autoscalingData), asCfg); err != nil {
|
|
return nil, fmt.Errorf("failed to parse %s config json: %w", autoscalingConfigName, err)
|
|
}
|
|
config.WVAAutoscalingConfig = asCfg
|
|
}
|
|
|
|
return config, nil
|
|
}
|