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 ScaleInference: TTFT, TPS & KV Cache
🚀HardInference Optimization

Inference: TTFT, TPS & KV Cache

Map prefill vs decode bottlenecks, measure TTFT and decode cadence, and size KV cache so concurrent sequences fit on one GPU.

30 min read
Learning path
Step 136 of 196 in the full curriculum
Capstone: Production AgentMulti-Query & Grouped-Query Attention

Personalize this lesson

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

At 09:17, an on-call engineer sees a familiar support report: the agent found the answer, but the chat window stays blank, then the reply arrives in uneven bursts. The GPU dashboard says its compute rate is healthy. Both observations can be true. The blank interval and the later stream are different pieces of one model call.

Trace that call from request arrival. Queueing and tokenization belong to its initial path. Then prefill runs the model over the prompt and builds reusable attention state. After the first token, decode repeats a one-token step until the response ends. Call that reusable state the key-value (KV) cache; it saves recomputation but consumes GPU memory.

Three counters make the trace operational: time to first token (TTFT) for the initial wait, tokens per second (TPS) for later output, and cache occupancy for the prompt plus generated tokens. A longer conversation can slow decode because full-attention steps read a longer cached prefix while still rereading the model weights.

Prefill versus decode: Autoregressive generation has two phases, and they often hit different hardware limits. That split is the reason later serving lessons exist, from KV cache paging to continuous batching.

Why doesn't a chat response appear all at once after you submit a prompt?

Answer

The model first runs a prefill pass over the whole prompt to build prompt state and produce the first token. After that, decode generates one token at a time because each new token depends on the tokens already produced.


The two phases of LLM inference

Use an unchunked request as the baseline. Prefill consumes prompt tokens together and writes their K/V state. Decode then consumes one new token per iteration, reads the cached prefix, predicts the next distribution, and appends one more K/V entry.

Two-panel inference pipeline: prefill processes many prompt tokens in one parallel pass, writes prompt K/V, and emits token 1; decode then generates one token per step while rereading weights and a growing KV cache.
After token 1 the work shape flips. Prefill is a parallel prompt pass that writes K/V. Low-batch decode is a serial loop that rereads weights plus a longer cached prefix.

The handoff is sequential even though the prompt work is parallel:

Diagram showing Prefill Phase (Compute-Bound), Decode Loop (Memory-Bound), Prompt tokens in parallel, and Compute & write prompt K/V.
Prefill Phase (Compute-Bound), Decode Loop (Memory-Bound), Prompt tokens in parallel, and Compute & write prompt K/V.

What is the cleanest mental split between prefill and decode?

Answer

In an unchunked baseline, prefill processes prompt tokens in parallel and builds the initial KV cache, so it drives time to first token. Decode processes one new token per step while reading model weights and cached K/V state, so it drives streaming speed and inter-token latency.

Phase 1: Prefill (processing the prompt)

In the baseline, all input tokens enter one forward pass in parallel. Each weight matrix can serve many prompt-token rows, giving the GPU large matrix multiplies to keep busy. For long prompts on modern accelerators, that shape is often compute-bound, although batch shape, kernel implementation, and hardware can move the boundary. Chunked prefill changes this schedule later for latency reasons. A toy trace looks like this:

text
1Input: "Explain why decode slows as context grows" 2→ Tokenize prompt 3→ Process all prompt tokens in one forward pass 4→ Produce KV cache entries for the prompt 5→ Produce logits (unnormalized scores) for the first output token

The wall-clock interval from request arrival to the first output token is TTFT. People feel that wait at the chat box, so the target belongs to the interaction, not to a generic model benchmark. Choose it from measured user tolerance and include the queueing and network path your product actually exposes.

Use CasePrimary pressureWhat to measure
Real-time voiceTurn-taking delayEnd-to-end TTFT and audio pipeline overhead
Code completionInterruption to typingTail TTFT for short prompts
Chat/conversationalVisible waitingTTFT plus streamed ITL
Batch processingJob completionThroughput and cost before TTFT

Phase 2: Decode (generating output tokens)

Once token 1 is sampled, the prompt needn't be processed again. The model generates subsequent tokens one at a time, autoregressively: feed the newest token through the model, reuse cached K/V for the prefix, append its K/V, and produce the next distribution. The trace below shows the loop:

text
1Step 1: Output so far: "Decode" → add to KV cache → forward pass → next token: "reads" 2Step 2: Output so far: "Decode reads" → add to KV cache → forward pass → next token: "cached" 3Step 3: Output so far: "Decode reads cached" → add to KV cache → forward pass → next token: "state" 4...

At low batch, each step does little matrix work for a large byte movement. The GPU reads model weights and the request's KV cache from GPU high-bandwidth memory (HBM), so the arithmetic units can wait on data instead of doing math. In a full-attention layer, a longer response also means a longer cached prefix to read. Per-token latency can rise even though the weight tensor is unchanged.

Read the following timeline as instrumentation, not as a model benchmark. Arrival is 0 ms; four output tokens are observed at 320, 355, 392, and 428 ms. The first timestamp closes TTFT. Only the gaps after it describe decode cadence.

measure-prefill-and-decode.py
1arrival_ms = 0 2token_times_ms = [320, 355, 392, 428] 3 4ttft_ms = token_times_ms[0] - arrival_ms 5itls_ms = [ 6 current - previous 7 for previous, current in zip(token_times_ms, token_times_ms[1:]) 8] 9mean_itl_ms = sum(itls_ms) / len(itls_ms) 10tps = 1000 / mean_itl_ms 11 12print("TTFT:", ttft_ms, "ms") 13print("decode ITLs:", itls_ms, "ms") 14print(f"decode TPS: {tps:.1f}")
Output
1TTFT: 320 ms 2decode ITLs: [35, 37, 36] ms 3decode TPS: 27.8

Why prefill and decode hit different roofs

The useful lens is arithmetic intensity: FLOPs performed per byte fetched from high-bandwidth memory (HBM). It asks whether a phase has enough reuse to keep math units busy.

During prefill, one weight matrix serves many prompt-token rows. That reuse raises FLOPs per byte.

