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 196 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/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics 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/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch 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/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData 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/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠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/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward 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/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel 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 & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/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 Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnInference & Production ScaleScaling LLM Inference
🚀HardInference Optimization

Scaling LLM Inference

Size LLM serving from HBM bandwidth and KV residency: pick a throughput-latency-cost operating point, then compose batching, paging, speculation, and precision as capacity levers.

26 min read
Learning path
Step 142 of 196 in the full curriculum
Continuous Batching & SchedulingModel Parallelism for LLM Inference

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

Continuous batching can refill decode slots as soon as a request finishes. A full queue can still hide a capacity failure: once the scheduler is willing to admit more work, what runs out first on the GPU serving a large language model (LLM)?

Picture a code-assistant pool during a release freeze. Every answer still arrives one token at a time, while short completions and long patch explanations share one replica.

Each decode step rereads model weights from GPU high-bandwidth memory (HBM) and consults a growing KV cache. Those bytes, rather than peak FLOPs, usually decide how many streams fit and what each token costs.

The scheduler can refill slots, but a replica still has finite budget. We will follow one mixed-length assistant: an 8B-class replica that fits on one 80 GB GPU, a 70B-class KV cache that overruns that card, and a throughput-latency-cost choice that works for both.

Why isn't "keep the batch full" enough to scale LLM serving?

Answer

A full batch still has to fit in HBM and still has to move weights plus KV state every decode step. If bandwidth or KV residency is the ceiling, extra admitted requests queue or stall instead of raising useful tokens per second.

Decode sits under a bandwidth roof

You already know the two phases from inference mechanics. Prefill reads known prompt tokens in parallel and is often compute-heavy.

Decode emits one new token per request step. This dependency is why a request can have plenty of prompt work behind it but still need many sequential steps to finish.

Users feel prefill as time to first token (TTFT). They feel decode as inter-token latency (ITL), also called time between tokens (TBT). DistServe defines time per output token (TPOT) as average time to generate each output token after first, so mean TPOT can look fine while p95 ITL is ugly.[1]Reference 1DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving.https://arxiv.org/abs/2401.09670

The scaling fact is the intensity split. Arithmetic intensity is FLOPs per byte loaded from HBM.

Prefill reuses each weight matrix across many prompt positions. Small-batch decode reuses those same weights for one new position per request, so each step looks more like a thin matrix-vector read.

The roofline model names the resulting ceiling: attainable work is the minimum of peak compute and memory bandwidth times intensity. Below the ridge, bandwidth is the limit. Above it, compute is the limit.[2]Reference 2Roofline: An Insightful Visual Performance Model for Multicore Architectureshttps://doi.org/10.1145/1498765.1498785[3]Reference 3Efficiently Scaling Transformer Inference.https://arxiv.org/abs/2211.05102

Before doing hardware arithmetic, predict where each phase lands. One-token decode should sit on the rising bandwidth slope; a large batch or long prefill should move right, toward the flat compute roof.

Roofline chart with arithmetic intensity in FLOPs per byte on the x axis and attainable performance in TFLOP/s on the y axis. Illustrative H100 SXM assumptions use 3.35 TB/s HBM bandwidth and a 1,000 TFLOP/s compute ceiling, with a ridge near 300 FLOPs per byte. Labeled decode B=1, decode B=32, and prefill points are teaching coordinates, not measured benchmarks.
Illustrative H100 SXM roofline: 3.35 TB/s HBM and a 1,000 TFLOP/s teaching ceiling put the ridge near 300 FLOPs per byte. Markers are illustrative coordinates, not measured kernels. Larger batches raise intensity by reusing weight reads; real placement depends on workload, precision, and runtime.

Name hardware before doing arithmetic. We use NVIDIA H100 SXM as a reference point: it has 80 GB of HBM and peak HBM bandwidth of 3.35 TB/s (3,350 GB/s in decimal units).[4]Reference 4H100 GPUhttps://www.nvidia.com/en-us/data-center/h100/

Llama 3 8B is an 8B-parameter dense model.[5]Reference 5The Llama 3 Herd of Models.https://arxiv.org/abs/2407.21783 An FP16 copy of those weights is about 16 GB (8e9 * 2 bytes).

If one uncached decode step streamed that whole footprint for one active token, the weight-read upper bound on H100 SXM would be 3350 / 16 ≈ 209 steps per second. That is a bandwidth thought experiment, not a promise about a runtime.

Real kernels also read KV state, miss peak bandwidth, and pay runtime overhead, so observed tokens per second are lower. Use the bound as a diagnosis: if a trace saturates HBM while tensor cores sit idle, buying more FLOPs will not fix decode.

