LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 158 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/8
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/11
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageClustering and PCACore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/24
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernanceData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/8
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned Classifier
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/15
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/19
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning & Test-Time Compute
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
Back to Topics
LearnInference & Production ScaleScaling LLM Inference
🚀HardInference Optimization

Scaling LLM Inference

Explains why decode-heavy LLM serving is often memory-bound and how KV-cache design, batching, PagedAttention, and speculative decoding improve scale.

43 min read
Learning path
Step 133 of 158 in the full curriculum
Continuous Batching & SchedulingModel Parallelism for LLM Inference

Continuous batching keeps decode slots useful. The capacity question for scaling large language model (LLM) inference is harder: when requests, model weights, and KV state all compete for HBM, what limits serving concurrency?

A code assistant that answers "why is this test failing?" still generates every response one token at a time. It's not because the model is "thinking." During decode, the serving stack keeps rereading billions of model weights from GPU memory and consulting a growing KV cache, and that memory movement takes time. Reading the prompt happens in parallel; streaming the answer repeatedly revisits the same large weights and cache state.

Tie together prefill, decode, batching, KV-cache memory, PagedAttention, disaggregation, speculative decoding, and quantization so serving bottlenecks become measurable instead of mysterious. The thread running through all of them is one decision: where to sit on the throughput, latency, and cost triangle for your workload. For the scheduling loop behind this capacity model, see continuous batching.

The two phases of generation

LLM inference is distinct from training because it consists of two very different computational phases: Prefill and Decode. Understanding this distinction is the first step to optimization.

A code-assistant request makes the two phases visible. When a user sends "Why did auth.test.ts fail after this refactor?", the system first has to read the entire prompt and attached context. That's the prefill phase. Then it starts answering, generating one token at a time: "The", "mock", "still", "uses", "the", "old", "schema." That's the decode phase.

Prefill: reading the prompt in one go

In the prefill phase, the model processes the entire user prompt in parallel. This is similar to training: the GPU receives a matrix of shape [batch_size, prompt_len, hidden_dim] and computes attention for all tokens simultaneously.

Because all the input tokens are known upfront, the attention mechanism can compute the interactions between every token in the prompt at once. This parallel processing allows the GPU to use its massive matrix multiplication engines efficiently. A long prefill usually dominates Time To First Token (TTFT), though TTFT also includes queueing and scheduling delay before the first output token is emitted. This visual shows how all tokens in the prompt are processed simultaneously to generate the first output token.

Prefill phase diagram showing known prompt tokens processed in parallel by large matrix operations before the first output token. Prefill phase diagram showing known prompt tokens processed in parallel by large matrix operations before the first output token.
Prefill is the parallel prompt-processing phase. It usually dominates TTFT for long prompts because the first output token can't be emitted until the prompt has been processed.

Key characteristics

  • Often compute-heavy: Prefill exposes large matrix multiplications. FlashAttention keeps attention exact while reducing HBM traffic relative to materializing the full attention matrix; it doesn't make all attention IO linear in sequence length.[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135
  • Parallel-friendly: Processing many prompt positions together can drive much higher tensor-core utilization than one-token decode. Whether it saturates compute depends on sequence shape, kernel, and hardware.
  • Latency: Time usually grows with prompt length, and long prompts often dominate TTFT.

Decode: answering one word at a time

Once the first token is generated, the model switches to autoregressive generation. It generates one token at a time, feeding it back as input for the next step.

Unlike the prefill phase, decoding can't be parallelized across tokens inside one request because each new token depends on the previous ones. The system is locked into a sequential, step-by-step loop. Users feel this as inter-token latency (ITL), also called time between tokens (TBT): the delay between visible streamed tokens. Time Per Output Token (TPOT) is closely related but isn't identical. DistServe, for example, defines TPOT as the average time to generate each output token after the first.[2]Reference 2DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving.https://arxiv.org/abs/2401.09670

A rate such as tokens per second (TPS) is another view of decode speed. Be careful about the denominator: one stream's TPS isn't the same metric as aggregate tokens per second across a whole server. TTFT affects initial responsiveness, while ITL/TBT and TPOT describe the pace after streaming starts. During autoregressive decoding, each generated token becomes input for the next step.

Decode loop diagram showing one output token produced per request step while shared weight reads and per-request KV state feed the next step. Decode loop diagram showing one output token produced per request step while shared weight reads and per-request KV state feed the next step.
Decode is a sequential memory loop. Batching can amortize repeated weight reads across requests, but each request still advances one generated token at a time.

Key characteristics

  • Often memory-bound: A decode step needs model weights and the KV state used by attention. At small or latency-sensitive batches, repeated reads commonly make HBM bandwidth the ceiling; batching can raise arithmetic intensity by sharing weight reads across active requests.
  • Low arithmetic intensity at small batches: The arithmetic intensity (FLOPs/byte, i.e., Floating Point Operations per byte of data loaded) can be low because the runtime moves large tensors for only one new position per sequence.

Why decode is memory-bound

Decode-heavy LLM serving is often memory-bandwidth bound, not compute-bound. In a compute-bound operation, the system is bottlenecked by the mathematical calculations it must perform. Training and long-prefill workloads typically expose much larger matrix operations than interactive decode, so they can drive compute hardware more effectively.

During token generation, the bottleneck often shifts toward memory movement. Each new token needs model weights and attention state, but contributes only one new position per active request. At modest decode batches, this produces low arithmetic intensity and makes HBM traffic a central constraint.

A 7 billion parameter model stored in 16-bit precision has weights of about 14 GB in decimal units. If one uncached decode step had to read that full weight footprint for one active token, the weight-read lower bound alone would be about 14 GB per step. Real kernels, cache reuse, batch size, tensor parallelism, and KV traffic determine the observed bandwidth cost.

When profiling confirms this bandwidth ceiling, serving work should focus on bytes moved, cache residency, batch policy, and queueing behavior rather than only raw floating-point throughput.

Inference phase comparison showing prefill as compute-heavy matrix work and small-batch decode as a memory-bandwidth loop dominated by repeated weight and KV reads. Inference phase comparison showing prefill as compute-heavy matrix work and small-batch decode as a memory-bandwidth loop dominated by repeated weight and KV reads.
Prefill and decode hit different ceilings. Inspect the phase shift: prefill keeps tensor cores busy, while small-batch decode often bottlenecks on repeated weight and KV reads.

