Design problems
AI-era systemsHardSDE-3 40 min

Design GPU model inference serving

Asked at
AmazonGoogleMetaMicrosoftNVIDIAOpenAI

The interviewer asks

“We want to stop paying per token to external providers for most traffic and serve open-weight LLMs (a 70B model and an 8B model, plus customer fine-tunes) on our own GPUs. Design the inference serving platform: how requests are batched, routed and scaled, and how you meet latency targets at a sensible cost.”

What a strong answer looks like

A strong answer starts from how LLM inference actually uses a GPU. Prefill (reading the prompt) is compute-bound; decode (generating tokens one at a time) is memory-bandwidth-bound, and the KV cache, not the weights, limits how many requests fit on a GPU. So the platform uses a serving engine with continuous batching and paged KV-cache memory (vLLM, TensorRT-LLM, SGLang), quantised weights (FP8), prefix caching and tensor parallelism across the GPUs in a node. In front sits a gateway with per-tenant token rate limits and priority classes, and a router that sends requests to the replica with the right model, most free KV cache and, ideally, the same prompt prefix already cached, never plain round robin. Autoscaling uses queue depth, KV-cache usage and time-to-first-token, not CPU, and must account for multi-minute cold starts of 70 GB of weights. Strong candidates derive fleet size from tokens per second, define SLOs as TTFT and time per output token, separate interactive from batch traffic, serve many fine-tunes as LoRA adapters on one base model, and handle GPU failures that take down a whole tensor-parallel group.

Concepts to mention

prefill is compute-bound, decode is memory-bandwidth-boundKV cache size per token limits batch sizecontinuous batching and PagedAttentionprefix caching and chunked prefillTTFT and time-per-output-token SLOstensor parallelism and failure domainsload- and prefix-aware routing (bounded-load consistent hashing)multi-LoRA serving on a shared base modelautoscaling on queue/KV/TTFT with cold-start mitigationinteractive vs preemptible batch priority

1 Requirements

Functional

  • OpenAI-compatible chat/completions API with streaming (SSE) for internal services
  • Serve a 70B model, an 8B model and hundreds of customer LoRA fine-tunes of the 8B model
  • Priority classes: interactive (chat, voice) and batch (summaries, offline evaluation, embeddings backfills)
  • Per-tenant and per-feature token rate limits and usage metering
  • Model rollout: deploy a new model version with canary traffic and fast rollback
  • Fallback to an external provider when our capacity is exhausted

Non-functional

  • Interactive SLOs: time to first token (TTFT) p95 < 800 ms; time per output token (TPOT) p95 < 50 ms (≥ 20 tokens/s per user)
  • Peak 500 requests/s on the 70B model (avg 1,500 input, 300 output tokens) and 2,000 requests/s on the 8B model
  • GPU utilisation: keep expensive GPUs > 60% busy over the day by filling troughs with batch work
  • Availability 99.9% for interactive traffic; a single GPU or node failure must not drop more than that node's in-flight requests
  • Cost per 1M output tokens below the external provider price we replace (tracked daily)

Out of scope (say it out loud)

  • Training and fine-tuning pipelines (we only serve their output)
  • Retrieval / RAG (callers provide prompts)
  • Building our own CUDA kernels
  • Image and video models (mention what changes)

2 Estimates

Weights (70B)~140 GB FP16, ~70 GB FP870B parameters × 2 bytes = 140 GB; FP8 halves it with small quality loss. Either way it must be split across several 80 GB GPUs (tensor parallelism)
KV cache per token (70B)~0.31 MBLlama-3-70B-like shape: 2 (K and V) × 80 layers × 8 KV heads × 128 head dim × 2 bytes ≈ 327 KB per token. A 2k-token request holds ~640 MB of KV cache while it runs
Concurrent requests per 8×H100 nodeKV room for ~800; run ~128-256640 GB HBM − 70 GB FP8 weights − ~50 GB activations/overhead ≈ 500 GB for KV ÷ 0.31 MB ≈ 1.6M tokens ≈ 800 requests of 2k tokens. In practice the 50 ms TPOT target caps the running batch lower (~128-256), and the spare KV holds the prefix cache
Decode throughput per node~3-5k output tokens/sEach decode step reads all weights once: 70 GB ÷ (8 × 3.35 TB/s HBM) ≈ 2.6 ms minimum; with large batches and KV reads, steps take ~25-35 ms for ~128 sequences ≈ 4k tokens/s. Assumption for planning; benchmark your own setup
70B fleet for decode~40-50 nodes500 req/s × 300 output tokens = 150k tokens/s ÷ ~3.5k per node ≈ 43 nodes
70B fleet for prefill~8-16 nodes equivalent500 × 1,500 = 750k prompt tokens/s × 2 × 70B FLOPs ≈ 105 PFLOP/s; at ~40% of H100 FP8 peak (~0.8 PFLOP/s usable) ≈ 130 GPUs ≈ 16 nodes; prefix caching of shared system prompts can halve this
Daily GPU cost~$30-35k/day for the 70B pool at peak size~55 nodes × 8 GPUs × ~$3/GPU-hour × 24 h ≈ $32k. Scaling down at night and filling troughs with batch work is where savings come from
Cold start~2-5 minutes per 70B replicaNode provisioning (if no spare) + pulling ~70 GB at ~1-2 GB/s from S3 (35-70 s) or local NVMe cache (~10-20 s) + loading to GPUs + CUDA graph capture and warm-up

