5.3.18 · D4MLOps & Deployment

Exercises — LLM serving (vLLM, quantized inference)

3,573 words16 min readBack to topic
Figure — LLM serving (vLLM, quantized inference)

Level 1 — Recognition

Here we only recognise and recall the moving parts. No long derivations yet.

L1.1 — Classify each of the following as belonging to the prefill phase or the decode phase, and say whether it is compute-bound or memory-bound: (a) processing a 500-token prompt in one matmul; (b) generating the 501st token; (c) generating the 502nd token.

L1.2 — In the KV cache formula , name in plain words what each of the six factors (, , , , , ) stands for.

L1.3 — Using the intro figure's two buckets, match each technique to the bucket/bottleneck it attacks: techniques = {PagedAttention, INT4 weight-only quantization, continuous batching}; bottlenecks = {KV-cache memory waste (teal bucket), weight HBM read cost (orange bucket), GPU idling on padding}.

Recall Solution L1.1

(a) Prefill, compute-bound. All 500 prompt tokens are pushed through the network together in one big matmul. The GPU has tons of parallel arithmetic to chew on, so the limit is how fast it can multiply, not how fast it can fetch memory.

(b) and (c) Decode, memory-bound. After the prompt, tokens come out one at a time. Each step does only a sliver of arithmetic (a single new token) but must re-read all the model weights from HBM and read the whole KV cache. The GPU sits waiting on memory, not computing — hence memory-bound.

The dividing line is simply: many tokens at once ⇒ compute-bound (prefill); one token at a time ⇒ memory-bound (decode).

Recall Solution L1.2
  • — we store two vectors per token: a Key vector and a Value vector.
  • batch size, how many separate requests share the GPU right now.
  • sequence length, number of tokens whose K/V we have cached.
  • number of layers; every transformer layer keeps its own K/V.
  • hidden size; the length of each K (or V) vector once all heads are stacked, since .
  • bytes per element (2 for FP16, 1 for INT8), i.e. how heavy each stored number is.
Recall Solution L1.3
  • PagedAttention → KV-cache memory waste (teal bucket). It changes the layout of the KV cache into fixed-size blocks so almost nothing is reserved-but-unused.
  • INT4 weight-only quantization → weight HBM read cost (orange bucket). Smaller weights mean fewer bytes to fetch every decode step.
  • Continuous batching → GPU idling on padding. It swaps finished requests out and new ones in each step so the machine never waits on the slowest sequence.

Level 2 — Application

Now we plug numbers into the formulas we just recalled.

L2.1 — Compute the KV cache in gigabytes for a single request on a model with layers, , sequence length , FP16 (), batch . (Use bytes.)

L2.2 — You have 40 GB of KV-cache budget on a GPU. Using the same model as L2.1 (, , ) at per request, how many requests can you batch? (Take the floor.)

L2.3 — A 13-billion-parameter model. How much HBM do its weights take in FP16, in INT8, and in INT4? By what factor does the per-step weight read shrink going FP16 → INT4?

Recall Solution L2.1

Plug straight in: What we did: multiplied all six factors. Why: every stored K or V number costs bytes, and there are of them. About 1 GB for one 2048-token conversation — this is why long chats are expensive.

Recall Solution L2.2

One request costs bytes (from L2.1). The budget is bytes. What we did: divided budget by per-request cost and floored (you cannot serve a fraction of a request). Why floor: the 38th request would overflow the budget and OOM.

Recall Solution L2.3

Weight bytes with :

  • FP16 (): bytes GB.
  • INT8 (): bytes GB.
  • INT4 (): bytes GB.

Shrink factor FP16 → INT4 . Since decode is memory-bound, reading fewer weight bytes per step is the whole point of weight-only quantization.


Level 3 — Analysis

Now we compare designs and reason about why one wins.

