Offload math
Every layer in RAM slows every token. A fraction f of the active bytes B sits in VRAM (bandwidth \text{BW}_g). The CPU reads the rest from system RAM (\text{BW}_c):
With 1,792 GB/s against ~90 GB/s (dual-channel DDR5), moving 10% of the weights to the CPU makes each token about 3× slower. Step 2 above shows this.
Offload is a capacity escape hatch. The better fixes are a smaller quant (step 5), a smaller context, an MoE model, or more VRAM.
The exception is MoE expert offload. llama.cpp keeps attention and shared weights on the GPU. It puts only the expert FFN tensors in system RAM (--n-cpu-moe N, or override-tensor rules). Each token touches few experts, so the CPU reads few bytes. A 100B-class MoE model then runs on a 24–32 GB card with fast RAM.
The knobs
Each knob trades one resource for another.
| Knob | llama.cpp | Ollama | vLLM | Trade-off |
|---|---|---|---|---|
| GPU layers | -ngl 99 |
num_gpu (auto) |
— (all GPU) | speed vs capacity |
| Context size | -c 16384 |
num_ctx |
--max-model-len |
KV memory vs window |
| KV precision | -ctk q8_0 -ctv q8_0 |
OLLAMA_KV_CACHE_TYPE=q8_0 |
--kv-cache-dtype fp8 |
½ KV memory, small quality cost |
| FlashAttention | -fa (often default on) |
OLLAMA_FLASH_ATTENTION=1 |
default | less memory, faster long prompts |
| Parallel sequences | --parallel N (server) |
OLLAMA_NUM_PARALLEL |
--max-num-seqs |
throughput vs KV per user |
| Batch sizes (prefill) | -b 2048 -ub 512 |
— | --max-num-batched-tokens |
TTFT vs memory spikes |
| CPU threads | -t N |
num_thread |
— | matters only for CPU or offloaded work |
| Memory mapping | --no-mmap, --mlock |
— | — | startup and paging behaviour |
| VRAM headroom | — | — | --gpu-memory-utilization 0.9 |
KV capacity vs OOM risk |
| Multi-GPU split | -ts 3,1, -sm layer/row |
auto | --tensor-parallel-size |
see Chapter 10 |
| Keep loaded | — | OLLAMA_KEEP_ALIVE |
— | cold-start TTFT vs freed VRAM |
Set the sampling values from the model card. Qwen3, for example, publishes separate settings for thinking and non-thinking modes. Wrong values often make a local model look worse than the API.
Lab: benchmark with llama-bench
llama-bench is the standard way to compare settings. It measures prompt processing (pp) and token generation (tg), repeats each test, and reports mean ± standard deviation. The lab uses the GGUF file behind your smallest Ollama chat model.
Lab · runs on your own machine
uv run labs/09_llama_bench.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
import shutil, subprocess
model = local.need_chat_model()
if model and shutil.which("llama-bench"):
path = local.blob_path(model)
cmd = ["llama-bench", "-m", str(path), "-p", "512", "-n", "128", "-ngl", "0,99", "-r", "3", "-o", "md"]
print(" ".join(cmd))
print(subprocess.run(cmd, capture_output=True, text=True, timeout=900).stdout)
elif model:
print("llama-bench not found; install llama.cpp (brew install llama.cpp) to run this lab")
Sample output on this course's Mac (Apple M4, 32 GB)
llama-bench -m /Users/mpl/.ollama/models/blobs/sha256-7f4030143c1c477224c5434f8272c662a8b042079a0a584f0a27a1684fe2e1fa -p 512 -n 128 -ngl 0,99 -r 3 -o md | model | size | params | backend | threads | test | t/s | | ------------------------------ | ---------: | ---------: | ---------- | ------: | --------------: | -------------------: | | qwen3 0.6B Q4_K - Medium | 492.75 MiB | 751.63 M | BLAS,MTL | 4 | pp512 | 159.59 ± 3.23 | | qwen3 0.6B Q4_K - Medium | 492.75 MiB | 751.63 M | BLAS,MTL | 4 | tg128 | 76.09 ± 1.47 | | qwen3 0.6B Q4_K - Medium | 492.75 MiB | 751.63 M | BLAS,MTL | 4 | pp512 | 2225.41 ± 83.14 | | qwen3 0.6B Q4_K - Medium | 492.75 MiB | 751.63 M | BLAS,MTL | 4 | tg128 | 95.84 ± 2.33 | build: 7fe450e19 (11146)
What to notice: compare -ngl 0 (all CPU) with -ngl 99 (all GPU). On a unified-memory Mac the gap is small, because both sides share one memory bus. On an RTX PC the tg gap is typically a factor of 10.
Explain it
- "A 12 GB card runs a 14B model at 4 tok/s. Why?" The Q4_K_M weights (about 8.5 GB) plus a large default context pushed layers to the CPU. Lower the context, use a q8_0 KV cache, or check
ollama ps. - "When is CPU offload a good idea?" For batch work that tolerates latency, and for MoE expert offload.
- "What makes a benchmark trustworthy?" A fixed model file and quant, a stated context and batch, warm-up, repeats with variance, and version numbers. Report both pp and tg.
Exercises
- Use the offload formula to find the GPU share where Qwen3 32B on an RTX 5070 Ti drops below 10 tok/s.
- Run
llama-benchwith-ctk q8_0 -ctv q8_0 -c 8192and compare it with the default.