Vibe Engines
YouTube
AI System Design

Design an LLM Inference Server

Learn AI system design by building an LLM inference serving system step by step.

The numbers to beat1 passprefill the prompt1 tokenper decode stepTTFTset by prefill

The whole design, in writing

Learn AI system design by building an LLM inference serving system step by step. An interactive guide covering the request queue, the prefill/decode split, continuous batching, the KV cache and paged attention, tensor sharding across GPUs, autoscaling on GPU pressure, and the latency-versus-throughput trade-offs of serving a model at scale.

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

Why is serving an LLM hard?

Running a model once on your laptop is easy. Serving it to thousands of users at once, fast and affordably, on hardware that costs dollars per hour, is not. A single request can hog a GPU for seconds while it generates tokens one at a time. How do you keep scarce GPUs full and users’ answers fast?

Clientprompt + stream
New in this step: Client.

Treat the GPU fleet as a precious, fixed resource and design everything around it: a queue to absorb spikes, continuous batching to keep GPUs full, a KV cache to make each token cheap, sharding to fit big models, and autoscaling to ride demand.

What the new pieces do

Clientclient
Sends a prompt and reads tokens back as they stream. Cares about time-to-first-token and tokens/sec.

Step 1 · The skeleton

A client, a router, a model

A client sends a prompt and wants tokens streamed back. The model lives on GPUs that can’t be exposed directly. What sits in between?

Clientprompt + streamRouteradmit + route
New in this step: Router.

Requests arrive for a model running on a GPU fleet. What fronts it?

  1. No auth, no load balancing, no limits — and a client pinned to one worker that might be busy. You need a router in front.

  2. The router authenticates, applies limits, and sends each request to a GPU replica that has room — then streams tokens back.

  3. Generations are dynamic and rarely identical, so caching whole answers barely helps. The expensive part is fresh inference, not delivery.

A Router fronts the fleet: it admits requests, applies rate limits, and routes each to a GPU Worker with capacity, then streams the generated tokens back to the Client. The classic client → router → workers spine, tuned for streaming.

What the new pieces do

Routerbackend
The front door. Authenticates, applies limits, and routes each request to a model replica with capacity.

Step 2 · Absorb the spikes

Put a queue in front of the GPUs

Traffic is bursty — quiet, then a flood. GPUs are a fixed number. If requests hit workers directly, a burst either overwhelms them or, when quiet, leaves them idle. How do you smooth this?

Routeradmit + routeRequest Queueadmission
New in this step: Request Queue.

GPU count is fixed; traffic is spiky. How do you avoid stampede AND starvation?

  1. Dropping on every spike is a terrible experience and wastes capacity that frees up moments later. Buffer first, drop only as a last resort.

  2. GPUs take minutes to provision and cost too much to hold per request. You can’t scale per-request at GPU granularity in real time.

  3. A queue absorbs bursts so GPUs stay fully fed without being stampeded, and admission control can shed load gracefully when the queue gets too deep.

A Request Queue sits between the router and the GPUs. Bursts fill the queue instead of crushing the workers; in quiet moments the queue drains and GPUs stay busy. Admission control can shed load ("busy, retry") when the queue grows too deep — protecting latency for everyone already in flight.

What the new pieces do

Request Queueservice
Holds incoming requests so the GPUs are never starved or stampeded — the buffer that absorbs spiky traffic.

Step 3 · How a token is made

Prefill, then decode

To serve a model well you have to know how it actually computes. Generating an answer isn’t one operation — it’s two very different phases with very different costs. What are they?

RouterRequest QueueGPU Workers
New in this step: GPU Workers. · swipe to pan the diagram

Generating an answer splits into two phases. What are they?

  1. Weights load once at startup, not per request. The per-request work is the two-phase prefill/decode, which dominates everything.

  2. Prefill runs the full prompt through the model in parallel (compute-heavy); decode then generates output tokens one at a time (memory-bandwidth-heavy). They scale differently.

  3. That’s a different architecture’s framing. Decoder-style LLM serving is specifically prefill (prompt) then autoregressive decode (output).

