By now you've deployed inference servers on Kubernetes and you know how to request a GPU for a pod. This week we tackle a harder problem: keeping the right number of those pods running as traffic rises and falls, and routing requests to them intelligently once they exist. Get this wrong and you either waste expensive GPU capacity sitting idle, or you starve users with a growing request queue while your dashboards insist everything is fine. Why HPA breaks down for GPU inference. The Kubernetes Horizontal Pod Autoscaler (HPA) was built for a CPU/memory world: a web server under load burns more CPU, HPA sees the spike, and it adds replicas. GPU inference does not behave that way. A vLLM or Triton pod can sit at 15% CPU utilization while its GPU is completely saturated and its internal request queue is backing up by the second — CPU is just idling while the accelerator does the real work. If you point HPA at CPU or memory for a GPU-bound service, it will conclude nothing is wrong right up until users start timing out. This is the single most important idea this week: for GPU inference, the correct autoscaling signals are GPU utilization (e.g., dcgm_fi_dev_gpu_util from NVIDIA DCGM) and queue depth (from the engine's own metrics), not CPU. The production pattern is to combine those metrics with KEDA (Kubernetes Event-Driven Autoscaling), which scales workloads using external metrics rather than being limited to the built-in resource metrics HPA understands. KEDA in one paragraph. KEDA installs a ScaledObject custom resource that watches an external metrics source — in our case, Prometheus scraping vLLM's built-in /metrics endpoint — and translates that reading into a replica count, which it then feeds to the HPA machinery under the hood. You set a min and max replica bound, a target queue depth (say, 5 pending requests per pod), and KEDA handles the rest: scale-out when the queue grows, scale-in (often more cautiously, with a cooldown) when it drains. This is exactly what you'll build hands-on in the lab. Enter the KAI Scheduler. KEDA decides how many replicas you need, but something still has to decide where those pods land on a GPU cluster, especially when GPUs are shared or jobs must be placed together. That's the job of the KAI Scheduler — a high-performance Kubernetes scheduler that NVIDIA (via its Run:ai acquisition) open-sourced under Apache 2.0 in April 2025; it's now a CNCF Sandbox project. KAI was purpose-built for the full AI lifecycle, from interactive notebooks to large distributed training and inference, and it solves three problems the default Kubernetes scheduler does not:
| Capability | Default kube-scheduler | KAI Scheduler |
|---|---|---|
| GPU sharing (multiple pods per physical GPU) | Not supported natively | Supported via fractional GPU requests |
| Gang scheduling (all-or-nothing placement for distributed jobs) | Not supported — schedules pods independently | Supported — a distributed job's pods are placed together or not at all |
| Priority-based preemption (inference over training) | Basic priority classes only | Fine-grained queues let high-priority inference preempt lower-priority training on the same cluster |
| Autoscaling integration | N/A | Integrates with KubeRay to autoscale Ray workers within min/max bounds while respecting gang constraints |
KAI is also compatible with node-level autoscalers like Karpenter, so pod-level scheduling decisions (KAI) and node-level infrastructure scaling (Karpenter) work together rather than fighting each other. Load balancing in front of your replicas. Once you have a healthy set of replica pods, a Kubernetes Ingress or an NGINX load balancer decides which pod gets the next request. Two common strategies:
Neither strategy "understands" GPU queue depth the way KEDA's metrics pipeline does — this is an active area of the field, and part of why teams increasingly push scaling logic down to queue-aware metrics rather than relying on the load balancer alone. Correcting three common misconceptions:
resources.requests field. The Cluster Autoscaler only reads resources.requests — it never inspects annotations — so a pod requesting a fractional GPU via annotation can fail to trigger node scale-up even though it's technically unschedulable.By the end of this week, you'll have configured both halves of this story yourself: a KEDA ScaledObject scaling on real queue-depth metrics, and a KAI Scheduler managing GPU sharing and gang placement underneath it.