Phase 17 · Lesson 3 of 29

GPU Autoscaling on Kubernetes — Karpenter, KAI Scheduler, Gang Scheduling

Three layers, not one. Karpenter provisions nodes dynamically (under one minute, 40% faster than Cluster Autoscaler). KAI Scheduler handles gang scheduling, topology awareness, and hierarchical queues — it prevents the 7-of-8 partial allocation trap where seven nodes wait and burn on one missing GPU. Application-level autoscalers (NVIDIA Dynamo Planner, llm-d Workload Variant Autoscaler) scale on inference-specific signals — queue depth, KV cache utilization — not CPU/DCGM duty cycle. The classic HPA trap is that DCGM_FI_DEV_GPU_UTIL is a duty-cycle measurement: 100% could be 10 requests or 100. vLLM pre-allocates KV cache memory, so memory never triggers scale-down. This lesson teaches you to compose the three layers and avoid the default Karpenter WhenEmptyOrUnderutilized policy that terminates running GPU jobs mid-inference.

Lesson 3 of 29Learn~75 minutesPython (stdlib, toy queue-depth autoscaler simulator)

Before this: Lesson 2 of Phase 17 (Inference Platform Economics), lesson 4 of Phase 17 (Serving Engine Internals)

On this page

Learning Objectives

  • Diagram the three autoscaling layers (node provisioning, gang scheduling, application-level) and name the tool used at each layer.
  • Explain why DCGM_FI_DEV_GPU_UTIL is the wrong HPA signal for vLLM and name two replacements (queue depth, KV cache utilization).
  • Describe gang scheduling and the partial-allocation failure mode KAI Scheduler prevents (7 of 8 GPUs idle).
  • Name the Karpenter consolidation policy (WhenEmptyOrUnderutilized) that terminates running GPU jobs and state the 2026 safe alternative.

The Problem

Your team ships an LLM-serving service on Kubernetes. You set up HPA with DCGM_FI_DEV_GPU_UTIL as the signal. The service pins at 100% utilization during business hours. HPA never scales up — it already thinks you're full. You add a replica manually; TTFT drops. HPA still doesn't scale. The signal is lying to you.

Separately, you use Cluster Autoscaler for nodes. A 1M-token prompt arrives at 2 a.m.; the cluster spends 3 minutes provisioning a node, and the request times out.

Separately again, you deploy a 70B model requiring 8 GPUs across 2 nodes. The cluster has 7 GPUs free, scattered across 3 nodes, and needs 1 more. Cluster Autoscaler provisions a node for the 1 missing GPU. The 7 reserved GPUs sit idle for 4 minutes burning money while Kubernetes gets the last GPU up.

Three layers, three different failure modes. GPU-aware autoscaling in 2026 is not "turn on HPA." It's composing node provisioning, gang scheduling, and application-signal autoscaling.

The Concept

Layer 1 — node provisioning (Karpenter)

Karpenter watches pending pods and provisions nodes within ~45-60 seconds (Cluster Autoscaler typically takes 90-120 seconds for GPU nodes). It picks instance types dynamically per the NodePool constraint — if your pod needs 8 H100s and the cluster has no matching node, Karpenter provisions one directly instead of scaling an existing group.

The consolidation trap: Karpenter's default consolidationPolicy: WhenEmptyOrUnderutilized is dangerous for GPU pools. It will terminate a running GPU node to migrate pods to a cheaper right-sized instance. For inference workloads that means evicting running requests and reloading a 70B model on the new node. Loss is minutes of capacity plus request failures.

Safe setting for GPU pools:

disruption:
  consolidationPolicy: WhenEmpty
  consolidateAfter: 1h

Lets Karpenter consolidate truly empty nodes after an hour but never evict a running job.

Layer 2 — gang scheduling (KAI Scheduler)

KAI Scheduler (project "Karp" then renamed) handles what default kube-scheduler does not:

Gang scheduling — schedule all-or-nothing. A distributed inference pod requiring 8 GPUs either all 8 start together or none do. Without this, you get the partial-allocation trap: 7 of 8 pods start, wait indefinitely, burn money.

Topology awareness — know which GPUs share NVLink, which sit on the same rack, which have InfiniBand between them. Place pods accordingly. A DeepSeek-V3 67B tensor-parallel workload must stay on one NVLink domain; KAI Scheduler respects that.