3 API

EndpointWhat it does
POST/v1/chat/completionsOpenAI-compatible; model can be 'base-70b', 'base-8b' or 'base-8b:lora/tenant42-v3'; stream=true returns SSE tokens. Headers: X-Tenant, X-Feature, X-Priority.
POST/v1/batchesSubmit a JSONL file of requests (in S3) for batch priority; results written back to S3 within a deadline (e.g. 24 h) at a lower internal price.
POST/v1/embeddingsEmbedding model endpoint, batched aggressively for throughput.
POST/admin/v1/deploymentsDeploy model@version with {gpu_type, tp_size, quantisation, min/max replicas, canary_percent}.
POST/admin/v1/lorasRegister a LoRA adapter (S3 path, base model); becomes servable without a new deployment.
GET/admin/v1/pools/{pool}/statsQueue depth, KV-cache usage, TTFT/TPOT percentiles, tokens/s and GPU utilisation per replica.

4 Data model

model_registryPostgres
model_idversionweights_uriformatquantisationtp_sizemax_contextgpu_typestatus (canary|live|retired)created_at

Small, transactional; the control plane reads it to build deployments.

lora_adaptersPostgres + S3
adapter_idtenant_idbase_models3_uriranksize_mblast_used_at

Adapters (~20-200 MB) are loaded on demand into replicas and cached LRU in GPU and host memory.

replica_stateIn-memory in the router, fed by replica heartbeats every 1 s
replica_idmodelloaded_loras[]queue_lenrunning_seqskv_cache_used_pctrecent_prefix_hashes (bloom)healthy

Routing needs fresh load data, so it is pushed by replicas, not looked up in a DB.

rate_limitsRedis Cluster
key = tpm:{tenant}:{model}token bucket {tokenslast_refill}

Charged by estimated tokens (input + max_tokens) at admission and corrected with actual usage at completion.

usage_eventsKafka → ClickHouse
request_idtenantfeaturemodellorapriorityinput_tokenscached_tokensoutput_tokensttft_mstpot_msreplica_idts

Feeds metering, cost dashboards and capacity planning.

weights_cacheS3 (source) + node-local NVMe
model/version/shard files (safetensors)

Nodes pre-pull popular models to NVMe so a new replica loads from local disk, not over the network.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALchat (SSE)submit JSONLtoken bucketadmitted requestsleast KV loadLoRA affinityfill idle capacityoverflow fallbackusageread queue/KVscale up/downdeploy replicasload weightsload LoRABatch jobsProduct servicesInference gatewayKV/prefix-awarerouter8B pool + LoRAadaptersControl plane70B pool (TP=8, vLLM)Batch queue (S3 jobs)Autoscaler (queue/KV)Kafka: usage eventsRedis token limitsS3 weights + NVMecacheClickHouse metricsExternal LLM provider
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Interactive chat request

  1. 1The support bot sends a streaming request for base-70b with X-Priority: interactive; the gateway authenticates and charges input + max_tokens against the tenant's tokens-per-minute bucket in Redis.
  2. 2The router picks a replica: hash the first ~1k tokens (system prompt + tools) to find replicas that likely hold this prefix in cache, then choose the one among them with the lowest queue and KV usage (bounded-load consistent hashing).
  3. 3The replica's scheduler adds the request to the running batch at the next step (continuous batching). Prefill runs in chunks of ~512-2,048 tokens interleaved with other requests' decode steps, so existing streams do not stall.
  4. 4Prefix caching finds the system-prompt KV blocks already in memory, so only the user's new 400 tokens are prefilled; first token comes out in ~300 ms.
  5. 5Each decode step produces one token for every sequence in the batch; tokens stream back through the router and gateway as SSE without buffering.
  6. 6On finish, the replica frees the request's KV pages, emits a usage event (input, cached, output tokens, TTFT, TPOT), and the gateway corrects the rate-limit bucket with actual tokens.

