Skip to content

Commit 79bffc5

Browse files
committed
fix scaling restriciton for pending state
1 parent c6d9c81 commit 79bffc5

3 files changed

Lines changed: 58 additions & 11 deletions

File tree

api/scaler/v1alpha1/budaiscaler_types.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -467,15 +467,15 @@ type StartingPodsConfig struct {
467467
StartingPodWeight *string `json:"startingPodWeight,omitempty"`
468468

469469
// MaxStartingPods is the maximum number of starting pods allowed before
470-
// gating further scale-up operations. Set to 0 to disable the gate.
471-
// Default: 0 (disabled)
470+
// gating further scale-up operations. Set to 0 to disable this gate.
471+
// Default: 0 (disabled, uses MaxStartingPodPercent instead)
472472
// +optional
473473
// +kubebuilder:validation:Minimum=0
474474
MaxStartingPods *int32 `json:"maxStartingPods,omitempty"`
475475

476476
// MaxStartingPodPercent is the maximum percentage of total pods that can
477-
// be in starting state before gating scale-up.
478-
// Set to 0 to disable. Default: 0 (disabled)
477+
// be in starting state before gating scale-up. Set to 0 to disable.
478+
// Default: 50 (gate scale-up if more than 50% of pods are starting)
479479
// +optional
480480
// +kubebuilder:validation:Minimum=0
481481
// +kubebuilder:validation:Maximum=100

pkg/context/context.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ const (
3838
DefaultPanicWindow = 60 * time.Second
3939
DefaultStableWindow = 180 * time.Second
4040
DefaultActivationScale = int32(1)
41+
42+
// Starting pods defaults - enabled by default for LLM workloads with long cold starts
43+
DefaultStartingPodWeight = 0.5 // Count starting pods as 50% capacity
44+
DefaultMaxStartingPods = 0 // Disabled (use percent-based instead)
45+
DefaultMaxStartingPodPercent = 50 // Gate scale-up if >50% of pods are starting
46+
DefaultBypassGateOnPanic = false
4147
)
4248

4349
// ScalingContext provides configuration for scaling algorithms.
@@ -204,11 +210,11 @@ func NewBaseScalingContext() ScalingContext {
204210
scaleDownPolicies: nil,
205211
scaleUpSelectPolicy: scalerv1alpha1.MaxChangePolicySelect,
206212
scaleDownSelectPolicy: scalerv1alpha1.MinChangePolicySelect,
207-
// Starting pods defaults
208-
startingPodWeight: 0.5, // Count starting pods as 50% capacity
209-
maxStartingPods: 0, // Disabled by default
210-
maxStartingPodPercent: 0, // Disabled by default
211-
bypassGateOnPanic: false, // Don't bypass gate on panic (important for LLM workloads)
213+
// Starting pods defaults - enabled by default for LLM workloads
214+
startingPodWeight: DefaultStartingPodWeight,
215+
maxStartingPods: DefaultMaxStartingPods,
216+
maxStartingPodPercent: DefaultMaxStartingPodPercent,
217+
bypassGateOnPanic: DefaultBypassGateOnPanic
212218
}
213219
}
214220

pkg/controller/budaiscaler/autoscaler.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,18 @@ func (a *AutoScaler) Scale(ctx context.Context, scaler *scalerv1alpha1.BudAIScal
132132

133133
// Build scaling request
134134
readyPodCount := a.countReadyPods(pods)
135-
startingPodCount := a.countStartingPods(pods)
135+
runningNotReadyCount := a.countStartingPods(pods)
136+
137+
// Count pending pods - these are also "starting" as they represent
138+
// capacity that's been requested but not yet available
139+
pendingPodCount, err := a.countPendingPods(ctx, scaler, scale)
140+
if err != nil {
141+
klog.V(4).InfoS("Failed to count pending pods", "scaler", scaler.Name, "error", err)
142+
pendingPodCount = 0
143+
}
144+
145+
// Total starting pods = running-but-not-ready + pending
146+
startingPodCount := runningNotReadyCount + pendingPodCount
136147

137148
request := algorithm.ScalingRequest{
138149
Scaler: scaler,
@@ -145,7 +156,8 @@ func (a *AutoScaler) Scale(ctx context.Context, scaler *scalerv1alpha1.BudAIScal
145156
}
146157

147158
klog.V(4).InfoS("Pod counts", "scaler", scaler.Name,
148-
"readyPods", readyPodCount, "startingPods", startingPodCount,
159+
"readyPods", readyPodCount, "runningNotReady", runningNotReadyCount,
160+
"pendingPods", pendingPodCount, "totalStarting", startingPodCount,
149161
"totalRunning", len(pods), "desiredReplicas", scale.Spec.Replicas)
150162

151163
// Get last scale time from status
@@ -279,6 +291,35 @@ func (a *AutoScaler) countStartingPods(pods []corev1.Pod) int32 {
279291
return count
280292
}
281293

294+
// countPendingPods counts pods that are in Pending state.
295+
// These are pods that have been requested but not yet scheduled or started.
296+
// They represent capacity that's been committed but not yet available.
297+
func (a *AutoScaler) countPendingPods(ctx context.Context, scaler *scalerv1alpha1.BudAIScaler, scale *Scale) (int32, error) {
298+
// Parse selector from scale
299+
selector, err := labels.Parse(scale.Status.Selector)
300+
if err != nil {
301+
return 0, fmt.Errorf("failed to parse selector: %w", err)
302+
}
303+
304+
// List all pods matching selector
305+
podList := &corev1.PodList{}
306+
err = a.client.List(ctx, podList, &client.ListOptions{
307+
Namespace: scaler.Namespace,
308+
LabelSelector: selector,
309+
})
310+
if err != nil {
311+
return 0, fmt.Errorf("failed to list pods: %w", err)
312+
}
313+
314+
var count int32
315+
for _, pod := range podList.Items {
316+
if pod.Status.Phase == corev1.PodPending && pod.DeletionTimestamp == nil {
317+
count++
318+
}
319+
}
320+
return count, nil
321+
}
322+
282323
// isPodReady checks if a pod is ready.
283324
func isPodReady(pod *corev1.Pod) bool {
284325
for _, condition := range pod.Status.Conditions {

0 commit comments

Comments
 (0)