decode-bandwidth-lower-bound.py
1parameters = 8_000_000_000 # Llama 3 8B-class dense count 2bytes_per_parameter = 2 # FP16 3h100_sxm_hbm_gb_s = 3_350 # decimal GB/s, NVIDIA H100 SXM 4 5weight_gb = parameters * bytes_per_parameter / 1_000_000_000 6ideal_steps_per_second = h100_sxm_hbm_gb_s / weight_gb 7 8print(f"FP16 weight footprint: {weight_gb:.1f} GB") 9print(f"H100 SXM 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: 16.0 GB 2H100 SXM weight-read upper bound: 209.4 single-token steps/s 3Observed TPS is lower once KV reads and runtime overhead are included.

You profile decode and see tensor cores idle while HBM bandwidth is near saturation. Should you first buy more FLOPs or reduce memory traffic?

Answer

Reduce memory traffic first. The profile says the bottleneck is bandwidth, so extra FLOPs will sit idle too. Better first moves are a larger effective decode batch, weight quantization, GQA/MQA, KV compression, or better cache packing.

KV bytes cap the batch you can form

Batching only helps if extra requests fit. The KV cache lesson derived the payload formula: each token stores a Key and a Value at every layer and KV head.

Llama 3 uses grouped-query attention with 8 KV heads at 8B, 70B, and 405B.[5]Reference 5The Llama 3 Herd of Models.https://arxiv.org/abs/2407.21783[6]Reference 6GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints.https://arxiv.org/abs/2305.13245 That head count is why KV can be much smaller than a full set of attention heads, but it still grows with layers, context, and concurrency.

For Llama 3 70B in FP16, one token is 2 * 80 * 8 * 128 * 2 = 327,680 bytes, or 320 KiB. Do a rough check before reading on: multiplying by 8,192 should land near a few GiB.

The exact estimate is 2.5 GiB per 8,192-token request. Sixty-four concurrent 8K requests need 160 GiB of KV alone, before weights. An 80 GB H100 can't hold that batch, so a plan to keep raising batch size until the roofline moves hits residency first.

Two charts for Llama 3 70B FP16 KV memory: one request grows from 0.31 GiB at 1K tokens to 10 GiB at 32K, then 64 concurrent 8K requests need 160 GiB against one 80 GB H100.
Llama 3 70B KV grows linearly with sequence length and batch size. At 8K context, 64 concurrent requests already ask for 160 GiB of KV, twice an 80 GB H100, before model weights.

The 8B replica in the same family is the one that fits. Llama 3 8B has 32 layers, 8 KV heads, and d_head = 128, so one token is 128 KiB and a 4K request is 0.5 GiB.[5]Reference 5The Llama 3 Herd of Models.https://arxiv.org/abs/2407.21783

That is the budget we will size later. The 70B numbers show the same formula crossing a GPU before FLOPs do.

llama70b-kv-residency.py
1def kv_cache_gib( 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, 8) -> float: 9 total_bytes = ( 10 2 * batch_size * seq_len * num_layers * num_kv_heads * head_dim * dtype_bytes 11 ) 12 return total_bytes / (1024 ** 3) 13 14one_8k = kv_cache_gib(1, 8_192, 80, 8, 128) 15batch_64 = kv_cache_gib(64, 8_192, 80, 8, 128) 16 17print(f"Llama 3 70B, one 8K request: {one_8k:.1f} GiB") 18print(f"64 active 8K requests: {batch_64:.0f} GiB") 19print("single-request estimate correct:", one_8k == 2.5) 20print("64-request estimate correct:", batch_64 == 160.0)
Output
1Llama 3 70B, one 8K request: 2.5 GiB 264 active 8K requests: 160 GiB 3single-request estimate correct: True 464-request estimate correct: True

That 160 GiB figure assumes every request occupies a live 8K payload. A max-context slab reservation is worse: it pays for tokens that never arrive.

PagedAttention allocates fixed-size blocks on demand and maps logical token order through a block table, so finished requests return pages to a shared pool.[7]Reference 7Efficient Memory Management for Large Language Model Serving with PagedAttention.https://arxiv.org/abs/2309.06180 Paging doesn't shrink bytes per live token. It stops the allocator from pinning unused tail slots.

KV allocator comparison: reserved slabs pin unused tail slots, while paged blocks place the same live blocks in a reusable physical pool.
Reserved slabs pin unused tail slots for the whole request lifetime. Paged blocks keep live KV in a shared pool, so the 2.5 GiB-per-8K formula is a payload, not a reservation tax.