decode-bandwidth-lower-bound.py
1parameters = 7_000_000_000 2bytes_per_parameter = 2 # FP16 3ideal_hbm_bandwidth_gb_s = 2_000 4 5weight_bytes = parameters * bytes_per_parameter 6ideal_steps_per_second = ideal_hbm_bandwidth_gb_s * 1_000_000_000 / weight_bytes 7 8print(f"FP16 weight footprint: {weight_bytes / 1_000_000_000:.2f} GB") 9print(f"ideal weight-read upper bound: {ideal_steps_per_second:.1f} single-token steps/s") 10print("Observed TPS is lower once KV reads and runtime overhead are included.")
Output
1FP16 weight footprint: 14.00 GB 2ideal weight-read upper bound: 142.9 single-token steps/s 3Observed TPS is lower once KV reads and runtime overhead are included.

The KV cache: saving state so you don't restart

Without caching, every decode step would recompute the Key and Value projections for earlier tokens before attending over the growing prefix. The KV cache stores those past Key and Value matrices, so we only need to compute them for the new token.

The KV cache acts as a handoff log between decode steps. Without it, the model would have to reread the entire prompt from the beginning every time it wanted to say the next word. With the KV cache, it remembers what it already processed and only computes fresh K/V state for the newest token.

KV cache packing comparison between reserved slabs and paged blocks. KV cache packing comparison between reserved slabs and paged blocks.
Once KV state persists across decode steps, memory packing becomes a scheduling constraint. Paged blocks make more HBM usable for active requests.

Once KV state persists across decode steps, you also need to pack it efficiently instead of reserving one giant contiguous region per request. Reserved slabs waste HBM; paged blocks reuse slots as requests finish. Each decode step then reuses prefix KV state and adds one fresh pair for the newest token.

KV cache append diagram showing prefix keys and values reused across decode steps while the newest token adds one fresh KV pair. KV cache append diagram showing prefix keys and values reused across decode steps while the newest token adds one fresh KV pair.
The KV cache keeps old keys and values alive across decode steps. Each new token adds one fresh KV pair instead of rebuilding the whole prefix.

Memory cost of KV cache

The KV cache can become one of the largest consumers of GPU memory during inference, sometimes exceeding the model weights themselves for long contexts. That memory footprint drives capacity planning and the maximum batch size a given GPU can support.

A hand calculation makes the memory shape visible before the code. Suppose we're serving a long-context code assistant with a model that has 80 layers, uses Grouped Query Attention with 8 KV heads, and each head has dimension 128. For one request with a sequence length of 8,192 tokens, stored in FP16 (2 bytes per element):

  • We need both K and V: that's a factor of 2
  • One request, 8,192 tokens, 80 layers, 8 heads, head size 128, 2 bytes each
  • Total bytes = 2 * 1 * 8,192 * 80 * 8 * 128 * 2 = 2,684,354,560 bytes
  • Divide by 1024^3: that's about 2.5 GiB per request

Now scale that up. For a production batch of 64 concurrent requests at an 8K context window, that's about 160 GiB of KV cache alone. That scale makes techniques like Grouped Query Attention (GQA), which reduces the number of KV heads from num_heads to num_kv_heads, standard in modern models.[3]Reference 3GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints.https://arxiv.org/abs/2305.13245

This Python function generalizes that exact calculation. It takes the model's architectural parameters and returns the KV cache memory in GiB.

KV cache capacity diagram showing per-request memory rising with context length, then multiplying across a 64-request batch until GPU memory fills. KV cache capacity diagram showing per-request memory rising with context length, then multiplying across a 64-request batch until GPU memory fills.
KV memory grows linearly with sequence length and batch size. For long contexts, KV cache alone can cap concurrency before model weights do.

