← LLM Deployment from First PrinciplesModule 1 of 9
How LLM Inference Actually Works.
Before you can deploy anything, you need a precise mental model of what a model does with a prompt. This module traces one request end to end through a decoder-only Transformer: text becomes , tokens become vectors, mixes them, and the model emits one token at a time. The punchline every infrastructure decision later depends on: generation splits into a compute-bound phase and a memory-bound phase — and decode, not compute, is what you are really buying GPUs for.
2 phases
prefill vs decode
the central split
~1 FLOP/B
decode intensity
deeply memory-bound
O(n)
KV cache growth
per token, per seq
TTFT · TPOT
the metrics
latency that matters
The request lifecycle
When you send a prompt, no text ever reaches the neural network. The string is first turned into a list of integer IDs by the , those IDs index into a table of learned vectors, and from then on the model manipulates only numbers. What comes back is, one at a time, the single most-likely next — sampled, appended to the sequence, and fed back in. Generation is a loop, not a function call.
At the systems level the path is: text → token IDs → embedding vectors → N Transformer layers → over the vocabulary → sample one token → repeat. Everything expensive happens in those N layers, and almost all of the interesting engineering later in this course exists to make that loop cheaper, more parallel, or more memory-efficient.
The single most important structural fact: the work divides into two phases with completely different performance characteristics. Processing the prompt () happens once, in parallel, and is limited by compute. Generating the answer () happens token by token, is inherently sequential, and is limited by memory bandwidth. Confuse these two and every capacity estimate you make will be wrong.
Tokenization
A is a subword unit, not a word or a character. Modern models use byte-level BPE (byte-pair encoding): starting from raw UTF-8 bytes, the tokenizer greedily merges the most frequent adjacent pairs into a fixed vocabulary of typically 32k–256k entries. Common words become a single token; rare words and code fragment into several. A useful rule of thumb is ~1 token ≈ 0.75 English words.
Byte-level BPE has one property that matters operationally: it can represent any input — there is no out-of-vocabulary case, because the fallback is always individual bytes. Emoji, Base64, a language the model barely saw in training — all encode. This is why your token counts (and therefore your costs and your context-window usage) are only loosely correlated with character counts.
Tokenization is deterministic, cheap, and CPU-side — it is never the bottleneck. But it shapes everything downstream: the vocabulary size sets the width of the embedding table and the final projection, and the token count sets how much you will allocate.
How text becomes tokens
"Explain the KV cache."
Illustrative byte-level BPE segmentation (␣ marks a leading space, which is part of the token). Note how a common word is one token while “tokenization” splits into two.
Embeddings & the residual stream
Each token ID is converted to a vector by a pure table lookup into the embedding matrix, which has shape — no matrix multiply, just picking a row. That -dimensional vector (e.g. 4096 for Llama-2-7B) is the token's position in the residual stream: the running hidden state that flows through every layer and that each layer reads from and adds back to.
The width is the single most important shape in the model. Almost every weight matrix is , so it dominates both parameter count and per-token compute. When you later reason about why a 70B model is ~140 GB in FP16, this is the lever: parameters scale with the square of the hidden size times the number of layers.
Because attention is order-blind, positional information is injected here too (via rotary embeddings, RoPE, in most modern models) so that the model can tell token 5 from token 50. From this point on, the sequence is a matrix of shape moving through the stack.
Attention and the KV cache
Inside each layer, lets every token read a weighted blend of other tokens. Each token produces a query (), and every token exposes a key () and a value (). The output for a token is : score your query against all keys, normalize, and gather a weighted sum of values. In a decoder this is causal — a token may only attend to itself and earlier positions.
Here is the insight that makes generation tractable. During decoding, the and vectors of every past token never change. Recomputing them for the whole history at every step would be quadratic and ruinous. So we cache them: the stores all past keys and values in HBM, and each new token computes just its own , attends against the cached /, and appends its own /. Generation becomes linear per step instead of quadratic.
That cache is not free. Its size per token is bytes — the 2 is one key plus one value — and it grows linearly with sequence length and with batch size. It is the dominant runtime memory consumer and, as the next sections show, the main reason decode is memory-bound. Modern models fight its growth with , where many query heads share a few KV heads.
KV-cache size calculator
How the per-token cache — and the total — grows with model, context and batch.
Model
KV dtype
Per token
320 KB
Total KV cache (8k × 1)
2.50 GB
At batch 1, this is memory that must live in HBM alongside the weights — often the first thing to run out.
Prefill vs decode — the central split
Prefill processes your entire prompt in a single forward pass. Because all prompt tokens go through together, the core operations are big matrix–matrix multiplies that saturate the GPU's : prefill is compute-bound, and its duration sets your . Longer prompts cost more prefill, roughly linearly (attention adds a quadratic term, but the matmuls dominate at typical lengths).
Decode then generates the answer one token at a time. Each step is a matrix–vector multiply: a single token's activations against the full weight matrices, plus a read of the entire . Because token cannot begin until token exists, decode is inherently sequential — you cannot parallelize across time within one sequence. This is why a 500-token answer takes ~500 sequential steps no matter how much compute you throw at it.
The play button below traces the loop: watch the prompt light up all at once (prefill), then tokens emit one by one (decode). Almost every optimization later in this course — continuous batching, paged KV cache, speculative decoding — is an attack on the cost or the sequentiality of that decode loop.
The autoregressive loop, live
130 ms
TTFT (prefill)
42 ms
TPOT (per token)
0 ms
elapsed
# The autoregressive decode loop, stripped to its essentials.
# This is what every inference engine is optimizing around.
def generate(model, prompt_ids, max_new_tokens, temperature=0.8, top_p=0.9):
# ---- PREFILL: whole prompt in ONE forward pass (compute-bound) ----
# Populates the KV cache for every prompt position.
logits, kv_cache = model.forward(prompt_ids, kv_cache=None)
next_id = sample(logits[-1], temperature, top_p) # first generated token
output = [next_id]
# ---- DECODE: one token per iteration (sequential, memory-bound) ----
for _ in range(max_new_tokens - 1):
# Only the single new token goes in; K/V for the past come from cache.
# Each step reads ALL weights + the ENTIRE kv_cache from HBM to make ONE token.
logits, kv_cache = model.forward(next_id, kv_cache=kv_cache)
next_id = sample(logits[-1], temperature, top_p)
if next_id == model.eos_id:
break
output.append(next_id)
return output
def sample(logits, temperature, top_p):
if temperature == 0:
return argmax(logits) # greedy: deterministic
probs = softmax(logits / temperature) # temperature scaling
probs = nucleus_filter(probs, top_p) # keep smallest set summing to p
return multinomial(probs) # draw one token
Why decode is memory-bound
To generate one token, the GPU must read every weight in the model from HBM at least once, plus the entire KV cache — and with all that traffic it produces a single token's worth of math. The ratio of useful arithmetic to bytes moved, the , is only ~1–2 FLOP per byte. A modern GPU wants a few hundred FLOP/byte before its compute units are the bottleneck. Decode lives deep in the region — often using single-digit percentages of the GPU's peak FLOPs.
This flips the usual intuition. For single-stream decode, tokens-per-second is governed by HBM bandwidth, not FLOPs: roughly . A 70B FP16 model touches ~140 GB per token; on an H100 at ~3.35 TB/s that is a hard floor near ~42 ms/token (~24 tok/s) before any overhead. The roofline below makes the gap between decode and the compute roof visual — and lets you see that a faster GPU raises the roof but does not move decode out of the memory-bound zone.
The escape hatch is batching: run many sequences at once so each expensive weight read is amortized across dozens of tokens. That raises effective arithmetic intensity and pushes you toward the compute roof — trading a little per-request latency for a large gain in aggregate . This single fact — decode is memory-bound, batching fixes it — is the economic foundation of every serving system in Modules 4–9.
The roofline — why decode wastes the GPU
Attainable throughput vs. arithmetic intensity. Left of the ridge you are memory-bound.
GPU
Will it fit? — VRAM budget
Weights + KV cache + workspace vs. the memory you actually have.
Model
Weight precision
GPU
100% of 1× H100
Sampling the next token
The final layer projects the hidden state to a vector of — one raw score per vocabulary entry. turns those scores into an actual token. Greedy decoding takes the argmax (fully deterministic). Temperature divides the logits before softmax: below 1 sharpens toward the top choice, above 1 flattens toward randomness.
On top of temperature, two truncation strategies keep sampling from picking absurd low-probability tokens. Top-k keeps only the highest-probability tokens; top-p (nucleus) keeps the smallest set whose probabilities sum to (e.g. 0.9), adapting the cutoff to how confident the model is. These are cheap CPU/GPU post-processing steps — negligible next to a forward pass — but they define the character of the output.
Sampling is also where features like logit bias, repetition penalties, and structured/JSON-constrained decoding hook in, by masking or reshaping the logits before the draw. Nothing here changes the performance story: decode is still one forward pass per token; sampling just decides which token that pass yields.
Measuring latency: TTFT, TPOT, TPS
Three numbers describe inference latency, and they map directly onto the two phases. (time-to-first-token) is how long the user waits before text starts appearing — dominated by queueing plus prefill, and it grows with prompt length. (time-per-output-token, a.k.a. inter-token latency) is the streaming speed once generation begins — governed by decode and therefore by HBM bandwidth.
TPS (tokens per second) comes in two flavors that are constantly confused. Per-stream TPS is just — the speed one user feels. System TPS is the total across all concurrent requests, which batching drives far higher than any single stream. A server can deliver 20 tok/s to each of 100 users (system TPS 2000) even though no single user exceeds 20.
This is the tension that defines serving. You tune batch size, scheduling, and the prefill/decode mix to maximize system TPS while keeping TTFT and TPOT inside an SLA. Total user-visible latency is simply — the timeline widget below breaks it apart.
Latency breakdown
Total = TTFT + (output tokens − 1) × TPOT. See how decode dominates long answers.
10.28 s
total latency
40
per-stream tok/s
Glossary
Every dotted-underlined term on this page links back here. Hover any of them in context, or browse the full definitions below.
Token
A token is a subword unit produced by the tokenizer (typically byte-level BPE). Common words are single tokens; rare words split into several. As a rule of thumb ~1 token ≈ 0.75 English words. The model only ever sees token IDs, never raw text.
Tokenizer
The tokenizer converts a UTF-8 string into a list of integer IDs and back. Byte-level BPE guarantees any input is representable (no out-of-vocabulary) by starting from raw bytes and greedily merging frequent pairs. Vocabulary sizes are typically 32k–256k.
Embedding
The embedding matrix has shape [vocab_size, d_model]. Converting a token ID to its vector is a pure row lookup — no matrix multiply. The same matrix is often tied to the output projection that produces logits.
d_model (hidden size)
d_model is the size of the hidden state carried through the network (e.g. 4096 for Llama-2-7B). Most weight matrices are O(d_model²), so it dominates parameter count and per-token compute.
Attention
Scaled dot-product attention computes softmax(QKᵀ/√d_k)·V. Each query token scores every key, normalizes with softmax, and gathers a weighted sum of value vectors. In decoders it is causal: a token may only attend to earlier positions.
KV cache
During generation, the K and V projections of every previous token are cached in HBM so they are not recomputed. Size grows linearly with sequence length and batch: 2 · n_layers · n_kv_heads · head_dim · dtype_bytes per token. It is the dominant runtime memory consumer and a key reason decode is memory-bound.
Prefill
Prefill runs one forward pass over all N prompt tokens at once. Because it is a big matrix–matrix multiply, it is compute-bound and saturates the GPU's Tensor Cores. Its latency determines time-to-first-token.
Decode
Decode produces tokens one by one; each step reads all model weights plus the KV cache from HBM to compute a single new token. It is a matrix–vector operation with arithmetic intensity ~1 FLOP/byte — deeply memory-bandwidth-bound.
Autoregressive generation
The model outputs a probability distribution over the next token, one is sampled, appended, and fed back in. Token t+1 cannot be computed until token t exists, which is why decode cannot be parallelized across time within a single sequence.
Logits
The final hidden state is projected to a vector of size vocab_size. These logits are turned into probabilities by softmax (after temperature scaling and top-k/top-p filtering) and then sampled.
Sampling
Greedy takes the argmax. Temperature scales logits before softmax (higher = more random). Top-k keeps the k highest-probability tokens; top-p (nucleus) keeps the smallest set whose cumulative probability exceeds p. These trade determinism for diversity.
Grouped-query attention (GQA)
GQA sits between multi-head attention (one KV head per query head) and multi-query attention (one KV head total). By using e.g. 8 KV heads for 64 query heads, it cuts KV-cache size and bandwidth several-fold with minimal quality loss — which is why modern models use it.
Memory-bound
A kernel is memory-bound when its arithmetic intensity (FLOPs per byte moved) is below the hardware's ridge point. Single-stream LLM decode moves ~1–2 FLOPs per byte against a GPU that wants a few hundred, so it uses only single-digit percentages of peak compute.
Arithmetic intensity
Defined as total FLOPs ÷ bytes read/written to HBM. Compared against a GPU's ridge point (peak FLOP/s ÷ peak bytes/s), it predicts whether a kernel is limited by math or memory. Batching raises effective intensity by reusing weights across many sequences.
HBM
HBM2e/HBM3/HBM3e are 3D-stacked DRAM dies giving enormous bandwidth (H100 HBM3 ≈ 3.35 TB/s) at capacities of 24–192 GB. Because decode is memory-bound, HBM bandwidth — not FLOPs — is usually the ceiling on tokens/sec.
VRAM
On discrete GPUs, VRAM is separate HBM/GDDR the CPU cannot directly address. A model runs only if weights + KV cache + activation/workspace buffers all fit. On Apple Silicon there is no separate VRAM — see unified memory.
Quantization
Reducing precision from FP16 (2 bytes) to INT8/INT4 (1 byte / 0.5 byte) shrinks the model and, because decode is memory-bound, speeds it up roughly proportionally. Methods like GPTQ and AWQ minimize the resulting accuracy loss.
GGUF
GGUF packs the tensors, tokenizer and hyperparameters into one file designed to be mmap'd, so the OS pages weights in on demand. It is the standard format for CPU/Apple-Silicon inference and for the Q2–Q8 k-quant schemes.
Safetensors
Safetensors stores a JSON header of tensor offsets followed by raw bytes, enabling zero-copy memory-mapped loading with no arbitrary code execution (unlike pickle). It is the default for Hugging Face FP16/BF16 checkpoints served by vLLM and TensorRT-LLM.
Unified memory
On Apple Silicon (and Grace-Hopper style parts) CPU and GPU address the same RAM, so there is no PCIe copy and the whole capacity can hold model weights. A 128 GB Mac can therefore run models a 24 GB discrete GPU cannot — but at the memory's lower bandwidth.
CUDA
CUDA exposes the GPU as a grid of thread blocks running the same kernel (SIMT). Threads are grouped into 32-wide warps scheduled on streaming multiprocessors. The mature CUDA ecosystem (cuBLAS, cuDNN, NCCL) is a major reason NVIDIA dominates inference.
Streaming Multiprocessor (SM)
A datacenter GPU has ~100+ SMs, each with warp schedulers, a large register file, L1/shared memory, CUDA cores and Tensor Cores. Thread blocks are assigned to SMs; occupancy (active warps per SM) hides memory latency.
Tensor Core
Tensor Cores perform fused matrix multiply-accumulate on small tiles in one operation, delivering the bulk of a GPU's FLOPs for FP16/BF16/FP8/INT8. They are why modern GPUs are orders of magnitude faster at matmuls than CUDA cores alone.
NVLink / NVSwitch
NVLink gives each H100 ~900 GB/s of GPU-to-GPU bandwidth; NVSwitch connects all GPUs in a node in an all-to-all fabric. This is what makes intra-node tensor parallelism viable, since it exchanges activations after every layer.
PCIe
PCIe Gen5 x16 offers ~64 GB/s per direction — roughly an order of magnitude below NVLink. It is fine for feeding data to a single GPU but a bottleneck for tensor parallelism, which is why NVLink exists.
MIG (Multi-Instance GPU)
MIG splits an A100/H100 into up to seven instances with dedicated SMs, cache and memory bandwidth, giving hard isolation for multi-tenancy or for serving small models that would otherwise waste a whole GPU.
TTFT (time to first token)
Time-to-first-token measures responsiveness: how long the user waits before text starts streaming. It is set by queueing + prefill time and scales with prompt length. Distinct from per-token speed (TPOT).
TPOT / ITL
TPOT is the average time between successive generated tokens during decode. Because decode is memory-bound, TPOT is roughly bytes-moved-per-token ÷ HBM bandwidth. Its reciprocal is per-stream tokens/sec.
Throughput vs latency
Batching many requests raises aggregate throughput (and GPU utilization) by reusing weight reads, but can raise individual latency. Serving is the art of maximizing throughput within a latency SLA (TTFT/TPOT targets).
FAQ
Decode is sequential — token t+1 depends on token t — so within a single stream you can't overlap steps in time. Extra GPUs help by (a) splitting one huge model across devices so each holds less (tensor parallelism, Module 5), or (b) serving more concurrent requests. Neither makes a single 500-token answer emit its tokens faster than one decode step at a time allows.
Yes — for a single stream. That waste is exactly what batching reclaims: by running many sequences through the same weight read, you convert idle compute into throughput. A well-batched server can approach the compute roof; an unbatched one leaves the GPU almost idle. This is why serving systems obsess over keeping the batch full.
Queue wait + prefill. Prefill scales with prompt length (roughly linearly for the matmuls, with a quadratic attention term at long contexts), so a 32k-token prompt has a much higher TTFT than a 200-token one. Prefix caching (Module 4) can eliminate prefill for a shared prompt prefix, collapsing TTFT.
Because it's the memory that grows with your workload. Weights are fixed, but KV cache scales with context length × batch size and lives in the same scarce HBM. It's usually the first resource you run out of, and it's read in full every decode step — so it drives both your memory ceiling and your bandwidth bill. PagedAttention (Module 4) exists to manage it efficiently.
Only the tokens you actually use cost KV cache — a 128k-capable model with a 500-token prompt allocates KV for ~500 tokens, not 128k (with good memory management). But long *actual* contexts are expensive: KV grows linearly with real sequence length, and attention cost grows with it too.
Increasingly, yes. Halving KV dtype from FP16 to FP8 halves both KV memory and the bandwidth read each step — a direct decode speedup — with small, usually acceptable quality impact for most workloads. It's one of the cheapest wins and is covered as KV quantization in Module 9.
References
The works the paper builds on — every entry links to the cited paper.
- [1]
Vaswani et al. (2017). Attention Is All You Need. NeurIPS — the Transformer architecture
- [2]
Kwon et al. (2023). Efficient Memory Management for LLM Serving with PagedAttention (vLLM). SOSP
- [3]
Dao et al. (2022). FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness. NeurIPS
- [4]
Ainslie et al. (2023). GQA: Training Generalized Multi-Query Transformer Models. EMNLP
- [5]
Frantar et al. (2022). GPTQ: Accurate Post-Training Quantization for Generative Pretrained Transformers. ICLR 2023
- [6]
Lin et al. (2023). AWQ: Activation-aware Weight Quantization for LLM Compression. MLSys 2024
- [7]
NVIDIA (2022). NVIDIA H100 Tensor Core GPU Architecture (Hopper Whitepaper). NVIDIA technical whitepaper
- [8]
Gerganov et al. (2023). llama.cpp — LLM inference in C/C++ (GGUF). GitHub
- [9]
Yu et al. (2022). Orca: A Distributed Serving System for Transformer-Based Generative Models. OSDI — iteration-level (continuous) batching