all_lessons/kubernetes_genai/10lesson 10 / 18

Part III - Production inference

Disaggregated Serving

Lesson 09 gave you autoscaling and routing that react to the real signals of LLM serving — tokens in flight, KV-cache occupancy, queue time. But all of that assumed a single kind of worker doing the whole job: read the prompt, then emit tokens. Inside that one worker live two phases with opposite appetites. Prefill chews through the whole prompt at once and is starved for raw compute; decode dribbles out one token at a time and is starved for memory bandwidth and steady, low-jitter timing. Disaggregated serving is the decision to stop running both phases on the same GPU — to split them onto separate, specialized pools — and this lesson is about when that split raises utilization and when it just hands you a distributed-systems problem with a state-transfer boundary in the middle.

Source coverage
PDF Chapter 4 — disaggregated serving: the prefill/decode split, specialized hardware pools, KV-cache transfer, and LeaderWorkerSet-style multi-pod coordination.
Linear position
Prerequisite: Lesson 09 (Autoscaling and AI-aware routing) — you can already scale and route on token throughput, KV-cache pressure, and queue depth, treating each model replica as one indivisible serving unit.
New capability: Split that unit in two — a prefill pool and a decode pool on separately sized hardware — and judge, with a break-even number, whether the utilization you reclaim beats the cost of shipping the KV cache between them.
The plan
Five moves. (1) Recall the prefill/decode asymmetry and show how a colocated server makes the two phases interfere (head-of-line blocking). (2) Define disaggregation precisely and name the new artifact it creates — a KV cache that must be transferred over the interconnect. (3) Do the worked number: size the KV cache for a real prompt and compute the transfer time, so the break-even is concrete rather than hand-wavy. (4) Lay out the colocated-vs-disaggregated trade table and the LeaderWorkerSet-style coordination the multi-pod unit needs. (5) Failure modes, a checklist, and the hand-off into observability, where a request that now crosses two pools has to be traceable end to end.

1 · Two phases, one GPU, and the interference between them

Recall the two phases of autoregressive generation. Prefill takes the entire input prompt and runs it through the model in a single forward pass, in parallel across all prompt tokens. Its by-product is the KV cache: for every layer and every attention head, the key and value vectors computed for each prompt token, stored in GPU memory so later steps do not recompute them. Prefill is compute-bound — it does a large matrix multiply over the whole prompt at once, so it saturates the GPU's arithmetic units. Decode then generates the answer one token at a time: each step reads the entire KV cache, attends over it, produces one new token, and appends that token's K and V to the cache. Decode is memory-bandwidth-bound — each step moves a lot of cached state through the chip but does comparatively little arithmetic, so it is gated by how fast the GPU can stream memory, and it is sensitive to jitter because a stall shows up directly as a stutter in the user's token stream.

In a colocated server — the normal case, where one replica does both phases — these two appetites share one GPU and one scheduler. That sharing is the problem. When a 4,000-token prompt arrives, its prefill monopolizes the compute units for a chunk of time, and the decode steps of other in-flight requests have to wait behind it. That wait is head-of-line blocking: a long, compute-heavy job at the front of the line stalls the short, latency-sensitive jobs behind it. The visible symptom is inter-token latency that spikes whenever a big prompt lands — users mid-stream see their tokens hitch even though their own request is tiny.

Why one pool can never be sized right
A colocated pool has one hardware profile but two jobs with opposite needs. Buy GPUs with huge compute for prefill and you are paying for arithmetic units that decode leaves idle while it waits on memory bandwidth. Buy GPUs tuned for bandwidth and you starve prefill. Whatever you pick, one phase runs on the wrong machine, and the two phases keep fighting over the same scheduler. That structural mismatch is the entire motivation for splitting them.

2 · What disaggregation actually does — and the artifact it creates

Disaggregated serving puts prefill on one pool of GPUs and decode on another. A request now flows across a boundary:

request --> [ prefill pool ]  build KV cache
                  |
                  |  KV transfer over the interconnect
                  v
            [ decode pool ]  read KV, emit tokens --> stream to user
