Skip to content
Open
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
8 changes: 0 additions & 8 deletions .github/workflows/project-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,6 @@ jobs:
path: ${{ env.GOPATH }}/src/sigs.k8s.io/rbgs
fetch-depth: 0

- name: Test Preparation
run: |
arch=$(go env GOARCH)
os=$(go env GOOS)
curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_${os}_${arch}.tar.gz | tar -xz -C /tmp/
sudo mv /tmp/kubebuilder_2.3.1_${os}_${arch} /usr/local/kubebuilder
export PATH=$PATH:/usr/local/kubebuilder/bin

- name: Run Unit Tests
run: |
make test
Expand Down
26 changes: 26 additions & 0 deletions api/workloads/v1alpha2/rolebasedgroup_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,23 @@ type InstanceComponent struct {
Template corev1.PodTemplateSpec `json:"template"`
}

// ScaleDownPolicyType defines how scale-down interacts with rolling updates.
type ScaleDownPolicyType string

const (
// ScaleDownPolicyDeferDuringRollout defers HPA-driven scale-down for
// partition-based workloads while a rolling update is in progress.
// This prevents rollout progress destruction caused by StatefulSet/LWS
// deleting already-updated pods from the highest ordinals.
ScaleDownPolicyDeferDuringRollout ScaleDownPolicyType = "DeferDuringRollout"

// ScaleDownPolicyUnrestricted allows scale-down at any time, even during
// an active rollout. Use with caution: for partition-based workloads,
// scale-down during rollout will delete already-updated pods and reset
// rollout progress.
ScaleDownPolicyUnrestricted ScaleDownPolicyType = "Unrestricted"
)