The GPU Workers do two phases. Prefill processes the entire prompt in one parallel pass (compute-bound). Decode then generates output one token per step, each step depending on the last (memory-bandwidth-bound). The asymmetry — fast parallel prefill, slow sequential decode — shapes every serving decision.

  • 1 passprefill the prompt
  • 1 tokenper decode step
  • TTFTset by prefill

What the new pieces do

GPU Workersservice
The model running on GPUs. Does a one-time prefill of the prompt, then decodes output one token per step.

Step 4 · Keep the GPUs full

Continuous batching

Decode generates one token per step per sequence — a single request barely uses the GPU. But requests start and finish at different times and have different lengths. Naive batching wastes the GPU waiting for the slowest one. How do you keep it packed?

Batch SchedulercontinuousMetrics / ControlTTFT · tok/s · util
New in this step: Batch Scheduler, Metrics / Control.

Requests have different lengths and arrive at different times. How do you batch?

  1. A single decode step uses a fraction of the GPU; everything else waits. You’re paying for hardware that mostly idles.

  2. The whole batch stalls on the longest sequence, and new arrivals wait for the next batch. Idle gaps everywhere.

  3. A scheduler packs many sequences into each GPU step, dropping finished ones and slotting in new arrivals immediately. The GPU stays saturated regardless of length mix.

A Batch Scheduler does continuous (in-flight) batching: at each decode step it runs all active sequences together, retires the ones that just finished, and slots waiting requests in immediately — no waiting for a batch to drain. The GPU stays full no matter how lengths and arrivals mix. Serving metrics (TTFT, tokens/sec, utilization) drive its decisions.

What the new pieces do

Batch Schedulerservice
Packs many in-flight requests into each GPU step, adding and retiring sequences token by token.
Metrics / Controlbus
Streams serving metrics — time-to-first-token, throughput, GPU memory — that drive batching and autoscaling.

Step 5 · Make each token cheap

The KV cache and paged attention

At each decode step, attention needs to look back over every previous token. Recomputing that for the whole sequence on every single token would be brutally quadratic. And the cache that avoids it can blow up GPU memory. How do you make decode both fast and memory-efficient?

GPU Workersbatched decodeKV Cachepaged attention
New in this step: KV Cache.

How do you avoid recomputing attention over the whole prompt every token?

  1. That’s the quadratic waste the KV cache exists to kill — recomputing all prior tokens for every new one is enormously expensive.

  2. Store the attention K/V per token so each new token reuses prior work — and page the cache in fixed blocks so many sequences pack into GPU memory without fragmentation.

  3. Answers rarely repeat verbatim, so that barely helps. The win is caching intermediate attention state within a single generation.

The KV Cache stores each token’s attention key/value state so every new token reuses prior work instead of recomputing — turning decode from quadratic to linear. Paged attention stores that cache in fixed-size blocks (like OS virtual memory), so many sequences share GPU memory without waste — which is exactly what lets continuous batching pack so many requests in.

What the new pieces do

KV Cachecache
Per-sequence attention state kept in GPU memory so each new token reuses prior work instead of recomputing.

Back of the envelope

KV cache
reuse attention state — decode goes O(n), not O(n²)
paged attention
fixed-block cache → no fragmentation, dense packing
memory caps concurrency
more KV memory = bigger batches
evict / preempt
pause low-priority sequences when memory is tight

Step 6 · Fit a giant model

Shard the weights across GPUs

A frontier model’s weights don’t fit in a single GPU’s memory. You can’t shrink the model. So how do you run something bigger than any one device?

GPU Workersbatched decodeModel Weightstensor-sharded
New in this step: Model Weights.

The model’s weights are larger than one GPU’s memory. Now what?

  1. Sometimes valid — but when you need the big model, you must run it as-is. The serving system has to handle models bigger than one device.

  2. Split each layer’s tensors across GPUs (tensor parallelism), with fast interconnect so they act as one logical worker. Pipeline parallelism splits by layer for even larger models.

  3. Disk is orders of magnitude too slow for per-token weight access. Weights must live in GPU memory — sharded if they don’t fit on one.