Qwen3.6-27B gives a concrete scale: its 27B-parameter BF16 language weights occupy about 54 GB (27×109×227 \times 10^{9} \times 227×109×2 bytes), before KV cache, runtime buffers, or vision-encoder tensors.[1]Reference 1Qwen3.6-27Bhttps://huggingface.co/Qwen/Qwen3.6-27B A long prompt can amortize that footprint across many matrix multiplications, so Tensor Cores often become the limiting resource. Short prompts or an underfilled batch can behave differently.

During low-batch decode, one new token supplies only a thin matrix-vector-shaped workload. Each step still moves weights and cached K/V, but there are fewer FLOPs to amortize those bytes. HBM traffic can therefore set token cadence before peak compute does.

Increasing decode batch can change that balance by letting one weight read serve multiple active sequences.

PhaseTokens ProcessedEffective BatchArithmetic IntensityBottleneck
PrefillNNN (prompt)Large (full prompt)High (many FLOPs/byte)Often compute (TFLOPS)
Decode1 at a time1Low (few FLOPs/byte)Often memory bandwidth (TB/s)

The roofline model[2]Reference 2Roofline: An Insightful Visual Performance Model for Multicore Architectureshttps://doi.org/10.1145/1498765.1498785 turns that lens into a ceiling: at intensity III, attainable throughput is bounded by min⁡(Pcompute,  I×BWHBM)\min(P_{compute},\; I \times BW_{HBM})min(Pcompute​,I×BWHBM​). The two roofs meet at a ridge point. Below it, bandwidth is the candidate limit; above it, compute is the candidate limit.

Treat that curve as a ceiling, not proof that a kernel reaches either roof.

For scale, NVIDIA lists 80 GB of HBM and 3.35 TB/s peak HBM bandwidth for H100 SXM.[3]Reference 3H100 GPUhttps://www.nvidia.com/en-us/data-center/h100/ Those are hardware specifications, not sustained application measurements.

Qwen3.6-27B's roughly 54 GB BF16 language weights fit in that capacity on paper, but its official 262,144-token native context still leaves KV-cache policy and runtime headroom to budget.[1]Reference 1Qwen3.6-27Bhttps://huggingface.co/Qwen/Qwen3.6-27B

That distinction motivates IO-aware kernels such as FlashAttention[4]Reference 4FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135 for long prefills, while PagedAttention[5]Reference 5Efficient Memory Management for Large Language Model Serving with PagedAttention.https://arxiv.org/abs/2309.06180 addresses KV-cache allocation and reuse during serving.

Put a measured point on the roofline

Use one kernel or one clearly defined phase at a time. Record elapsed time, FLOPs, and bytes moved through HBM for a correctness-checked workload. Then compute Pmeasured=FLOPs/timeP_{measured}=\text{FLOPs}/\text{time}Pmeasured​=FLOPs/time and I=FLOPs/HBM bytesI=\text{FLOPs}/\text{HBM bytes}I=FLOPs/HBM bytes, and plot (I,Pmeasured)(I, P_{measured})(I,Pmeasured​) against roofs for the same GPU, precision, and kernel scope. Allocated cache capacity is not the same as bytes actually moved, so use profiler traffic where possible.

Near the sloped roof with HBM traffic near an empirically sustained bandwidth ceiling, bandwidth is a plausible limit. Near the flat, precision-specific compute roof with unsaturated HBM, compute is more plausible. A point far below both roofs calls for checking launch overhead, underfilled work, synchronization, communication, or accounting before changing model architecture.

To find which input moved the point, hold engine, hardware, precision, and correctness constant while varying one knob:

  • Batch: Raising decode batch can reuse weights across sequences and move intensity right, but may worsen per-request ITL.
  • Sequence: A longer prompt changes prefill work. A longer full-attention decode sequence adds K/V reads and can move intensity left.
  • Bytes: Changing weight or KV precision changes traffic and often the compute roof too. Remeasure and redraw both axes instead of comparing points from different precisions.

Why is prefill usually compute-bound while single-stream decode is usually memory-bandwidth-bound?

Answer

Prefill reuses loaded weights across many prompt tokens in parallel, producing many FLOPs per byte fetched. Decode handles one new token at a time, so it repeatedly reads model weights and the growing KV cache for relatively thin matrix-vector work.


Four numbers for one request

The alert from the opening has two clocks. TTFT stops when token 1 appears; decode metrics start with the gaps that follow. Keep them separate before diagnosing a slow request.

Request timeline for timestamps 320, 355, 392, and 428 ms: TTFT is the 320 ms wait through queue, tokenize, prefill, and token 1; the following decode gaps are 35, 37, and 36 ms ITL.
TTFT is the 320 ms wait through token 1. The next three gaps are 35, 37, and 36 ms. Those decode gaps are ITL; their mean is TPOT, and 1000 / 36 ms is about 27.8 decode tokens/s.

TTFT (time to first token)

TTFT is wall-clock time until the first output token appears.[6]Reference 6Metricshttps://docs.vllm.ai/en/stable/design/metrics/ At kernel level, prefill is usually largest component. User-visible TTFT also includes tokenization, queueing, scheduling, and network overhead. Some engines start their timer when a request is scheduled and report queue time separately, so check definitions before comparing series.

Once queueing is controlled, longer prompts generally add prefill work, but exact scaling depends on prompt batch, attention implementation, and hardware. Set TTFT SLO from your product interaction, not from a headline benchmark.

TPS (tokens per second)

Here, TPS means decode throughput: how fast later output tokens arrive after token 1. For one request, use 1000/mean ITLms1000 / \text{mean ITL}_{ms}1000/mean ITLms​. Some dashboards divide all output tokens by end-to-end request time instead, folding prefill into the rate and making a long prompt look like a slow generator.

At low batch, HBM bandwidth often limits decode. Architecture, quantization, parallelism, engine, context length, and batch shape all change the result, so measure the deployed configuration. Production engines use batched inference, often with continuous batching,[7]Reference 7Orca: A Distributed Serving System for Transformer-Based Generative Models.https://www.usenix.org/conference/osdi22/presentation/yu to raise aggregate throughput across requests.

