ML Systems Lab Open interactive version →
Advanced 29 min read model servingONNXTorchScriptGPUlatency

DL Model Serving

Batching, model parallelism, TorchScript/ONNX, GPU memory, latency SLAs

Quantization shrank the model and sped up each individual forward pass. Serving is the next layer up: how to run that model efficiently under real traffic, not just one request at a time. You put a GPT-2 model behind an API. The obvious way to serve it: take one request, run it, return the answer, take the next. Each forward pass takes 50ms, so you get 20 requests per second. But watch the GPU while this happens — it is 95% idle. A GPU is a machine built to do thousands of multiplications at once, and you are feeding it one request at a time. It is a delivery truck making one trip per parcel.

So fill the truck. Stack 32 requests together and run them in a single forward pass. Because the GPU was mostly empty, those 32 finish in roughly the same 50ms as one did — 640 requests per second from the exact same hardware, no model change at all. Batching is the first and biggest lever in serving.


Dynamic batching: don't wait forever for a full truck

Waiting for exactly 32 requests is bad if traffic is slow — early requests sit around. The production fix is *dynamic batching:* set a small deadline, say 5ms, and run whatever has arrived by then. Ten requests? Batch the ten. Forty? Take a batch and queue the rest. You capture most of the batching gain while keeping the wait bounded. Every serving framework (vLLM, TGI, ONNX Runtime) does this with one config flag.


Generation has a second problem batching can't fix

When an LLM writes a reply one token at a time, producing token number *t* means paying attention to all *t−1* tokens before it. Do this naively and every new token re-computes the attention for every earlier token — the total work grows like n², so a long reply gets punishingly slow near the end.

The fix is the KV cache. The first time you process a token, you compute its attention "key" and "value" and *save them.* Every later token just reuses the saved keys and values instead of recomputing them — the work drops from n² to n. For a 512-token reply that is roughly a 512× cut in attention compute. The cost is memory: those saved tensors pile up with every token and every concurrent user. For LLaMA-7B a single token's cache is 512 KiB (2 × 32 layers × 4096 dims × 2 bytes, for K and V), so a 512-token chat holds exactly 256 MiB — and an 80 GB A100, with the ~14 GB model already loaded, has about 66 GB left for cache: room for roughly 260 such chats before it has to start queuing. This is why long context is expensive: the cache, not the weights, runs you out of memory.


One more generation bottleneck: every token, even the easy ones, pays for a full pass.

Even with the KV cache, generating text one token at a time means paying for a full forward pass through the *big* model for every single token — including the easy, predictable ones ("of," "the," a comma most sentences obviously need). Most of any sentence is exactly that predictable. What if a much smaller, much cheaper model guessed the next few tokens, and the big model only had to *check* those guesses instead of generating each one from scratch?

That's the move: let a small, fast "draft" model guess the next K tokens, then have the big model verify all K *in a single forward pass* — which costs about the same as generating one token, because checking K candidate tokens in parallel is no more expensive than one pass's worth of compute. This is speculative decoding. When the draft guessed right, you got K tokens for the price of one — typical speedups are 2–3×. Wherever the draft guessed wrong, the big model's own prediction at that position is used instead, so correctness is never traded away, only speed. The whole theme of serving: the bottleneck is almost never raw model size — it is how well you keep the GPU full through batching, caching, and quantization.

Key points

Takeaway

Throughput and latency are opposing objectives — batching 32 requests gives 32× throughput but adds queuing time, and KV cache gives 512× compute reduction for generation but consumes memory that limits concurrency — optimize for one explicitly before touching model size or architecture.

Recap

Check your understanding

Q1. A transformer model has 175B parameters in FP16. How much GPU memory is required for model weights alone? How many A100 80GB GPUs do you need? Select the TWO correct statements.

Q2. Batching requests increases GPU utilization but increases latency. How does dynamic batching work, and what is the p99 latency problem?

Q3. KV-cache stores key and value tensors from previous tokens to avoid recomputation during autoregressive generation. How does it save computation, and what is its memory cost for GPT-3 (175B) generating a sequence of length 1000?

Try it interactively

ML Systems Lab is a free interview-prep platform for ML engineers — work through the full interactive module, quizzes, and drills.

Open ML Systems Lab →