The Model Weights are tensor-sharded across several GPUs: each layer’s matrices are split so the GPUs compute a forward pass together over fast interconnect, acting as one logical worker. Even larger models add pipeline parallelism (split by layer). The fleet is then many such multi-GPU workers behind the scheduler.

What the new pieces do

Model Weightsstore
The parameters, split across multiple GPUs when the model is too big for one device.

Step 7 · Ride the demand

Autoscale on GPU pressure

Demand swings through the day. Provision for the peak and you burn money on idle GPUs at 3am; provision for the average and you melt at peak. GPUs aren’t instant to add. How do you size the fleet?

RouterRequest QueueBatch SchedulerGPU WorkersKV CacheAutoscaler
New in this step: Autoscaler. · swipe to pan the diagram

Demand swings hour to hour and GPUs are slow to provision. How do you size the fleet?

  1. You pay for peak capacity around the clock, idling expensive GPUs most of the day. Bleeds money.

  2. The queue smooths small bursts, but a sustained peak just grows the queue and latency without end. Buffering isn’t capacity.

  3. An autoscaler watches the real pressure signals and adds/removes GPU replicas, keeping a warm pool so scale-up isn’t cold-start slow. Match capacity to demand.

An Autoscaler watches the true pressure signals — queue depth and GPU utilization — and adds or removes replicas to hold latency targets. Because GPUs are slow to start, it keeps a warm pool and scales ahead of the curve. Under extreme load it sheds or routes overflow to a smaller, faster model.

What the new pieces do

Autoscalerservice
Watches queue depth and GPU utilization and adds or removes replicas to hold latency under load.

The payoff

You built an inference server

From "run it once" to serving at scale: a router and queue, the prefill/decode split, continuous batching, a paged KV cache, tensor-sharded weights, and autoscaling — all balancing the latency/throughput trade-off.

ClientRouterRequest QueueBatch SchedulerGPU WorkersKV CacheModel WeightsAutoscalerMetrics / Control
The finished design, end to end. · swipe to pan the diagram

Now flood the queue and feel the central tension: as load climbs, time-to-first-token rises, and you watch batching, the KV cache, and autoscaling fight to protect tail latency before the fleet stalls.

Everything you assembled, in order

  • Router + stream — admit, route, stream tokens — optimize TTFT
  • Request Queue — shock absorber between spiky demand and fixed GPUs
  • Prefill / decode — parallel prompt pass, then one token per step
  • Continuous batching — add/retire sequences each step — GPUs stay full
  • KV cache + paging — cheap tokens, dense concurrency, memory-bound
  • Tensor sharding — split big models across GPUs to fit and scale
  • Autoscaler — scale on queue depth + GPU util, keep warm pools
  • Latency vs throughput — the trade-off every knob above is balancing

Deep cut · 35:06

The building was never full — it was reserved