ITL (inter-token latency)

ITL is the gap between consecutive output tokens.[6]Reference 6Metricshttps://docs.vllm.ai/en/stable/design/metrics/ For one isolated request, ITL in seconds is the inverse of TPS (ITL=1/TPSITL = 1/TPSITL=1/TPS). For millisecond traces, use TPS=1000/ITLmsTPS = 1000/ITL_{ms}TPS=1000/ITLms​.

Higher ITL makes a stream feel choppier. Under batching, scheduling and resource contention can widen individual gaps even when aggregate TPS improves. Alert on distributions from product traffic, not one copied threshold.

TPOT (time per output token)

TPOT averages time per generated output token after token 1. It may be the mean of one request's ITL values or an aggregate statistic across requests, depending on the benchmark definition.[6]Reference 6Metricshttps://docs.vllm.ai/en/stable/design/metrics/

Read both numbers together. ITL exposes jitter and stalls; TPOT summarizes decode pacing. A prefill interruption or scheduling decision can worsen either one without changing model weights.

The four metrics are summarized below:

MetricWhat it measuresPhaseWhat drives itUseful aggregation
TTFTTime until first output token appearsPrefill pathPrompt length, model size, queueingMedian and tail latency
TPSSpeed of token generation after the firstDecodeMemory bandwidth, batchingPer-request and aggregate rate
ITLTime between consecutive tokensDecodeScheduling and contentionDistribution of token gaps
TPOTAverage time per output token after firstDecodeScheduling, batching, contentionRequest or benchmark mean

If users complain that nothing appears for a long time, which metric do you inspect first? What if streaming looks choppy after it starts?

Answer

Inspect TTFT first when the initial pause is too long, because that points to prefill, queueing, tokenization, or scheduling before the first token. Inspect ITL, TPOT, and TPS when streaming is choppy after generation begins, because those describe decode pacing and system contention.

This tiny router shows how an alert can choose an investigation path. Its thresholds are illustrative policy inputs, not benchmark results:

route-latency-investigation.py
1def investigate(ttft_p95_ms: int, itl_p95_ms: int) -> str: 2 if ttft_p95_ms > 900 and itl_p95_ms <= 80: 3 return "inspect queueing and prefill" 4 if itl_p95_ms > 120: 5 return "inspect decode scheduling and memory pressure" 6 return "within example thresholds" 7 8print("long initial pause:", investigate(ttft_p95_ms=1100, itl_p95_ms=60)) 9print("choppy stream:", investigate(ttft_p95_ms=350, itl_p95_ms=160))
Output
1long initial pause: inspect queueing and prefill 2choppy stream: inspect decode scheduling and memory pressure

Back-of-the-envelope: a bandwidth upper bound

Before running a serving benchmark, estimate one optimistic limit. For a single low-batch decode stream, most model weights must be read for each token, so memory bandwidth is a useful first bound:

Max TPS≈HBM BandwidthModel size (bytes)\text{Max TPS} \approx \frac{\text{HBM Bandwidth}}{\text{Model size (bytes)}}Max TPS≈Model size (bytes)HBM Bandwidth​

For this calculation, assume dense Qwen3.6-27B language weights in BF16, one H100 SXM, weights resident on that GPU, one low-batch stream, and ideal peak HBM bandwidth. The model card implies about 54 GB of language weights, before KV cache, runtime buffers, extra tensors, and allocator headroom.[1]Reference 1Qwen3.6-27Bhttps://huggingface.co/Qwen/Qwen3.6-27B

NVIDIA lists 3.35 TB/s peak HBM bandwidth for H100 SXM.[3]Reference 3H100 GPUhttps://www.nvidia.com/en-us/data-center/h100/ Treat that specification as an optimistic roof, not a sustained application measurement.

Ideal weight-read bound≈3,350 GB/s54 GB≈62 tokens/sec\text{Ideal weight-read bound} \approx \frac{3{,}350 \text{ GB/s}}{54 \text{ GB}} \approx 62 \text{ tokens/sec}Ideal weight-read bound≈54 GB3,350 GB/s​≈62 tokens/sec

Treat 62 tok/s as a theoretical peak-HBM ceiling, not a measured single-stream result. Two refinements keep this estimate honest:

  1. Sustained bandwidth must come from a benchmark on selected hardware and kernel mix. Replace 3,350 GB/s with that measured value; skip any universal peak-to-sustained percentage.
  2. KV-prefix traffic grows with sequence length sss: each full-attention decode step also reads prior K/V for that request and any batch mates. As sss rises, weight streaming is no longer the only HBM term, so the effective ceiling falls below this pure weight-read bound.

The bound omits activation memory, kernel overhead, scheduling, and interconnect traffic when the model is sharded. Benchmark selected engine, parallelism layout, model precision, prompt/output distribution, and concurrency to get real TPS. Batched inference can improve aggregate throughput by sharing weight reads, but it changes arithmetic intensity and per-request ITL at the same time.

estimate-bandwidth-bound.py
1def ideal_weight_stream_tps( 2 model_gb: float, bandwidth_gb_per_s: float, tensor_parallel_gpus: int 3) -> float: 4 aggregate_bandwidth = bandwidth_gb_per_s * tensor_parallel_gpus 5 return aggregate_bandwidth / model_gb 6 7model_gb = 54 8h100_capacity_gb = 80 9tensor_parallel_gpus = 1 10 11print("fits on one H100-80GB:", model_gb <= h100_capacity_gb) 12bound = ideal_weight_stream_tps(model_gb, 3350, tensor_parallel_gpus) 13print(f"single-GPU ideal weight-read bound: {bound:.1f} tokens/s")
Output
1fits on one H100-80GB: True 2single-GPU ideal weight-read bound: 62.0 tokens/s

