The Serving Layer
Users see TTFT and inter-token latency, not FLOPs. The API layer streams SSE deltas, handles cancellation, and exports the metrics you tune against.
- SSE streaming
- chat templates
- cancellation
- TTFT/ITL/TPOT
- load balancing
Motivation
The problem
Eighteen chapters of engine, and no way to call it. The serving layer is the part users judge you on, and a surprising number of production incidents start here. HTTP is not the hard part. The trouble is that the engine's properties leak through it in awkward ways.
Responses take tens of seconds, so they must stream. Clients disconnect mid-generation, and the engine has to notice or it leaks KV blocks. Chat models need messages rendered into a prompt with the exact control tokens they were trained on. And the metrics you export here are the only visibility you have into everything upstream.
Core idea
The solution
An OpenAI-compatible endpoint over server-sent events. Compatibility is worth more than elegance here: every client library, evaluation harness and proxy already speaks it.
@app.post("/v1/chat/completions")
async def chat(req: ChatRequest, raw: Request):
prompt = apply_chat_template(req.messages, tokenizer)
async def stream():
try:
async for token_text in engine.generate(prompt, req.sampling):
if await raw.is_disconnected():
await engine.abort(req.id) # free the KV blocks
return
chunk = {"choices": [{"delta": {"content": token_text}}]}
yield f"data: {json.dumps(chunk)}\n\n"
yield "data: [DONE]\n\n"
finally:
# Runs on client disconnect, server shutdown and exceptions
# alike. This is the line that stops the memory leak.
await engine.abort(req.id)
return StreamingResponse(stream(), media_type="text/event-stream")The finally block is not boilerplate
Mechanics
How it works
Chat templates are model-specific and unforgiving
A chat model was fine-tuned on one exact serialisation of a conversation: specific role markers, specific whitespace, a specific generation prompt at the end. Llama-3, Qwen and Mistral all differ.
Get it wrong and the model still responds, slightly worse, forever. Templates therefore ship with the model, as a Jinja string in the tokenizer config rather than something hardcoded in the server. Render that template; do not concatenate the strings yourself.
The metrics that matter
- TTFT — measured from when the request arrives, not from when the engine starts it. Queue time is the part that goes wrong under load, and excluding it hides exactly the failure you need to see.
- ITL / TPOT — the gaps between successive tokens: ITL is the distribution of individual gaps, while TPOT is a request's average, (E2E − TTFT)/(tokens − 1). Report the distribution: a mean of 20 ms with a P99 of 900 ms is a system that visibly stutters, and the mean will never show it.
- Throughput — aggregate output tokens per second. Your capacity number, and the one that trades against the other two.
- Queue depth, preemption rate, cache hit rate — the leading indicators. Preemption rate climbing is the earliest sign of the thrashing from S10.
Explore
Try it
- TTFT (measured)
- —
- ITL (measured)
- —
- output tok/s
- —
- elapsed
- 0.00 s
press the button
The user judges the API by two numbers only: how long the cursor sat still before the first word, and whether the words after it arrive steadily. Total tokens per second across the fleet is your metric, not theirs.
(no events yet)
Server-sent events: one data: line per token, terminated by data: [DONE]. Each delta carries only the new content, not the accumulated text; the client concatenates.
Drag concurrency to 128: throughput per GPU is far better, and both TTFT and inter-token latency get visibly worse. Turn off the prefix-cache hit with a 16k prompt and TTFT dominates everything. These three controls are the whole tuning surface a serving operator has.
The stream is real; the timings come from a latency model that responds to the controls. Run it at concurrency 1 and at 128, and watch the two numbers users care about degrade in exchange for throughput you cannot see. Then disable the prefix-cache hit with a 16k prompt. TTFT swamps everything, and no argument for S07 is clearer than that.
Build it
Implementation
Stop strings are the detail that catches people, because they are defined over text while generation happens in tokens.
class StopChecker:
"""Stop strings are text; generation is tokens. Buffer accordingly."""
def __init__(self, stops: list[str]):
self.stops = stops
self.window = ""
self.keep = max((len(s) for s in stops), default=0)
def push(self, text: str) -> tuple[str, bool]:
self.window += text
for s in self.stops:
if (i := self.window.find(s)) != -1:
# Emit only what precedes the stop string, then finish.
return self.window[:i], True
if self.keep <= 1: # nothing can straddle a boundary
safe, self.window = self.window, ""
return safe, False
# Hold back the last keep-1 chars: a stop string may span chunks.
split = max(0, len(self.window) - (self.keep - 1))
safe, self.window = self.window[:split], self.window[split:]
return safe, FalseDo not emit text you might have to retract
max(len(stop)) − 1 characters until you know they are safe.$ python code/s19_server.pyOnly NumPy is required — setup instructions.
Production notes
In production
- vLLM — FastAPI + Uvicorn exposing chat, completions and embeddings, with the engine in a separate process so HTTP handling never blocks the GPU loop.
- baseRT — one CLI that serves several models behind a single OpenAI-compatible HTTP API, which is the shape most local deployments want.
- llama.cpp server — the same API surface in a single C++ binary; the reference for "how small can this be".
- Routing layers — above the server, a load balancer that is prefix-cache-aware rather than round-robin, so the S07 cache is not destroyed by the deployment topology.
Exercises
- 1Implement the
n>1parameter using the copy-on-write block sharing from S06, and verify four samples cost roughly one prompt of KV rather than four. - 2Add per-key rate limiting and a queue-depth-based admission controller that returns HTTP 429 rather than accepting a request it cannot serve within the SLO.
- 3Write a load generator that reports TTFT and ITL percentiles, then use it to find the concurrency at which your P99 inter-token latency crosses 50 ms. That number is your real capacity.
Continue
What's next
Every component now exists. S20 assembles them into one engine, benchmarks it feature by feature, and shows which of the nineteen techniques actually earned its place.
Check yourself
Questions
Answer before you look. A wrong answer tells you more than a right one, because the explanation points at the part of the chapter to reread.
A client closes the browser tab mid-generation and nothing aborts the request. What is the consequence?