prefill pool → compute-optimized GPUs, sized for throughput on whole prompts; isolated from decode so a long prompt cannot stall anyone's token stream
KV transfer → the K/V tensors built during prefill are copied from a prefill GPU to a decode GPU over the interconnect (NVLink, PCIe, or the network)
decode pool → bandwidth-optimized GPUs, sized for steady low-jitter token emission; never blocked by prefill bursts

The payoff is specialization: size and tune each pool for its bottleneck, isolate prefill bursts from decode jitter, and scale the two queues independently. The price is a brand-new distributed artifact. In a colocated server the KV cache is a local object — it is built and consumed on the same GPU and never moves. The moment you split the phases, the KV cache must be transferred: every byte of key/value state that prefill produced has to travel to whichever decode worker will continue the request. KV transfer is that copy, and its cost — in bandwidth consumed and latency added before the first token — is the tax you pay for the split. Disaggregation pays only when the utilization you reclaim is worth more than this tax.

The one-sentence mental model
Prefill wants compute; decode wants memory bandwidth and low jitter. Disaggregation works when the GPU time you save by dedicating right-sized hardware to each phase exceeds the KV-transfer and scheduling overhead the split introduces — and that comparison has an actual number.

3 · The KV-transfer test, worked out

The size of a request's KV cache is not a vibe — it is a formula. For every prompt token you store, per layer, a key and a value vector across all attention heads:

KV bytes = 2 (K,V) × layers × heads × head_dim × tokens × bytes_per_elem

Take a Llama-3-8B-class model: 32 layers, 32 heads, head_dim 128 (so hidden size 4096), serving in FP16 (2 bytes per element). A 4,000-token prompt produces:

2 × 32 × 32 × 128 × 4000 × 2 B = 2 × 32 × 4096 × 4000 × 2 B2.1 GB of KV state for that single request.

That 2.1 GB is what has to move from a prefill GPU to a decode GPU before the first answer token can be produced. Now put it on a link:

Intra-node NVLink (~400 GB/s effective)
2.1 GB / 400 GB/s ≈ 5 ms. Negligible next to a multi-hundred-ms prefill. Disaggregation across NVLink-connected GPUs is cheap.
Same-node PCIe Gen5 (~50 GB/s effective)
2.1 GB / 50 GB/s ≈ 42 ms. Noticeable but often tolerable — added to time-to-first-token, weighed against the utilization win.
Across nodes, 100 GbE (~10 GB/s effective)
2.1 GB / 10 GB/s ≈ 210 ms per request. This alone can rival or exceed the prefill it was meant to offload — the gain is erased.
Across nodes, slow/contended 10 GbE (~1 GB/s)
2.1 s. Disaggregation here is strictly worse than colocating. Never split across a link this slow.

The break-even reasoning falls straight out of those numbers. Colocating wastes some GPU time to interference; call the reclaimed time per request ΔT_save (the prefill/decode stall you avoid plus the utilization you regain from right-sized pools). Disaggregating adds the transfer time T_kv plus scheduling/coordination overhead T_sched. Disaggregation pays only when ΔT_save > T_kv + T_sched. On NVLink T_kv is ~5 ms and almost any interference saving wins; over a contended network T_kv balloons to hundreds of ms or seconds and there is no realistic ΔT_save that beats it. This is why the first design rule is placement: the prefill and decode pools must sit where the interconnect between them is fast.

Two levers that shrink the tax
The KV size scales linearly with prompt length and with precision, so two things move the break-even in your favor. (1) Shorter prompts transfer less — a 500-token prompt is ~260 MB, eight times cheaper to move than the 4,000-token one. (2) Smaller KV precision (FP8 or quantized KV cache, lesson 08) halves or quarters the bytes. If your traffic is long-prompt, high-precision, and cross-node, disaggregation is fighting uphill; if it is short-prompt or NVLink-local, the tax is rounding error.