Research note: For low-batch decode, quantization can move the weight-streaming bound by reducing bytes read per token. PagedAttention[5]Reference 5Efficient Memory Management for Large Language Model Serving with PagedAttention.https://arxiv.org/abs/2309.06180 improves KV-cache packing and sharing, while continuous batching[7]Reference 7Orca: A Distributed Serving System for Transformer-Based Generative Models.https://www.usenix.org/conference/osdi22/presentation/yu improves utilization across requests. These techniques address related serving constraints, but they aren't interchangeable.

Why does the rough bandwidth estimate divide HBM bandwidth by model size?

Answer

For a low-batch decode stream, each token step must effectively move the weight footprint through GPU memory. Dividing ideal HBM bandwidth by the weight footprint gives an optimistic upper bound before KV-cache reads, kernels, communication, and runtime overhead.


The KV cache: the dynamic capacity bottleneck

The 62 tok/s calculation asked how fast bytes might move. Capacity asks a different question: after weights and runtime buffers are resident, how much HBM remains for active request state? Often, KV cache consumes most of it.

Cached keys and values

During attention, each eligible layer computes Key and Value projections for each token. If a decode step discarded those tensors, it would have to process the whole prefix again to recreate them. That repeated work grows with every generated token.

What the cache is: Reusable attention state for tokens already seen. For each processed token, the model stores key routing vectors (K) and value content vectors (V). When the next token attends to earlier context, it reads those tensors instead of re-deriving the whole prefix. The cache grows with each token, and its size determines how many concurrent sequences fit in GPU memory.

The trace below shows how the cache expands with each generated token:

text
1Token 1: Compute K₁, V₁ → Store in cache 2Token 2: Compute K₂, V₂ → Store; Attend to [K₁,K₂], [V₁,V₂] 3Token 3: Compute K₃, V₃ → Store; Attend to [K₁,K₂,K₃], [V₁,V₂,V₃] 4...

Growing state creates an allocator problem as well as a byte-count problem. Systems such as vLLM use PagedAttention[5]Reference 5Efficient Memory Management for Large Language Model Serving with PagedAttention.https://arxiv.org/abs/2309.06180 to divide KV storage into fixed-size blocks (pages), much like operating-system virtual memory. A live sequence can then occupy non-contiguous blocks instead of reserving one maximum-length span. Paging reduces fragmentation and over-reservation; partially filled tail blocks and metadata overhead still remain.

What does the KV cache save, and what does it cost?

Answer

It saves compute by avoiding recomputation of old key and value projections at every decode step. It costs GPU memory that grows with layers, KV heads, head dimension, sequence length, precision, and active requests.

KV cache memory formula

For one sequence with a standard growing K/V cache:

KV Cache=2×L×nkv×dh×s×p\text{KV Cache} = 2 \times L \times n_{kv} \times d_h \times s \times pKV Cache=2×L×nkv​×dh​×s×p

Read it left to right: for every layer that stores K/V, every KV head, and every position, store one Key vector and one Value vector (the leading 222), each of dimension dhd_hdh​ and element size ppp. For BBB concurrent sequences, multiply by BBB. The linear dependence on sss and BBB is why a cache budget disappears quickly at long context or high concurrency.