Hierarchical queues — multiple teams compete for the same GPU pool with priority and quota. Team A's production pinch gets preempted by Team B's training job only if priority rules allow.

KAI is deployed alongside kube-scheduler as a secondary scheduler; you annotate workloads to use it. Ray and vLLM production-stack both integrate.

Layer 3 — application-level signals

The HPA trap: DCGM_FI_DEV_GPU_UTIL is a duty-cycle metric — it measures whether the GPU was doing work at each sampling interval. 100% utilization could mean 10 concurrent requests or 100; the GPU was busy either way. Scaling on duty cycle is scaling blindly.

Worse, vLLM and similar engines pre-allocate KV cache memory (up to --gpu-memory-utilization). Memory usage stays near 90% even at one request. Memory-based HPA never scales down.

2026 replacement signals:

  • Queue depth (number of requests waiting for prefill).
  • KV cache utilization (what fraction of blocks are allocated to active sequences).
  • Per-replica P99 TTFT (your SLA signal).
  • Goodput (requests meeting all SLOs per second).

NVIDIA Dynamo Planner and llm-d Workload Variant Autoscaler consume these signals and scale replicas. They replace HPA entirely for LLM serving.

When to use what

Scale decision Tool
Add/remove nodes Karpenter
Schedule multi-GPU jobs KAI Scheduler
Add/remove replicas Dynamo Planner / llm-d WVA (or custom HPA on queue depth)
Choose GPU type Karpenter NodePool
Preempt low-priority KAI Scheduler queues

Disaggregated prefill/decode complicates everything

If you run disaggregated prefill/decode (Phase 17 · 17), you have two pod classes with different scaling triggers: prefill pods scale on queue depth, decode pods scale on KV cache pressure. llm-d exposes these as separate Services with per-role HPA. Do not try to put a single HPA in front of both.

Cold start matters here too

Cold-start mitigation (Phase 17 · 10) is where node provisioning time becomes user-visible. Karpenter's 45-60 second warm-up plus a 20GB model load plus engine init means a from-zero request takes 2-5 minutes. Keep a warm pool (min_workers=1) for SLO-critical paths, or use Modal-style checkpointing at application layer.

Numbers you should remember

  • Karpenter node provisioning: ~45-60s vs Cluster Autoscaler ~90-120s (GPU nodes).
  • KAI Scheduler prevents partial-allocation waste — 7-of-8 trap.
  • DCGM_FI_DEV_GPU_UTIL as HPA signal: broken; use queue depth or KV utilization.
  • Karpenter WhenEmptyOrUnderutilized: terminates running GPU jobs. Use WhenEmpty + consolidateAfter: 1h for inference.
autoscaling

Use It

code/main.py simulates a three-layer autoscaler on a bursty GPU workload. Compares naive HPA (duty cycle), queue-depth HPA, and KAI-gang-scheduled scaling. Reports unmet requests, idle-GPU minutes, and a composite score.

Ship It

This lesson produces outputs/skill-gpu-autoscaler-plan.md. Given cluster topology, workload shape, and SLO, it designs a three-layer autoscaling plan.

Exercises

  1. Run code/main.py. Under a bursty workload, how many requests does naive duty-cycle HPA drop that queue-depth HPA catches? Where does the difference come from?
  2. Design a Karpenter NodePool for a cluster serving Llama 3.3 70B FP8 on H100 SXM5. Specify capacity-type, disruption.consolidationPolicy, consolidateAfter, and a taint that keeps non-GPU workloads off these nodes.
  3. Your team reports that deployments are stuck in Pending because "GPUs available but pod won't schedule." Diagnose — is this Karpenter, kube-scheduler, or KAI Scheduler? Which metrics confirm?
  4. Pick a signal to autoscale disaggregated prefill pods and a different signal for decode pods. Justify both.
  5. Compute the cost of the WhenEmptyOrUnderutilized consolidation trap on a 24x7 production service that averages 60 request-dropping events/day at P99 TTFT > 10s.

Key Terms