KV-transfer break-even — does the split pay?
Set the per-request prompt length (sets KV-cache size), the interconnect bandwidth between pools (sets transfer time), and the interference you reclaim by isolating the phases. The bar compares the GPU time you save against the KV-transfer time you add. Green = disaggregation pays; red = it costs more than it saves. Model is Llama-3-8B-class (32 layers, hidden 4096, FP16).
KV-cache size
KV transfer time
Net (save − transfer)
Verdict
Show the core JS
// KV bytes = 2 * layers * hidden * tokens * bytes_per_elem  (heads*head_dim = hidden)
const kvBytes = 2 * 32 * 4096 * tokens * 2;          // FP16, Llama-3-8B-class
const kvGB    = kvBytes / 1e9;
const tKv_ms  = (kvGB / bwGBs) * 1000;               // transfer time over the link
const net_ms  = reclaimedSave_ms - tKv_ms;           // > 0 => disaggregation pays
// add a small fixed scheduling overhead to T_kv in practice (T_sched)

4 · The trade, and the multi-pod shape that runs it

Colocated (one pool, both phases)Disaggregated (split pools)
BuysSimple: one Deployment, one autoscaler, one trace per request. KV cache is local — never transferred. Easy to reason about and debug.Right-sized hardware per phase; prefill bursts isolated from decode jitter; queues tuned and scaled independently; higher aggregate utilization when bottlenecks differ.
CostsOne hardware profile must serve two opposite appetites, so one phase always runs on the wrong machine; prefill bursts cause head-of-line blocking on decode.KV cache becomes a distributed artifact that must be transferred (the tax); a request crosses two services and device pools; two autoscalers can fight; partial-request failure handling and tracing get harder.

Note the coupled-queue trap. A disaggregated system has two autoscalers — one for the prefill pool, one for the decode pool — but the two queues are not independent: every prefill completion becomes a decode arrival. If the prefill autoscaler scales up aggressively, it floods the decode queue; if decode is the bottleneck and back-pressures, prefill work piles up with nowhere to go. The two control loops can oscillate against each other. The fix is that admission control must see both queues: you do not admit a request the decode pool cannot eventually absorb, no matter how much prefill capacity is free.

The LeaderWorkerSet-style shape

Multi-pod serving — whether disaggregated, or tensor-parallel where one model is sharded across several GPUs (lesson 07) — needs the platform to treat a group of pods as one logical serving unit. The common Kubernetes shape is leader-worker: a leader pod owns the external endpoint, readiness, and coordination, while worker pods run shards or phase-specific execution. The hard part is lifecycle: every member must agree on model version, runtime config, and topology. A rollout cannot update the leader and leave workers on a mismatched artifact; a readiness check must mean the whole group is healthy, not just that the leader process is alive; and a failure policy must decide whether losing one worker drains the group, restarts it, or routes around it.

This is exactly what LeaderWorkerSet-style abstractions encode — group identity and group lifecycle — so model-parallel and disaggregated serving are not hand-stitched from unrelated Deployments that have no idea they belong together. The rule of thumb: if a request's correctness depends on several pods being in lockstep, you want a group abstraction, not N independent Deployments.

Going deeper
For the inference-runtime side of the same split — how a serving engine like vLLM actually wires prefill and decode workers and moves the KV cache between them — see vLLM P/D disaggregation. For the systems theory of why the phases have opposite resource profiles and how the transfer cost is modeled, see System ML — disaggregated serving.
Design rule
Do not start with disaggregation. Start colocated, and measure prefill and decode separately. Split only when the measurements show genuinely opposite bottlenecks, the interconnect between the would-be pools is fast enough that T_kv is small, and the platform is mature enough to operate a two-stage request path with group lifecycle and both-queue admission control. Until all three hold, disaggregation is a distributed-systems problem you are buying without a payoff.

5 · Where it goes wrong, and what to check