The interactive build above lays out the serving stack: prefill and decode, continuous batching, the paged KV cache, preemption, prefix caching, tensor parallelism and autoscaling. This film follows one question into the GPU at nine at night, finds a chip that “ran out of memory” with most of that memory empty, and then runs the two algorithms that fix it — the scheduler loop and the block allocator — line by line as pseudo code, while the building obeys them.

  • See why it breaks: a static batch waits on its longest answer, and whole-answer reservations leave most of the KV-cache memory empty — only 20–40% held real notes in the systems the PagedAttention paper measured.
  • Take it into the interview: write the continuous-batching loop and the paged allocator from memory, then name what each fix costs — slower tokens per person, special attention code, preemption work, a warm pool paid to wait.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. A sequence gets preempted when KV memory runs out. Does it restart from token 0, or resume where it left off?

    It depends on what the scheduler chose to do with the evicted KV state: if the state was swapped out to CPU memory rather than discarded, the sequence resumes from where it was once memory frees up — slower than staying resident, but not from scratch. If the state was simply dropped (cheaper, no swap overhead), the sequence has to recompute its prefill from the start. Production schedulers usually swap for short preemptions and only fully drop under sustained, severe pressure, because recomputing a long prefill is expensive.

  2. Continuous batching mixes a 50-token request with a 4000-token one. Does the long one hog memory and starve the short ones?

    Yes, proportionally — KV cache memory scales with sequence length, so one very long generation can consume the memory budget of many short ones combined, capping how many total sequences fit in a batch regardless of how "few" requests are technically running. This is exactly why paged attention’s fixed-block allocation matters: it lets the scheduler admit and evict at block granularity instead of needing one long sequence’s worst case reserved up front.

  3. How would speculative decoding — a small draft model guessing several tokens for the big model to verify at once — interact with continuous batching?

    It adds a second, smaller model into the batch scheduler’s accounting: the draft model runs ahead generating candidate tokens, and the main model verifies a chunk of them in one parallel step instead of one token at a time — when the draft guesses right, decode effectively speeds up for that sequence. The scheduler now has to budget GPU time and KV memory for both models simultaneously, and batching logic has to handle variable "tokens produced per step" per sequence instead of a clean one-token-per-step assumption.

  4. Tensor parallelism needs fast interconnect between GPUs. What actually breaks if you shard a model across GPUs on slow interconnect (say, across separate machines without NVLink)?

    Every layer of the forward pass requires the sharded GPUs to synchronize and exchange partial results, so slow interconnect turns tensor parallelism’s per-layer communication into the bottleneck — you can end up GPU-idle waiting on network transfers more than you’re compute-bound, which can make a "sharded" model SLOWER than a smaller one that fits on a single device. This is why tensor parallelism is typically kept within a single high-bandwidth node (NVLink), and pipeline parallelism (which communicates far less often, only between stages) is preferred for splitting across separate machines.

  5. How does scheduling change when one GPU pool serves several different models instead of one?

    Continuous batching assumes all in-flight sequences share one model’s weights and can be batched into the same forward pass — different models can’t share a batch step, so a multi-model fleet either partitions GPUs per model (simpler, but loses the flexibility to shift capacity between models on demand) or uses a scheduler that can rapidly swap which model’s weights are resident, trading some latency for better overall GPU utilization across an uneven mix of model demand.

Check yourself — the answers, and why

Eight steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Continuous (in-flight) batching beats static batching because…

    • It uses less memory
    • It adds and retires sequences every step, so the GPU never waits for the slowest one
    • It’s simpler

    Static batches stall on the longest sequence; continuous batching slots new requests in and drops finished ones each step, keeping GPUs saturated.

  2. The KV cache makes decode…

    • Quadratic
    • Linear — each token reuses cached attention state instead of recomputing
    • Stateless

    Caching per-token key/value state means a new token reuses prior work, turning O(n²) recomputation into O(n).

  3. Time-to-first-token is dominated by…

    • Decode
    • Prefill — the one-time parallel pass over the prompt
    • Network

    Prefill processes the whole prompt before the first output token; decode then governs tokens/sec.

  4. You should autoscale an inference fleet on…

    • CPU usage
    • Queue depth and GPU/KV-cache utilization
    • Request count alone

    GPU saturation and queue depth predict rising latency; CPU metrics miss it, and raw request count ignores token length.

  5. The KV cache runs out of room with too many long sequences active. What does the scheduler do?

    • Crash and restart the worker
    • Preempt lower-priority sequences — evict or swap their KV state — rather than fail admission outright
    • Silently truncate everyone’s context

    Memory pressure becomes a scheduling decision: preemption (with swap-and-resume where possible) keeps the fleet serving instead of failing outright or corrupting in-flight sequences.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Serve + stream: take a prompt, admit it, and stream tokens back — optimizing time-to-first-token.
  • Absorb spikes: a request queue buffers bursts and sheds load rather than stampeding the GPUs.
  • Keep GPUs full: continuous batching adds and retires sequences every decode step.
  • Cheap tokens: a paged KV cache reuses attention state so decode is linear, not quadratic.
  • Fit + scale: tensor-shard big models across GPUs; autoscale replicas on GPU pressure.

The qualities that shape everything

Each one names the mechanism that buys it.