L3.1 (fragmentation) — A naive server reserves max_len = 2048 KV slots per request but a user only ends up using tokens. What fraction of that request's reserved KV memory is wasted? Now switch to PagedAttention with block_size = 16: what is the worst-case number of wasted tokens, and what wasted fraction does that give for the same 120-token request?

L3.2 (quantization error) — Weights lie in . We use symmetric-style quantization here — meaning we describe error using the scale of the full range , the "real units per integer step" (see definition callout below). Compare INT8 () vs INT4 () by comparing their scale . By what factor is the INT4 step larger than the INT8 step? Does that match the "INT4 is twice as bad" intuition?

L3.3 (where quantization does NOT help) — Explain, using the prefill/decode distinction, why W4A16 quantization can actually make large-batch prefill slower, even though it speeds up decode.

Recall Solution L3.1

Naive. Reserved , used , wasted tokens. That is the internal-fragmentation disaster the parent note warns about.

Paged. Blocks hold 16 tokens each. tokens need blocks (since ). Those 8 blocks hold slots, so wasted tokens. The worst case for any request is block_size - 1 = 15 wasted tokens (only the tail block is ever under-full). What we did: counted only the tail-block slack. Why: paging allocates a block when needed, so every block except the last is full — waste is bounded by one block, independent of max_len.

Recall Solution L3.2

.

  • INT8:
  • INT4:

Ratio . The INT4 step is ~17× larger, and quantization error is bounded by , so INT4's error ceiling is ~17× INT8's — not 2×. The linear "twice as bad" intuition is wrong because levels scale as : vs . This is exactly why 4-bit needs outlier tricks (GPTQ and AWQ, group-wise scales).

Recall Solution L3.3

Decode is memory-bound: it re-reads all weights per single-token step, so reading 4× fewer weight bytes (INT4) directly helps — quantization wins here.

Large-batch prefill is compute-bound: many prompt tokens flow through in one big matmul, and the GPU is busy multiplying, not waiting on weight memory. But W4A16 (4-bit weights, 16-bit activations) must first dequantize INT4 weights back to FP16 before that matmul. That dequant is extra work with no memory-read savings to pay for it (compute, not memory, was the limit). So the extra dequant step can make big-batch prefill slower. Fix if you want prefill speed too: use W8A8 (8-bit weights and 8-bit activations) so the matmul itself runs in INT8 (true compute speedup), rather than weight-only INT4.


Level 4 — Synthesis

Now combine KV cache, weights, and quantization into a single serving-fits decision. Keep the intro figure's two buckets (orange = weights, teal = KV) in mind — every part below just fills these buckets and checks against the 80 GB budget.

L4.1 (total footprint) — A GPU has 80 GB HBM. You serve a 13B model (, , ). You want concurrent requests each at , with an FP16 KV cache () and FP16 weights (). (a) Weight memory in FP16? (b) Total KV cache memory for all 16 requests? (c) Does it fit in 80 GB? If not, by how much do you overflow?

L4.2 (quantize to fit) — Take the failing case from L4.1. Quantize weights to INT4 (), so this is a W4A16 setup — the KV cache stays FP16 (). New weight memory? New total? Does it fit now, and with how much headroom?

L4.3 (rescue with more batch) — After INT4 weights (L4.2), how many extra requests could you add before running out of the 80 GB, given each request's FP16 KV cache () at costs the amount you computed per-request? (Floor the answer.)

Recall Solution L4.1

Parameters restated: , , , , , KV precision (FP16), weight precision (FP16).

(a) Weights FP16 (orange bucket): bytes GB.

(b) KV cache (teal bucket), : apply . First one request (): bytes GB. Times 16:

(c) Total: GB. Purely by the two formulas this barely squeaks under (), leaving only GB — but this ignores activations, CUDA context, and fragmentation overhead, which cost several GB in practice. So it is effectively an OOM: treat it as "does not safely fit."

Recall Solution L4.2