Failure modes

  • KV transfer erases the gain. The pools land on a slow or contended link, so T_kv (hundreds of ms to seconds per request) rivals the prefill you offloaded. Signal: time-to-first-token gets worse after disaggregating, and the interconnect NICs sit near saturation.
  • The two autoscalers fight. Prefill and decode queues are coupled (each prefill becomes a decode arrival), so independent control loops oscillate — prefill scales up, floods decode, decode back-pressures, prefill piles up. Signal: replica counts and queue depths swing in anti-phase that never settles.
  • Lost requests at the boundary. A request completes prefill, the KV state is in flight or parked, and the decode worker (or the transfer) fails — the request vanishes between pools with no owner to retry it. Signal: a gap between prefill completions and decode starts in the traces.
  • Debuggability collapses. One request now spans two services and two device pools, so a latency spike or error has no single place to look. Signal: incidents take far longer because no one trace shows the whole journey.
  • Group lifecycle skew. A rollout updates some pods of a leader-worker group but not others, leaving the unit on mismatched model versions or topology. Signal: readiness reports green while outputs are wrong or the group silently degrades.

Implementation checklist

  • Have you measured prefill and decode bottlenecks separately on a colocated baseline before splitting, and confirmed the bottlenecks are genuinely opposite?
  • Are the prefill and decode pools placed where KV-transfer bandwidth and latency make T_kv small relative to the interference you reclaim?
  • Does admission control see both queues, so you never admit work the decode pool cannot absorb?
  • Is there an owner and a retry/cleanup path for a request that completed prefill but failed before or during decode — including freeing orphaned KV state?
  • Does a single request get one end-to-end trace that spans both pools (set up in lesson 11)?
  • Is the multi-pod unit managed as a group (LeaderWorkerSet-style) so version, topology, and readiness stay consistent across a rollout?
  • Could KV precision (FP8/quantized) or shorter effective prompts shrink the transfer tax enough to change the verdict?

Checkpoint exercise

Try it
You serve a Llama-3-8B-class model. Average prompt is 3,000 tokens; you are considering splitting prefill and decode onto two pools. Compute the per-request KV-cache size (use the §3 formula), then compute T_kv for three placements: same-node NVLink (~400 GB/s), same-node PCIe (~50 GB/s), and cross-node 100 GbE (~10 GB/s). For each, state whether disaggregation can plausibly pay if isolating the phases reclaims ~80 ms of interference per request. Then decide which placement you would require before approving the split, and what you would change (KV precision? prompt length cap?) to make a marginal case pay.

Where this points next

Every failure mode above shares one root cause: a request that used to live on one GPU now crosses two pools, a transfer, and two control loops — and you cannot fix what you cannot see. A KV transfer eating the gain, two autoscalers oscillating, a request lost at the boundary, a leader-worker group skewed by a bad rollout — each is invisible until you have a trace that follows one request end to end and metrics that distinguish prefill time from decode time from transfer time. Lesson 11, Observability for LLM Serving, builds exactly that machinery: the LLM-specific metrics (TTFT, inter-token latency, KV occupancy, queue time), the distributed traces that span pools, and the signals that let you tell whether a disaggregated split is paying off or quietly costing you.

Takeaway
Prefill is compute-bound and bursty; decode is memory-bandwidth-bound and jitter-sensitive. On one colocated GPU they interfere — a long prefill head-of-line-blocks everyone's decode — and no single hardware profile fits both. Disaggregation splits them onto specialized pools, which buys right-sized hardware, isolation, and independent scaling, but creates a new artifact: the KV cache must be transferred over the interconnect. That transfer has a real size (2 × layers × hidden × tokens × bytes — ~2 GB for a 4k-token Llama-8B prompt) and a real time that depends entirely on the link: ~5 ms on NVLink, ~200 ms across a 100 GbE network, seconds on a slow one. Disaggregation pays only when the reclaimed interference exceeds T_kv + T_sched, which is why placement comes first. Watch the coupled-queue trap (admission control must see both queues), the boundary failure (own and retry partially completed requests), and group lifecycle (a LeaderWorkerSet-style abstraction keeps the multi-pod unit consistent). Measure colocated first; split only when the bottlenecks are opposite, the link is fast, and the platform can operate a two-stage path.

Interview prompts