the wizard

What an inference engine does: batching, KV memory and quantization

The program that runs an LLM for many people at once: loading the weights, batching requests, managing KV-cache memory. Measured on a real model.

Part 5 of 6 · How an LLM runs
  1. How an LLM works: from your text to the next token
  2. How attention works in an LLM, with real numbers
  3. What the KV cache is, and why LLMs need it
  4. Prompt caching explained: why an LLM re-reads your whole chat
  5. What an inference engine does: batching, KV memory and quantization
  6. Why LLMs run on GPUs: reading a prompt vs writing a reply

About this series

In part 1, an LLM was two things: the weights, and the program that runs them, the inference engine. All of a model's "knowledge" is in the weights. The engine's job is to run them fast and cheaply, often for many people at once. This part is about what that takes.

Three engines you'll meet: llama.cpp, whose stated goal is running models "with minimal setup and state-of-the-art performance on a wide range of hardware", and vLLM and SGLang, both built for serving many requests at once. My measurements come from SmolLM2-135M, a small open model, run with ONNX Runtime on one CPU core.


Job 1: load the weights, maybe smaller

The weights have to sit in memory for the whole time the model runs: 16.1 GB for Llama 3 8B at 16 bits per number (part 1). One way to need less is quantization: storing each number in fewer bits. llama.cpp supports 1.5-bit up to 8-bit integer formats.

Fewer bits means less memory and less to read. It also means slightly different numbers, so a quantized model is a slightly different model. I compared SmolLM2's 32-bit file with its 8-bit version, from the same release:

32-bit 8-bit
File size 540 MB 136 MB
Time per token (one CPU core) 51.4 ms 44.6 ms
Top 2 after "The capital of France is" the 26.2%, Paris 9.4% the 25.8%, Paris 11.9%
Top pick after "I opened the fridge and found a" box 5.4% bottle 7.1%

A quarter of the size and a bit faster, but the 8-bit version picked a different top word for the fridge prompt. Writing 40 tokens with the top pick each time, the two versions agreed for the first 3 tokens and then went separate ways. That's one small model and one way of quantizing; how much changes depends on both. The point stands either way: check a quantized model on your own task.

Job 2: batch many requests together

To write one token for one person, the model reads all of its weights (part 6 is about why that's slow). An engine that runs several people's next tokens in the same pass reads the weights once for all of them. That's batching.

I ran SmolLM2 writing replies for 1 to 16 requests at once, one token per request per step:

Measured on SmolLM2-135M, one CPU core: writing replies for several requests at once.
Tokens per second
tokens per second, all requests together
1 request17.3
2 requests36.4
4 requests66.1
8 requests103.8
16 requests152.8
Time per step
milliseconds per step
1 request57.8
2 requests54.9
4 requests60.5
8 requests77.1
16 requests104.7

Sixteen requests at once wrote 8.8× as many tokens per second as one, while each step took only 1.8× as long. Every person waits a little longer per token; the machine does far more work.

Job 3: keep the batch full

Requests don't arrive together and don't finish together. If an engine waited for every request in a batch to finish before starting new ones, short replies would sit idle until the longest one ended. The Orca paper (OSDI 2022) proposed scheduling at the level of each step instead: the batch is rebuilt every iteration, so a finished request leaves at once and a new one can join. vLLM lists this as continuous batching of incoming requests.

Job 4: find room for every KV cache

Every request in the batch needs its own KV cache (part 3), and each one grows by a token per step. The vLLM paper names this as the limit: the KV cache for each request "is huge and grows and shrinks dynamically", and memory wasted on it limits the batch size. Its fix, PagedAttention, stores the cache in small fixed-size blocks, the way an operating system pages memory, so little is wasted and requests can share blocks. The paper reports 2 to 4× the throughput of the systems it compared against, at the same latency.

SGLang's paper describes RadixAttention, which keeps KV caches around for reuse across calls, so requests that start with the same tokens skip re-reading them: the idea behind prompt caching (part 4), inside the engine.

Job 5: pick and return the tokens

Finally, the engine turns each request's scores into a token (temperature and the weighted pick from part 1), sends it back as it goes, and stops at the end token or the length limit.

Next, part 6: why all of this runs on GPUs, and why writing one token at a time is slow even on the fastest ones.


How I measured

← All memories