In the KV-cache formula, which terms can the serving stack change without shortening user context?

Answer

Grouped-query or multi-query attention reduces num_kv_heads. KV-cache quantization reduces dtype_bytes. Batch size is a workload and admission choice. Layer count and head dimension stay fixed for a chosen checkpoint.

The throughput, latency, cost triangle

Once bandwidth and KV residency are in view, "maximize tokens per second" is not a complete objective. Start with product promise: interactive users care about TTFT and p95 ITL, while offline jobs care more about completion rate.

Larger batches amortize weight reads and usually raise aggregate tokens per second (TPS). They also make requests compete for HBM, which can stretch TTFT and p95 ITL.

Cost per token follows sustained throughput, not the sticker GPU price. A slower, latency-protected replica can therefore be more expensive per token and still be correct for its workload.

Qualitative normalized curves showing aggregate throughput flattening as batch pressure rises while p99 latency bends upward, with no benchmark values implied.
These normalized curves show shape, not benchmark points. Measure the workload, then choose the highest batch pressure that still meets TTFT, p95 ITL/TBT, and p99 latency limits.
MetricRaised byWhat you usually give up
Throughput (tokens/s across the replica)Larger effective decode batchesTTFT or ITL once HBM, queues, or kernels are pressured
Latency (TTFT, p95 ITL, p99)Smaller admitted batches, decode-first budgetsUnused FLOPs and a higher cost per token
Cost per tokenHigher sustained tokens per GPU-hourHeadroom against latency SLOs

Cost per token is hourly GPU spend divided by tokens actually produced:

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​

Treat $3.00/hour as a worked fixture for August 2026, not a live cloud quote. Suppose a well-batched replica sustains 2,500 decode tokens/s across active requests:

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

Starve the batch to 250 tokens/s on the same GPU-hour and cost per million jumps to $3.33. Utilization is a 10x multiplier. Batching is not only 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 blunt rule: push two corners hard and the third drifts. Cap batch size for a 500 ms TTFT chat SLO, and cost per token climbs because the GPU is underused.

Push batch size for overnight summarization, and tail latency rises because no human is waiting on each token. There is no universal best point, only one that meets the product latency SLO at acceptable cost.

Operating pointBatch pressureCost per tokenLatencyTypical fit
Latency-firstSmallHighLowInteractive chat, inline completion
BalancedMediumMediumMediumGeneral assistants
Throughput-firstLargeLowHighOffline summarization, eval sweeps

Production tip: Pick the operating point from the product SLO, then size hardware to it. Watch GPU KV usage, prefill backlog, and decode queue depth together. High KV usage plus rising TTFT usually means residency is capping concurrency. Low KV usage with idle compute means you're leaving throughput, and cheap tokens, on the table.

Two teams serve the same 8B model on the same GPU SKU but report a 5x gap in cost per million tokens. The interactive team is more expensive. Why isn't that automatically a misconfiguration?

Answer

Interactive serving caps batch size to protect TTFT and ITL, so each GPU-hour spreads over fewer tokens. The batch team pushes until throughput saturates. Both can be correct operating points for their SLOs.

Batching raises arithmetic intensity

The previous chapter treated continuous batching as slot reuse: when request A finishes, D can enter at the next decode iteration instead of waiting for C's long answer.[8]Reference 8Orca: A Distributed Serving System for Transformer-Based Generative Models.https://www.usenix.org/conference/osdi22/presentation/yu Slot reuse is only half the story. One decode step still streams the weight tensor roughly once for the whole batch. More active tokens on that step means more useful FLOPs per byte, which is how serving climbs the roofline without changing the model.

Static batching fights that climb. If three assistant requests need 2, 2, and 5 remaining tokens, a request-level batch of size 3 spends later steps on padding or idle slots. Iteration-level scheduling keeps the weight read attached to live work.

Predict the comparison before running it: same weight-read steps should produce more tokens when finished slots refill immediately. This sketch counts weight-read steps against emitted tokens. It isn't a production scheduler: no token budget, no prefill chunking, no KV admission check. It isolates why membership changes raise work per read.