Term What people say What it actually means
Karpenter "the node provisioner" Kubernetes node autoscaler; sub-minute provisioning
Cluster Autoscaler "the old scaler" Kubernetes node autoscaler predecessor; slower, group-based
KAI Scheduler "the GPU scheduler" Secondary scheduler for gang + topology + queues
Gang scheduling "all or nothing" Schedule N pods atomically or defer all of them
Topology awareness "rack-aware" Place pods based on NVLink/IB/rack placement
DCGM_FI_DEV_GPU_UTIL "GPU utilization" Duty-cycle metric; NOT a scaling signal for LLMs
Queue depth "waiting requests" Correct HPA signal for prefill-bound scaling
KV cache utilization "memory pressure" Correct HPA signal for decode-bound scaling
Consolidation "Karpenter consolidation" Node termination to cheaper instance type
WhenEmpty + 1h "safe consolidation" Policy that doesn't evict running GPU jobs

Further Reading

Check yourself

Quick quiz

  1. Which signal does HPA typically scale on by default that the lesson calls broken for vLLM-style serving?

    1. P99 TTFT
    2. DCGM_FI_DEV_GPU_UTIL duty cycle
    3. Queue depth
    4. KV cache utilization
    Show the answer

    B. DCGM_FI_DEV_GPU_UTIL duty cycle

  2. Which problem does gang scheduling in KAI Scheduler primarily prevent?

    1. Tokenizer GIL contention
    2. The partial-allocation trap where 7 of 8 GPUs sit idle waiting on the eighth
    3. Cold-start latency
    4. GPU memory fragmentation
    Show the answer

    B. The partial-allocation trap where 7 of 8 GPUs sit idle waiting on the eighth

  3. Why is Karpenter's default consolidationPolicy WhenEmptyOrUnderutilized dangerous for inference GPU pools?

    1. It only consolidates spot instances
    2. It prevents Karpenter from provisioning new nodes
    3. It terminates running GPU nodes to migrate pods, which evicts running requests and reloads weights
    4. It refuses to scale up under burst
    Show the answer

    C. It terminates running GPU nodes to migrate pods, which evicts running requests and reloads weights

  4. Roughly how much faster is Karpenter at provisioning a GPU node compared to Cluster Autoscaler?

    1. About 5% faster
    2. The same
    3. Roughly 40% faster (~45-60s vs ~90-120s)
    4. About 10x slower
    Show the answer

    C. Roughly 40% faster (~45-60s vs ~90-120s)

  5. For disaggregated prefill / decode pods (Phase 17 · 17), which scaling signals does the lesson recommend?

    1. Cluster Autoscaler for both
    2. A single HPA on duty cycle covering both pod classes
    3. Manual scaling only
    4. Queue depth for prefill pods and KV cache pressure for decode pods, as separate per-role HPAs
    Show the answer

    D. Queue depth for prefill pods and KV cache pressure for decode pods, as separate per-role HPAs

  6. Which Karpenter disruption setting does the lesson recommend for an inference GPU pool to avoid evicting running jobs?

    1. Always run with spot instances and no consolidation
    2. Disable Karpenter entirely
    3. consolidationPolicy: WhenEmptyOrUnderutilized with consolidateAfter: 0s
    4. consolidationPolicy: WhenEmpty with consolidateAfter: 1h
    Show the answer

    D. consolidationPolicy: WhenEmpty with consolidateAfter: 1h

Questions people ask

What is Karpenter used for with GPUs?

Karpenter is an open-source tool for Kubernetes that adds new machines when work is waiting and removes them when they go idle. For AI workloads it can pick GPU machine types on the fly, which helps teams avoid paying for large GPU servers around the clock.

What is gang scheduling and why does it matter for AI training?

Gang scheduling starts a group of related tasks only when every piece of hardware they need is free at once. Without it, a job that needs eight GPUs may grab seven and wait, burning money on idle chips. Schedulers like KAI handle this for large training and serving jobs.

Next lessonServing Engine Internals — PagedAttention, Continuous Batching, Chunked PrefillPrevious: Inference Platform Economics — Fireworks, Together, Baseten, Modal, Replicate, Anyscale

Lessons from AI Engineering from Scratch by Rohit Ghumare and contributors, MIT license. Notes for owners by Lozo. License

Next step

Rather have it built for you?

Reading is optional. Tell me what's stuck and I'll build it into the business you already run.

  • A real conversation, not a pitch
  • You keep your accounts
  • Nothing sends without your yes