System Design Arena
System Design: LLM Inference Serving Platform at Scale
The infra-engineering counterpart to agent and RAG system design questions: design the serving layer that turns a trained LLM into a product endpoint — continuous batching, KV cache management, and GPU capacity planning under a strict latency budget.
Case Study
LLM Inference Serving at Scale
Same conversation, diagrams, and wrap-up you expect—now framed with clearer scaffolding and iconography.
Prompt
Design the inference serving platform for a company's LLM product: millions of requests/day, a mix of short chat replies and long streaming generations, on a fixed and expensive pool of GPUs, with a p99 time-to-first-token SLO.
Interview snapshot
- • Topic: LLM Inference Serving at Scale
- • Expected depth: 45 - 60 minutes
- • Focus areas: APIs, scale estimation, resilient architecture
- • Wrap-up: risk, monitoring, disaster recovery
Key takeaways
- • Accept chat-style and long-form generation requests, with support for streaming token output to the client.
- • Time-to-first-token p99 under a strict budget (e.g. under 1s) even at peak concurrency.
- • POST /v1/generate { prompt, maxTokens, stream } - starts a generation, returns a stream (SSE/WebSocket) of tokens or a single completion.
🎙️ Interview mode
Practice this like a real interview
Don't read the answer first. Work the framework against the bare prompt, then compare against what the interviewer expected at each step.
Scorecard
What the interviewer is evaluating
Staff-level signal
Does the candidate know the two-phase nature of inference (prefill vs decode) and design batching, KV-cache memory, and routing around it?
🧠 Staff engineer judgment
Utilization means nothing if TTFT blows up. Pick the SLO (latency vs throughput) per workload and run separate pools — one pool cannot serve both masters well.
Calibration
The common (bad) answer
Request → LB → GPU server pool → response
❌ Why this scores poorly: Treats an LLM server like a stateless web server. Generation holds gigabytes of KV cache for seconds-to-minutes per request — memory, not CPU, is the resource being scheduled.
✓ What a strong answer adds
- Continuous batching: new requests join the decode batch every step — the single biggest throughput win.
- KV cache as the scarce resource: paged attention, prefix sharing, admission control on memory.
- Separate prefill (compute-bound) from decode (memory-bound); route/schedule them differently.
- Streaming tokens with backpressure; time-to-first-token and inter-token latency as the SLOs.
- Quantization and speculative decoding as cost levers with measured quality deltas.
Build it up
Step-by-Step Walkthrough
Serving an LLM is nothing like serving a classifier: output length is unknown, one request can squat on a GPU for seconds, and the GPU pool is the dominant cost. The journey is three realizations — batch continuously, treat memory as the real bottleneck, and route by phase.
Why the classifier playbook fails
Start the obvious way: a load balancer over GPU workers, one request per GPU, run the generation, return the text. A fraud classifier is one forward pass with a fixed-size output; an LLM generates token by token, autoregressively, so a request occupies its GPU for an unknown number of steps. Utilization craters while queued requests wait for whole generations to finish.
Static batching — collect N requests, run them together — is the classic fix and it fails here for the same reason: the batch completes when its longest sequence completes, so a 10-token reply waits, finished, while a 2,000-token essay grinds on. Also non-negotiable from day one: stream tokens over SSE as they decode, because the product SLO is p99 time-to-first-token, not time-to-last.
Continuous batching: the scheduler joins every decode step
Move scheduling inside the decode loop. A continuous-batching scheduler on each worker admits new requests into the active batch at any decoding step and evicts each sequence the moment it emits its stop token — the GPU never idles waiting for a batch to drain, and short requests are no longer hostage to long ones.
This is the single highest-leverage idea in the design (it is what vLLM-class engines do), routinely worth multiple-fold throughput on real traffic. Say the batch-size trade-off out loud: deeper batches raise throughput but stretch per-step latency, so the scheduler holds a target that keeps time-to-first-token inside SLO rather than maximizing occupancy blindly.
?The whiteboard moment
Draw static vs. continuous batching as timelines — finished-but-waiting gaps versus a full pipe. 'The batch composition changes every decode step' is the sentence interviewers listen for; it shows you know why LLM serving is its own discipline.
The KV cache is the real capacity limit
Push concurrency up and you hit a wall that is not compute: the KV cache. Every token's attention keys and values are cached so decoding does not recompute the whole sequence per token — and that cache grows linearly with sequence length, living in GPU memory alongside weights that are already enormous (a 70B model in fp16 needs ~140 GB, forcing tensor parallelism across GPUs before request one arrives). Concurrent capacity is a memory budget, not a FLOPs budget.
Naive per-request contiguous allocation fragments that memory badly. Paged attention fixes it — the KV cache managed like OS virtual memory, in fixed-size pages — eliminating fragmentation and enabling prefix sharing: requests with a common system prompt reuse the same cached pages instead of duplicating them. A scheduler-integrated KV cache manager decides admission by whether pages are free.
!The capacity-planning follow-up
'How many concurrent requests fit on a GPU?' The answer is arithmetic: memory left after weights, divided by per-token KV cost times expected sequence length. Interviewers want the formula sketched, not a guess — and the observation that long-context requests are memory hogs even at low QPS.
Route by phase and by priority
Generation has two unlike phases: prefill (process the whole prompt, compute-bound, bursty) and decode (one token per step, memory-bound, steady). Mixed on one pool, a huge prompt's prefill stalls every decoding stream sharing the GPU. At sufficient scale, split them — dedicated prefill and decode pools with KV-cache pages transferred between them — so time-to-first-token and steady streaming stop fighting for the same cycles.
In front, the router becomes the policy layer: authenticate, meter tokens per tenant (accurately even for cancelled streams), route by requested model across sizes, and enforce priority admission — interactive traffic ahead of batch/offline jobs, with low-priority work queued or shed under pressure before high-priority SLOs bend. Overload must degrade by policy, not by whoever happened to arrive first.
Autoscale on leading indicators, roll out with drains
Scaling on GPU utilization fails in a way worth narrating: utilization reads high right up until the moment latency collapses, because a saturated batch and a healthy one look identical to that gauge. The autoscaler must watch leading indicators — queue depth and projected time-to-first-token — and pre-warm capacity, since pulling tens of gigabytes of weights onto a fresh GPU takes minutes, not milliseconds.
Zero-downtime rollout follows the classic drain pattern with a twist: workers on new weights take only new requests while old workers finish their in-flight generations — some running for minutes — before retiring. Close by walking the converged lane architecture: router → continuous-batching scheduler → KV cache manager → prefill/decode pools → autoscaler, and state the two governing constraints in one breath: the GPU pool is the cost, the KV cache is the capacity.
Blueprint
Architecture Diagram
The narrated components above, laid out as an actual flow — so you can see how a request moves through the system, not just read a list of pieces.
Core Architecture
Router/gateway
authenticates the request, applies rate limits and priority tiering, and routes to a model-specific pool based on requested model and current load.
Continuous-batching scheduler
runs on each GPU worker, admitting new requests into the active batch at each decode step and evicting finished ones, maximizing GPU occupancy.
KV cache manager
allocates/frees per-request cache memory (often via paged attention, treating cache like OS virtual memory pages to avoid fragmentation and support cache reuse across requests with shared prefixes).
Prefill/decode separation (at
Prefill/decode separation (at sufficient scale): dedicated pools for the compute-bound prompt-processing phase vs. the memory-bound token-generation phase, connected by cache transfer between them.
Autoscaler
scales GPU worker pools on queue depth and projected time-to-first-token, not raw utilization, since utilization lags the point where latency actually degrades.
Flow Diagram
How the API maps to this flow
- 1
POST /v1/generate { prompt, maxTokens, stream }— starts a generation, returns a stream (SSE/WebSocket) of tokens or a single completion. - 2
POST /v1/generate/{requestId}/cancel— stops an in-flight generation early and stops metering further tokens. - 3
GET /v1/models— lists available model versions/sizes and their current queue depth/load, for client-side routing decisions. - 4
GET /v1/usage— per-tenant token usage for billing and rate-limit enforcement.
?But why does this actually hold up at scale?
A 70B-parameter model in fp16 needs ~140 GB just for weights — already more than one 80 GB GPU, so multi-GPU tensor parallelism is a baseline requirement, not an optimization.
Video walkthrough
How LLM Inference Actually Works
ShowOffer · 40 min
KV cache, batching and the latency budget — the vocabulary you need before designing the serving layer.
Work the prompt yourself first — guided practice grades your own answer step by step. Watching someone else design it feels like progress and teaches you far less.
Interview flow
Dialogue timeline
Interviewer
Why is serving an LLM harder than serving a typical ML model, like a fraud classifier?
Candidate
A classifier is one forward pass with a fixed-size output. An LLM generates token-by-token, autoregressively, so a single request can occupy a GPU for seconds and the output length is unknown up front. That changes everything about batching, scheduling, and capacity planning versus a stateless request-response model.
Interviewer
How do you get good GPU utilization when requests arrive at different times and need different numbers of tokens?
Candidate
Continuous batching: instead of waiting to fill a static batch before starting, the scheduler adds a new request into the batch at any decoding step and evicts a request as soon as it finishes, so the GPU never sits idle waiting for the slowest sequence in a static batch to complete.
Interviewer
What's the KV cache and why does it matter for capacity planning?
Candidate
Each token's attention keys/values are cached so the model doesn't recompute the whole sequence on every new token — it's what makes autoregressive decoding tractable. But that cache grows linearly with sequence length and sits in GPU memory, so it's usually the actual bottleneck on how many concurrent requests fit on a GPU, not compute.
Interviewer
The product wants sub-second time-to-first-token even at peak load. How do you hit that without massively overprovisioning GPUs?
Candidate
Separate the prefill (processing the prompt) from decode (generating tokens) onto different pools if traffic allows — prefill is compute-bound and bursty, decode is memory-bound and steady. Add request-level admission control so a flood of long-generation requests can't starve short interactive ones, and autoscale the pool on a leading indicator like queue depth, not just raw GPU utilization, since utilization can look high right before latency collapses.
Scoping
Requirements & trade-offs
Functional Requirements
- —Accept chat-style and long-form generation requests, with support for streaming token output to the client.
- —Support multiple model versions/sizes concurrently (e.g. a fast small model and a slower large model) with per-request routing.
- —Enforce per-tenant rate limits and priority tiers (interactive traffic vs. batch/offline jobs).
- —Expose usage metering (tokens in/out) for billing, accurate even for requests that are cancelled mid-stream.
Non-Functional Requirements
- —Time-to-first-token p99 under a strict budget (e.g. under 1s) even at peak concurrency.
- —High GPU utilization — the GPU pool is the dominant cost, so idle GPU time is wasted money at real scale.
- —Graceful degradation under overload: shed or queue low-priority requests before violating latency SLOs on high-priority ones.
- —Zero-downtime model version rollout — swapping model weights shouldn't drop in-flight generations.
Blueprint
Architecture modules
Module 1
API Endpoints
- •POST /v1/generate { prompt, maxTokens, stream } - starts a generation, returns a stream (SSE/WebSocket) of tokens or a single completion.
- •POST /v1/generate/{requestId}/cancel - stops an in-flight generation early and stops metering further tokens.
- •GET /v1/models - lists available model versions/sizes and their current queue depth/load, for client-side routing decisions.
- •GET /v1/usage - per-tenant token usage for billing and rate-limit enforcement.
Module 2
Core Architecture
- •Router/gateway: authenticates the request, applies rate limits and priority tiering, and routes to a model-specific pool based on requested model and current load.
- •Continuous-batching scheduler: runs on each GPU worker, admitting new requests into the active batch at each decode step and evicting finished ones, maximizing GPU occupancy.
- •KV cache manager: allocates/frees per-request cache memory (often via paged attention, treating cache like OS virtual memory pages to avoid fragmentation and support cache reuse across requests with shared prefixes).
- •Prefill/decode separation (at sufficient scale): dedicated pools for the compute-bound prompt-processing phase vs. the memory-bound token-generation phase, connected by cache transfer between them.
- •Autoscaler: scales GPU worker pools on queue depth and projected time-to-first-token, not raw utilization, since utilization lags the point where latency actually degrades.
Module 3
Back-of-the-Envelope
- •A 70B-parameter model in fp16 needs ~140 GB just for weights — already more than one 80 GB GPU, so multi-GPU tensor parallelism is a baseline requirement, not an optimization.
- •KV cache per token for a large model can be several hundred KB; a 4K-token conversation across many concurrent users adds up to tens of GB of cache memory competing with weights for the same GPU memory.
- •1M requests/day averaging 300 output tokens, ~15 tokens/sec/request during decode -> back-solve for concurrent-sequences-per-GPU from available memory after weights and cache overhead, then GPU count from required throughput.
- •A queue-depth-based autoscaler needs a cold-start budget: spinning up a new GPU worker (model load included) can take tens of seconds to minutes, so scale-out has to lead demand, not react to it.
Module 4
Failure Modes & Guardrails
- •Head-of-line blocking: one request asking for 8K output tokens can starve short interactive requests in a naive batch — priority-aware scheduling and per-request token caps prevent this.
- •OOM from cache growth: a burst of long-context requests can exhaust GPU memory mid-batch — admission control needs to reserve cache headroom, not just check memory at request start.
- •Silent quality regression: swapping model versions without shadow traffic/canary comparison risks shipping a regression to 100% of users at once.
- •Metering drift: if token counting happens only at request completion, cancelled or timed-out streams undercount usage — meter incrementally as tokens are generated.
Module 5
Design Playbook
- •Open by clarifying the traffic mix (interactive chat vs. long batch generation) — it decides whether prefill/decode separation is worth the complexity.
- •Name continuous batching and KV cache explicitly; 'we batch requests together' without these terms reads as surface-level to an infra-focused interviewer.
- •Do the memory math out loud — weights + KV cache vs. GPU memory is the real capacity-planning constraint, not FLOPs.
- •Bring up autoscaling on a leading indicator (queue depth) instead of trailing GPU utilization, and explain why.
- •Close with rollout safety: canarying a new model version and rolling back without dropping in-flight streams.