Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

ch01 · What an Inference Server Actually Does

The problem

A language model is a function. You give it a sequence of token ids, it gives you back a probability distribution over what comes next. Calling it is one line.

A serving system is not a function. It has to accept requests it did not expect, from people who are watching, at a rate it does not control, using a device that costs more per hour than the engineer maintaining it. Between “call the model” and “serve the model” lies everything this book is about.

So let us build the naive thing and watch it fail. Not a strawman — the actual code most people write first, which wraps a forward pass in a loop and puts an HTTP handler in front of it.

The idea

Before any optimisation, be clear about what a request costs. Here is the full lifecycle of one request, from the socket to the last token:

  1. Receive the HTTP request and parse it.

  2. Apply the chat template, turning a list of messages into one string.

  3. Tokenise that string into token ids.

  4. Prefill: run all the prompt’s tokens through the model in one pass, producing a distribution for the position after the prompt.

  5. Sample one token from that distribution.

  6. Decode: run that single token through the model to get the next distribution. Repeat.

  7. Detokenise each new token into text, incrementally, as it is produced.

  8. Stream each fragment back to the caller.

  9. Stop on an end token, a token budget, or the caller hanging up.

Steps 4 and 6 are the only ones that touch the accelerator. Everything else is bookkeeping. That proportion — one or two expensive steps surrounded by many cheap ones — is worth holding on to, because a surprising amount of serving work is making sure the cheap steps never get in the way of the expensive ones.

Two of those steps deserve their real names now, because the whole book turns on the distinction:

They are the same arithmetic on very different shapes, and ch03 · The Arithmetic of Inference shows they are limited by completely different things. Almost every technique in this book exists because of that split.

The build

The engine interface is deliberately small. An engine takes requests, and advancing it one step produces some tokens:

base.py
class Engine(Protocol):
    name: str

    def add_request(self, request: Request) -> None:
        """Queue a request. Admission to the running set is the scheduler's decision, not this."""
        ...

    def step(self) -> list[StepOutput]:
        """Advance the engine by one iteration and return whatever tokens that produced."""
        ...

Chapter 1’s implementation makes two choices that are both wrong, on purpose:

naive.py
class NaiveEngine:
    """Serves requests strictly in arrival order, recomputing the prefix on every token."""

    name = "naive"

    def __init__(self, model: TinyGPT, *, use_cache: bool = False) -> None:
        self.model = model
        self.use_cache = use_cache
        self.waiting: list[RequestState] = []
        self.current: RequestState | None = None
        self._generator = torch.Generator(device="cpu")

One request at a time. A request that arrives while another is running waits. Not for a slot, not for a batch — for the entire preceding request to finish generating every one of its tokens.

No KV cache. Look closely at the decode branch:

naive.py
            # Decode without a cache: re-run the entire sequence. This is the quadratic cost
            # chapter 5 removes, and it is the single largest avoidable waste in this engine.
            ids = torch.tensor([state.all_token_ids], dtype=torch.long)
            logits, _ = self.model(ids)

To produce token 200, it runs the model over all 199 previous tokens again. Every one of those was computed on the previous step, and thrown away. ch05 · The KV Cache fixes exactly this.

One detail that is easy to miss: step() returns after a single token, even though this engine serves a single request. That is not how you would naturally write it, but it is what lets the harness in ch02 · Measuring What Matters distinguish the time to the first token from the time between subsequent ones. Those two numbers describe completely different experiences for the person waiting, and an engine that reports only “the request took 4 seconds” hides which one went wrong.

The measurement

Here is the baseline. Same trace, same model, same machine, at three arrival rates:

ConfigurationTTFT p50TTFT p95ITL p50Output tok/sGoodput req/s
Naive, 1 req/s0.0174s0.6816s0.0131s35.071.043
Naive, 4 req/s2.948s4.784s0.0138s72.090.447
Naive, 8 req/s4.278s7.239s0.0137s73.70.274
Naive, 16 req/s4.811s8.463s0.0137s74.490.277

Conditions: TinyGPT (reference, random weights) (5,838,080 params), 4x x86_64 CPU, torch 2.14.0+cu130, poisson, prompt 32-96, output 16-48, seed 7, arrival rate 1.0/s, measured 2026-09-13T15:25:43+00:00.

Read the first column downwards. At one request per second the server copes. By four, the median user waits seconds before a single character appears — and the output token rate has almost stopped climbing. From there, doubling the load again barely moves it.

That flat output rate is the engine’s capacity. Beyond it, extra load does not produce extra work; it produces a queue. And because the SLO in this run allows one second to the first token, almost nothing at high load meets it.

Look at the goodput column as the rate climbs, though. It goes down.

ConfigurationTTFT p50TTFT p95ITL p50Output tok/sGoodput req/s
Naive, 4 req/s2.948s4.784s0.0138s72.090.447
Naive, 8 req/s4.278s7.239s0.0137s73.70.274
Naive, 16 req/s4.811s8.463s0.0137s74.490.277

Conditions: TinyGPT (reference, random weights) (5,838,080 params), 4x x86_64 CPU, torch 2.14.0+cu130, poisson, prompt 32-96, output 16-48, seed 7, arrival rate 4.0/s, measured 2026-09-13T15:27:26+00:00.

This is the single most important shape in the book, and the reason ch02 · Measuring What Matters is about measurement rather than optimisation. A server past its capacity does not merely stop improving. It gets worse, because it spends its fixed throughput on requests whose owners have already given up waiting. Throughput says the machine is busy. Goodput says the work is wasted.

The cost

Nothing has been traded away yet — this is the baseline. But name what is wrong now, because each item is a chapter:

What it does wrongCostFixed in
Recomputes the whole prefix every tokenQuadratic work per requestch05 · The KV Cache
Serves one request at a timeDevice mostly idle; queue grows without boundch06 · Static Batching and Its Limits, ch07 · Continuous Batching
Holds a request’s memory for its whole lifetimeConcurrency capped far below what memory allowsch08 · Paged Attention and the Block Manager
Re-processes prompts it has already seenDuplicate prefill across requestsch09 · Prefix Caching

Key takeaways

Looking ahead

Every number above came from a harness that has not been described yet, and the claims in this chapter are only worth as much as that harness is. ch02 · Measuring What Matters builds it: what to measure, why means are useless here, and why a load generator written the obvious way will quietly tell you your overloaded server is healthy.

Further reading

The lifecycle above is the one every production engine implements. For how the mature systems structure it, the vLLM and SGLang papers in Appendix E are the primary sources, and both are readable once you have built the naive version yourself.