Where:

  • LLL = number of layers that store growing K/V (not every layer in a hybrid model)
  • nkvn_{kv}nkv​ = number of KV heads (reduced with GQA/MQA[8]Reference 8GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints.https://arxiv.org/abs/2305.13245)
  • dhd_hdh​ = head dimension
  • sss = sequence length
  • ppp = bytes per element (2 for FP16/BF16, 1 for an 8-bit cache such as INT8 or FP8)

Concrete example: Qwen3.6-27B-style GQA sizing

ParameterValue
Gated Attention layers (LLL)16 (64 total; 48 Gated DeltaNet layers keep fixed-size state)
KV heads (nkvn_{kv}nkv​)4 (GQA, not 24 query heads!)
Head dim (dhd_hdh​)256
Sequence length (sss)4,096
DtypeFP16 (2 bytes)
KV Cache=2×16×4×256×4096×2≈0.27 GB  (≈0.25 GiB) per sequence\begin{aligned} \text{KV Cache} &= 2 \times 16 \times 4 \times 256 \times 4096 \times 2 \\ &\approx \textbf{0.27 GB} \; (\approx \textbf{0.25 GiB}) \textbf{ per sequence} \end{aligned}KV Cache​=2×16×4×256×4096×2≈0.27 GB(≈0.25 GiB) per sequence​

The arithmetic is 222 (K and V) × 16 attention layers × 4 KV heads × 256 values per head × 4,096 positions × 2 bytes. Qwen3.6-27B's official configuration has 64 layers arranged as 16 repeats of three Gated DeltaNet layers plus one Gated Attention layer.[1]Reference 1Qwen3.6-27Bhttps://huggingface.co/Qwen/Qwen3.6-27B Only those 16 attention layers belong in this standard growing-KV calculation. The 48 DeltaNet layers have separate recurrent state, which must be budgeted separately. This example sizes language-side attention state, not vision-encoder memory.

With GQA, this cache is 6× smaller than storing separate K/V tensors for all 24 query heads in those 16 layers. The same geometry with 24 KV heads would need about 1.61 GB (1.5 GiB) per sequence.

calculate-kv-cache-footprint.py
1def kv_cache_bytes( 2 layers: int, kv_heads: int, head_dim: int, tokens: int, bytes_per_value: int 3) -> int: 4 return 2 * layers * kv_heads * head_dim * tokens * bytes_per_value 5 6gqa_bytes = kv_cache_bytes(16, 4, 256, 4096, 2) 7mha_bytes = kv_cache_bytes(16, 24, 256, 4096, 2) 8 9print(f"GQA cache: {gqa_bytes / 1e9:.2f} GB") 10print(f"MHA cache: {mha_bytes / 1e9:.2f} GB") 11print("MHA / GQA:", mha_bytes // gqa_bytes)
Output
1GQA cache: 0.27 GB 2MHA cache: 1.61 GB 3MHA / GQA: 6
Qwen3.6-27B-style KV cache: per-sequence FP16 memory grows from 0.13 GB at 2K tokens to 0.27 GB at 4K and 0.54 GB at 8K, 8-bit cache is half that, and a 4K MHA cache with 24 heads is 1.61 GB versus 0.27 GB with 4 GQA KV heads.
For this GQA layout, KV memory scales linearly with context length $s$. Switching the cache from 4 KV heads to 24 query heads at 4K jumps from 0.27 GB to 1.61 GB per sequence. 8-bit cache halves the payload at each length.

Production note: A weight-fit check is not an admission policy. Qwen3.6-27B's BF16 language weights are about 54 GB, so they fit on one H100-80GB on paper, but remaining HBM must also cover KV cache, DeltaNet state, runtime buffers, prefix cache, optional vision-encoder weights, and allocator overhead. Its 262,144-token native window is usable only when policy reserves enough memory for active prompt-plus-output tokens.[1]Reference 1Qwen3.6-27Bhttps://huggingface.co/Qwen/Qwen3.6-27B

Try it yourself: A colleague says a 7B model (32 layers, 8 KV heads, 128 head dimension, FP16) can serve 200 concurrent users on one 80 GB GPU. The weights take about 14 GB. Use the formula, then leave a runtime reserve, to test that claim.

check-capacity-with-runtime-headroom.py
1def kv_gb_per_sequence(tokens: int) -> float: 2 values = 2 * 32 * 8 * 128 * tokens 3 return values * 2 / 1e9 4 5users = 200 6raw_total_gb = 14 + users * kv_gb_per_sequence(tokens=2048) 7print(f"raw weights plus KV: {raw_total_gb:.2f} GB") 8for reserve_gb in (8, 16): 9 admitted = raw_total_gb + reserve_gb <= 80 10 print(f"with {reserve_gb} GB runtime reserve: {admitted}")
Output
1raw weights plus KV: 67.69 GB 2with 8 GB runtime reserve: True 3with 16 GB runtime reserve: False

Raw weights plus KV leave a narrow margin. Whether 200 active sequences fit depends on measured activations, workspace, allocator behavior, and fragmentation for the serving engine. The True result under one arbitrary reserve is not a concurrency promise.

Why must the KV-cache formula use num_key_value_heads instead of num_attention_heads for GQA models?

Answer

GQA shares one K/V head across multiple query heads. The cache stores keys and values, not every query head, so using the full query-head count overestimates cache memory by the query-to-KV grouping factor.


Dynamic token budgeting

Context length is an architecture limit, not a concurrency budget. The deployable budget is leftover GPU memory after weights and runtime buffers, divided across active requests. One extra reserved token for request A leaves less room for request B.

When a request arrives, the engine estimates its prompt-plus-output KV demand. If that demand plus live cache exceeds remaining capacity, the scheduler must queue, reject, or preempt it. Capacity and latency belong on the same dashboard because overcommit is not a recovery plan.

The function below turns that policy into an upper bound. It takes GPU memory, static weight footprint, runtime reserve, cache shape (layers, KV heads, head dimension), and concurrency, then returns prompt-plus-output tokens per user:

dynamic-token-budgeting.py
1def max_context_for_budget( 2 gpu_memory_gb: float, 3 model_memory_gb: float, 4 runtime_reserve_gb: float, 5 num_layers: int, 6 num_kv_heads: int, 7 head_dim: int, 8 dtype_bytes: int = 2, # FP16 9 num_concurrent: int = 1, 10) -> int: 11 """Quick planning estimate using decimal GB for consistency with GPU datasheets.""" 12 available_memory = (gpu_memory_gb - model_memory_gb - runtime_reserve_gb) * 1e9 13 14 # Memory per token in KV cache 15 bytes_per_token = 2 * num_layers * num_kv_heads * head_dim * dtype_bytes 16 17 # Divide by concurrent users 18 budget_per_user = available_memory / num_concurrent 19 20 return int(budget_per_user / bytes_per_token) 21 22# Example: Qwen3.6-27B-style dimensions on one H100-80GB 23max_tokens = max_context_for_budget( 24 gpu_memory_gb=80, 25 model_memory_gb=54, # BF16 dense weights 26 runtime_reserve_gb=8, # engine buffers, workspaces, and allocator margin 27 num_layers=16, # Qwen3.6-27B: 16 Gated Attention layers 28 num_kv_heads=4, # GQA: 4 KV heads (not 24 query heads!) 29 head_dim=256, 30 num_concurrent=50, 31) 32print(f"max context per user: {max_tokens:,} tokens")
Output
1max context per user: 5,493 tokens

This calculation gives a planning limit, not a safe production limit. If 100 users need 5,000 reserved tokens each and the budget falls short, options include additional memory, lower precision, fewer active tokens, or application caps. Validate any choice with activations, communication buffers, allocator slack, DeltaNet state, and serving-runtime measurements.

Common mistake: Plugging query heads into a KV formula. Qwen3.6-27B lists 24 query heads but 4 KV heads for its Gated Attention path.[1]Reference 1Qwen3.6-27Bhttps://huggingface.co/Qwen/Qwen3.6-27B Using 24 overestimates this cache by 6×. Read num_key_value_heads and the model's layer layout; using all 64 layers would also count 48 DeltaNet layers outside this growing-KV formula.

An online admission check can reserve capacity from each request's prompt-plus-output token budget instead of assuming every request consumes the maximum architectural context:

admit-request-by-token-budget.py
1BYTES_PER_TOKEN = 2 * 16 * 4 * 256 * 2 2 3def kv_gb(tokens: int) -> float: 4 return tokens * BYTES_PER_TOKEN / 1e9 5 6def admit(existing_tokens: list[int], new_tokens: int, kv_budget_gb: float) -> bool: 7 needed = sum(kv_gb(tokens) for tokens in existing_tokens) + kv_gb(new_tokens) 8 return needed <= kv_budget_gb 9 10active = [4096] * 40 11print("admit 16K request:", admit(active, 16_384, kv_budget_gb=12)) 12print("admit 64K request:", admit(active, 65_536, kv_budget_gb=12))
Output
1admit 16K request: True 2admit 64K request: False

Why is maximum context length a production policy, not a model-card number alone?

Answer

Every extra active token consumes KV-cache memory. Allowing every user to use the model's full architectural context can collapse concurrency, so production systems cap prompt length, output length, and active tokens based on GPU memory budgets and latency goals.


Stop a new prefill from stalling live streams

Capacity and scheduling solve different failures. A request can fit in HBM and still make every active stream wait if its long prefill monopolizes a shared worker. The next choices either bound that interference or reduce per-request cache bytes.

Chunked prefill

On shared hardware, a large prefill competes with decode for the same iteration budget. Prioritizing it can improve the new request's TTFT while stalling existing streams, creating a TTFT-TPS tradeoff.

Chunked prefill makes that tradeoff explicit. Split a long prompt into smaller chunks, run one bounded slice, serve active decode steps, then continue the prompt. Without chunking, a 10K-token prefill can occupy the iteration budget until completion. With 2K-token chunks, active streams get turns between slices while the new request advances toward token 1:

text
1Without chunked prefill: 2 [Prefill 10K tokens ===========================] [Decode...Decode...Decode...] 3 ↑ All decode requests stall during this prefill 4 5With chunked prefill (chunk=2048): 6 [Prefill chunk1][Decode][Prefill chunk2][Decode][Prefill chunk3][Decode]... 7 ↑ Decode requests continue between chunks

Why this helps

Mixing compute-heavy prefill slices with memory-heavy decode can protect streaming cadence and tail latency. Chunk size is an operating point: smaller slices give decodes more frequent turns but can add scheduling overhead or delay the new request. vLLM documents this control,[9]Reference 9Optimization and Tuning.https://docs.vllm.ai/en/latest/configuration/optimization.html and Sarathi studies piggybacking decodes on chunked prefills.[10]Reference 10Sarathi-Serve: Efficient LLM Inference by Piggybacking Decodes with Chunked Prefills.https://arxiv.org/abs/2308.16369 Measure p95/p99 ITL and TTFT together.

interleave-prefill-with-active-decode.py
1def chunked_schedule(prompt_tokens: int, chunk_tokens: int) -> list[str]: 2 actions: list[str] = [] 3 remaining = prompt_tokens 4 while remaining: 5 processed = min(chunk_tokens, remaining) 6 actions.append(f"prefill {processed}") 7 remaining -= processed 8 actions.append("decode active streams") 9 return actions 10 11for action in chunked_schedule(prompt_tokens=6144, chunk_tokens=2048): 12 print(action)
Output
1prefill 2048 2decode active streams 3prefill 2048 4decode active streams 5prefill 2048 6decode active streams

Why does chunked prefill improve tail latency for existing decode streams?

Answer

A long prefill is compute-heavy and can monopolize the GPU. Chunking breaks that work into smaller pieces, letting the scheduler interleave decode steps so active users keep receiving tokens instead of waiting behind one giant prompt.

Prefill-decode disaggregation

Chunking still makes one GPU serve two competing phases. If prompt bursts remain the cause of tail ITL, place prefill and decode on separate GPU pools. Long-prompt prefill is often compute-heavy; low-batch decode is often bandwidth-heavy. Splitwise,[11]Reference 11Splitwise: Efficient Generative LLM Inference Using Phase Splitting.https://arxiv.org/abs/2311.18677 DistServe,[12]Reference 12DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving.https://arxiv.org/abs/2401.09670 and Mooncake[13]Reference 13Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving.https://arxiv.org/abs/2407.00079 study this separation when its isolation benefit outweighs KV-transfer cost:

Diagram showing Prefill pool compute, KV state, and Decode pool memory.
Prefill pool compute, KV state, and Decode pool memory.

What changes

Separate pools remove much of the head-of-line interference: a prefill burst no longer consumes decode iterations on same worker. They also let capacity follow workload shape, adding prefill workers for prompt bursts and decode workers for active sequences. Finally, each pool can use a topology that suits its phase rather than forcing one compromise.

The cost is moving initial KV state across an interconnect before decode starts. Disaggregation is attractive only when measured transfer time and operational complexity are smaller than interference it removes. Long prompts, bursty arrivals, and strict TTFT/ITL isolation are signals to test it, not guarantees.

The comparison below uses illustrative measurements. Replace them with paired runs from same hardware, engine, precision, workload, and correctness check before making a deployment choice.

compare-disaggregation-overhead.py
1def choose_layout(shared_phase_interference_ms: int, kv_transfer_ms: int) -> str: 2 if kv_transfer_ms < shared_phase_interference_ms: 3 return "separate prefill and decode pools" 4 return "keep phases colocated" 5 6print("bursty workload:", choose_layout(shared_phase_interference_ms=95, kv_transfer_ms=20)) 7print("small prompts:", choose_layout(shared_phase_interference_ms=8, kv_transfer_ms=20))
Output
1bursty workload: separate prefill and decode pools 2small prompts: keep phases colocated

When is prefill-decode disaggregation worth its extra KV-transfer cost?

Answer

It's most useful when prefill bursts hurt streaming latency or when prompt length and active-user load scale differently. Separate GPU pools let you size compute-heavy prefill and memory-bandwidth-heavy decode independently, but the KV transfer must be cheaper than the interference it removes.

KV cache quantization

If cache capacity, rather than latency interference, is the failing resource, reduce bytes per cached value. An 8-bit format such as FP8 uses about half the raw payload of FP16/BF16. Research systems also explore 3-bit and 2-bit caches, but quality and kernel support are model-, hardware-, and engine-specific.[14]Reference 14KVQuant: Towards 10 Million Context Length LLM Inference with KV Cache Quantizationhttps://arxiv.org/abs/2401.18079[15]Reference 15KIVI: A Tuning-Free Asymmetric 2bit Quantization for KV Cachehttps://arxiv.org/abs/2402.02750 vLLM documents FP8 KV-cache configuration and calibration.[16]Reference 16Quantized KV Cachehttps://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache/ See our model quantization deep-dive for weight and activation trade-offs:

KV Cache (8-bit)=KV Cache (FP16/BF16)2\text{KV Cache (8-bit)} = \frac{\text{KV Cache (FP16/BF16)}}{2}KV Cache (8-bit)=2KV Cache (FP16/BF16)​

An 8-bit K/V payload uses 1 byte per value instead of 2, so raw cache bytes are halved for the same sequence length and concurrency. Scaling metadata, padding, and runtime buffers mean allocated memory savings can be smaller.

Weight quantization shrinks static model memory. KV quantization targets dynamic per-request state, which grows with context and concurrency. If KV is the dominant resource and all other reservations stay fixed, halving raw bytes can approach a 2× cache-only concurrency gain. Real gain is lower when weights, allocator overhead, and runtime buffers consume most HBM.

The following planning example reuses Qwen3.6-27B-style GQA dimensions. Each active user reserves 5,120 prompt-plus-output tokens of FP16 KV state. The budget is the 18 GB left after 54 GB of language weights and an 8 GB runtime reserve on one 80 GB GPU, not the whole card:

estimate-kv-quantization-gain.py
1def users_from_kv_budget(kv_budget_gb: float, gb_per_user: float) -> int: 2 return int(kv_budget_gb / gb_per_user) 3 4tokens_per_user = 5120 5fp16_gb_per_user = 2 * 16 * 4 * 256 * tokens_per_user * 2 / 1e9 6fp8_gb_per_user = fp16_gb_per_user / 2 7kv_budget_gb = 80 - 54 - 8 8 9print(f"KV budget after weights and reserve: {kv_budget_gb:.0f} GB") 10print("FP16 users from remaining KV budget:", users_from_kv_budget(kv_budget_gb, fp16_gb_per_user)) 11print("FP8 users from remaining KV budget:", users_from_kv_budget(kv_budget_gb, fp8_gb_per_user))
Output
1KV budget after weights and reserve: 18 GB 2FP16 users from remaining KV budget: 53 3FP8 users from remaining KV budget: 107

Why can KV-cache quantization improve concurrency even if model weights are already quantized?

Answer

Weight quantization shrinks the static model footprint. KV-cache quantization shrinks per-token, per-request state, which is the dynamic memory cost that grows with context length and concurrent users.


One request, two bottlenecks

A request crosses two different operating regimes. Prefill turns prompt tokens into logits and initial K/V state, often making compute the first limit on TTFT. Decode then emits one token per step, often making HBM traffic the first limit on streaming cadence. The KV cache connects them and grows with every active token, so capacity can become the first limit on concurrency.

The useful diagnosis is causal: a long prompt expands prefill work and TTFT; a low-batch decode stream repeatedly moves weights and cached state; a longer context adds K/V reads; a larger batch can trade per-request ITL for aggregate reuse. Those are hypotheses to test with measurements, not labels to copy from a model card.

Build a serving budget

For a real serving budget, record hardware and engine, model and precision, prompt/output distribution, arrival process, concurrency, correctness result, and measured HBM/compute counters. Then report weight memory, KV bytes per token, TTFT and ITL p50/p99, raw throughput, and goodput under the product SLO. The first release constraint should be visible as compute, bandwidth, capacity, or an unmodeled overhead.

Build a serving-budget table from one measured workload. Record the inputs, calculate weight and KV reservations, compare TTFT and ITL with the SLO, and trace the first constraint to compute, bandwidth, capacity, or overhead. Keep that table as the release artifact, then rerun it when model, prompt mix, precision, or concurrency changes.

Complete the lesson

Mastery Check

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

1.Token timestamps for a streamed response are measured relative to request arrival: [320, 355, 392, 428] ms. Which interpretation is correct for this single request?

Correct answer: TTFT is 320 ms; decode ITLs are 35, 37, and 36 ms; TPOT is 36 ms; decode TPS is about 27.8 tokens/s.

TTFT is measured from request arrival to the first output token, so it's 320 ms. Decode ITLs are the gaps between consecutive streamed tokens: 35, 37, and 36 ms. TPOT for this request is the mean decode gap, 36 ms, and TPS is 1000 / 36, about 27.8 tokens/s. The first-token delay is not averaged into decode pacing.

2.Why is prefill usually compute-bound while single-stream decode is usually memory-bandwidth-bound?

Correct answer: Prefill reuses loaded weights across many prompt tokens in parallel, producing many FLOPs per byte fetched. Decode handles one new token at a time, so it repeatedly reads model weights and the growing KV cache for relatively thin matrix-vector work.

The decisive difference is arithmetic intensity. Prefill amortizes weight reads across many prompt tokens, so the GPU can do many matrix-multiply FLOPs per byte fetched. Low-batch decode has much thinner work per step: it handles one new token while still streaming weights and cached K/V state from HBM, so memory bandwidth often caps throughput before peak TFLOPS do.

3.A team deletes all cached K/V tensors after each emitted token to free GPU memory, but wants identical autoregressive outputs. What computational consequence follows?

Correct answer: Each decode step must recompute K/V for the full prior prefix, trading lower cache memory for repeated work.

The model still needs key and value tensors for earlier tokens when the newest token attends to its prefix. If those tensors are deleted, they must be regenerated by processing the prior prefix again. The KV cache avoids that repeated computation by retaining the historical K/V state, at the cost of memory that grows with sequence length.

4.A full-attention decode stream grows from 4,000 cached tokens to 32,000 while model weights, batch size, and hardware stay fixed. Its inter-token latency rises. Which mechanism best explains the slowdown?

Correct answer: Each decode step reads a longer K/V prefix, increasing cache memory traffic even though the model weight footprint is unchanged.

Caching avoids recomputing old K/V projections, but full attention for the newest token still reads keys and values from the growing prefix. That extra HBM traffic can raise inter-token latency as context grows. The weights don't grow, and a working KV cache avoids rerunning full prefill on every step.

5.A Qwen3.6-27B-style GQA sizing example has 16 Gated Attention layers, 24 query heads, 4 KV heads, head dimension 256, a 4,096-token active sequence, and an FP16 KV cache. What per-sequence KV-cache footprint should a capacity plan use?

Correct answer: About 0.27 GB, using 2 x 16 x 4 x 256 x 4,096 x 2 because only the Gated Attention layers store standard K/V tensors.

The KV-cache formula is 2 x layers x KV heads x head dimension x sequence length x bytes per element. For Qwen3.6-27B, use the 16 Gated Attention layers, not the 64 total layers. In GQA, the cache stores the 4 K/V heads, not the 24 query heads, so using query heads would overestimate this example by 6x.

6.An engine budgets KV cache with Qwen3.6-27B-style dimensions where bytes per token = 2 x 16 x 4 x 256 x 2 = 65,536 bytes. Forty active requests each reserve 4,096 tokens, and the KV budget for all active work is 12 GB. Which admission decision is correct for a new request that reserves either 16,384 or 65,536 prompt-plus-output tokens?

Correct answer: Admit the 16,384-token request and reject or queue the 65,536-token request, because the larger reservation would exceed the 12 GB KV budget.

The existing 40 requests reserve 40 x 4,096 x 65,536 bytes, about 10.7 GB. Adding 16,384 tokens needs about 1.1 GB more, for about 11.8 GB total, which fits under 12 GB. Adding 65,536 tokens needs about 4.3 GB more, which would push total KV memory above 12 GB. Admission should use reserved prompt-plus-output tokens against the KV budget instead of the model's architectural context length alone.

7.Qwen3.6-27B in BF16 has about 54 GB of dense weights. One H100 SXM GPU has ideal HBM bandwidth of 3,350 GB/s. For a low-batch decode weight-streaming ceiling, use Max TPS ~= HBM bandwidth / total model weight size, ignoring KV-cache reads, kernels, and scheduling losses. What upper bound does this give?

Correct answer: About 62 tokens/s, computed as 3,350 GB/s divided by 54 GB, before KV-cache reads, kernels, and scheduling losses.

The simplified weight-streaming ceiling divides ideal HBM bandwidth by the dense weight footprint: 3,350 / 54 is about 62.0 tokens/s. This is only a ceiling. Real TPS is lower after KV-cache reads, kernel overhead, sustained-bandwidth limits, and scheduling effects.

8.A serving worker is decoding tokens for active chats. When a new 10,000-token prompt is processed as one prefill, those active streams stop receiving tokens until the prefill finishes. You can't add a separate GPU pool this quarter, and existing users' inter-token latency must stay protected. What scheduler change targets this failure mode?

Correct answer: Split the long prefill into chunks and interleave decode steps between chunks.

A long compute-heavy prefill monopolizes the shared worker and delays decode steps for existing users. Chunked prefill breaks that block into smaller units so the scheduler can run decode between chunks. This can protect tail ITL for active streams, even if the new long-prompt request may take longer to finish prefill than in a single uninterrupted block.

9.A shared serving pool handles both long-prompt prefills and active decode streams. During prompt bursts, active streams pick up about 95 ms of extra inter-token delay. A prototype that sends prompt work to a separate prefill pool and transfers KV state to a decode pool adds 20 ms of KV-transfer time. The team wants to isolate TTFT bursts from streaming cadence and scale prompt load separately. What deployment choice does this measurement support?

Correct answer: Separate prefill and decode pools, because the measured KV-transfer cost is lower than the shared-pool interference and the phases scale on different resources.

Prefill-decode disaggregation is useful when the isolation benefit is larger than the cost of moving KV state. Here, 20 ms of transfer cost is less than 95 ms of shared-pool interference, and the workload also benefits from scaling prompt-heavy prefill separately from active-user decode. Weight quantization and larger contexts do not directly isolate prefill bursts from decode cadence.

10.An inference service has already quantized model weights. It still rejects many long-conversation requests because admitted sequences exhaust the remaining KV-cache budget, while TTFT and ITL for admitted requests are within SLO. Which change targets the limiting resource most directly?

Correct answer: Use an 8-bit KV cache, because it reduces the per-token, per-request K/V payload and can increase concurrency when KV memory is dominant.

Weight quantization shrinks the static model footprint, but this scenario is limited by dynamic per-request KV memory. Moving the KV cache from 16-bit to 8-bit roughly halves the raw K/V payload for the same sequence length and concurrency, though real gains are smaller after runtime buffers and allocator overhead. Chunked prefill and disaggregation target scheduling interference, not KV capacity, and raising the context limit increases KV demand.

10 questions remaining.

Next Step
Continue to Multi-Query & Grouped-Query Attention

The KV formula you just used stores $n_{kv}$ heads, not every query head. The next lesson is why models share those K/V heads (MQA and GQA), and what that sharing does to memory, bandwidth, and quality.

PreviousCapstone: Production Agent
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Qwen3.6-27B

Qwen Team · 2026

https://huggingface.co/Qwen/Qwen3.6-27B

Roofline: An Insightful Visual Performance Model for Multicore Architectures

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

https://doi.org/10.1145/1498765.1498785

H100 GPU

NVIDIA · 2026

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

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

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

https://arxiv.org/abs/2205.14135

Efficient Memory Management for Large Language Model Serving with PagedAttention.

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

https://arxiv.org/abs/2309.06180

Metrics

vLLM · 2026

https://docs.vllm.ai/en/stable/design/metrics/

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

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

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

https://arxiv.org/abs/2305.13245

Optimization and Tuning.

vLLM · 2026

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

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

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

https://arxiv.org/abs/2308.16369

Splitwise: Efficient Generative LLM Inference Using Phase Splitting.

Patel, P., et al. · 2023

https://arxiv.org/abs/2311.18677

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

Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving.

Qin, Y., et al. · 2024

https://arxiv.org/abs/2407.00079

KVQuant: Towards 10 Million Context Length LLM Inference with KV Cache Quantization

Hooper, C., Kim, S., Gholami, A., et al. · 2024 · arXiv preprint

https://arxiv.org/abs/2401.18079

KIVI: A Tuning-Free Asymmetric 2bit Quantization for KV Cache

Liu, Z., Chen, B., Hu, X., et al. · 2024 · arXiv preprint

https://arxiv.org/abs/2402.02750

Quantized KV Cache

vLLM Team · 2026 · vLLM Documentation

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

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.