Kueue Integration
AIPerf integrates with Kueue to provide gang-scheduling, quota management, and prioritization for benchmark JobSets. This page explains what Kueue buys you, how to install and configure it, and how AIPerfJobs bind to Kueue resources.
What Kueue Is
Kueue is a Kubernetes-native job queueing system maintained by the upstream
kubernetes-sigs project. It sits between job controllers (JobSet, Job,
RayJob, etc.) and the cluster scheduler, admitting workloads only when the
cluster has enough free capacity in a named ClusterQueue (cluster-scoped
quota pool) addressed through a LocalQueue (namespace-scoped handle). A
workload either runs in full or stays suspended; Kueue never schedules a
partial JobSet.
Useful upstream entry points:
- Overview
- Concepts: ClusterQueue, LocalQueue, ResourceFlavor
- JobSet integration
- Workload Priority Classes
Why Use Kueue With AIPerf
An AIPerf benchmark runs as a JobSet with two replicated jobs: one
controller pod (control plane, records manager, results sidecar, and the
optional telemetry containers) and N workers pods. All pods must start
together, or the controller sits idle while workers slowly trickle in and
the benchmark either fails or produces misleading numbers.
Kueue solves three problems that matter for benchmark campaigns:
- Gang scheduling. Kueue suspends the JobSet until the cluster can admit every pod at once. On a mixed cluster, this prevents a half-scheduled run that would skew latency metrics.
- Priority and preemption across campaigns. A
WorkloadPriorityClasslets a smoke-test job jump the queue ahead of a long soak-test, or lets a tenant’s urgent run preempt an idle reservation. - Fair sharing across teams. A single ClusterQueue with
borrowingLimitrules lets multiple namespaces share GPU capacity without one team starving another — useful when several groups run benchmarks on the same cluster.
Without Kueue, AIPerfJobs are submitted directly to the JobSet controller and compete for nodes using default Kubernetes scheduling.
Install
AIPerf does not vendor or pin a Kueue release; install any version whose
kueue.x-k8s.io/v1beta1 API AIPerf targets (LocalQueue, ClusterQueue,
ResourceFlavor, WorkloadPriorityClass). Follow the upstream
installation guide. A typical
install applies the released manifest bundle:
The operator runs a Kueue Queue preflight check during reconcile that
verifies the Kueue CRDs are present and that any referenced LocalQueue
actually exists (see src/aiperf/operator/preflight/_infra.py). If Kueue is
not installed and no queue was requested, the check is marked SKIP and
AIPerfJobs run without gang-scheduling. If an explicit queue was requested,
the check fails because the resulting suspended JobSet would otherwise never
be admitted. This check is operator-side only — the client-side
aiperf kube preflight command does not include it.
Configuring a ClusterQueue and LocalQueue
A minimal setup for a benchmark cluster looks like this. Adjust the
nominalQuota values to match your real GPU inventory.
Optional: add a WorkloadPriorityClass for high-priority runs.
Binding an AIPerfJob to a Queue
AIPerf stamps the kueue.x-k8s.io/queue-name label onto the JobSet (and
starts it suspended) whenever it can resolve a queue name from either
spec.scheduling.queueName or the operator-side
AIPERF_K8S_JOBSET_KUEUE_DEFAULT_QUEUE_NAME env var. Kueue’s own
namespace-annotation default works differently.
1. CLI flags
The aiperf kube profile command (and every other submit subcommand)
accepts --queue-name and --priority-class:
These flags live in the Kubernetes Scheduling group and are defined on
KubeOptions in src/aiperf/config/kube.py.
2. CR YAML
Set the same fields directly in the AIPerfJob spec under scheduling:
3. Cluster-wide default via Helm
The aiperf-operator Helm chart exposes a kueue.defaultQueueName value
(see deploy/helm/aiperf-operator/values.yaml). When set, the chart sets
AIPERF_K8S_JOBSET_KUEUE_DEFAULT_QUEUE_NAME on the operator container so
AIPerf’s own manifest builder applies the queue label and starts the JobSet
suspended.
Kueue also honours a kueue.x-k8s.io/default-queue-name annotation on the
namespace, which you apply yourself with kubectl annotate (see
Troubleshooting below). AIPerf’s own manifest builder does not read that
annotation: AIPerfJobSetSpec._resolved_queue_name in
src/aiperf/kubernetes/jobset.py consults only
spec.scheduling.queueName and the AIPERF_K8S_JOBSET_KUEUE_DEFAULT_QUEUE_NAME
env var, so the annotation alone does not make AIPerf add the queue label or
start the JobSet suspended. Operator preflight does read it, and treats an
annotated namespace as a satisfied queue configuration. To make Kueue
gang-scheduling default-on from AIPerf’s side, set
AIPERF_K8S_JOBSET_KUEUE_DEFAULT_QUEUE_NAME (and optionally
AIPERF_K8S_JOBSET_KUEUE_DEFAULT_PRIORITY_CLASS) on the operator
deployment.
The chart can also provision the queue objects themselves instead of you
applying the YAML above by hand. Set kueue.createQueues=true and the chart
renders a ResourceFlavor, ClusterQueue, and LocalQueue from
deploy/helm/aiperf-operator/templates/kueue-queues.yaml, using
kueue.flavorName, kueue.clusterQueueName, kueue.localQueueName, and the
kueue.resources quota map (cpu, memory, and an optional gpu entry that
is skipped when empty). It is off by default so the chart renders on clusters
without Kueue CRDs, and even when enabled the template is additionally gated
on the kueue.x-k8s.io/v1beta1 API being present. When defaultQueueName is
left empty and createQueues=true, the operator’s default queue falls back to
kueue.localQueueName.
WARNING:
kueue.createQueues=truecreates theLocalQueuein the chart’s release namespace (aiperf-systemby default), not in the namespaces your benchmarks run in. ALocalQueueis namespace-scoped and Kueue only admits a workload whosekueue.x-k8s.io/queue-namelabel resolves to aLocalQueuein the workload’s own namespace. A benchmark submitted intomy-benchmarkswith the chart’s queue name therefore sits inQueuedforever with no error: the JobSet stays suspended and no pod is ever created.Create one
LocalQueueper benchmark namespace, pointing at the sharedClusterQueuethe chart made:The
ClusterQueueandResourceFlavorare cluster-scoped, so those the chart really can own for you; only theLocalQueuehandle has to be repeated per namespace. A missingLocalQueuedoes not surface as a CLI error — the JobSet simply stays suspended indefinitely — so create the per-namespaceLocalQueuebefore submitting your first benchmark.
What the Operator Does
Concretely:
src/aiperf/kubernetes/jobset.pytranslatesspec.scheduling.queueNameandpriorityClassinto JobSet labels (kueue.x-k8s.io/queue-name,kueue.x-k8s.io/priority-class) viaKueueLabelsinsrc/aiperf/kubernetes/constants.py.- When a
queueNameis set, the JobSet is created withspec.suspend: true. Kueue unsuspends it once theWorkloadit generates has been admitted. src/aiperf/operator/handlers/monitor.py::_handle_kueue_suspensionwatches the JobSet’sspec.suspendfield. While the JobSet is suspended and carries a Kueue queue label, the operator surfaces theQUEUEDphase on the AIPerfJob status (seePhase.QUEUEDinsrc/aiperf/kubernetes/phase.py).- Once Kueue admits the workload, the JobSet unsuspends, pods start, and
the monitor transitions the phase to
INITIALIZING->RUNNING.
You can observe admission directly with kubectl:
Troubleshooting
Job stuck in QUEUED phase. Kueue has accepted the Workload but has
not admitted it. Check in order:
kubectl describe workload -n my-benchmarks— look at theQuotaReservedandAdmittedconditions. The message usually names the resource that is over quota.kubectl get clusterqueue aiperf-cluster-queue -o yaml— comparespec.resourceGroups.*.nominalQuotato what the JobSet requests. Each worker pod requests theAIPERF_K8S_WORKER_POD_CPU/AIPERF_K8S_WORKER_POD_MEMORYbudget (default3350m/6Gi), split across its containers, andspec.resourceModedecides whether limits are emitted alongside requests; seeconfiguration.mdfor defaults.- Another admitted Workload may be holding the quota. List active
workloads with
kubectl get workloads -A -o wideand cancel stale ones.
Operator preflight fails: Kueue LocalQueue '<name>' not found. The
resolved scheduling.queueName references a LocalQueue that does not exist
in the target namespace. Either create it (see the YAML above) or drop the
field. _verify_kueue_local_queue reports FAIL for both causes but names
them separately: “Kueue is not installed, but LocalQueue ‘<name>’ was
explicitly requested” versus “Kueue LocalQueue ‘<name>’ not found”. The
SKIP outcome comes from _check_kueue_queue, and only when no queue was
requested at all.
Operator preflight warns: Kueue is installed but no queue configured.
Kueue is present on the cluster but the job bypasses it. Fix by either passing
--queue-name, setting spec.scheduling.queueName in the CR, or adding
the namespace default:
priorityClass set but preemption never happens. The
WorkloadPriorityClass value must be higher than currently admitted
workloads, and the ClusterQueue must enable preemption
(spec.preemption.reclaimWithinCohort / withinClusterQueue). See the
upstream
preemption guide.
See Also
getting-started.md— install the operator and run your first job.configuration.md— full AIPerfJob spec reference, including theschedulingblock.production.md— multi-tenant and HA considerations.