batch-amortizes-weight-reads.py
1from collections import deque 2 3static_remaining = [2, 2, 5] 4static_steps = max(static_remaining) 5static_tokens = sum(static_remaining) 6 7queue = deque([2, 2, 5, 3, 1]) 8active: list[int] = [] 9max_batch = 3 10weight_reads = 0 11emitted = 0 12 13while queue or active: 14 while queue and len(active) < max_batch: 15 active.append(queue.popleft()) 16 if not active: 17 break 18 weight_reads += 1 19 nxt: list[int] = [] 20 for remaining in active: 21 remaining -= 1 22 emitted += 1 23 if remaining > 0: 24 nxt.append(remaining) 25 active = nxt 26 27print(f"static batch: {static_tokens} tokens in {static_steps} weight reads") 28print(f"continuous batch: {emitted} tokens in {weight_reads} weight reads") 29print(f"tokens per weight read (static): {static_tokens / static_steps:.2f}") 30print(f"tokens per weight read (continuous): {emitted / weight_reads:.2f}")
Output
1static batch: 9 tokens in 5 weight reads 2continuous batch: 13 tokens in 5 weight reads 3tokens per weight read (static): 1.80 4tokens per weight read (continuous): 2.60

Same five weight-read steps, more tokens, because D and E filled slots that static batching would have padded. The intensity gain still stops where KV residency stops. A scheduler that admits past the cache budget just swaps bandwidth headroom for eviction and preemption.

Why does batching help decode even though every request still needs its own next token?

Answer

The model weights can be read once for a larger set of active tokens, so that memory movement is amortized across requests. Batching doesn't remove the sequential dependency inside each request. It raises useful work per weight read until KV memory or latency SLOs say stop.

Sharing, chunking, and phase isolation

Three more levers change the same budget without training a new model. Each targets a different source of pressure: repeated prompt work, prompt interference, or movement between phase-specific workers.

Prefix sharing. Many assistant requests start from the same system prompt. Prefix caching is the lookup that finds reuse across independent requests.

Copy-on-write is what happens after sharing exists: continuations keep the same physical blocks until one has to write, then runtime clones only the dirty block.[7]Reference 7Efficient Memory Management for Large Language Model Serving with PagedAttention.https://arxiv.org/abs/2309.06180 For Llama 3 8B, a shared 4K system prompt is 0.5 GiB the first time and nearly free for each later session that hits the cache, until someone diverges.

Copy-on-write KV cache diagram showing two continuations sharing prefix blocks sys0, sys1, and user0 until branch B diverges and clones only the write block while A keeps the shared prefix and appends a private tail.
Copy-on-write shares immutable prefix blocks, then clones only the block that must diverge. Independent requests still need an explicit prefix-cache lookup and an isolation policy before this sharing can start.

Chunked prefill. A 10K RAG prompt on the same worker as live streams can stall ITL. Sarathi-Serve and current vLLM V1 scheduling bound that interference by giving leftover max_num_batched_tokens to prefill after decode and splitting a prompt that doesn't fit.[9]Reference 9Sarathi-Serve: Efficient LLM Inference by Piggybacking Decodes with Chunked Prefills.https://arxiv.org/abs/2308.16369[10]Reference 10Optimization and Tuning.https://docs.vllm.ai/en/latest/configuration/optimization.html

Chunking doesn't add HBM. It trades TTFT on a long prompt for smoother ITL on streams already admitted. Details live in the scheduling chapter.

Prefill-decode disaggregation. Splitwise and DistServe move the two phases onto separate pools when avoided interference pays for KV transfer.[11]Reference 11Splitwise: Efficient Generative LLM Inference Using Phase Splitting.https://arxiv.org/abs/2311.18677[1]Reference 1DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving.https://arxiv.org/abs/2401.09670 This changes worker placement, not payload size.

For a Llama 3 70B 8K request, payload is 2.5 GiB. Over a 200 GiB/s link, the one-way floor is 12.5 ms before protocol and scheduling. That is a different break-even from the smaller Qwen handoff in the scheduling chapter: long 70B prefixes make transfer expensive, so disaggregation must save more queueing than 12.5 ms plus overhead.

kv-handoff-lower-bound.py
1kv_cache_gib = 2.5 # Llama 3 70B, one 8K request 2interconnect_gib_s = 200 # binary GiB/s 3 4ideal_transfer_ms = kv_cache_gib / interconnect_gib_s * 1000 5mixed_ms = (kv_cache_gib * 1024**3) / (200 * 1_000_000_000) * 1000 6 7print(f"KV state to transfer: {kv_cache_gib:.1f} GiB") 8print(f"ideal one-way floor at {interconnect_gib_s} GiB/s: {ideal_transfer_ms:.1f} ms") 9print(f"mixed GiB / decimal-GB/s floor (for comparison): {mixed_ms:.1f} ms") 10print("Queueing saved must exceed transfer plus protocol and scheduling overhead.")
Output
1KV state to transfer: 2.5 GiB 2ideal one-way floor at 200 GiB/s: 12.5 ms 3mixed GiB / decimal-GB/s floor (for comparison): 13.4 ms 4Queueing saved must exceed transfer plus protocol and scheduling overhead.
Diagram showing HBM bandwidth, Larger decode batch, KV residency, and More live requests.
HBM bandwidth, Larger decode batch, KV residency, and More live requests.