Scale up for the evening peak

  1. 1The autoscaler sees TTFT p95 trending past 600 ms and average KV usage above 85% across the 70B pool for 2 minutes.
  2. 2It first pauses batch work on the pool (batch requests are preemptible), freeing capacity in seconds.
  3. 3It asks the control plane for 6 more replicas; nodes with the model already on local NVMe are preferred, so replicas are ready in ~2 minutes instead of ~5.
  4. 4New replicas pass a canary prompt check and register with the router; the router ramps their share gradually to avoid sending a burst to cold caches.
  5. 5If queue wait still exceeds the SLO, the gateway sends overflow interactive traffic to the external provider for the same model family, tagged for cost tracking.

7 Deep dives

Prefill vs decode, and why the KV cache is the real limit

LLM inference has two phases with opposite profiles. Prefill processes all prompt tokens in parallel; it is compute-bound and fast per token, and it determines time to first token. Decode generates one token per step for each sequence; every step must read the full model weights from GPU memory, so at small batch sizes it is memory-bandwidth-bound and the GPU's compute sits idle. The fix is batching many sequences per step so one weight read serves many tokens. But each running sequence keeps its KV cache (the attention keys and values for every past token) in GPU memory, about 0.31 MB per token for a 70B model with grouped-query attention. A node with ~500 GB free for KV holds ~1.6M tokens, so batch size, and therefore throughput, is capped by memory, and long-context requests are expensive because they hold memory for their whole life. This is why quantising weights to FP8 (more room for KV), quantising the KV cache itself, and limiting max context per feature directly raise throughput. It also explains the SLOs: TTFT is governed by prefill and queueing; time per output token by batch size and model size. Pushing batch size up raises throughput but slows every user's token rate, so the scheduler caps the batch to keep TPOT under 50 ms.

Continuous batching and paged KV memory

Static batching (wait for N requests, run them together until all finish) wastes the GPU: short answers finish early and their slots sit idle while the longest request completes, and new requests wait for the next batch. Continuous (iteration-level) batching, popularised by Orca and vLLM, re-forms the batch at every decode step: finished sequences leave, waiting ones join immediately. Memory management makes this possible. Pre-allocating max-context KV for every request wastes most of it; PagedAttention stores KV in fixed-size blocks (e.g. 16 tokens) allocated on demand, like virtual memory pages, so fragmentation is small and identical prefixes can share blocks. Prefix caching builds on this: requests that start with the same system prompt and tool definitions reuse the cached blocks and skip that part of prefill, often saving 30-70% of prefill work for products with long fixed prompts. Chunked prefill splits a long prompt into pieces mixed with decode steps, so one 30k-token document does not freeze every other user's stream. When KV memory runs out, the engine preempts the lowest-priority sequences (drop and recompute later, or swap to CPU memory), which is how batch traffic yields to interactive traffic.

Routing: load-aware and cache-aware, never round robin

Requests differ wildly in cost: a 200-token chat and a 20k-token summary are both 'one request'. Round robin therefore overloads some replicas while others idle. The router should balance on real load signals pushed by replicas every second: waiting queue length, running sequences, and KV-cache usage. It should also be cache-aware. If requests for the same feature (same system prompt, same tools) go to the same few replicas, their prefix is already cached and prefill is much cheaper; scattering them destroys the cache. A practical approach is consistent hashing on a hash of the prompt prefix with bounded load: go to the hashed replica unless it is above, say, 1.25× average load, then try the next one on the ring. For LoRA fine-tunes, add adapter affinity: route tenant42-v3 to replicas that already hold that adapter, loading it elsewhere only when needed; engines such as vLLM and LoRAX can batch requests for different adapters on the same base model, so hundreds of fine-tunes share one GPU pool instead of each needing its own. Stickiness for multi-turn chat (same conversation → same replica) also reuses the conversation's KV cache between turns.

Autoscaling with slow cold starts and expensive GPUs

