AI Runtime Platform
AI workloads often start as API calls but become production systems that need identity, policy, scaling, telemetry, and clear ownership
Every model call: policy + trace + budget
Andrey Lesnikov · Senior DevOps Platform Engineer · 2026-10-21 · sources checked 2026-10-21
Part of GPU scheduling on Kubernetes
When a Kubernetes cluster gets its first few GPU nodes, the default scheduler is usually enough. Problems start when several teams share the cluster. Training jobs want all their pods or none. Inference deployments want guaranteed capacity at peak. Someone's idle notebook holds a GPU for days. At that point people search for "Kueue vs Volcano", and lately "Kueue vs KAI".
These three projects are often listed side by side, but they do not sit at the same layer. This guide compares them by architecture first and by features second, because the architectural difference decides most of the trade-offs.
A note on scope: I run inference on Kubernetes in my own AI Runtime Platform (vLLM and KServe paths, KEDA scaling). My GPU Admission on Kueue project is In progress, not yet published: there is no public repository and no measurements yet. So everything below comes from the projects' current documentation (links in References), not from benchmarks of mine. There are no performance numbers here because I do not have any I could stand behind.
schedulerName: volcano) plus its own job API. The CNCF accepted it as a batch scheduling project.schedulerName: kai-scheduler and a queue label.So Kueue works next to the default scheduler. Volcano and KAI replace it for the pods that opt in.
Kueue's core objects are:
A minimal GPU setup in the current docs' API version (kueue.x-k8s.io/v1beta2; older releases used v1beta1, so check what your installed version serves with kubectl api-resources | grep kueue):
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
name: gpu-a
spec:
nodeLabels:
nvidia.com/gpu.present: "true"
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: team-a-cq
spec:
namespaceSelector: {}
cohortName: gpu-pool
resourceGroups:
- coveredResources: ["cpu", "memory", "nvidia.com/gpu"]
flavors:
- name: gpu-a
resources:
- name: "cpu"
nominalQuota: 32
- name: "memory"
nominalQuota: 256Gi
- name: "nvidia.com/gpu"
nominalQuota: 4
borrowingLimit: 2
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
namespace: team-a
name: team-a
spec:
clusterQueue: team-a-cq
The quota values above are placeholders. Kueue accounts quota nominally: it admits against what you declare, not against live node capacity. If you declare more GPUs than you have, pods get admitted and then sit Pending as unschedulable. The llm-d guide on sharing a GPU budget calls this out explicitly. Without Topology Aware Scheduling, Kueue checks aggregate quota only. An 8-GPU pod can be admitted when 8 GPUs are free in total but split across two nodes, and then it never schedules. The Kueue docs recommend enabling TAS for batch and ML workloads for this reason.
A workload joins a queue with a label. For a batch Job:
apiVersion: batch/v1
kind: Job
metadata:
name: finetune-1
namespace: team-a
labels:
kueue.x-k8s.io/queue-name: team-a
spec:
suspend: true
template:
spec:
restartPolicy: Never
containers:
- name: train
image: registry.example.com/train:1.0
resources:
limits:
nvidia.com/gpu: "1"
Inference with Kueue. Kueue lists "mixing training and inference" as a feature: serving workloads such as Deployments or StatefulSets can be managed next to batch. The llm-d project documents a pattern where each model Deployment points at its own LocalQueue. Kueue holds every replica pod at a scheduling gate until quota is free, and models in one cohort lend idle GPUs to each other and preempt to take their floor back. The same guide lists the sharp edges: the queue-name label on a Deployment is effectively one-way, gated pods pile up if the HPA asks for more than the budget, and rollouts need slack in the budget. AWS documents a similar approach for SageMaker HyperPod on EKS: high-priority classes for inference, reserved quota for spikes, and optional borrowing.
Where Kueue fits best: you want to keep kube-scheduler (and things that depend on it, such as the cluster autoscaler's ProvisioningRequest integration), you need multi-tenant quotas with borrowing and preemption, and you run a mix of Jobs, JobSets, Kubeflow, Ray and plain pods. MultiKueue adds dispatching across clusters.
Volcano brings a full scheduler and a richer job abstraction. Its docs list gang scheduling, binpack, DRF, proportion/capacity scheduling with queue quotas, heterogeneous device scheduling (including GPU sharing in CUDA and MIG modes, plus NPU), network-topology-aware scheduling, NUMA-aware scheduling, online/offline colocation, multi-cluster scheduling and descheduling. It integrates with Spark, PyTorch, TensorFlow, Ray, Flink, MPI and Kubeflow.
Queues are scheduling.volcano.sh/v1beta1 objects. The fields depend on the enabled plugin: capability is the cap, deserved is used by the capacity plugin, weight by the proportion plugin, and guarantee reserves resources other queues cannot use:
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
name: training
spec:
reclaimable: true
capability:
cpu: "64"
memory: 512Gi
nvidia.com/gpu: "8"
guarantee:
resource:
cpu: "16"
memory: 128Gi
Jobs use batch.volcano.sh/v1alpha1 with minAvailable for gang semantics:
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
name: ddp-train
spec:
schedulerName: volcano
queue: training
minAvailable: 4
maxRetry: 3
policies:
- event: PodEvicted
action: RestartJob
tasks:
- name: worker
replicas: 4
template:
spec:
restartPolicy: OnFailure
containers:
- name: worker
image: registry.example.com/ddp:1.0
resources:
limits:
nvidia.com/gpu: "1"
Where Volcano fits best: large batch and HPC-style training where gang scheduling, topology and job lifecycle policies (restart on eviction, task dependencies) matter more than staying on the default scheduler. You also get one scheduler for both online and offline workloads if you use its colocation features. The cost is operating a second scheduler and learning its plugin and configuration model.
KAI Scheduler describes itself as a Kubernetes scheduler that "optimizes GPU resource allocation for AI and machine learning workloads". It can run alongside other schedulers and requires the NVIDIA GPU Operator for GPU workloads. Its listed features include batch (gang) scheduling, bin packing and spread, workload priority with priority separated from preemptibility, hierarchical queues with quotas, limits and over-quota weights, DRF-based fairness and reclaim, time-based fairshare, a minimum guaranteed runtime before preemption, workload consolidation, elastic workloads, topology-aware scheduling, hierarchical PodGroups, DRA support, and GPU sharing.
A default two-level queue hierarchy is created at install. A queue uses scheduling.run.ai/v2:
apiVersion: scheduling.run.ai/v2
kind: Queue
metadata:
name: inference
spec:
parentQueue: default-parent-queue
resources:
gpu:
quota: 4
limit: -1
overQuotaWeight: 1
cpu:
quota: -1
limit: -1
overQuotaWeight: 1
memory:
quota: -1
limit: -1
overQuotaWeight: 1
Pods opt in with two fields:
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
labels:
kai.scheduler/queue: inference
spec:
schedulerName: kai-scheduler
containers:
- name: main
image: ubuntu
args: ["sleep", "infinity"]
resources:
limits:
nvidia.com/gpu: "1"
GPU sharing is chosen at install time: NonMemoryEnforced, HamiCore (memory isolation based on the HAMi project), NvFractions (fractioning with CUDA memory limit enforcement) or Disabled. The project documents integrations with KubeRay and with Grove/Dynamo for disaggregated serving.
Where KAI fits best: GPU-heavy clusters where fractional GPUs, hierarchical team quotas with over-quota sharing, and time-based fairness are the main requirements. Like Volcano, it is a scheduler you operate in addition to kube-scheduler.
| Question | Kueue | Volcano | KAI Scheduler |
|---|---|---|---|
| Layer | Admission/quota controller; kube-scheduler places pods | Separate batch scheduler + job API | Separate AI/GPU scheduler |
| How workloads opt in | kueue.x-k8s.io/queue-name label |
schedulerName: volcano, queue |
schedulerName: kai-scheduler, kai.scheduler/queue label |
| Queue model | ClusterQueue + LocalQueue, cohorts, borrowing limits | Queue with capability/deserved/guarantee/weight (plugin-dependent) | Hierarchical queues with quota, limit, over-quota weight |
| All-or-nothing | Supported (timeout-based with ready pods; TAS for placement-checked admission) | Gang scheduling via minAvailable/PodGroup |
Batch/gang scheduling, hierarchical PodGroups |
| GPU sharing | Quota for DRA devices, including partitionable devices | CUDA/MIG sharing, vCUDA | Fractional GPUs (several modes) |
| Serving workloads | Deployments/StatefulSets/pods supported | Online/offline colocation | Inference listed as a target; Grove/Dynamo integration |
| Multi-cluster | MultiKueue | volcano-global | Not listed in the README |
The table only lists what each project's docs state as of 21 October 2026. Read "not listed" as "check yourself", not as "missing".
These are decision rules, not results:
maxSurge.Cases
AI workloads often start as API calls but become production systems that need identity, policy, scaling, telemetry, and clear ownership
Every model call: policy + trace + budget
LLM workloads move faster than traditional platform controls and can quickly become expensive, opaque, and hard to operate
LLM serving with operability signals
AI and cloud costs can grow quietly when usage is disconnected from teams, services, and deployment changes
Token spend visible before finance week