Repository navigation
model-serving: add rolloutStrategy.updateMethod and in-place eligibility checks #1882
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After the timeout, a Warning event named
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is the explanation. |
||
|
|
||
| // RollingUpdateConfiguration configures ServingGroupRollingUpdate. | ||
| // It must not be set when type is RoleRollingUpdate; configure maxUnavailable | ||
| // maxSurge, and partition on each Role instead. | ||
|
|
@@ -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 { | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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 | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.