Fast time-to-first-token
A router admits and routes to a GPU replica with capacity and streams tokens back — the path is built around streaming, and TTFT is the metric that matters.
Spiky demand on a fixed GPU fleet
A request queue decouples arrival rate from service rate, absorbing bursts and shedding load ("busy, retry") instead of stampeding or starving the workers.
Keep expensive GPUs saturated
Continuous (in-flight) batching runs all active sequences each decode step, retiring finished ones and slotting new arrivals in immediately.
Make each token cheap and concurrency dense
A KV cache reuses per-token attention state (decode goes O(n), not O(n²)), and paged attention stores it in fixed blocks so many sequences pack into GPU memory.
Run a model bigger than one GPU
Tensor-shard each layer’s weights across several GPUs over fast interconnect so they compute a forward pass as one logical worker.
Hold latency as demand swings
An autoscaler watches queue depth and GPU utilization, adding/removing replicas and keeping a warm pool because GPU starts are slow.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Fresh inference behind a router over a CDN cache of common answers

Generations are dynamic and rarely identical, so caching whole answers barely helps; the expensive part is fresh inference, which a router admits and routes to a GPU with capacity.

A request queue over spinning up a GPU per request

GPUs take minutes to provision and cost too much to hold per request; a queue absorbs bursts so a fixed fleet stays fed, with admission control to shed load gracefully.

Continuous batching over static batches

A static batch stalls on its longest sequence while new arrivals wait for it to drain; adding and retiring sequences every step keeps the GPU full regardless of the length mix.

A paged KV cache over recomputing attention every token

Recomputing all prior tokens per new token is quadratic waste; caching per-token key/value state makes decode linear, and paging it in fixed blocks packs many sequences into GPU memory.

Autoscale on GPU pressure over provisioning for peak

Sizing for peak idles expensive GPUs most of the day; scaling on queue depth and GPU utilization with a warm pool matches capacity to demand without cold-start stalls.

The answer, out loud

What a strong answer to “Design an LLM Inference Server” sounds like, first question to last trade-off. It is about 6 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    Pin down what “serving” means

    Before I draw anything, I want to agree on what we’re serving. Users send a prompt and read the answer as it streams back, so I’ll treat time-to-first-token as the number users feel and tokens per second as the number the business pays for. The fleet is a fixed set of GPUs that cost dollars an hour and take minutes to add. So the real question is: how do we keep scarce, expensive GPUs full while every user still gets a fast first token? Everything I build next is an answer to that.

  2. 3–8 min

    The skeleton, and the shock absorber

    The skeleton is a client, a router and the GPU workers. The router authenticates, applies rate limits and sends each request to a replica with room, then streams tokens back. I’m not putting a cache of whole answers in front — generations are rarely identical, so the expensive part is fresh inference, not delivery. Traffic is spiky and the GPU count isn’t, so a request queue sits between the router and the workers. It decouples arrival rate from service rate: a burst fills the queue instead of crushing the workers, and when the queue gets too deep, admission control says “busy, retry” rather than letting everyone time out.

    Built in step 2: Put a queue in front of the GPUs
  3. 8–14 min

    How a token is actually made

    Now I need to say how the model computes, because it drives every decision after this. Generation is two phases. Prefill runs the whole prompt through the model in one parallel pass — it’s compute-bound, and it sets time-to-first-token. Decode then produces one token per step, each depending on the last — it’s memory-bandwidth-bound, and it sets tokens per second. The two scale differently, so I’ll tune them separately rather than talk about “latency” as one number.

    Built in step 3: Prefill, then decode
  4. 14–22 min

    Keep the GPUs full

    One decode step for one sequence uses a sliver of the GPU, so batching is where the money is. Static batching — wait for N requests, run them to the end together — stalls the whole batch on its longest answer while new arrivals wait. I’d use continuous batching: at every decode step the scheduler runs all active sequences, retires the ones that just finished and slots waiting ones in immediately, so the GPU stays saturated whatever the length mix. The cost is that a bigger batch nudges any single request’s latency up, and I’d name that as the central trade-off of the whole design.

    Built in step 4: Continuous batching
  5. 22–30 min

    Memory is the real limit

    Attention looks back over every earlier token, so without a cache each new token recomputes the whole sequence — quadratic. A KV cache keeps each token’s key and value state in GPU memory, which makes decode linear. But that cache is now what limits concurrency: how many sequences I can batch is mostly a KV-memory question. Reserving each answer’s worst-case length up front leaves most of that memory empty — the PagedAttention paper measured only 20 to 40 percent of it holding real state. So I page it: fixed-size blocks, allocated as a sequence grows, like virtual memory. When memory still runs out, the scheduler preempts lower-priority sequences — swapping their state out to resume later, or dropping it and recomputing the prefill — so memory pressure becomes a scheduling decision, not an outage.

    Built in step 5: The KV cache and paged attention
  6. 30–36 min

    Fit the model, then ride the demand

    If the weights don’t fit on one GPU, I tensor-shard each layer across several GPUs inside one node, over fast interconnect, so they act as one worker. Across machines I’d rather use pipeline parallelism, because tensor parallelism synchronizes on every layer and a slow link turns that into the bottleneck. For demand: provisioning for peak idles GPUs most of the day, and provisioning for average melts at peak — a queue is buffering, not capacity. So an autoscaler watches queue depth and GPU and KV-cache utilization, not CPU, and keeps a warm pool because GPU starts are slow. Under extreme load it sheds, or routes overflow to a smaller, faster model.

    Built in step 6: Shard the weights across GPUs
  7. 36–42 min

    What I’d watch, and how it fails

    On the dashboard: time-to-first-token, tokens per second, GPU utilization and KV-cache headroom, because those four drive both batching and scaling. The two failures I’d plan for first. The queue floods and time-to-first-token climbs for everyone — the answer is shedding, warm replicas and overflow to a smaller model. Or a few very long generations fill the KV cache, which caps how many sequences fit however few requests are running — the answer is preemption at block granularity, which is exactly what paging made possible.

  8. 42–45 min

    Close on the trade-off

    To close, I’d restate the design in one breath: a router and a queue in front, prefill and decode treated as different problems, continuous batching over a paged KV cache, sharded weights, and autoscaling on GPU pressure. Every one of those knobs trades latency against throughput. With more time I’d go to speculative decoding and to serving several models from one pool next, because both break assumptions I’ve leaned on — one token per step, and one model per batch.