CPU utilisation tells you nothing here, and even GPU utilisation is misleading because a memory-bound decode can show high utilisation at low throughput. Scale on the signals users feel: queue wait time, TTFT p95, and KV-cache usage. Cold start is the hard part: a 70B replica needs nodes (may take minutes if capacity is scarce), ~70 GB of weights and warm-up, so 2-5 minutes in total. Reactive scaling alone will breach the SLO during a sudden spike. Combine three tools. Predictive scaling from daily and weekly patterns (Indian traffic has a clear evening peak, and sales and cricket matches are scheduled). A warm buffer of 10-15% spare capacity. And a fast safety valve: batch traffic on the same pool is paused within seconds, and overflow interactive traffic can go to an external provider. Speed up cold starts by caching weights on node-local NVMe, using a fast loading format (safetensors with parallel reads), and keeping CUDA graphs and images pre-built. Scale down slowly, during troughs, and fill night-time capacity with batch jobs (offline evals, embedding backfills, summaries) so reserved GPUs are not idle; reserved or committed GPUs for the base load are far cheaper than on-demand, and spot GPUs fit only the preemptible batch tier.

8 Trade-offs

Self-host vs external API

External APIs need no GPUs or MLOps team and offer the strongest models. Self-hosting wins on cost at high, steady volume, keeps data in India for regulated workloads, and allows fine-tunes and custom latency targets. Break-even depends on utilisation: idle reserved GPUs are pure loss, so most teams self-host the high-volume, well-defined workloads and keep an external provider for hard tasks and overflow.

Tensor parallelism degree

TP=8 across one node gives the most KV memory per replica and lowest per-token latency, but every step needs all-reduce over NVLink, and one GPU failure takes the whole replica down. Two TP=4 replicas per node give more independent replicas and smaller failure domains with slightly higher latency. Never stretch TP across nodes for interactive traffic unless the interconnect is very fast.

Throughput vs latency (batch size)

Bigger batches give more tokens per GPU-second (lower cost) but slower tokens per user. Separate pools or priority classes let batch traffic run with large batches and interactive traffic with capped batch size and SLO-driven admission.

Quantisation

FP8 weights roughly halve memory and speed up both phases on H100-class GPUs with small quality loss; INT4 saves more but hurts quality on reasoning tasks. Decide per model with an evaluation run, not by default.

9 Bottlenecks

Long-context requests

A few 100k-token requests can consume a replica's KV memory and block hundreds of short requests. Cap context per feature, route long-context traffic to a dedicated pool, and use chunked prefill so long prompts do not freeze decoding for others.

Prefill-heavy bursts

RAG features with 8k-token prompts make prefill the bottleneck and TTFT spikes. Raise prefix-cache hit rates through stable prompt ordering (static parts first), and at larger scale consider disaggregated serving: separate prefill and decode GPU pools with KV transfer between them, as in NVIDIA Dynamo and similar designs.

10 Failure handling

GPU fault in a tensor-parallel group

An ECC error or NCCL hang on one GPU stalls the whole TP=8 replica. Health checks run a tiny canary generation every few seconds with a timeout; on failure the router stops sending traffic, in-flight requests fail fast with a retryable error (the gateway retries once on another replica if no tokens were streamed yet), and the node is drained and replaced.

Bad model version

A new quantised build produces subtly worse answers. Canary at 1-5% traffic with automatic comparison of evaluation scores and error rates from the eval platform; rollback is a router weight change because the old replicas stay up until the canary is promoted.

Capacity exhausted (no GPUs available in region)

Admission control protects SLOs for admitted requests: interactive requests beyond capacity go to the external provider or get a fast 429 with Retry-After; batch work waits. Never let queues grow unbounded, because every queued request already holds a client connection.

11 What impresses

  • Derives KV-cache bytes per token and concurrent capacity per node from the model shape
  • Explains why decode is memory-bound and what that means for batching
  • Sizes the fleet separately for prefill FLOPs and decode tokens/s
  • Designs prefix- and adapter-aware routing instead of round robin
  • Handles 2-5 minute cold starts with predictive scaling, warm buffer and preemptible batch
  • Treats the external provider as overflow and makes the self-host decision on utilisation and cost

12 Mistakes

  • Round-robin load balancing across replicas
  • Autoscaling GPU pods on CPU utilisation
  • Ignoring KV-cache memory and assuming batch size is free
  • Static batching or one request per GPU at a time
  • One deployment per customer fine-tune instead of LoRA adapters
  • Only measuring average latency, not TTFT and TPOT percentiles

13 Follow-ups

Q1How does speculative decoding work and when does it help?
Q2How would you serve a mixture-of-experts model differently?
Q3How do you guarantee one tenant's batch job never slows another tenant's chat?
Q4How would you decide between H100s and cheaper GPUs like L40S for the 8B model?
Q5What changes for serving an image diffusion model on the same platform?
Q6How do you meter and bill internal teams fairly for GPU usage?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free