Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ spec:
required:
- protectionLevel
type: object
progressDeadlineSeconds:
description: |-
ProgressDeadlineSeconds is the maximum time for a ServingGroup or Role instance updated in place to become ready on the target image.
After it passes, Pods that are not ready follow the RecoveryPolicy.
It only applies to in-place updates, and defaults to 1800 seconds.
format: int32
minimum: 60
type: integer
rollingUpdateConfiguration:
description: |-
RollingUpdateConfiguration configures ServingGroupRollingUpdate.
Expand Down Expand Up @@ -229,6 +237,18 @@ spec:
- ServingGroupRollingUpdate
- RoleRollingUpdate
type: string
updateMethod:
default: Recreate
description: |-
UpdateMethod selects how outdated ServingGroups or Roles are moved to the desired revision.
Supported values are Recreate (default) and InPlace.

InPlace patches container images on existing Pods when only regular-container images change.
Otherwise the ServingGroup, or the Role instance under RoleRollingUpdate, is recreated, and an InPlaceUpdateFallback event reports why.
enum:
- Recreate
- InPlace
type: string
required:
- type
type: object
Expand Down
26 changes: 26 additions & 0 deletions client-go/applyconfiguration/workload/v1alpha1/rolloutstrategy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions docs/kthena/docs/reference/crd/workload.serving.volcano.sh.md
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,8 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `type` _[RolloutStrategyType](#rolloutstrategytype)_ | Type selects the granularity of rolling updates. Supported values are<br />ServingGroupRollingUpdate and RoleRollingUpdate. It defaults to<br />ServingGroupRollingUpdate.<br />ServingGroupRollingUpdate uses rolloutStrategy.rollingUpdateConfiguration;<br />rolling update settings on individual Roles do not take effect.<br />RoleRollingUpdate uses the rolling update configuration on each Role;<br />rolloutStrategy.rollingUpdateConfiguration must not be set.<br />Kthena performs RoleRollingUpdate across all ServingGroups at the same time.<br />Therefore, we recommend using it only in scenarios with a single ServingGroup. | ServingGroupRollingUpdate | Enum: [ServingGroupRollingUpdate RoleRollingUpdate] <br /> |
| `updateMethod` _[UpdateMethod](#updatemethod)_ | UpdateMethod selects how outdated ServingGroups or Roles are moved to the desired revision.<br />Supported values are Recreate (default) and InPlace.<br />InPlace patches container images on existing Pods when only regular-container images change.<br />Otherwise the ServingGroup, or the Role instance under RoleRollingUpdate, is recreated, and an InPlaceUpdateFallback event reports why. | Recreate | Enum: [Recreate InPlace] <br /> |
| `progressDeadlineSeconds` _integer_ | ProgressDeadlineSeconds is the maximum time for a ServingGroup or Role instance updated in place to become ready on the target image.<br />After it passes, Pods that are not ready follow the RecoveryPolicy.<br />It only applies to in-place updates, and defaults to 1800 seconds. | | Minimum: 60 <br /> |
| `rollingUpdateConfiguration` _[RollingUpdateConfiguration](#rollingupdateconfiguration)_ | RollingUpdateConfiguration configures ServingGroupRollingUpdate.<br />It must not be set when type is RoleRollingUpdate; configure maxUnavailable<br />maxSurge, and partition on each Role instead. | | |
| `evictionStrategy` _[EvictionStrategySpec](#evictionstrategyspec)_ | EvictionStrategy defines the protection policy during node eviction. | | |

Expand Down Expand Up @@ -1141,3 +1143,20 @@ _Appears in:_
| `mode` _string_ | Mode reports whether the unit is currently in "Stable" or "Panic" mode. | | |


#### UpdateMethod

_Underlying type:_ _string_

UpdateMethod defines how outdated Pods are moved to the desired revision.



_Appears in:_
- [RolloutStrategy](#rolloutstrategy)

| Field | Description |
| --- | --- |
| `Recreate` | RecreateUpdateMethod deletes outdated Pods and creates new ones.<br /> |
| `InPlace` | InPlaceUpdateMethod patches container images on existing Pods when possible, and recreates them otherwise.<br /> |


29 changes: 29 additions & 0 deletions pkg/apis/workload/v1alpha1/model_serving_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,24 @@ type RolloutStrategy struct {
// +kubebuilder:validation:Enum={ServingGroupRollingUpdate,RoleRollingUpdate}
Type RolloutStrategyType `json:"type"`

// UpdateMethod selects how outdated ServingGroups or Roles are moved to the desired revision.
// Supported values are Recreate (default) and InPlace.
//
// InPlace patches container images on existing Pods when only regular-container images change.
// Otherwise the ServingGroup, or the Role instance under RoleRollingUpdate, is recreated, and an InPlaceUpdateFallback event reports why.
//
// +kubebuilder:default=Recreate
// +kubebuilder:validation:Enum={Recreate,InPlace}
// +optional
UpdateMethod UpdateMethod `json:"updateMethod,omitempty"`

// ProgressDeadlineSeconds is the maximum time for a ServingGroup or Role instance updated in place to become ready on the target image.
// After it passes, Pods that are not ready follow the RecoveryPolicy.
// It only applies to in-place updates, and defaults to 1800 seconds.
// +kubebuilder:validation:Minimum=60
// +optional
ProgressDeadlineSeconds *int32 `json:"progressDeadlineSeconds,omitempty"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VanderChen
I believe we still need a time interval that is different from RestartGracePeriodSeconds. This is because the cold start time for an LLM pod and the pod error tolerance period are two distinct concepts, and the time required for each is difficult to measure using a single time interval.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should happen when this deadline expires, and why do we need a separate timeout rather than leaving a stalled update for the user to resolve?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the timeout, a Warning event named ProgressDeadlineExceeded is emitted first, and then the RecoverPolicy is applied.

@kube-gopher kube-gopher Oct 20, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VanderChen
I believe we still need a time interval that is different from RestartGracePeriodSeconds. This is because the cold start time for an LLM pod and the pod error tolerance period are two distinct concepts, and the time required for each is difficult to measure using a single time interval.

This is the explanation.
Our previous consideration was that we only need to wait for a RestartGracePeriodSeconds timeout and then proceed with the RecoveryPolicy.


// RollingUpdateConfiguration configures ServingGroupRollingUpdate.
// It must not be set when type is RoleRollingUpdate; configure maxUnavailable
// maxSurge, and partition on each Role instead.
Expand Down Expand Up @@ -214,6 +232,17 @@ const (
RoleRollingUpdate RolloutStrategyType = "RoleRollingUpdate"
)

// UpdateMethod defines how outdated Pods are moved to the desired revision.
type UpdateMethod string

const (
// RecreateUpdateMethod deletes outdated Pods and creates new ones.
RecreateUpdateMethod UpdateMethod = "Recreate"

// InPlaceUpdateMethod patches container images on existing Pods when possible, and recreates them otherwise.
InPlaceUpdateMethod UpdateMethod = "InPlace"
)

// RollingUpdateConfiguration defines availability and partition settings for
// the rollout granularity where it is configured.
type RollingUpdateConfiguration struct {
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/workload/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 95 additions & 0 deletions pkg/model-serving-controller/inplace/eligibility.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
Copyright The Volcano 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 inplace

import (
"bytes"

workloadv1alpha1 "github.com/volcano-sh/kthena/pkg/apis/workload/v1alpha1"
"github.com/volcano-sh/kthena/pkg/model-serving-controller/utils"
)

// UpdateMethodOf returns Recreate when no update method is set.
func UpdateMethodOf(ms *workloadv1alpha1.ModelServing) workloadv1alpha1.UpdateMethod {
if ms.Spec.RolloutStrategy == nil || ms.Spec.RolloutStrategy.UpdateMethod == "" {
return workloadv1alpha1.RecreateUpdateMethod
}
return ms.Spec.RolloutStrategy.UpdateMethod
}

// OnlyImagesChanged reports whether newMS renders the same Pods as oldMS except for regular-container images.
// It copies the new images into oldMS and compares the revision data, so every other field that defines a revision is covered.
func OnlyImagesChanged(oldMS, newMS *workloadv1alpha1.ModelServing) (bool, error) {
patched := oldMS.DeepCopy()
for i := range patched.Spec.Template.Roles {
role := &patched.Spec.Template.Roles[i]
newRole := findRole(newMS, role.Name)
if newRole == nil {
return false, nil
}
copyImages(role, newRole)
}
oldData, err := utils.BuildRevisionData(patched)
if err != nil {
return false, err
}
newData, err := utils.BuildRevisionData(newMS)
if err != nil {
return false, err
}
return bytes.Equal(oldData, newData), nil
}

// OnlyRoleImagesChanged is OnlyImagesChanged for one Role, compared by its template hash.
func OnlyRoleImagesChanged(oldMS, newMS *workloadv1alpha1.ModelServing, roleName string) bool {
oldRole, newRole := findRole(oldMS, roleName), findRole(newMS, roleName)
if oldRole == nil || newRole == nil {
return false
}
patched := oldRole.DeepCopy()
copyImages(patched, newRole)
return utils.CalRoleTemplateHash(*patched) == utils.CalRoleTemplateHash(*newRole)
}

func findRole(ms *workloadv1alpha1.ModelServing, roleName string) *workloadv1alpha1.Role {
for i := range ms.Spec.Template.Roles {
if ms.Spec.Template.Roles[i].Name == roleName {
return &ms.Spec.Template.Roles[i]
}
}
return nil
}

func copyImages(role, from *workloadv1alpha1.Role) {
copyTemplateImages(&role.EntryTemplate, &from.EntryTemplate)
if role.WorkerTemplate != nil && from.WorkerTemplate != nil {
copyTemplateImages(role.WorkerTemplate, from.WorkerTemplate)
}
}

// copyTemplateImages copies images by container name, so any other container change still shows.
func copyTemplateImages(template, from *workloadv1alpha1.PodTemplateSpec) {
images := make(map[string]string, len(from.Spec.Containers))
for _, container := range from.Spec.Containers {
images[container.Name] = container.Image
}
for i := range template.Spec.Containers {
if image, ok := images[template.Spec.Containers[i].Name]; ok {
template.Spec.Containers[i].Image = image
}
}
}
Loading
Loading