Part 6 of 6 · Inference Engineering

Autoscaling

Scaling LLM replicas up and down — but on the right signal, around a cold start measured in minutes, and sized with Little's Law.

Dims everything but the section you're reading.
Color key — each role keeps its own hue Green = where you are / progress Blue = keywords Violet = math Coral = analogy
01 / 06 Scale on the right signal
  1. 01 Scale on the right signal
  2. 02 The cold-start surprise
  3. 03 Strategies for the lag
  4. 04 Sizing with Little's Law
  5. 05 HPA & KEDA, two changes
  6. 06 On your cluster
01

Scale on the right signal

TL;DR · Autoscale LLM replicas on concurrency, queue depth, or TTFT — never on CPU, and don't trust GPU utilization either.

A standard web service scales on CPU because CPU tracks how busy it is. An LLM server breaks that habit: the metric that classic autoscalers reach for is exactly the one that lies to you here.

The default metric is the wrong one

Classic autoscaling watches CPU and adds replicas when it climbs. For LLM serving that signal is meaningless — the heavy work runs on the GPU, so CPU stays low even while the server is saturated.

GPU utilization misleads too

The tempting fix — scale on GPU utilization — also misleads. A single decode step can pin the GPU near 100% while the server still has plenty of room to batch more requests, so high util doesn't mean "full".

Scale on demand you can feel

Use signals that track real backlog: concurrency (requests in flight), queue depth (num_requests_waiting), or TTFT (time-to-first-token). When the queue grows or first tokens slow down, that's genuine demand — add replicas.

Keywords — tap to unfold the plain meaning

Scale on concurrency / queue depth (num_requests_waiting) / TTFT — not CPU. GPU utilization is misleading for LLMs.

02

The cold-start surprise

TL;DR · A new LLM replica isn't ready in seconds — it's minutes, because it must pull a big image and load tens of GB of weights into VRAM.

In a stateless web tier, "add a replica" is nearly instant, so you can scale reactively. With LLMs the new replica takes minutes to wake up — which changes the whole shape of how you scale.

Two ways LLM autoscaling differs

Two things separate this from stateless web autoscaling. First, the scaling metric: concurrency / queue depth / TTFT instead of CPU, because GPU util misleads. Second, the cold start duration.

Cold start runs in minutes

Cold start here means minutes, not seconds: the node pulls a multi-GB container image, then loads tens of GB of weights into VRAM. Until that finishes the replica can serve nothing.

So scale before you're desperate

Because the new replica lags by minutes, reactive scaling arrives too late. You scale early — at the first sign of pressure — and keep some warm capacity ready, so demand is met while the next replica boots.

Keywords — tap to unfold the plain meaning

Analogy In a stateless web tier, adding a replica is like hiring one more waiter — they're on the floor in seconds. Opening a new LLM replica is like opening a whole new kitchen: you wheel in the ovens, fire them up, and stock the pantry before a single plate goes out. That's minutes, not seconds — so you light the second kitchen before the dinner rush, not after the tickets pile up.

A new replica costs minutes — image pull plus loading tens of GB of weights into VRAM. Scale up early and keep warm capacity, never purely reactive.

03

Strategies for the lag

TL;DR · Five levers cover the minutes-long lag: a baseline with headroom, a warm pool, predictive scaling, faster cold starts, and scale-to-zero only when you can tolerate it.

If you can't make the new replica appear instantly, you work around the wait. Each strategy either keeps spare capacity ready or shrinks the cold start itself.

Keep capacity ahead of demand

Baseline + headroom keeps always-on replicas sized to absorb normal bursts. A warm pool goes further: pre-loaded replicas on standby, weights already in VRAM, ready the instant traffic spikes.

Anticipate, don't just react

Predictive scaling adds capacity ahead of known patterns — the daily peak, the scheduled batch job — so the minutes-long boot finishes before the load actually arrives.

Shrink the cold start itself

Faster cold start attacks the wait directly: NVMe weight caching so weights load from fast local disk, smaller images to pull, and quantized variants that are simply fewer bytes to move into VRAM.

Scale-to-zero, with eyes open

Scale-to-zero drops idle replicas to none — cheapest, but the first request after that eats the full minutes-long cold start. Use it only when that first-request latency is acceptable.

Keywords — tap to unfold the plain meaning

Baseline + headroom, warm pool, predictive scaling, faster cold start (NVMe cache, smaller images, quantized variants), and scale-to-zero only when minutes-long first-request latency is OK.

04

Sizing with Little's Law

TL;DR · One replica's throughput ≈ its concurrency cap ÷ average latency; replicas needed ≈ peak QPS ÷ that. Two divisions and you have your floor.

Strategy is good, but you still need a number: how many replicas? Little's Law turns the concurrency cap and the latency you measured into a count you can put in your config.

One replica's ceiling

A replica runs at most --max-num-seqs sequences at once — its concurrency cap. Divide that cap by average latency and you get its max throughput in requests per second.

From ceiling to replica count

Now divide your peak QPS (queries per second at the busiest moment) by one replica's throughput. That's roughly how many replicas you need to keep up — the minimum before you add headroom.