Same model, but now INT4 weights (), KV cache unchanged at FP16 (). This is a W4A16 configuration (4-bit weights, 16-bit activations).

New weight memory (orange bucket shrinks): (down from 26 GB — saved 19.5 GB).

KV cache unchanged (teal bucket): still GB, because weight-only quantization does not touch the KV cache.

New total: GB.

Fit? Yes: , headroom GB. Quantizing weights bought back almost 20 GB — plenty of room now for activations and more batch.

Recall Solution L4.3

Free space after L4.2 GB bytes. Each request's FP16 KV cache () bytes (from L4.1(b), the value). So you could add 5 more requests ( from 16 to 21) before the KV cache eats the headroom. This is the whole story of the parent note: shrink weights → free HBM → bigger batch → more tokens/sec.


Level 5 — Mastery

One open-ended decision problem that forces you to pick the right tool for the right bottleneck. Refer back to the intro figure: your first move is always to ask which bucket (or the padding-idle problem) is the wall here?

L5.1 (diagnose and choose) — Two production scenarios. For each, identify the dominant bottleneck and choose the single most effective technique from {PagedAttention, continuous batching, W4A16 weight-only quantization, W8A8 quantization}. Justify with the formulas.

  • Scenario A: A chatbot with thousands of short sessions (average 60 tokens each), but the server reserves max_len = 2048 KV slots per request. The GPU keeps OOMing even though average usage is tiny. Which bucket is overflowing, and which single technique fixes it?
  • Scenario B: A batch document-summarisation job: huge prompts (many thousands of tokens each) processed at large batch size. Latency does not matter, but you want maximum tokens/sec throughput, and the model barely fits on the GPU. Which phase dominates, which bottleneck follows, and which single technique helps most?
Recall Solution L5.1

Scenario A → PagedAttention. Diagnose: the overflowing bucket is the teal KV cache, and the cause is memory waste, not weight cost. Each request reserves 2048 slots but uses 60, wasting of its KV allocation (internal fragmentation). The GPU OOMs on reserved-but-empty KV memory. Fix: PagedAttention allocates 16-token blocks on demand, so a 60-token session uses blocks slots — waste drops to tokens (). Freed memory ⇒ far larger batch ⇒ no OOM. (Continuous batching is a great complement but the root cause here is fragmentation.)

Scenario B → W8A8 quantization. Diagnose: huge prompts at large batch means prefill dominates, and prefill is compute-bound. Weight-only INT4 (W4A16) would not help — it even adds dequant overhead in compute-bound prefill (see L3.3). You also want the model to fit more comfortably. Fix: W8A8 (8-bit weights and 8-bit activations) halves weight bytes (helps it fit) and runs the matmul in INT8 for a genuine compute speedup — exactly what a compute-bound prefill workload needs. Since latency doesn't matter, you can also crank batch size for throughput. Do not reach for PagedAttention as the primary lever here: prompts are long, so there is little tail-block waste to reclaim relative to the compute cost.

Master takeaway: first ask "is this memory-bound (decode/KV) or compute-bound (large-batch prefill)?", then pick the lever that attacks that wall — memory tools (PagedAttention, W4A16) for memory walls, INT8 matmul (W8A8) for compute walls.


Quick self-check

Bottleneck of the decode phase
Memory-bound — reads all weights + full KV cache per single token.
Worst-case wasted tokens per request under PagedAttention with block_size B
(only the tail block is under-full).
Ratio of INT4 to INT8 quantization step for the same value range
larger step at INT4.
What does W4A16 mean
4-bit weights, 16-bit (FP16) activations — the weight-only recipe that fixes memory-bound decode.
Which lever attacks compute-bound large-batch prefill
W8A8 (INT8 matmul gives a true compute speedup); weight-only INT4 does not.
Does INT4 weight-only quantization shrink the KV cache
No — it only shrinks weights (orange bucket); KV cache (teal) stays FP16 unless separately quantized.