type ScalingAdapter struct {
// Enable indicates whether the ScalingAdapter is enabled for the Role.
// +optional
Expand All @@ -348,6 +365,15 @@ type ScalingAdapter struct {
// (group-name, role-name) take precedence and cannot be overridden.
// +optional
Labels map[string]string `json:"labels,omitempty"`

// ScaleDownPolicy controls scale-down behavior during rolling updates.
// "DeferDuringRollout" defers scale-down for partition-based workloads
// while a rolling update is in progress.
// "Unrestricted" allows scale-down at any time.
// Defaults to "Unrestricted" when unset.
// +optional
// +kubebuilder:validation:Enum=DeferDuringRollout;Unrestricted
ScaleDownPolicy *ScaleDownPolicyType `json:"scaleDownPolicy,omitempty"`
}

// RoleBasedGroupStatus defines the observed state of RoleBasedGroup.
Expand Down
10 changes: 10 additions & 0 deletions api/workloads/v1alpha2/rolebasedgroupscalingadapter_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ type RoleBasedGroupScalingAdapterSpec struct {
ScaleTargetRef *AdapterScaleTargetRef `json:"scaleTargetRef"`
}

// AdapterConditionScaleDownDeferred indicates that a scale-down request is being
// held because a partition-based rolling update is in progress for the target role.
// This condition is only present when gating is active; it is removed when scaling
// proceeds normally.
const AdapterConditionScaleDownDeferred = "ScaleDownDeferred"

// RoleBasedGroupScalingAdapterStatus shows the current state of a RoleBasedGroupScalingAdapter.
type RoleBasedGroupScalingAdapterStatus struct {
// Phase indicates the current phase of the RoleBasedGroupScalingAdapter.
Expand All @@ -46,6 +52,10 @@ type RoleBasedGroupScalingAdapterStatus struct {

// LastScaleTime is the last time the RoleBasedGroupScalingAdapter scaled the number of pods,
LastScaleTime *metav1.Time `json:"lastScaleTime,omitempty"`

// Conditions represent the latest available observations of the adapter's state.
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

type AdapterScaleTargetRef struct {
Expand Down
12 changes: 12 additions & 0 deletions api/workloads/v1alpha2/zz_generated.deepcopy.go

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

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

17 changes: 15 additions & 2 deletions client-go/applyconfiguration/workloads/v1alpha2/scalingadapter.go

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

9 changes: 9 additions & 0 deletions config/crd/bases/workloads.x-k8s.io_rolebasedgroups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32899,6 +32899,15 @@ spec:
RoleBasedGroupScalingAdapter resource. Controller-managed labels
(group-name, role-name) take precedence and cannot be overridden.
type: object
scaleDownPolicy:
description: |-
ScaleDownPolicy controls scale-down behavior during rolling updates.
"DeferDuringRollout" defers scale-down for partition-based workloads
while a rolling update is in progress.
enum:
- DeferDuringRollout
- Unrestricted
type: string
type: object
servicePorts:
items:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,59 @@ spec:
description: RoleBasedGroupScalingAdapterStatus shows the current state
of a RoleBasedGroupScalingAdapter.
properties:
conditions:
description: Conditions represent the latest available observations
of the adapter's state.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
lastScaleTime:
description: LastScaleTime is the last time the RoleBasedGroupScalingAdapter
scaled the number of pods,
Expand Down
9 changes: 9 additions & 0 deletions config/crd/bases/workloads.x-k8s.io_rolebasedgroupsets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33714,6 +33714,15 @@ spec:
RoleBasedGroupScalingAdapter resource. Controller-managed labels
(group-name, role-name) take precedence and cannot be overridden.
type: object
scaleDownPolicy:
description: |-
ScaleDownPolicy controls scale-down behavior during rolling updates.
"DeferDuringRollout" defers scale-down for partition-based workloads
while a rolling update is in progress.
enum:
- DeferDuringRollout
- Unrestricted
type: string
type: object
servicePorts:
items:
Expand Down
2 changes: 2 additions & 0 deletions doc/TOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
- [Rolling Update](../examples/basics/rolling-update.yaml)
- Failure Handling
- [Restart Policy](../examples/basics/restart-policy.yaml)
- Autoscaling
- [PD-Disagg with Scaling Adapter](../examples/pd-disagg/sglang/sglang-pd.yaml)
- Scheduling
- [Gang Scheduling](../examples/basics/scheduler-plugins-gang.yaml)
- Monitoring
Expand Down
105 changes: 104 additions & 1 deletion doc/features/autoscaler.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,110 @@
# Autoscaling

RBG scaling provides a scalingAdapter that supports HPA, KEDA, or KPA to adjust the number of replicas for Roles.
RBG provides a `ScalingAdapter` that supports HPA, KEDA, or KPA to adjust the number of replicas for Roles.

The RBGSA exposes `status.readyReplicas` mirrored from the parent RBG's `status.roleStatuses[].readyReplicas`, allowing consumers to read readiness directly without a cross-resource lookup.

![autoscaler](../img/autoscaler.jpg)

## Enabling the Scaling Adapter

To enable autoscaling for a role, set `scalingAdapter.enable: true` in the role spec. RBG will automatically create a `RoleBasedGroupScalingAdapter` (RBGSA) resource that exposes a scale subresource for the HPA to target.

```yaml
apiVersion: workloads.x-k8s.io/v1alpha2
kind: RoleBasedGroup
metadata:
name: my-inference
spec:
roles:
- name: worker
replicas: 4
scalingAdapter:
enable: true
workload:
apiVersion: apps/v1
kind: StatefulSet
template:
# ...
```

Once created, an HPA can target the RBGSA:

```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-inference-worker
spec:
scaleTargetRef:
apiVersion: workloads.x-k8s.io/v1alpha2
kind: RoleBasedGroupScalingAdapter
name: my-inference-worker
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
```

### ScalingAdapter Configuration

| Field | Type | Default | Description |
|-----------------|---------------------|----------------|-----------------------------------------------------------------------------|
| enable | bool | false | Whether the scaling adapter is enabled for the role. |
| scaleDownPolicy | ScaleDownPolicyType | Unrestricted | Controls scale-down behavior during rolling updates. |

### ScaleDownPolicy Values

| Value | Description |
|--------------------|-----------------------------------------------------------------------------------------------------------------|
| Unrestricted | Allows scale-down at any time, even during an active rollout. This is the default behavior. |
| DeferDuringRollout | Defers scale-down for partition-based workloads (StatefulSet, LWS) while a rolling update is in progress. |

## Scale-Down During Rolling Updates

### The Problem

For partition-based workloads (StatefulSet, LeaderWorkerSet), rolling updates proceed from the highest ordinals down — pods with ordinal >= partition run the new template. However, StatefulSet also deletes from the highest ordinals on scale-down. This means **scale-down during a rollout kills the already-updated pods first**, resetting rollout progress to zero.

```text
Before scale-down: replicas=10, partition=7
Pods: [0 1 2 3 4 5 6 | 7 8 9]
── old template ── ─ new ─

HPA scales down to 6:
StatefulSet deletes pods 9, 8, 7 (the updated ones)

After scale-down: replicas=6, partition=6
Pods: [0 1 2 3 4 5]
── all old template ──
Rollout progress: 3 → 0 updated pods
```

### The Solution: DeferDuringRollout

Setting `scaleDownPolicy: DeferDuringRollout` gates scale-down at the RBGSA controller level while a partition-based rolling update is in progress:

```yaml
scalingAdapter:
enable: true
scaleDownPolicy: DeferDuringRollout
```

When gating is active:
- The RBGSA sets a `ScaleDownDeferred` condition (visible via `kubectl get rbgsa -o yaml`)
- A Warning event is emitted on the RBGSA resource
- The controller requeues every 30 seconds to re-check rollout status
- Once the rollout completes (`UpdatedReplicas == Replicas`), the pending scale-down proceeds

Scale-up operations are never gated — new pods land on the new template automatically (their ordinal >= partition).

**Note**: This policy only affects partition-based workloads (StatefulSet, LeaderWorkerSet, RoleInstanceSet). Deployment-backed roles are unaffected because Kubernetes Deployments handle concurrent scaling and rollout natively via ReplicaSet proportioning.

## Example YAMLs

- [PD-Disagg with Scaling Adapter](../../examples/pd-disagg/sglang/sglang-pd.yaml)
Loading