diff --git a/.changes/unreleased/added-20261020-223519.yaml b/.changes/unreleased/added-20261020-223519.yaml new file mode 100644 index 0000000..9ca580a --- /dev/null +++ b/.changes/unreleased/added-20261020-223519.yaml @@ -0,0 +1,3 @@ +kind: Added +body: |- + Add horizontal autoscaling configuration for admission service diff --git a/config/crd/kai.scheduler_configs.yaml b/config/crd/kai.scheduler_configs.yaml index 5c26062..70221bb 100644 --- a/config/crd/kai.scheduler_configs.yaml +++ b/config/crd/kai.scheduler_configs.yaml @@ -48,6 +48,36 @@ spec: admission: description: Admission holds KAI admission webhooks properties: + autoscaling: + description: Autoscaling configures horizontal autoscaling for + the admission service + properties: + enabled: + description: Enabled enables horizontal autoscaling for the + admission service. + type: boolean + maxReplicas: + description: MaxReplicas specifies the maximum number of admission + replicas. + format: int32 + minimum: 1 + type: integer + targetCPUUtilizationPercentage: + description: |- + TargetCPUUtilizationPercentage specifies the target average CPU utilization + across admission pods. + format: int32 + maximum: 100 + minimum: 1 + type: integer + targetInFlightRequests: + description: |- + TargetInFlightRequests specifies the target average number of in-flight + webhook requests per admission pod. + format: int32 + minimum: 1 + type: integer + type: object blockNvidiaVisibleDevices: description: |- BlockNvidiaVisibleDevices prevents pods from overriding the NVIDIA_VISIBLE_DEVICES diff --git a/kai/v1/admission/admission.go b/kai/v1/admission/admission.go index b67a5eb..c025b6a 100644 --- a/kai/v1/admission/admission.go +++ b/kai/v1/admission/admission.go @@ -36,6 +36,10 @@ type Admission struct { // +kubebuilder:validation:Optional Replicas *int32 `json:"replicas,omitempty"` + // Autoscaling configures horizontal autoscaling for the admission service + // +kubebuilder:validation:Optional + Autoscaling *Autoscaling `json:"autoscaling,omitempty"` + // GPUSharing enables GPU sharing functionality for the admission service // +kubebuilder:validation:Optional GPUSharing *bool `json:"gpuSharing,omitempty"` @@ -80,6 +84,35 @@ type Admission struct { InPlacePodResize *InPlacePodResize `json:"inPlacePodResize,omitempty"` } +// Autoscaling defines horizontal autoscaling configuration for the admission service. +type Autoscaling struct { + // Enabled enables horizontal autoscaling for the admission service. + // +kubebuilder:validation:Optional + Enabled *bool `json:"enabled,omitempty"` + + // MaxReplicas specifies the maximum number of admission replicas. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Minimum=1 + MaxReplicas *int32 `json:"maxReplicas,omitempty"` + + // TargetInFlightRequests specifies the target average number of in-flight + // webhook requests per admission pod. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Minimum=1 + TargetInFlightRequests *int32 `json:"targetInFlightRequests,omitempty"` + + // TargetCPUUtilizationPercentage specifies the target average CPU utilization + // across admission pods. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=100 + TargetCPUUtilizationPercentage *int32 `json:"targetCPUUtilizationPercentage,omitempty"` +} + +func (a *Autoscaling) SetDefaultsWhereNeeded() { + a.Enabled = common.SetDefault(a.Enabled, ptr.To(false)) +} + // InPlacePodResize configures KEP-1287 in-place pod resize admission behaviour. type InPlacePodResize struct { // ValidateQuota enables best-effort hierarchical queue quota checks on @@ -110,6 +143,10 @@ func (b *Admission) SetDefaultsWhereNeeded(replicaCount *int32, globalVPA *commo b.Webhook.SetDefaultsWhereNeeded() b.Replicas = common.SetDefault(b.Replicas, ptr.To(ptr.Deref(replicaCount, 1))) + if b.Autoscaling != nil { + b.Autoscaling.SetDefaultsWhereNeeded() + } + b.GPUSharing = common.SetDefault(b.GPUSharing, ptr.To(gpuSharingEnabledForMode(gpuSharingMode))) b.QueueLabelSelector = common.SetDefault(b.QueueLabelSelector, ptr.To(false)) b.BlockNvidiaVisibleDevices = common.SetDefault(b.BlockNvidiaVisibleDevices, ptr.To(false)) diff --git a/kai/v1/admission/admission_test.go b/kai/v1/admission/admission_test.go index c67d7e2..1f837d8 100644 --- a/kai/v1/admission/admission_test.go +++ b/kai/v1/admission/admission_test.go @@ -56,4 +56,42 @@ var _ = Describe("Admission", func() { Admission.SetDefaultsWhereNeeded(nil, nil, common.GpuSharingModeHamiCore) Expect(*Admission.GPUSharing).To(BeFalse()) }) + It("does not configure autoscaling by default", func(ctx context.Context) { + admission := &Admission{} + + admission.SetDefaultsWhereNeeded(nil, nil, common.GpuSharingModeNonMemoryEnforced) + + Expect(admission.Autoscaling).To(BeNil()) + }) + + It("sets autoscaling defaults when configured", func(ctx context.Context) { + admission := &Admission{ + Autoscaling: &Autoscaling{}, + } + + admission.SetDefaultsWhereNeeded(nil, nil, common.GpuSharingModeNonMemoryEnforced) + + Expect(*admission.Autoscaling.Enabled).To(BeFalse()) + Expect(admission.Autoscaling.MaxReplicas).To(BeNil()) + Expect(admission.Autoscaling.TargetInFlightRequests).To(BeNil()) + Expect(admission.Autoscaling.TargetCPUUtilizationPercentage).To(BeNil()) + }) + + It("preserves explicit autoscaling configuration", func(ctx context.Context) { + admission := &Admission{ + Autoscaling: &Autoscaling{ + Enabled: ptr.To(true), + MaxReplicas: ptr.To(int32(5)), + TargetInFlightRequests: ptr.To(int32(20)), + TargetCPUUtilizationPercentage: ptr.To(int32(80)), + }, + } + + admission.SetDefaultsWhereNeeded(nil, nil, common.GpuSharingModeNonMemoryEnforced) + + Expect(*admission.Autoscaling.Enabled).To(BeTrue()) + Expect(*admission.Autoscaling.MaxReplicas).To(Equal(int32(5))) + Expect(*admission.Autoscaling.TargetInFlightRequests).To(Equal(int32(20))) + Expect(*admission.Autoscaling.TargetCPUUtilizationPercentage).To(Equal(int32(80))) + }) }) diff --git a/kai/v1/admission/zz_generated.deepcopy.go b/kai/v1/admission/zz_generated.deepcopy.go index e68c352..1220413 100644 --- a/kai/v1/admission/zz_generated.deepcopy.go +++ b/kai/v1/admission/zz_generated.deepcopy.go @@ -36,6 +36,11 @@ func (in *Admission) DeepCopyInto(out *Admission) { *out = new(int32) **out = **in } + if in.Autoscaling != nil { + in, out := &in.Autoscaling, &out.Autoscaling + *out = new(Autoscaling) + (*in).DeepCopyInto(*out) + } if in.GPUSharing != nil { in, out := &in.GPUSharing, &out.GPUSharing *out = new(bool) @@ -93,6 +98,41 @@ func (in *Admission) DeepCopy() *Admission { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Autoscaling) DeepCopyInto(out *Autoscaling) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.MaxReplicas != nil { + in, out := &in.MaxReplicas, &out.MaxReplicas + *out = new(int32) + **out = **in + } + if in.TargetInFlightRequests != nil { + in, out := &in.TargetInFlightRequests, &out.TargetInFlightRequests + *out = new(int32) + **out = **in + } + if in.TargetCPUUtilizationPercentage != nil { + in, out := &in.TargetCPUUtilizationPercentage, &out.TargetCPUUtilizationPercentage + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Autoscaling. +func (in *Autoscaling) DeepCopy() *Autoscaling { + if in == nil { + return nil + } + out := new(Autoscaling) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InPlacePodResize) DeepCopyInto(out *InPlacePodResize) { *out = *in