Bandwidth sets how hard you can push the batch. KV residency sets how many requests that batch can contain. The operating point is where those two ceilings meet the product's latency SLO.

When can prefill-decode disaggregation make latency worse?

Answer

When KV-cache transfer, coordination, or extra scheduling cost more than the head-of-line blocking they remove. Short prompts, light traffic, or a slow interconnect often favor a colocated worker.

Speculative decoding buys tokens per weight read

Batching amortizes one target-model weight read across requests. Speculative decoding applies the same idea within one request: one target read can score several future tokens.

A small draft model proposes k tokens. The large target model scores that span in one forward pass. Accepted prefix tokens plus a correction can replace several ordinary target decode steps.[12]Reference 12Fast Inference from Transformers via Speculative Decoding.https://arxiv.org/abs/2211.17192 The dedicated speculative-decoding lesson goes deeper; capacity reading here is simple: fewer target-weight streams per emitted token, if the draft is right often enough.

Speculative decoding flow: a draft proposes four tokens, one target pass accepts three and rejects one, then the target emits the accepted prefix plus a correction, four tokens from one verification pass.
Follow the accepted prefix, the rejected token, and the corrected fourth token. This example emits four tokens from one target verification pass, but the draft model's four proposal steps still have to be paid.

The accept/reject rule from Leviathan et al. can keep the target model's distribution. On a mismatch it resamples from the residual max(p - q, 0).

If all k drafts survive, it samples one extra token from the target distribution at the last draft position. Exactness doesn't imply speedup. Low acceptance, an expensive draft, or a clumsy kernel can lose to ordinary decoding.

This stdlib step uses a two-token vocabulary so you can check the residual path without a model runtime. p is the target distribution, q is the draft distribution, and the draft proposed token 0 then token 1.

speculative-accept-reject.py
1import random 2 3def residual_sample(p: list[float], q: list[float], rng: random.Random) -> int: 4 leftover = [max(pt - qt, 0.0) for pt, qt in zip(p, q)] 5 total = sum(leftover) 6 if total <= 0: 7 return max(range(len(p)), key=lambda i: p[i]) 8 cutoff = rng.random() * total 9 running = 0.0 10 for i, mass in enumerate(leftover): 11 running += mass 12 if cutoff <= running: 13 return i 14 return len(p) - 1 15 16def speculative_step( 17 draft_tokens: list[int], 18 draft_dists: list[list[float]], 19 target_dists: list[list[float]], 20 rng: random.Random, 21) -> list[int]: 22 accepted: list[int] = [] 23 for i, token in enumerate(draft_tokens): 24 p = target_dists[i] 25 q = draft_dists[i] 26 if rng.random() < min(1.0, p[token] / q[token]): 27 accepted.append(token) 28 continue 29 return accepted + [residual_sample(p, q, rng)] 30 extra_dist = target_dists[len(draft_tokens)] 31 cutoff = rng.random() 32 running = 0.0 33 extra = len(extra_dist) - 1 34 for i, mass in enumerate(extra_dist): 35 running += mass 36 if cutoff <= running: 37 extra = i 38 break 39 return accepted + [extra] 40 41rng = random.Random(0) 42draft_tokens = [0, 1] 43draft_dists = [[0.8, 0.2], [0.3, 0.7]] 44target_dists = [[0.7, 0.3], [0.2, 0.8], [0.4, 0.6]] 45emitted = speculative_step(draft_tokens, draft_dists, target_dists, rng) 46 47print(f"emitted tokens: {emitted}") 48print(f"target forwards paid in this sketch: 1") 49print(f"draft proposals paid: {len(draft_tokens)}")
Output
1emitted tokens: [0, 1, 1] 2target forwards paid in this sketch: 1 3draft proposals paid: 2

With this seed both drafts survive, so the step emits two accepted tokens plus one extra from the target. Follow-on methods such as EAGLE draft from the target model's hidden states instead of a separately trained small model.[13]Reference 13EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty.https://arxiv.org/abs/2401.15077

Serving question stays the same: did accepted tokens repay the extra draft and verification work?

Why can speculative decoding be exact yet still slower than normal decoding?

Answer

The accept/reject rule can preserve the target distribution, but runtime depends on acceptance and overhead. If the draft is wrong too often, or verification is expensive, saved target passes don't cover the extra work.

Lower precision cuts the bytes in that read

