Master Local AI
  1. 0Load
  2. 1You type
  3. 2Tokenize
  4. 3To the GPU
  5. 4Prefill
  6. 5Decode
  7. 6Show text
Can it run?

Chapter 1 · Novice

What Running a Model Actually Does

Prefill reads the prompt at once. Decode writes one token per pass.

  1. 0Loadonce: file → VRAM
  2. 1You typetext in the app
  3. 2TokenizeCPU: text → IDs
  4. 3To the GPUIDs cross PCIe
  5. 4Prefillwhole prompt, one pass
  6. 5Decoderead all weights → 1 token
  7. 6Show textID → word · repeat 5

1Watch

Now

Press a step on the right, or change any control under the machine.

Readout

Key to the machine

An old print shop. The press prints your whole prompt in one stroke: prefill. A carriage sweeps the type case (the weights) once per letter: decode. Watch the sweep.
How the numbers work

Prefill sends every prompt token through the model in one parallel pass, so its time (TTFT) is set by the GPU's tensor FLOPs; a 32k-token prompt on a small card makes the platen strain for seconds. The printed sheet is laid into the galley as slugs: that is the KV cache, and one slug always holds 512 tokens (past 14 slugs the rows stack higher, and the plate on the galley says how high). Decode makes one token per pass, and every pass sweeps the entire type case, re-reading all the active weights plus the cache so far, so tokens per second are set by memory bandwidth; the counter on the crate counts the tokens (the crate is full at 2,048). A cache bigger than the memory left beside the weights runs past the stop bar into the overflow tray. Weights bigger than the card move part of the type case into the tray (system RAM, or the SSD on unified-memory machines), where the carriage crawls. Weights are GGUF Q4_K_M; prefill assumes 50% of peak dense FP16 tensor FLOPs, decode 70% of peak bandwidth.

  • Press and sheet = prefill
  • Galley slugs = KV cache
  • Carriage = one weight sweep per token
  • Overflow tray = system RAM
Speed
Open full screen ↗

2Try this

Then change anything yourself.

3Read on ↓

One request, end to end

A request passes through five steps.

  1. Chat template. The app writes the messages in the model's exact prompt format. A wrong template is the most common cause of bad output.
  2. Tokenize. Text becomes integer token IDs (about 3–4 English characters each).
  3. Prefill. All prompt tokens go through the network in one pass. Each layer stores keys and values in the KV cache.
  4. Decode. The model outputs logits (a score for every token). A sampler picks one token. The model reads the cache and repeats until a stop token.
  5. Detokenize and stream. The text returns to the app.

Why the two phases differ

Prefill is limited by compute. Decode is limited by memory bandwidth. A dense model with N parameters does about 2N floating-point operations (FLOPs) per token.

  • Prefill does about 2NP FLOPs for P prompt tokens. It reads the weights once for all of them, so tensor cores set the speed (TTFT).
  • Decode reads every weight again for each token. That is only 2/b FLOPs per byte (b = bytes per weight): 1 at FP16, 4 at 4-bit. A GPU can do hundreds, so bandwidth is the limit:
\text{tokens/s} \approx \eta\,\frac{\text{bandwidth}}{N_\text{active}\cdot b + \text{KV bytes read}}

Here \eta \approx 0.6\text{–}0.8 is the share of peak bandwidth that real kernels reach. Chapter 3 tests this formula on this machine.

The KV cache

The KV cache stores the keys and values of every earlier token. Without it, each new token would recompute all of them.

\text{KV bytes/token} = 2 \times L \times H_{kv} \times d_\text{head} \times \text{bytes}

The 2 counts K and V. L is the number of layers. H_{kv} is the number of key/value heads. Grouped-query attention (GQA) uses fewer KV heads than query heads, which shrinks the cache.

Model Layers Q heads KV heads head dim KV per token (FP16) 8k context 128k context
Qwen3 0.6B 28 16 8 128 112 KiB 0.94 GB 15.0 GB
Llama 3.2 3B 28 24 8 128 112 KiB 0.94 GB 15.0 GB
Llama 3.1 8B 32 32 8 128 128 KiB 1.07 GB 17.2 GB
Qwen3 8B 36 32 8 128 144 KiB 1.21 GB 19.3 GB
Mistral Small 3.1 24B 40 32 8 128 160 KiB 1.34 GB 21.5 GB
Gemma 3 27B 62 32 16 128 496 KiB 1.11 GB 11.2 GB
Qwen3 32B 64 64 8 128 256 KiB 2.15 GB 34.4 GB
Llama 3.1 70B 80 64 8 128 320 KiB 2.68 GB 42.9 GB
Qwen3 30B-A3B (MoE) 48 32 4 128 96 KiB 0.81 GB 12.9 GB
gpt-oss-20b (MoE) 24 64 8 64 48 KiB 0.20 GB 3.2 GB
gpt-oss-120b (MoE) 36 64 8 64 72 KiB 0.31 GB 4.8 GB

Gemma 3 and gpt-oss use sliding-window layers. Those layers keep only the last window, so long context costs less.

At 128k tokens, Llama 3.1 8B's KV cache (≈17 GB at FP16) is about 3.5× its 4-bit weights. Long context is a memory problem first. Chapter 8 covers paged KV.

Sampling

The sampler turns probabilities into one token.

  • Temperature sharpens or flattens the distribution.
  • Top-k keeps the k most likely tokens.
  • Top-p keeps the smallest set whose probabilities add up to p.

Lab: measure the two phases

Ollama reports prefill time and decode time for every request. These are TTFT and TPOT.

Lab · runs on your own machine

uv run labs/01_prefill_decode.pyDownload all labs (.zip)

Needs uv and, for the live parts, Ollama with a small chat model: ollama pull qwen3:0.6b.

Show the code
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
import _local as local

model = local.need_chat_model()
if model:
    local.generate(model, "Hi", num_predict=4)  # warm-up: loads weights into memory
    for prompt_words in (20, 400, 1600):
        prompt = "Summarize this: " + " ".join(["The GPU streams weights from VRAM for every token."] * (prompt_words // 9))
        r = local.generate(model, prompt, num_predict=64)
        print(f"prompt {r['prompt_tokens']:>5} tok · prefill {r['prefill_s']*1000:7.1f} ms ({r['prefill_tps']:8.0f} tok/s) · "
              f"decode {r['output_tokens']} tok at {r['decode_tps']:6.1f} tok/s")
Sample output on this course's Mac (Apple M4, 32 GB)
prompt    37 tok · prefill   124.3 ms (     298 tok/s) · decode 64 tok at   89.8 tok/s
prompt   499 tok · prefill   280.0 ms (    1782 tok/s) · decode 64 tok at   84.6 tok/s
prompt  1962 tok · prefill  1185.2 ms (    1655 tok/s) · decode 64 tok at   63.8 tok/s

What to notice: prefill tokens/s is 10–100× higher than decode tokens/s. Decode speed barely changes with prompt length until the KV cache gets large.

Explain it

  1. Why is TTFT different from tokens/s? Prefill is one compute-bound pass. Decode is many bandwidth-bound passes.
  2. A 20k-token RAG prompt feels slow. Where do you look? Measure prefill time. Then try prefix caching and fewer retrieved chunks.
  3. What is GQA? Query heads share K/V heads. Llama 3.1 8B goes from 32 to 8 KV heads, so the cache is 4× smaller.

Exercises

  1. How many 8k-token Qwen3 8B conversations fit in the KV space of a 16 GB RTX 5070 Ti (≈5 GB weights, 1 GB overhead)?
  2. Derive the 2N FLOPs-per-token rule from one m \times n matrix-vector product.