What this teaches

Learn AI system design by building an LLM inference serving system step by step. An interactive guide covering the request queue, the prefill/decode split, continuous batching, the KV cache and paged attention, tensor sharding across GPUs, autoscaling on GPU pressure, and the latency-versus-throughput trade-offs of serving a model at scale.

Key takeaways

  • Router + stream — admit, route, stream tokens — optimize TTFT
  • Request Queue — shock absorber between spiky demand and fixed GPUs
  • Prefill / decode — parallel prompt pass, then one token per step
  • Continuous batching — add/retire sequences each step — GPUs stay full
  • KV cache + paging — cheap tokens, dense concurrency, memory-bound
  • Tensor sharding — split big models across GPUs to fit and scale
  • Autoscaler — scale on queue depth + GPU util, keep warm pools
  • Latency vs throughput — the trade-off every knob above is balancing

Concepts covered

  • Why is serving an LLM hard?
  • A client, a router, a model
  • Put a queue in front of the GPUs
  • Prefill, then decode
  • Continuous batching
  • The KV cache and paged attention
  • Shard the weights across GPUs
  • Autoscale on GPU pressure
RUN IT YOURSELF

Why batching wins: forward passes

An LLM server batches many requests into one expensive GPU forward pass — that is where throughput comes from. This sketch counts the forward passes for different batch sizes, in real Python, running live. Edit the numbers and hit Run.

HOW TO READ THE CODE — 4 IDEAS
  1. Each forward pass through the model is expensive; you want as few as possible.
  2. Batching serves many requests in a single pass (step 2).
  3. Greedily fill each batch up to max_batch (step 1); the last one takes the remainder.
  4. Fewer passes = higher throughput — though bigger batches add a little latency.
CPython · WebAssembly
built to be reasoned about, not memorized — make the calls, flood the queue, run the quiz.
Finished this one? 0 / 61 AI System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More AI System Designs