Search before asking
KubeRay Component
ray-operator
Description
Summary
When KubeRay runs with Kueue, a worker group blocked by quota is indistinguishable
from a slow-starting one, so the Ray Autoscaler waits up to 60 minutes before
falling back to a lower-priority worker group. This issue proposes a group-level
scaleGate signal on the RayCluster CRD so fallback happens within one reconcile
cycle.
Background
The Ray Autoscaler (v2) supports Priority-Aware Worker Group Selection.
Each worker group carries a priority, and when several groups can satisfy the same
resource demand the Autoscaler prefers the higher-priority one. If the preferred
group cannot allocate nodes, the Autoscaler falls back to a lower-priority group.
This fallback loop is the mechanism we want to trigger quickly when a group is
blocked by quota.
Kueue enforces quota through Pod scheduling gates. When a worker group exceeds its
quota, Kueue keeps the newly created Pods gated (spec.schedulingGates contains
kueue.x-k8s.io/admission) and marks the associated Workload with
QuotaReserved=False (Reason: Pending). The Pod is created successfully, so from
the Autoscaler's point of view the node request looks like it is launching slowly,
not failing.
Problem
Current end-to-end behavior when Kueue blocks admission on quota:
- The KubeRay operator creates the Pod, but it stays gated by
kueue.x-k8s.io/admission.
- Kueue sets the
Workload status to QuotaReserved=False (Reason: Pending).
- The Ray Autoscaler sees a pending Pod, treats the instance as
ALLOCATED, and
waits for RAY_AUTOSCALER_RECONCILE_ALLOCATE_STATUS_TIMEOUT_S (default 3600s)
before declaring failure and attempting fallback.
- Result: an hour-long delay before the workload scales up on a fallback group.
Lowering the timeout globally is not a fix — that env var covers the whole
ALLOCATED → RUNNING transition, of which waiting on Kueue is only one part, so a
short value would kill pods that are legitimately still starting.
Goal: signal quota exhaustion for a specific worker group to the Autoscaler
within one reconcile cycle, bypassing the allocate-status timeout.
Proposed design
Add an optional string-list ScaleGate field to ScaleStrategy on each worker group
- KubeRay Operator: Neither reads nor writes this field.
- Kueue (or other schedulers): Add their gate identifier to scaleGate when a worker group is blocked (e.g., due to quota exhaustion), and remove it once admitted. For Kueue, the identifier is
kueue.k8s.io/quota-exceeded
- Ray Autoscaler: Reads scaleGate and skips scaling up on that worker group whenever the list is non-empty.
Pros:
- Extensible: Schedulers interacting with RayCluster CRDs can integrate gate signals without modifying Ray or KubeRay codebases.
- Decoupled: KubeRay remains independent and avoids introducing external Go module dependencies.
Cons:
- Scheduler Support: Schedulers that do not natively manage or interact with RayCluster CRDs cannot write to this field.
- Potential Multiple Writers: Currently only Kueue writes to this filed, but potentially more schedulers or other components can write their gate identifier to this field.
API change
ray-operator/apis/ray/v1/raycluster_types.go:
// ScaleStrategy to remove workers
type ScaleStrategy struct {
// WorkersToDelete workers to be deleted
WorkersToDelete []string `json:"workersToDelete,omitempty"`
// ScaleGate contains a list of scaling gates blocking this worker group from
// scaling up. When Kueue encounters a quota-exceeded error for this worker
// group, it appends "kueue.k8s.io/quota-exceeded" to ScaleGate.
// The Autoscaler initiates fallback behavior whenever ScaleGate is non-empty.
// +optional
// +listType=set
ScaleGate []string `json:"scaleGate,omitempty"`
}
| Dimension |
scaleGate field (this proposal) |
| Signal scope |
Per worker group |
| Where it lives |
RayCluster.spec.workerGroupSpecs[i].scaleStrategy |
| Who writes it |
Kueue |
| Autoscaler input |
Reads one field per group from the cluster spec |
| Write cardinality |
One field per affected group |
| Discoverability |
Visible in the RayCluster spec |
| CRD schema change |
Yes |
Open design questions
Raised during review of #5094:
Extensibility to other schedulers. Schedulers other than Kueue (Volcano,
YuniKorn, scheduler-plugins, KAI) are not aware of KubeRay CRDs and would not
write a RayCluster spec.
Alternative A: KubeRay derives the signal and surfaces it in RayCluster.status. The Ray Autoscaler would
consume the gate signal from RayCluster.status instead of spec.
Pros:
- Can support schedulers that do not interact directly with RayCluster custom resources.
Cons:
- Introduces cross-project Go module dependencies into the KubeRay operator.
- every scheduler support needs kuberay side change
Alternative B: Ray Autoscaler reads scheduler signals directly
The Autoscaler bypasses KubeRay entirely to query scheduler APIs (such as Kueue's Workload CRD) directly.
Pros:
- Can support schedulers that do not interact directly with RayCluster custom resources.
Cons:
- Introduces scheduler-specific dependencies directly into the Ray Autoscaler.
- every scheduler support needs ray side change
Add a scaleGate field to the RayCluster CR, plus a second field indicating whether scaleGate is owned by Kueue. If it is owned by Kueue, KubeRay only propagates the information from the CR spec to the RayCluster CR status. If not, KubeRay reads the scheduler's PodGroup CRs and writes the information into the RayCluster CR status.
Scheduler can define there own gate identifier, for Kueue, it's kueue.k8s.io/quota-exceeded. The other logic is not needed for current proposal, as worker group will be gated as long as scaleGate is not empty.
multiple writers to ScaleStrategy field
-
Ray autoscaler raplace entire ScaleStrategy in worker_delete_patch
-- solution: modify worker_delete_patch logic to only modify workersToDelete in ScaleStrategy
-
Kueue and potential other scheduler write to ScaleGate filed.
-- solution: follow the existing example PodSchedulingGate to use +listType=map
Proof of concept
Use case
No response
Related issues
#4846
ray-project/ray#62996
Are you willing to submit a PR?
Search before asking
KubeRay Component
ray-operator
Description
Summary
When KubeRay runs with Kueue, a worker group blocked by quota is indistinguishable
from a slow-starting one, so the Ray Autoscaler waits up to 60 minutes before
falling back to a lower-priority worker group. This issue proposes a group-level
scaleGatesignal on the RayCluster CRD so fallback happens within one reconcilecycle.
Background
The Ray Autoscaler (v2) supports Priority-Aware Worker Group Selection.
Each worker group carries a priority, and when several groups can satisfy the same
resource demand the Autoscaler prefers the higher-priority one. If the preferred
group cannot allocate nodes, the Autoscaler falls back to a lower-priority group.
This fallback loop is the mechanism we want to trigger quickly when a group is
blocked by quota.
Kueue enforces quota through Pod scheduling gates. When a worker group exceeds its
quota, Kueue keeps the newly created Pods gated (
spec.schedulingGatescontainskueue.x-k8s.io/admission) and marks the associatedWorkloadwithQuotaReserved=False(Reason:Pending). The Pod is created successfully, so fromthe Autoscaler's point of view the node request looks like it is launching slowly,
not failing.
Problem
Current end-to-end behavior when Kueue blocks admission on quota:
kueue.x-k8s.io/admission.Workloadstatus toQuotaReserved=False(Reason:Pending).ALLOCATED, andwaits for
RAY_AUTOSCALER_RECONCILE_ALLOCATE_STATUS_TIMEOUT_S(default 3600s)before declaring failure and attempting fallback.
Lowering the timeout globally is not a fix — that env var covers the whole
ALLOCATED → RUNNINGtransition, of which waiting on Kueue is only one part, so ashort value would kill pods that are legitimately still starting.
Goal: signal quota exhaustion for a specific worker group to the Autoscaler
within one reconcile cycle, bypassing the allocate-status timeout.
Proposed design
Add an optional string-list
ScaleGatefield toScaleStrategyon each worker groupkueue.k8s.io/quota-exceededPros:
Cons:
API change
ray-operator/apis/ray/v1/raycluster_types.go:scaleGatefield (this proposal)RayCluster.spec.workerGroupSpecs[i].scaleStrategyOpen design questions
Raised during review of #5094:
Alternative A: KubeRay derives the signal and surfaces it in
RayCluster.status. The Ray Autoscaler wouldconsume the gate signal from
RayCluster.statusinstead ofspec.Pros:
Cons:
Alternative B: Ray Autoscaler reads scheduler signals directly
The Autoscaler bypasses KubeRay entirely to query scheduler APIs (such as Kueue's Workload CRD) directly.
Pros:
Cons:
Scheduler can define there own gate identifier, for Kueue, it's
kueue.k8s.io/quota-exceeded. The other logic is not needed for current proposal, as worker group will be gated as long as scaleGate is not empty.Ray autoscaler raplace entire ScaleStrategy in worker_delete_patch
-- solution: modify worker_delete_patch logic to only modify
workersToDeletein ScaleStrategyKueue and potential other scheduler write to
ScaleGatefiled.-- solution: follow the existing example PodSchedulingGate to use
+listType=mapProof of concept
Use case
No response
Related issues
#4846
ray-project/ray#62996
Are you willing to submit a PR?