Math, decoded

Treplicamax-num-seqsavg latency  →  Npeak QPSTreplica
  • max-num-seqsthe concurrency cap — how many sequences one replica serves at once (e.g. 8 or 64)
  • avg latencyhow long, on average, one request takes start to finish
  • Treplicaone replica's max throughput — its concurrency cap divided by latency
  • peak QPSqueries per second at the busiest moment you must serve
  • Nreplicas needed: peak QPS divided by what one replica can handle

A replica's max throughput ≈ its concurrency cap (--max-num-seqs) ÷ average latency; replicas needed ≈ peak QPS ÷ that. This is Little's Law applied to LLM serving — concurrency relates to throughput through latency.

Keywords — tap to unfold the plain meaning

Analogy One kitchen can plate so many tickets at once — its station count — and each ticket takes about so long. Divide the stations by the time per ticket and you know how many plates that kitchen pushes per minute. Divide the dinner-rush order rate by that, and you know how many kitchens to keep lit. Little's Law is just doing the same arithmetic for replicas instead of kitchens.

Replica throughput ≈ concurrency cap ÷ latency; replicas ≈ peak QPS ÷ that. Size from the cap and the latency you actually measured.

05

HPA & KEDA, two changes

TL;DR · Keep your Kubernetes HPA or KEDA — just make two changes: scale on a custom metric, and treat cold start as pod-startup-latency on steroids.

You don't throw out the platform. The familiar Horizontal Pod Autoscaler still drives this — you only swap the metric it watches and adjust how fast it scales each direction.

Change 1 — a custom metric

Run the HPA (Horizontal Pod Autoscaler) or KEDA as usual, but feed it a custom metricnum_requests_waiting, concurrency, or TTFT — instead of CPU. The autoscaler is fine; the default signal isn't.

Change 2 — respect the cold start

Treat cold start as pod-startup-latency on steroids: it's a multi-GB weight load, not a quick boot. So pair it with warm pools, NVMe weight caches, and asymmetric windows — scale-up-early with slow-scale-down.

Autoscaling loop: a custom metric (queue depth / TTFT) drives the HPA or KEDA controller, which adjusts replica count; new replicas take minutes to cold-start before serving. scale on the queue, not CPU — and budget for the cold start custom metric num_requests_waiting HPA / KEDA decide replica count replicas vLLM pods new replica cold start: minutes image + 27 GB → VRAM serving drains the queue — feedback closes the loop
The same HPA/KEDA control loop you already run — only the input metric and the cold-start budget change. New replicas need minutes before they serve, so you scale up early.

Keywords — tap to unfold the plain meaning

SRE note HPA/KEDA, two changes. (1) Scale on a custom metric — num_requests_waiting / concurrency / TTFT, not CPU. (2) Treat cold start as pod-startup-latency on steroids (multi-GB weight load) → warm pools, NVMe weight caches, scale-up-early / slow-scale-down windows. Companion lab: autoscaling-concurrency-cold-starts.ipynb.

Keep HPA or KEDA; change two things — scale on a custom metric (not CPU), and budget for cold start with warm pools, NVMe caches, and scale-up-early / slow-scale-down windows.

06

On your cluster

TL;DR · Autoscale vLLM on num_requests_waiting + TTFT-P99 (not GPU util), budget a 27 GB FP8 weight load for cold start, and cap each replica with --max-num-seqs.

Here it all lands on the 4×H100 box: the signal to scale on, the number to budget for cold start, and the per-replica cap that feeds Little's Law.

The signal to wire up

Autoscale your vLLM replicas on num_requests_waiting plus TTFT-P99 (the 99th-percentile time-to-first-token) — not GPU utilization. Those two track real backlog the way CPU and util can't.

The cold-start budget

Budget for a 27 GB FP8 weight load on every cold start. Recall the byte math: FP8 is 1 byte per number; FP16/BF16 is 2 bytes — so the same model at BF16 would be ~54 GB to move into VRAM.

The per-replica cap

Each replica's concurrency cap is --max-num-seqs — on this box, 8 or 64. That number is the numerator in Little's Law: it sets one replica's ceiling, and therefore how many replicas peak QPS demands.

Cluster note On the 4×H100 box: autoscale vLLM on num_requests_waiting + TTFT-P99 (not GPU util). Cold-start budget = 27 GB FP8 weight load (FP8 = 1 byte/number; FP16/BF16 = 2 bytes). Per-replica cap = --max-num-seqs (8 / 64). Companion lab: autoscaling-concurrency-cold-starts.ipynb.

Keywords — tap to unfold the plain meaning

Check yourself

  1. Autoscaling our LLM is different from our web tier because…?
  2. Which signals should drive the autoscaler, and why is GPU utilization misleading?
  3. Using Little's Law, how do you turn --max-num-seqs, latency, and peak QPS into a replica count?

Request queue/concurrency/TTFT drives scaling (not CPU); GPU util misleads. Cold starts span minutes (tens of GB into VRAM), so keep warm baselines and scale up early. Size with Little's Law: peak QPS ÷ (concurrency cap ÷ latency).

Reached the end — nice. This lesson now counts toward your progress.