If decode rereads weights, fewer bytes per weight is a direct bandwidth lever. Start by asking whether the profile points to weight traffic or KV traffic.

Many stacks start from BF16/FP16, then move to FP8 when hardware and kernels exist, or to INT8/INT4-style weight-only quantization when the goal is a thinner decode stream.[14]Reference 14FP8 Formats for Deep Learning.https://arxiv.org/abs/2209.05433[15]Reference 15GPTQ: Accurate Post-Training Quantization for Generative Pre-Trained Transformershttps://arxiv.org/abs/2210.17323

KV-cache quantization is a different knob: it targets residency and long-context batch size once the cache, not the weights, is the cap.[16]Reference 16SnapKV: Compressing KV Cache by Selecting Global Attention Patterns.https://arxiv.org/abs/2403.19925[17]Reference 17Quantized KV Cachehttps://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache/

Bar chart of bytes per weight for a full read: FP16 at 2.0, FP8 and INT8 at 1.0, and INT4 at 0.5.
For memory-bound decode, weight precision is bytes moved per step. INT4 is one quarter of FP16 traffic before dequantization and kernel overhead. KV quantization is a separate lever for cache residency.

On the 8B replica, an FP16 full-weight read is 16 GB. INT4 is 4 GB of weight payload before scales and overhead.

That can move the single-stream ceiling, but only if the kernel doesn't spend savings on dequantization and quality still clears your eval. The next lesson, model quantization, is where those codecs live.

weight-bytes-by-precision.py
1parameters = 8_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: 16.0 GB of weights per full read 2INT8: 8.0 GB of weights per full read 3INT4: 4.0 GB of weights per full read 4INT4 weight traffic is 0.25x FP16 before kernel overhead.

Common mistake: 4-bit weights aren't mainly a disk-size trick. The serving win is fewer bytes reread during decode. Whether that becomes a latency win still depends on kernels, dequant overhead, and quality.

Why does weight-only INT4 often help decode latency more directly than prefill latency?

Answer

Decode repeatedly streams model weights for small per-token work, so fewer bytes per weight cut bandwidth pressure. Prefill uses larger matrix operations and can be compute-heavy, so weight compression isn't always the dominant lever there.

When one request no longer fits

The 8B replica was a fit-and-batch problem. Some traffic is not. Before choosing hardware, test long-context demand: a 1,000,000-token prompt on Llama 3 70B needs 2 * 1_000_000 * 80 * 8 * 128 * 2 bytes of KV, about 305 GiB.

That isn't a "buy one more H100" situation. One request has crossed the single-device residency boundary.

Context parallelism (CP) splits the sequence dimension across devices, not the layer stack or the batch. Each GPU holds a shard of the prompt and its KV.

Attention still needs cross-shard Keys and Values, so implementations use ring-style exchange: while a device computes blockwise attention on local queries, K/V blocks rotate around the ring.[18]Reference 18Ring Attention with Blockwise Transformers for Near-Infinite Context.https://arxiv.org/abs/2310.01889 Context length can scale roughly with device count until communication becomes the next roof.

That isn't tensor parallelism, which shards weights inside a layer, or pipeline parallelism, which shards layers. Those appear when weights don't fit.

Llama 3 70B in FP16 is about 140 GB of parameters, so it can't reside on one 80 GB GPU even at batch 1.[5]Reference 5The Llama 3 Herd of Models.https://arxiv.org/abs/2407.21783 The next chapter, model parallelism, makes that split its main subject.

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 Llama 3 70B 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 Llama 3 70B request KV: 305.2 GiB 2evenly sharded over 4 devices: 76.3 GiB/device 3Communication and runtime buffers still add overhead.

What bottleneck does context parallelism target, and what bottleneck can it introduce?

Answer

It targets per-request context and KV that no longer fit on one GPU. It can introduce communication overhead because devices must exchange partial attention state across sequence shards.

When the budget lies

These traces show up when formulas look fine on paper but users or profiles disagree. Start with the symptom, name the ceiling it points to, then test one targeted fix.

"I bought a higher-FLOP GPU and decode barely moved"

Peak compute went up, but tokens per second didn't. Decode was already bandwidth-bound, so extra tensor cores wait on the same HBM reads. Profile HBM first. If it's saturated, raise effective batch, quantize weights, shrink KV, or choose a higher-bandwidth SKU.

"Throughput is up and chat feels worse"

Aggregate TPS rose after you lifted max batch, but users report sluggish streams. You optimized the cost corner and spent the latency corner; mean TPOT can still look acceptable. Gate on p95 ITL/TBT and TTFT, not replica tokens/s alone. Split interactive traffic from offline batch.

