Kueue Scheduler Backend
Michelangelo AI can delegate batch-job admission to Kueue, giving you quota-aware queueing and gang admission on your compute clusters. The backend is opt-in per compute cluster: clusters that don't opt in keep the default immediate-admission behavior, so mixed fleets work throughout a migration.
How it works
The control plane deliberately does not create Kueue Workload objects itself. Instead:
- A compute cluster opts in by setting
scheduler_type: SCHEDULER_TYPE_KUEUEon itsClusterspec. - With
jobs.scheduler.backend: kueue, the control plane validates at enqueue time that a job bound for a Kueue-managed cluster has an existing LocalQueue to land in. Jobs bound for other clusters pass through untouched. - When the job is dispatched, the k8s engine stamps exactly one label on the RayCluster it creates:
kueue.x-k8s.io/queue-name, resolved by the control plane from the job's project. User-supplied labels never cross the cluster boundary, so the queue a job lands in can never be chosen by the job author. - Kueue's own RayCluster integration on the compute cluster suspends the labeled RayCluster, admits it against ClusterQueue quota when capacity is available, and gang-admits all of its pods together.
While a RayCluster waits for admission (KubeRay reports it suspended), the controller surfaces a Queued condition with reason AwaitingAdmission; when the cluster becomes ready, the condition flips to False with reason ClusterAdmitted.
Queue topology
The intended topology is one ClusterQueue per compute cluster, one LocalQueue per project:
- Each compute cluster enforces its own quota independently (no MultiKueue).
- Each project gets a LocalQueue pointing at the cluster's ClusterQueue, named by the
localQueueTemplateconvention --ma-{project}by default. - The project identity is the
ma/project-namelabel when your deployment stamps it, otherwise the job's namespace (Michelangelo AI jobs run in their project's namespace).
LocalQueues live in the namespace where the control plane dispatches RayClusters on the compute cluster (default). In this phase they are operator-managed: create them when you onboard a project, and the control plane validates their existence at dispatch time rather than creating them on demand. A job whose LocalQueue is missing fails visibly (Enqueued=False, reason KueueQueueNotFound) instead of being dispatched into an unadmittable state.
Enabling the backend
1. Install Kueue on the compute cluster
Kueue 0.15+ is recommended (its API defaults to kueue.x-k8s.io/v1beta2, which matches the control plane's default; see Compatibility for older releases). The default Kueue configuration already enables the RayCluster integration:
helm install kueue oci://registry.k8s.io/kueue/charts/kueue \
--version 0.15.0 --namespace kueue-system --create-namespace
Create the cluster's quota and the per-project queues:
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
name: default-flavor
spec: {}
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: cluster-queue
spec:
namespaceSelector: {}
queueingStrategy: BestEffortFIFO
resourceGroups:
- coveredResources: ["cpu", "memory"]
flavors:
- name: default-flavor
resources:
- name: cpu
nominalQuota: "64"
- name: memory
nominalQuota: 256Gi
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
name: ma-my-project # localQueueTemplate applied to the project
namespace: default # where dispatched RayClusters land
spec:
clusterQueue: cluster-queue
2. Mark the compute cluster
apiVersion: michelangelo.api/v2
kind: Cluster
metadata:
name: my-compute-cluster
namespace: ma-system
spec:
schedulerType: SCHEDULER_TYPE_KUEUE
# ... existing kubernetes/rest connection settings
3. Enable the backend on the control plane
Helm:
controllermgr:
jobs:
scheduler:
backend: kueue
# Optional:
# kueue:
# localQueueTemplate: "ma-{project}" # default
# localQueueOverrides:
# some-project: shared-queue
# apiVersion: v1beta2 # default; v1beta1 for Kueue < 0.15
The controllermgr restarts automatically on the config change (checksum annotation). An unrecognized backend value fails at startup rather than silently running the default backend.
Configuration reference
Config path jobs.scheduler (Helm: controllermgr.jobs.scheduler):
| Key | Default | Meaning |
|---|---|---|
backend | default | default for immediate admission; kueue to validate and label jobs bound for Kueue-managed clusters. |
kueue.localQueueTemplate | ma-{project} | LocalQueue naming convention; {project} is replaced with the job's project. |
kueue.localQueueOverrides | {} | Per-project explicit LocalQueue names; an override wins over the template. |
kueue.apiVersion | v1beta2 | kueue.x-k8s.io API version used for the LocalQueue existence check on compute clusters. |
Observability
- Waiting for quota: the RayCluster carries
Queued=True(reasonAwaitingAdmission) while suspended, flipping toFalse/ClusterAdmittedon admission. A queued job is therefore distinguishable from one stuck launching. - Misconfiguration: a missing LocalQueue sets
Enqueued=Falsewith reasonKueueQueueNotFoundand an actionable message; an unreachable compute cluster during validation setsKueueQueueCheckFailedand the job is retried. - Metrics: the
kueueschedulercontroller scope emitskueue.queue_not_found_countandkueue.queue_check_failed_count. - On the compute cluster, Kueue's own metrics and
kubectl get workloads/localqueuesshow admission state and quota usage.
Compatibility
- Kueue version: the control plane only performs an existence
GETagainstkueue.x-k8s.io, so there is no client-library coupling. The defaultapiVersion: v1beta2requires Kueue 0.15+; setkueue.apiVersion: v1beta1to run against older Kueue releases. - RayJobs: Michelangelo AI RayJobs target an existing RayCluster via
ClusterSelector, which Kueue's RayJob integration deliberately skips -- admission is enforced on the RayCluster, which is the resource-owning object. RayJobs never get a queue label. waitForPodsReady: avoid enabling Kueue'swaitForPodsReadyfor RayClusters admitted through this backend. A RayCluster's pods can legitimately start over a window (e.g. autoscaling workers), andwaitForPodsReadycan evict and requeue the whole cluster on that window.
Rollback
Each step is independently reversible:
- Set
jobs.scheduler.backend: default(or remove it) to stop validating and labeling; already-dispatched suspended clusters still get admitted by Kueue on the compute cluster. - Remove
scheduler_typefrom aClusterspec to return that cluster to immediate admission while the backend stays on for others. - Uninstalling Kueue from a compute cluster leaves any still-suspended RayClusters suspended: terminate them (or clear
spec.suspendon the KubeRay object) before removing Kueue.
Try it in the sandbox
ma sandbox create
ma sandbox demo kueue
ma sandbox sync --set controllermgr.jobs.scheduler.backend=kueue
ma sandbox demo kueue installs Kueue on the sandbox's compute cluster (use --compute-cluster-name if you created a dedicated one), creates a demo ClusterQueue and the demo project's LocalQueue, and marks the Cluster CR as Kueue-managed. Submit a Ray task and watch it flow through Queued -> admitted; shrink the ClusterQueue quota below the job's request to see it wait.