memory-cost-of-kv-cache.py
1def kv_cache_memory( 2 batch_size: int, 3 seq_len: int, 4 num_layers: int, 5 num_kv_heads: int, 6 head_dim: int, 7 dtype_bytes: int = 2 # FP16 8) -> float: 9 """Calculate KV cache memory in GiB.""" 10 # 2 for K and V, per layer, per head 11 total_bytes = ( 12 2 * batch_size * seq_len * num_layers * num_kv_heads * head_dim * dtype_bytes 13 ) 14 return total_bytes / (1024 ** 3) 15 16# Example model: 80 layers, 8 KV heads (GQA), head_dim=128 17# Batch=1, seq_len=8192, FP16 (2 bytes): 18# 2 * 1 * 8192 * 80 * 8 * 128 * 2 = ~2.5 GiB per request 19one_request = kv_cache_memory( 20 batch_size=1, 21 seq_len=8192, 22 num_layers=80, 23 num_kv_heads=8, 24 head_dim=128, 25) 26production_batch = kv_cache_memory( 27 batch_size=64, 28 seq_len=8192, 29 num_layers=80, 30 num_kv_heads=8, 31 head_dim=128, 32) 33 34print(f"one 8K request: {one_request:.1f} GiB") 35print(f"64 active 8K requests: {production_batch:.0f} GiB") 36print("single-request estimate correct:", one_request == 2.5) 37print("64-request estimate correct:", production_batch == 160.0)
Output
1one 8K request: 2.5 GiB 264 active 8K requests: 160 GiB 3single-request estimate correct: True 464-request estimate correct: True

Throughput vs. latency trade-off

There's an inherent tension between maximizing system throughput and minimizing per-request latency.

Chart showing throughput rising with batch pressure while p99 latency rises faster once batch size gets large. Chart showing throughput rising with batch pressure while p99 latency rises faster once batch size gets large.
Batching improves aggregate tokens per second, but user-facing latency can worsen once large batches push harder on shared memory bandwidth. Track throughput and TTFT or TPOT together.
MetricOptimized ByTrade-off
Throughput (tokens/sec)Larger effective batchesCan increase TTFT or inter-token latency once shared resources are pressured.
Latency (ms/token)Smaller admitted batchesCan leave throughput unused and raise cost per token.

Production tip: Monitor GPU KV-cache usage, prefill backlog, and decode queue depth together. High KV usage plus rising TTFT usually means memory pressure is capping concurrency. Low KV usage with idle compute means you're leaving throughput on the table.

The throughput, latency, cost triangle

Throughput and latency are two corners of a third constraint that the business cares about: cost per token. These three pull against each other, and picking where to sit on that triangle is the central job of an inference engineer.

Cost per token is simpler than it looks. If you rent a GPU at a fixed hourly rate and it sustains some number of tokens per second, then:

cost per token=GPU $ per hoursustained tokens per second×3600\text{cost per token} = \frac{\text{GPU \$ per hour}}{\text{sustained tokens per second} \times 3600}cost per token=sustained tokens per second×3600GPU $ per hour​

Sustained throughput, not the sticker hourly rate, dominates the answer. A faster, pricier GPU can still be cheaper per token if its throughput rises faster than its price. A hand calculation shows the tradeoff. Suppose one GPU costs $3.00/hour and a well-batched deployment sustains 2,500 decode tokens/second across all active requests:

  • Tokens per hour = 2,500 * 3,600 = 9,000,000
  • Cost per token = $3.00 / 9,000,000 = $0.00000033
  • Cost per million tokens = about $0.33

Now starve the batch. If under-configured batching or idle capacity drops sustained throughput to 250 tokens/second, the same GPU-hour spreads over one-tenth the tokens, so cost per million jumps to about $3.33. Utilization is a direct 10x multiplier on cost. Batching is more than a latency knob; it moves the cost corner of the triangle.

cost-per-million-tokens.py
1def cost_per_million(hourly_cost: float, sustained_tps: int) -> float: 2 return hourly_cost / (sustained_tps * 3600) * 1_000_000 3 4well_batched = cost_per_million(3.00, 2_500) 5starved = cost_per_million(3.00, 250) 6 7print(f"2,500 tokens/s: ${well_batched:.2f} per million tokens") 8print(f"250 tokens/s: ${starved:.2f} per million tokens") 9print(f"cost multiplier: {starved / well_batched:.0f}x")
Output
12,500 tokens/s: $0.33 per million tokens 2250 tokens/s: $3.33 per million tokens 3cost multiplier: 10x

The triangle has a simple rule: you can usually optimize two corners hard, but the third drifts. Push batch size for throughput and cost, and tail latency rises. Cap batch size for tight latency SLOs, and your cost per token climbs because the GPU is underused. There is no single best operating point, only the one that fits your product's latency SLO at acceptable cost.

Operating pointBatch sizeCost per tokenLatency (TTFT/TPOT)Typical fit
Latency-firstSmallHighLowInteractive chat, code completion
BalancedMediumMediumMediumGeneral chat assistants
Throughput-firstLargeLowHighOffline batch jobs, summarization, evals

Production tip: Pick the operating point from the product SLO, then size hardware to it. An interactive assistant with a 500 ms TTFT budget can't run the same batch size as an overnight document-summarization job, even on identical GPUs. The summarization job can push batch size until cost per token bottoms out because no human is waiting on each token.

Chunked prefills

Long prefills (e.g., Retrieval-Augmented Generation (RAG), where retrieved documents are appended to the user's prompt, creating contexts of 10,000 tokens) can delay decode turns on a shared worker. The duration depends on model, kernel, hardware, and prompt length, but enough long prompts can noticeably worsen active streams in a multi-tenant service.

To reduce that interference, engineers can break large prefills into smaller chunks. Current vLLM V1 scheduling prioritizes pending decode requests, then uses remaining max_num_batched_tokens capacity for prefill work. If a prefill doesn't fit, the scheduler chunks it. This lets bounded prefill work share a batch with active decodes instead of forcing one huge uninterrupted prefill step. It doesn't guarantee a latency SLO when queues or kernels are already overloaded. Smaller token budgets can improve ITL, while larger budgets can improve TTFT and throughput.[4]Reference 4Optimization and Tuning.https://docs.vllm.ai/en/latest/configuration/optimization.html

Chunked prefill timeline showing decode-first batches using remaining token budget for bounded prefill chunks. Chunked prefill timeline showing decode-first batches using remaining token budget for bounded prefill chunks.
Chunked prefill bounds how much prompt work joins each decode-first batch. The long request may wait longer for its first token, while active streams keep making progress.

prefill-chunk-budget.py
1import math 2 3prompt_tokens = 10_000 4chunk_tokens = 512 5chunks = math.ceil(prompt_tokens / chunk_tokens) 6last_chunk = prompt_tokens - chunk_tokens * (chunks - 1) 7 8print(f"prefill chunks: {chunks}") 9print(f"largest admitted prefill chunk: {chunk_tokens} tokens") 10print(f"last chunk: {last_chunk} tokens")
Output
1prefill chunks: 20 2largest admitted prefill chunk: 512 tokens 3last chunk: 272 tokens

Disaggregated inference

Disaggregated inference separates prefill and decode across worker pools rather than running both phases on one worker. Systems such as Splitwise and DistServe show when this can improve goodput: avoided interference must repay KV-transfer and coordination overhead.[5]Reference 5Splitwise: Efficient Generative LLM Inference Using Phase Splitting.https://arxiv.org/abs/2311.18677[2]Reference 2DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving.https://arxiv.org/abs/2401.09670

The problem: conflicting optimization targets

Prefill and decode phases have opposite hardware needs:

  • Prefill is often compute-heavy: Long prompts create large matrix operations over many positions
  • Decode is often bandwidth-heavy: Small-batch token generation repeatedly reads weights and KV state

When both phases run on the same GPU, they interfere. A long prefill can "block" decode requests, causing head-of-line blocking where a massive prompt stalls generation for all other users.

Prefill-decode disaggregation

Serving systems often separate the phases:

  1. Prefill cluster (compute-optimized): Dedicated prefill workers process incoming prompts in parallel. These nodes excel at the compute-heavy attention operations.

  2. Decode cluster (bandwidth-optimized): Separate workers handle token generation. These nodes are tuned for the memory-bound sequential decoding loop and steady high-concurrency decode traffic.

  3. KV cache handoff: After prefill completes, the KV cache is transferred over a fast interconnect from the prefill worker to a decode worker, which continues generation.

Disaggregated inference architecture showing compute-optimized prefill workers handing KV cache over a fast interconnect to bandwidth-optimized decode workers. Disaggregated inference architecture showing compute-optimized prefill workers handing KV cache over a fast interconnect to bandwidth-optimized decode workers.
Prefill-decode disaggregation separates the compute-heavy prompt phase from the bandwidth-heavy streaming phase. It helps when avoided queueing costs exceed KV-transfer overhead.

Benefits of disaggregation

  • Can reduce head-of-line blocking: Long prefills no longer directly occupy decode workers
  • Potentially right-sized hardware: Each phase can run on workers chosen for its measured bottleneck
  • Separate scaling knobs: Prefill and decode clusters can scale independently based on workload patterns
  • Potential efficiency gain: Separate pools can better match the two workload profiles when transfer overhead is acceptable

Disaggregation is a design pattern, not a mandatory default. The KV-transfer cost has to be lower than the queueing and interference it removes, which is why it becomes more attractive as prompts get longer and decode traffic gets denser.[5]Reference 5Splitwise: Efficient Generative LLM Inference Using Phase Splitting.https://arxiv.org/abs/2311.18677[2]Reference 2DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving.https://arxiv.org/abs/2401.09670

Disaggregation also changes how you autoscale. Because prefill load tracks incoming prompt tokens and decode load tracks active generation, the two pools scale on different signals. A burst of long RAG prompts points toward more prefill capacity; a surge in concurrent streaming conversations points toward more decode capacity. Useful signals include queue depth, KV-cache utilization, and TTFT/TPOT percentiles, with cold-start time included because loading model weights onto a new worker isn't instant.

kv-handoff-lower-bound.py
1kv_cache_gib = 2.5 2interconnect_gb_s = 200 3 4transfer_bytes = kv_cache_gib * 1024**3 5ideal_transfer_ms = transfer_bytes / (interconnect_gb_s * 1_000_000_000) * 1000 6 7print(f"KV state to transfer: {kv_cache_gib:.1f} GiB") 8print(f"ideal one-way transfer floor at {interconnect_gb_s} GB/s: {ideal_transfer_ms:.1f} ms") 9print("Queueing saved must exceed transfer plus scheduling overhead.")
Output
1KV state to transfer: 2.5 GiB 2ideal one-way transfer floor at 200 GB/s: 13.4 ms 3Queueing saved must exceed transfer plus scheduling overhead.

Batching strategies: scheduler slots

Naive batching strategies waste capacity because requests generate different numbers of tokens. Picture a GPU scheduler handling code-assistant requests: one needs a two-line answer, another streams a long patch explanation, and a third is still prefilled from a large file context.

Static batching: waiting for the whole batch

In static batching, the scheduler groups requests and keeps membership fixed for that run. If one request needs hundreds more decode steps, the shorter requests finish early but their slots often turn into padding or sit idle until the longest request finishes. In serving terms, requests are grouped into a batch and padded to the length of the longest active sequence. The timeline below shows shorter requests wasting compute cycles while the batch waits on the longest request.

Static versus continuous batching timeline showing slot reuse. Static versus continuous batching timeline showing slot reuse.
Static batching holds slots until the batch cycle ends. Continuous batching changes membership at token-step boundaries so finished requests leave and queued requests enter.

The problem with static batching

Static batching creates two inefficiencies. First, the GPU is forced to process "padding tokens" that don't contribute to the final output, wasting compute cycles and memory bandwidth. Second, once shorter requests finish, their batch slots usually can't be reused until the scheduler rebuilds the batch around the longest surviving sequence. This degrades both latency and overall throughput, especially when request lengths vary widely.

Continuous batching: filling open decode slots

Continuous batching (introduced by Orca) operates at the iteration level.[6]Reference 6Orca: A Distributed Serving System for Transformer-Based Generative Models.https://www.usenix.org/conference/osdi22/presentation/yu The scheduler releases one finished request, can pull the next queued request into the open slot, and keeps useful work flowing when demand exists.

In serving terms, instead of waiting for a whole batch cycle to finish, the scheduler can eject completed requests and insert new ones after every token generation step. See our continuous batching deep-dive for scheduling algorithms and preemption strategies. The timeline above shows slot reuse; its actual benefit depends on queued work, KV capacity, and latency policy.

Benefits of continuous batching

Continuous batching provides three useful advantages under mixed, queued workloads:

  • Less slot waste: Finished requests can leave at iteration boundaries rather than remaining as padding or idle slots.
  • Lower completion delay for short requests: A completed request need not wait for the longest request in a fixed batch, though queueing and large active batches can still worsen latency.
  • Policy control: The scheduler can decide how to admit prefills alongside ongoing decode while respecting KV memory and latency SLOs.

Continuous batching uses a scheduling loop that manages active requests dynamically. This intentionally simplified sketch shows the shape of a continuous batcher. It takes a queue of incoming requests, admits work up to the maximum batch size, and then runs one decoding step for all active requests. A production scheduler also applies a token budget, chunks long prefills, and may co-batch prefill with decode.

benefits-of-continuous-batching.py
1import torch 2 3# Minimal request stub for illustration 4class Request: 5 def is_done(self) -> bool: return False 6 def get_next_token(self) -> torch.Tensor: return torch.tensor([0]) 7 def update(self, logits: torch.Tensor): pass 8 9class ContinuousBatcher: 10 def __init__(self, model: torch.nn.Module, max_batch_size: int = 64): 11 self.model = model 12 self.max_batch = max_batch_size 13 self.active_requests: list[Request] = [] 14 self.queue: list[Request] = [] 15 16 def step(self): 17 """ 18 Executes a single generation step for the current batch. 19 """ 20 # 1. Remove completed requests 21 self.active_requests = [ 22 req for req in self.active_requests if not req.is_done() 23 ] 24 25 # 2. Add new requests from queue (up to max batch size) 26 while self.queue and len(self.active_requests) < self.max_batch: 27 new_req = self.queue.pop(0) 28 # Pseudo-code only: real schedulers budget, chunk, or disaggregate prefill. 29 # new_req.run_prefill(self.model) 30 self.active_requests.append(new_req) 31 32 # 3. Run one decode step for all active requests 33 if self.active_requests: 34 # Gather current input tokens from all requests 35 input_tokens = torch.stack([req.get_next_token() for req in self.active_requests]) 36 37 # Forward pass (batched) 38 logits = self.model.decode(input_tokens) 39 40 # Update requests with new tokens 41 for i, req in enumerate(self.active_requests): 42 req.update(logits[i])

Memory management: PagedAttention (vLLM)

Traditional KV-cache allocation is like reserving a full pallet position for every request, even if it only needs a small bin. PagedAttention is like a shared bin system that assigns fixed-size slots on demand: Request A gets slots 7, 2, and 5 (non-contiguous, but tracked by a block table). When a request finishes, its slots become available again. Paging sharply reduces worst-case reservation waste, but a partially filled final block and bookkeeping still consume memory.

The problem

KV cache is allocated per-request, but request lengths vary. Pre-allocating the maximum possible sequence length for every request wastes a massive amount of memory. For example, if the system allocates 4096 tokens per request by default:

RequestTokens NeededTokens AllocatedMemory Wasted
Request A100409697.5%
Request B3000409626.8%

PagedAttention solution

PagedAttention applies the operating system concept of virtual memory to KV cache management.[7]Reference 7Efficient Memory Management for Large Language Model Serving with PagedAttention.https://arxiv.org/abs/2309.06180 Instead of contiguous physical memory, we divide the KV cache into fixed-size "blocks" (pages). The figure maps logical blocks to non-contiguous physical GPU memory through a block table.

PagedAttention block-table diagram mapping logical KV blocks to physical GPU pages. PagedAttention block-table diagram mapping logical KV blocks to physical GPU pages.
PagedAttention separates logical token order from physical HBM placement. A block table lets the runtime use non-contiguous pages while attention still sees the right sequence.

Impact of PagedAttention

By avoiding large contiguous reservations and allocating fixed-size blocks on demand, PagedAttention lets the runtime fit more useful KV state into the same HBM budget.[7]Reference 7Efficient Memory Management for Large Language Model Serving with PagedAttention.https://arxiv.org/abs/2309.06180 It doesn't eliminate all slack: each live request can still leave a partially filled last block, and the block table has overhead. In practice, it substantially reduces memory lost to worst-case preallocation.

paged-kv-slack.py
1import math 2 3requests = [100, 3_000] 4max_context = 4_096 5block_tokens = 16 6reserved_tokens = len(requests) * max_context 7paged_tokens = sum(math.ceil(tokens / block_tokens) * block_tokens for tokens in requests) 8 9print(f"max-context reservation: {reserved_tokens} token slots") 10print(f"paged allocation: {paged_tokens} token slots") 11print(f"remaining final-block slack: {paged_tokens - sum(requests)} token slots")
Output
1max-context reservation: 8192 token slots 2paged allocation: 3120 token slots 3remaining final-block slack: 20 token slots

Copy-on-write for shared blocks

PagedAttention's copy-on-write mechanism matters whenever multiple active continuations share the same prompt prefix. In the original vLLM setting, this is especially important for beam search and parallel sampling, where several continuations reuse the same prompt blocks before they diverge.[7]Reference 7Efficient Memory Management for Large Language Model Serving with PagedAttention.https://arxiv.org/abs/2309.06180 This visual shows two continuations initially pointing to the same shared prefix blocks before branching.

Copy-on-write KV cache diagram showing two continuations sharing prefix blocks until one branch diverges and allocates a private block. Copy-on-write KV cache diagram showing two continuations sharing prefix blocks until one branch diverges and allocates a private block.
Copy-on-write shares immutable prefix KV blocks across continuations, then clones only the block that must diverge. That saves memory without corrupting another branch's view.

Initially, the shared prefix blocks have a reference count greater than one. Appending new tokens usually allocates fresh blocks for each continuation. If a continuation needs to write into a block that's still shared, the runtime first clones that block so the other continuations keep their original view. That's what copy-on-write means here: share immutable prefix state aggressively, then split only when sequences diverge.[7]Reference 7Efficient Memory Management for Large Language Model Serving with PagedAttention.https://arxiv.org/abs/2309.06180

Context parallelism and long-context serving

As context windows grow into the hundreds of thousands or millions of tokens, a single GPU often can't hold the full KV cache or attention working set for one request. Context Parallelism (CP) addresses this by splitting the input sequence itself across multiple GPUs.[8]Reference 8Ring Attention with Blockwise Transformers for Near-Infinite Context.https://arxiv.org/abs/2310.01889

How context parallelism works

Instead of splitting layers (tensor parallelism) or batches (data parallelism), CP splits the sequence dimension:

  1. A 1M token sequence is divided into N chunks (e.g., 250K tokens per GPU on 4 GPUs)
  2. Each GPU processes its chunk independently during the prefill phase
  3. Attention is computed using ring-style communication patterns to handle cross-chunk dependencies
  4. The KV cache is distributed across the GPU cluster

This approach becomes useful once a single request's context no longer fits comfortably on one accelerator.

Ring attention for context parallelism

Modern implementations use ring attention (Liu et al., 2024) or similar distributed attention algorithms to manage communication overhead. GPUs form a logical ring. While each device computes blockwise attention for its local query block, Key and Value blocks circulate to the next device and arrive from the previous one. This can extend supported context length roughly linearly with the number of devices, at least until communication becomes the next bottleneck.[8]Reference 8Ring Attention with Blockwise Transformers for Near-Infinite Context.https://arxiv.org/abs/2310.01889

context-parallel-kv-shards.py
1def kv_gib(sequence_tokens: int) -> float: 2 total_bytes = 2 * sequence_tokens * 80 * 8 * 128 * 2 3 return total_bytes / 1024**3 4 5total_kv = kv_gib(1_000_000) 6devices = 4 7print(f"one 1M-token request KV: {total_kv:.1f} GiB") 8print(f"evenly sharded over {devices} devices: {total_kv / devices:.1f} GiB/device") 9print("Communication and runtime buffers still add overhead.")
Output
1one 1M-token request KV: 305.2 GiB 2evenly sharded over 4 devices: 76.3 GiB/device 3Communication and runtime buffers still add overhead.

Speculative decoding: draft then verify

Speculative decoding uses a small draft model to propose the next few tokens. The large target model checks the proposed span in a verification pass. If enough draft tokens survive acceptance, this can replace several target decode passes; if not, draft and verification work can lose to ordinary decoding.

Speculative decoding lets a large target model supervise a fast draft model. The draft model guesses the next 5 tokens. The target checks the span together. If the first 3 are accepted, one target verification pass can emit those accepted tokens plus a correction or continuation token. The accounting must still include the draft model's work.

To visualize this, consider how speculative decoding coordinates the interaction between the two models.[9]Reference 9Fast Inference from Transformers via Speculative Decoding.https://arxiv.org/abs/2211.17192 We use a fast, small draft model to propose a sequence of multiple tokens. The large target model then verifies these proposed tokens in parallel, accepting correct ones and correcting any mistakes.

Speculative decoding visual showing a draft model proposing four tokens, the target model accepting three and rejecting one in a single verification pass, then emitting corrected output with fewer target passes. Speculative decoding visual showing a draft model proposing four tokens, the target model accepting three and rejecting one in a single verification pass, then emitting corrected output with fewer target passes.
Speculative decoding saves target passes only when draft tokens survive verification. Follow the accepted prefix, the rejected token, and the corrected emitted text.

Speculative decoding relies on the asymmetric cost of the two paths. Drafting may be cheap enough, while verification can score all k draft positions in one target-model pass. If acceptance is high, one target pass can replace several ordinary target decode passes. Total latency still includes draft generation, verification, sampling, rejected-token correction, and kernel overhead.

This function shows one exact speculative step for a single sequence. It first samples k draft tokens from the small model, then runs the large target model once on [prompt + draft_tokens], and finally performs the accept/reject test from Leviathan et al. with residual resampling on the first mismatch.[9]Reference 9Fast Inference from Transformers via Speculative Decoding.https://arxiv.org/abs/2211.17192

speculative-decoding-the-smart-assistant.py
1import torch 2import torch.nn.functional as F 3 4def next_token_probs(model, input_ids: torch.Tensor) -> torch.Tensor: 5 with torch.no_grad(): 6 logits = model(input_ids).logits[0, -1] 7 return F.softmax(logits, dim=-1) 8 9def speculative_step( 10 draft_model, 11 target_model, 12 input_ids: torch.Tensor, 13 k: int = 4, 14) -> list[int]: 15 """ 16 Return one speculative chunk for a single sequence. 17 Assumes input_ids has shape [1, seq_len]. 18 """ 19 assert input_ids.shape[0] == 1, "single-sequence example" 20 21 draft_tokens: list[int] = [] 22 draft_dists: list[torch.Tensor] = [] 23 draft_input = input_ids 24 25 for _ in range(k): 26 q = next_token_probs(draft_model, draft_input) 27 token = torch.multinomial(q, num_samples=1).item() 28 draft_tokens.append(token) 29 draft_dists.append(q) 30 token_tensor = torch.tensor([[token]], device=input_ids.device) 31 draft_input = torch.cat([draft_input, token_tensor], dim=1) 32 33 # One target-model pass verifies all draft positions at once. 34 with torch.no_grad(): 35 target_logits = target_model(draft_input).logits[0] 36 37 target_dists = F.softmax( 38 target_logits[input_ids.size(1) - 1 : input_ids.size(1) + k], 39 dim=-1, 40 ) 41 42 accepted: list[int] = [] 43 for i, token in enumerate(draft_tokens): 44 p = target_dists[i] 45 q = draft_dists[i] 46 acceptance = min(1.0, (p[token] / q[token]).item()) 47 48 if torch.rand(()) < acceptance: 49 accepted.append(token) 50 continue 51 52 residual = torch.clamp(p - q, min=0) 53 if residual.sum() <= 0: 54 replacement = torch.argmax(p).item() 55 else: 56 residual = residual / residual.sum() 57 replacement = torch.multinomial(residual, num_samples=1).item() 58 return accepted + [replacement] 59 60 # If all k draft tokens are accepted, sample one extra token from p. 61 extra = torch.multinomial(target_dists[k], num_samples=1).item() 62 return accepted + [extra]

Follow-on work such as EAGLE uses the target model's hidden states to predict future tokens instead of relying on a separately trained small draft model.[10]Reference 10EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty.https://arxiv.org/abs/2401.15077 The important takeaway isn't that one speculative variant always wins. It's that these methods trade extra compute for fewer expensive target-model decode passes, so the real payoff depends on acceptance rate, hardware, and implementation overhead.

speculative-pass-accounting.py
1ordinary_target_passes = 5 2draft_proposals = 4 3accepted_prefix = 3 4target_verification_passes = 1 5emitted_tokens = accepted_prefix + 1 6 7print(f"ordinary target passes for {ordinary_target_passes} tokens: {ordinary_target_passes}") 8print(f"one verification emits in this example: {emitted_tokens} tokens") 9print(f"extra draft passes paid: {draft_proposals}") 10print("Speedup requires cheap drafting and high acceptance.")
Output
1ordinary target passes for 5 tokens: 5 2one verification emits in this example: 4 tokens 3extra draft passes paid: 4 4Speedup requires cheap drafting and high acceptance.

Hardware-aware optimization: quantization and precision

Inference performance isn't just about algorithms. Modern GPUs and specialized accelerators provide hardware-level features that change the efficiency equation.

Low-precision inference and quantization

Many serving stacks still use BF16/FP16 as a baseline, but lower-precision modes are increasingly common because they cut model-weight bandwidth and, in some systems, shrink the KV footprint enough to raise concurrency.[11]Reference 11FP8 Formats for Deep Learning.https://arxiv.org/abs/2209.05433[12]Reference 12GPTQ: Accurate Post-Training Quantization for Generative Pre-Trained Transformershttps://arxiv.org/abs/2210.17323[13]Reference 13Quantized KV Cachehttps://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache/

  • FP8 (8-bit floating point): Useful when your hardware and kernels support it, because it lowers bandwidth pressure while preserving more dynamic range than integer-only formats.[11]Reference 11FP8 Formats for Deep Learning.https://arxiv.org/abs/2209.05433
  • INT8/INT4-style weight quantization: Common for weight-only inference, where the target is fewer bytes reread on every decode step without fully quantizing the rest of the runtime.[12]Reference 12GPTQ: Accurate Post-Training Quantization for Generative Pre-Trained Transformershttps://arxiv.org/abs/2210.17323
  • KV-cache compression or quantization: Targets the other major memory consumer during long-context serving, which matters once the KV cache rather than weights caps concurrency.[14]Reference 14SnapKV: Compressing KV Cache by Selecting Global Attention Patterns.https://arxiv.org/abs/2403.19925[13]Reference 13Quantized KV Cachehttps://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache/

Quantization approaches include:

  • Weight-only quantization: Keep activations in higher precision (BF16/FP16) but compress model weights to 4-8 bits
  • KV-cache compression/quantization: Reduce KV bytes, or compress less useful KV state, when long contexts would otherwise cap batch size and residency
Quantization bandwidth visual showing FP16, FP8, INT8, and INT4 as progressively narrower weight streams feeding memory-bound decode, with KV quantization shown as a separate cache-capacity lever. Quantization bandwidth visual showing FP16, FP8, INT8, and INT4 as progressively narrower weight streams feeding memory-bound decode, with KV quantization shown as a separate cache-capacity lever.
For memory-bound decode, lower precision helps by narrowing repeated weight reads. KV quantization is a separate lever for long-context residency and batch capacity.

Common mistake: Beginners often think 4-bit quantization is just about saving disk space. The real win is reducing bytes moved during decode. Whether that turns into a large latency win still depends on kernels, dequant overhead, and quality constraints.

weight-bytes-by-precision.py
1parameters = 7_000_000_000 2bytes_per_weight = {"FP16": 2.0, "INT8": 1.0, "INT4": 0.5} 3 4for precision, width in bytes_per_weight.items(): 5 traffic_gb = parameters * width / 1_000_000_000 6 print(f"{precision}: {traffic_gb:.1f} GB of weights per full read") 7 8print("INT4 weight traffic is 0.25x FP16 before kernel overhead.")
Output
1FP16: 14.0 GB of weights per full read 2INT8: 7.0 GB of weights per full read 3INT4: 3.5 GB of weights per full read 4INT4 weight traffic is 0.25x FP16 before kernel overhead.

Inference-first silicon beyond GPUs

GPUs still dominate general-purpose LLM serving, but some deployments use inference-first accelerators with larger on-chip memory, dataflow execution, or more deterministic scheduling. Those accelerators usually come with a narrower software stack: you may get excellent latency or cost for a specific serving pattern, but at the price of custom compilation, fewer kernels, and less ecosystem flexibility.

When scaling inference breaks down

These symptoms show up in production logs and profiling traces. Each one points to a different bottleneck and fix.

"Training optimizations don't speed up my inference"

  • Symptom: You upgrade math kernels or buy more peak FLOPs, but decode barely speeds up.
  • Cause: Training is compute-bound, while decode is often memory-bound. If the GPU is already waiting on weights and KV reads, more math capacity does little.
  • Fix: Profile bandwidth first. If HBM is near ceiling, prioritize batching, quantization, GQA, or KV-cache management before chasing more tensor-core throughput.

"Short requests are as slow as long ones"

  • Symptom: Small chats wait almost as long as large ones even when the queue looks short.
  • Cause: Static batching pads to the longest request and can't reuse finished slots until the batch cycle ends.
  • Fix: Switch to continuous batching so completed requests leave immediately and queued work fills open slots on the next decode step.

"Speculative decoding made latency worse"

  • Symptom: Draft-model serving added overhead, but target-model passes did not fall enough to repay it.
  • Cause: Acceptance rate is too low or implementation overhead is too high. Wrong draft tokens force extra verification and correction work.
  • Fix: Measure accepted tokens per draft span before rollout. If most proposals are rejected, keep standard decoding or use a stronger draft path.

"HBM usage grows almost linearly even when prefixes are shared"

  • Symptom: HBM usage spikes even though many sessions start from the same long system prompt or retrieved prefix.
  • Cause: The runtime isn't reusing cached prefix KV state across independent requests, or its isolation policy doesn't allow that reuse.
  • Fix: Enable an explicit prefix-caching feature with an appropriate tenant and privacy policy. Copy-on-write can preserve shared blocks after reuse is established; it's not by itself a cross-request cache.

"Throughput keeps rising but users complain about slowness"

  • Symptom: Aggregate TPS looks better, but TTFT or TPOT percentiles get worse and chat feels sluggish.
  • Cause: Larger batches improve throughput while making active requests compete harder for bandwidth.
  • Fix: Use SLO-aware scheduling. Cap batch size or split latency-sensitive traffic from background work, and watch throughput together with p95 or p99 latency.

Try it yourself: the VRAM calculator

Here's a practical check you can do with pen and paper or a short Python script.

  • Problem: You want to serve a 7B-class model on a single GPU with 80 GiB of HBM. The model has 28 layers, uses Grouped Query Attention with 4 KV heads, head dimension 128, and you plan to use FP16 (2 bytes per element). Your average code-assistant request has a 4,096-token context. What's the maximum batch size you can support before the KV cache alone fills the GPU, assuming you need to leave 20 GiB for weights and runtime overhead?

  • Hint: Start by computing the KV cache for one request, then see how many fit in the remaining 60 GiB.

Worked solution

For one request at 4,096 tokens:

text
1KV bytes = 2 * batch * seq_len * layers * kv_heads * head_dim * dtype_bytes 2 = 2 * 1 * 4096 * 28 * 4 * 128 * 2 3 = 234,881,024 bytes 4 ≈ 0.22 GiB per request

Available memory for KV cache: 80 GiB total - 20 GiB reserved = 60 GiB

Maximum batch size = 60 GiB / 0.21875 GiB per request ≈ 274 requests (rounding the per-request figure to 0.22 GiB gives about 273, so treat this as a ballpark, not a precise count)

In practice, you'd run at a lower batch size to leave headroom for activation buffers, temporary tensors, allocator slack, and bursty long-context requests. A production engineer might cap this far below the arithmetic ceiling and monitor actual HBM usage.

vram-capacity-headroom.py
1import math 2 3kv_bytes = 2 * 1 * 4_096 * 28 * 4 * 128 * 2 4kv_per_request_gib = kv_bytes / 1024**3 5kv_budget_gib = 80 - 20 6arithmetic_ceiling = math.floor(kv_budget_gib / kv_per_request_gib) 7 8print(f"KV per request: {kv_per_request_gib:.5f} GiB") 9print(f"KV budget after reserved memory: {kv_budget_gib} GiB") 10print(f"arithmetic-only batch ceiling: {arithmetic_ceiling}")
Output
1KV per request: 0.21875 GiB 2KV budget after reserved memory: 60 GiB 3arithmetic-only batch ceiling: 274

Mastery check

What strong answers show

  • Foundational: Why decode-heavy LLM serving is often memory-bandwidth bound even when peak FLOPs look huge.
  • Intermediate: How prefill and decode differ, and why TTFT and TPOT need separate dashboards.
  • Advanced: How KV-cache size scales with batch size, context length, layers, KV heads, head dimension, and precision.
  • Advanced: Why static batching wastes slots, while continuous batching uses iteration-level admission to keep decode work flowing.
  • Advanced: How PagedAttention uses virtual-memory-style blocks to pack KV state, and why cross-request prefix reuse requires explicit caching policy in addition to copy-on-write.
  • Advanced: When speculative decoding, disaggregated inference, context parallelism, and quantization help, and when their overheads can erase the win.
  • Advanced: How throughput, latency, and cost per token trade off, and how to pick an operating point from a product SLO instead of chasing one metric.

Follow-up questions

How does KV-cache size scale with sequence length and batch size?

KV cache stores key and value tensors from previous tokens across all layers, so memory grows linearly with sequence length, batch size, number of layers, number of KV heads, head dimension, and bytes per element. A useful formula is 2 * batch * seq_len * layers * kv_heads * head_dim * bytes. GQA reduces the kv_heads term, and KV-cache quantization reduces bytes.

When does speculative decoding hurt performance?

Speculative decoding hurts when the draft path is inaccurate or expensive enough that extra draft work and rejection handling cost more than the target-model passes saved. Low acceptance, poor kernel fusion, or tiny latency-sensitive batches can erase the win.

How does tensor parallelism interact with batching?

Tensor parallelism splits layer math across multiple GPUs, so every decode step includes collective communication such as all-reduce or all-gather. Larger batches can amortize that communication overhead, but small interactive batches may become communication-bound before they become compute-bound. Batching and tensor parallelism need to be tuned together.

What are the tradeoffs between throughput and latency?

Serving systems balance aggregate throughput against per-request latency. Continuous batching can increase total tokens per second, but larger batches also make active requests compete for the same memory bandwidth. Production schedulers need latency SLOs, not raw tokens/sec alone.

How do you estimate cost per token for a self-hosted model?

Take the GPU hourly rate, divide by sustained tokens per second times 3,600, then multiply by one million for cost per million tokens. The dominant variables are measured sustained throughput (driven by batching, quantization, and model size) and utilization. A GPU sitting idle multiplies cost per token directly, so a 5x cheaper sticker price can still lose if it delivers less than one-fifth the throughput.

A long RAG prefill is stalling active chat streams. Do you try chunked prefill or full disaggregation first?

Usually try chunked prefill first if the main problem is one large prompt monopolizing a shared worker for too long. Move to prefill-decode disaggregation when that interference remains large enough that separate pools and KV handoff beat their transfer and coordination overhead.

Course handoff

Serving capacity depends on phase behavior, memory bandwidth, KV-cache residency, scheduler policy, and precision choices. Those tools let you read a slow serving trace and separate prefill queueing, decode bandwidth pressure, fragmented KV memory, and speculative-decoding overhead.

Practice drill

Turn the capacity model into a worksheet for one production route:

  1. Compare TTFT, TPOT, goodput, and cost per million tokens across at least three batch-size or concurrency settings.
  2. Recompute cost per million tokens once with high utilization and once with latency-capped low utilization.
  3. Mark which intervention you would test first: chunked prefill, continuous batching, PagedAttention, speculative decoding, quantization, or disaggregation.
  4. Define one rollback trigger tied to user latency and one tied to cost or GPU utilization.

The useful artifact isn't a single "best" throughput number. It's a measured operating envelope that tells another engineer when to tune, when to split pools, and when to back out.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score above 75% to mark this lesson complete.

1.A serving trace for an interactive chat shows high TTFT on long prompts, but once streaming starts the time between tokens is stable. Which interpretation matches the prefill/decode split?
2.A model has 80 layers, 8 KV heads, head dimension 128, and stores KV in FP16. For one 8,192-token request, roughly how much KV-cache memory is needed?
3.A team adds speculative decoding with a small draft model. The target model's output distribution must remain unchanged, but latency is worse in production. Which diagnosis is consistent with the mechanism?
4.A 7B model's decode path saturates HBM while tensor cores remain idle. Weight-only INT4 reduces a full weight read from 14 GB to 3.5 GB. What conclusion follows?
5.Variable-length requests waste KV memory, and independent requests from the same tenant reuse a long system prompt. Which design addresses both safe prefix reuse and allocation waste?
6.A shared worker handles active chat streams and an incoming 10,000-token RAG prompt. After the prompt arrives, TPOT for existing streams spikes, but the long request can tolerate a later first token. Which scheduling change targets that symptom without adding a KV handoff between workers?
7.A GPU costs $3.00 per hour. One deployment sustains 2,500 tokens/s; a latency-capped deployment of the same model sustains 250 tokens/s. What follows for cost per million tokens?
8.A static decode batch contains four requests with output lengths of 20, 20, 20, and 200 tokens, and more requests are queued. What changes under iteration-level continuous batching?
9.Long RAG prompt bursts still disrupt active streams after chunking. A fast interconnect and independently scalable worker pools are available. Which measurement would justify prefill-decode disaggregation?
10.One 1-million-token request needs about 305.2 GiB of KV memory, so it cannot fit on one accelerator. What does context parallelism do across four devices, and what new limit can appear?

10 questions remaining.

Next Step
Continue to Model Parallelism for LLM Inference

Batching and KV-cache planning show where serving memory goes; model parallelism teaches what changes when one production model must be split across several GPUs.

PreviousContinuous Batching & Scheduling
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.

Dao, T., Fu, D. Y., Ermon, S., Rudra, A., & Ré, C. · 2022 · NeurIPS 2022

DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving.

Zhong, Y., et al. · 2024 · OSDI 2024

GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints.

Ainslie, J., et al. · 2023 · EMNLP 2023

Optimization and Tuning.

vLLM · 2026

Splitwise: Efficient Generative LLM Inference Using Phase Splitting.

Patel, P., et al. · 2023

Orca: A Distributed Serving System for Transformer-Based Generative Models.

Yu, G.-I., et al. · 2022 · OSDI 2022

Efficient Memory Management for Large Language Model Serving with PagedAttention.

Kwon, W., et al. · 2023 · SOSP 2023

Ring Attention with Blockwise Transformers for Near-Infinite Context.

Liu, H., et al. · 2024 · arXiv preprint

Fast Inference from Transformers via Speculative Decoding.

Leviathan, Y., Kalman, M., & Matias, Y. · 2023 · ICML 2023

EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty.

Li, Y., et al. · 2024 · ICML 2024

FP8 Formats for Deep Learning.

Micikevicius, P., et al. · 2022

GPTQ: Accurate Post-Training Quantization for Generative Pre-Trained Transformers

Frantar, E., et al. · 2023 · ICLR 2023

Quantized KV Cache

vLLM Team · 2026 · vLLM Documentation

SnapKV: Compressing KV Cache by Selecting Global Attention Patterns.

Li, Y., et al. · 2024

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.