"Speculative decoding added latency"

A draft model is in the path, and p50 decode got worse. Acceptance may be too low, or draft-plus-verify overhead may exceed the saved target passes. Measure accepted tokens per draft span before rollout. If most proposals die early, keep ordinary decode.

"HBM climbs even though every session shares a system prompt"

KV usage looks close to batch * full_prompt despite a shared prefix. Copy-on-write preserves blocks only after a prefix-cache hit. Without a lookup and a tenant isolation policy, each request builds its own prompt KV. Enable prefix caching with an explicit sharing policy, then confirm block reference counts in the allocator.

A dashboard shows high TTFT, a rising prefill backlog, stable TPOT, and moderate KV memory. Which subsystem should you inspect first?

Answer

Inspect prefill scheduling and admission first. Stable TPOT suggests decode isn't currently degrading, while high TTFT plus a prefill backlog points to prompt work or queueing before the first token.

Size the 8B replica

Put pieces on one GPU. We want to serve Llama 3 8B FP16 on a single 80 GiB H100: about 16 GiB for weights, 8 GiB reserved for activations, allocator slack, and runtime, and average assistant context of 4,096 tokens. Before calculating, predict whether arithmetic-only KV ceiling is dozens or hundreds of requests.

One 4K request works out as:

text
1KV bytes = 2 * 1 * 4096 * 32 * 8 * 128 * 2 2 = 536,870,912 bytes 3 = 0.5 GiB

KV budget = 80 - 16 - 8 = 56 GiB

Maximum batch = 56 / 0.5 = 112 concurrent 4K requests

That's a payload ceiling, not a production setting. Long-context bursts, fragmentation, CUDA graphs, and latency SLOs all eat headroom. A serving engineer caps well below 112 and watches measured HBM plus p95 ITL.

vram-capacity-headroom.py
1import math 2 3kv_bytes = 2 * 1 * 4_096 * 32 * 8 * 128 * 2 4kv_per_request_gib = kv_bytes / 1024**3 5weight_gib = 16 6runtime_gib = 8 7kv_budget_gib = 80 - weight_gib - runtime_gib 8arithmetic_ceiling = math.floor(kv_budget_gib / kv_per_request_gib) 9 10print(f"KV per 4K request: {kv_per_request_gib:.2f} GiB") 11print(f"KV budget after weights and runtime: {kv_budget_gib} GiB") 12print(f"arithmetic-only batch ceiling: {arithmetic_ceiling}")
Output
1KV per 4K request: 0.50 GiB 2KV budget after weights and runtime: 56 GiB 3arithmetic-only batch ceiling: 112

Why isn't that 112-request ceiling a safe production max concurrency?

Answer

It ignores fragmentation, temporary tensors, prefix-cache behavior, traffic with longer contexts, and latency SLOs. Production capacity should come from measured headroom, not from a closed-form KV quotient alone.

Complete the lesson

Mastery Check

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

1.An H100 SXM has 3.35 TB/s of HBM bandwidth. An 8B FP16 checkpoint is about 16 GB of weights. Tensor cores are idle during decode and HBM is saturated. What does the weight-read bound tell you?

Correct answer: About 209 single-token steps/s is an optimistic ceiling; extra FLOPs won't raise decode until you cut bytes moved or raise work per weight read.

3,350 GB/s divided by 16 GB is about 209 steps/s if a decode step streamed the whole FP16 footprint for one token. Saturated HBM with idle tensor cores is a bandwidth roof, so the useful moves are batching, quantization, and KV reductions rather than more peak FLOPs.

2.Llama 3 70B stores FP16 KV with 80 layers, 8 KV heads, and head dimension 128. How much KV does one 8,192-token request need, and what happens at 64 concurrent 8K requests?

Correct answer: About 2.5 GiB per request and 160 GiB for 64 requests, which already exceeds one 80 GB H100 before weights.

Bytes = 2 * 8192 * 80 * 8 * 128 * 2, which is 2.5 GiB. Multiplying by 64 concurrent requests gives 160 GiB of KV payload, so residency, not FLOPs, rejects that batch on an 80 GB GPU.

3.A GPU-hour costs $3.00. 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?

Correct answer: About $0.33 versus $3.33 per million tokens; the 10x gap can be valid if the slower setup is protecting TTFT or ITL.

Cost per million is hourly spend divided by hourly token output, times one million. Cutting sustained throughput by 10x raises token cost by 10x, which an interactive SLO may still justify.

4.Static batching runs remaining lengths 2, 2, and 5 as one request-level batch. Continuous batching then admits queued lengths 3 and 1 into freed slots. What did iteration-level scheduling change about decode intensity?

Correct answer: The same number of weight-read steps emitted more tokens, because live requests replaced padding after short jobs finished.

One decode step still streams weights roughly once for the batch. Filling vacated slots with queued work raises tokens per weight read. Surviving requests keep their KV; they aren't restarted.

5.A team adds speculative decoding. The target distribution must stay unchanged, but decode latency gets worse. Which diagnosis matches the mechanism?

Correct answer: Exactness can still hold while latency gets worse, if draft acceptance is low or draft-plus-verify work is expensive.

Exactness and speedup are separate. Draft generation, target verification, residual correction, and kernel overhead have to cost less than the ordinary target decode steps you avoid.

6.Weight-only INT4 cuts an 8B full-weight read from 16 GB to 4 GB. Decode is HBM-saturated. What conclusion follows?

Correct answer: INT4 can cut dominant weight traffic to one quarter, but realized speedup still depends on kernels, dequantization, and quality.

The profile identifies memory traffic as the ceiling. INT4 reduces bytes reread, but KV traffic, dequantization, kernel support, and eval quality prevent a guaranteed 4x latency win.

7.A Llama 3 70B 8K KV payload is 2.5 GiB. A 200 GiB/s interconnect gives a 12.5 ms one-way floor. When is prefill-decode disaggregation justified?

Correct answer: When decode queueing and interference avoided exceed KV-transfer plus coordination overhead.

Disaggregation pays a KV handoff. For this 70B 8K request the idealized floor is already 12.5 ms, so the split helps only if it saves more than that transfer plus protocol and scheduling cost.

8.One 1-million-token Llama 3 70B request needs about 305.2 GiB of KV. What does context parallelism do across four devices, and what new limit can appear?

Correct answer: It shards sequence and KV to about 76.3 GiB per device, and cross-shard attention communication can become the next bottleneck.

Context parallelism partitions the sequence dimension of one request. Ring-style exchange lets each query shard attend to remote K/V blocks, but that communication and its buffers limit scaling. Prefill-decode disaggregation is a different split.

8 questions remaining.

Next Step
Continue to Model Parallelism for LLM Inference

An 8B replica can be a bandwidth-and-KV budget on one GPU. Llama 3 70B's FP16 weights are already about 140 GB, so the next lesson is how to split one model across accelerators when a single copy no longer fits.

PreviousContinuous Batching & Scheduling
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

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

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

https://arxiv.org/abs/2401.09670

Roofline: An Insightful Visual Performance Model for Multicore Architectures

Williams, S., Waterman, A., & Patterson, D. · 2009

https://doi.org/10.1145/1498765.1498785

Efficiently Scaling Transformer Inference.

Pope, R., et al. · 2023 · arXiv preprint

https://arxiv.org/abs/2211.05102

H100 GPU

NVIDIA · 2026

https://www.nvidia.com/en-us/data-center/h100/

The Llama 3 Herd of Models.

Dubey, A., et al. · 2024 · arXiv preprint

https://arxiv.org/abs/2407.21783

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

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

https://arxiv.org/abs/2305.13245

Efficient Memory Management for Large Language Model Serving with PagedAttention.

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

https://arxiv.org/abs/2309.06180

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

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

https://www.usenix.org/conference/osdi22/presentation/yu

Sarathi-Serve: Efficient LLM Inference by Piggybacking Decodes with Chunked Prefills.

Agrawal, A., et al. · 2023 · arXiv preprint

https://arxiv.org/abs/2308.16369

Optimization and Tuning.

vLLM · 2026

https://docs.vllm.ai/en/latest/configuration/optimization.html

Splitwise: Efficient Generative LLM Inference Using Phase Splitting.

Patel, P., et al. · 2023

https://arxiv.org/abs/2311.18677

Fast Inference from Transformers via Speculative Decoding.

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

https://arxiv.org/abs/2211.17192

EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty.

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

https://arxiv.org/abs/2401.15077

FP8 Formats for Deep Learning.

Micikevicius, P., et al. · 2022

https://arxiv.org/abs/2209.05433

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

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

https://arxiv.org/abs/2210.17323

SnapKV: Compressing KV Cache by Selecting Global Attention Patterns.

Li, Y., et al. · 2024

https://arxiv.org/abs/2403.19925

Quantized KV Cache

vLLM Team · 2026 · vLLM Documentation

https://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache/

Ring Attention with Blockwise Transformers for Near-Infinite Context.

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

https://arxiv.org/abs/2310.01889

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.