← All courses ← Course home

Lesson 01 of 05

Keep the prompt cache warm

In a local agent loop, most of the wait is the model reading the prompt again. Keep the start of the prompt the same on every turn, and the server can skip that work.

Agent brief (llms.md)

Environment setup

Do this once before the first runnable example. You need Python 3.11 or newer and uv.

# 1. Install uv (skip if you have it)
curl -LsSf https://astral.sh/uv/install.sh | sh

# 2. Make a project folder with Python 3.11+
mkdir local-lab && cd local-lab
uv init --bare --python 3.11
uv venv

# 3. Add the course packages
uv add numpy

# 4. Optional: PyTorch, only if you also want to run models directly in Python.
#    On Apple Silicon this wheel uses MPS. On other machines it uses the CPU.
uv add torch
uv run python -c "import torch; print('mps' if torch.backends.mps.is_available() else 'cpu')"

Install llama.cpp. On macOS: brew install llama.cpp. On Linux or Windows, download a release from github.com/ggml-org/llama.cpp/releases. Check it with llama-server --version.

Run every script with uv run python script.py. The examples were checked on llama.cpp build b11624 and Ollama 0.40.4 on a Linux CPU box. Your numbers will be different on your machine. That is the point: measure on your own hardware.

What the cache does

llama-server keeps the tokens of the last prompt in each slot. When a new request starts with the same tokens, the server reuses them. It only reads the new part. This is called the prompt cache, or prefix cache. It is on by default (--cache-prompt).

The match is from the first token forward. If one token near the top changes, everything after it must be read again. A timestamp in the first line of the system prompt is enough to break it.

FirstInstructions
ThenTool list
ThenChat history
LastNew user turn

Start a server

llama-server -hf Qwen/Qwen3-1.7B-GGUF:Q8_0 --port 8080 -c 8192 -np 1

Every chat response from llama-server has a timings object. Two fields matter here: cache_n is how many prompt tokens came from the cache, and prompt_n is how many tokens the server had to read.

Measure it

This script sends the same question three ways: with a stable system prompt, with a timestamp at the top, and with the timestamp moved into the user turn.

"""Show the prompt cache working (or not) on llama-server, using the timings it returns."""
import datetime
import json
import time
import urllib.request

URL = "http://127.0.0.1:8080/v1/chat/completions"
RULES = "You are a support agent for a notes app. " + "Follow the house rules. " * 300


def ask(system, question):
    body = {
        "messages": [{"role": "system", "content": system}, {"role": "user", "content": question}],
        "max_tokens": 20,
        "chat_template_kwargs": {"enable_thinking": False},
    }
    req = urllib.request.Request(URL, json.dumps(body).encode(), {"Content-Type": "application/json"})
    start = time.perf_counter()
    r = json.load(urllib.request.urlopen(req))
    t = r["timings"]
    print(f"  wall={time.perf_counter() - start:5.2f}s  cache_n={t['cache_n']:5d}  "
          f"prompt_n={t['prompt_n']:5d}  prompt_ms={t['prompt_ms']:7.0f}")


print("A. stable system prompt, changing part last")
for i in range(3):
    ask(RULES, f"Question {i}: how do I export a note?")

print("B. timestamp at the top of the system prompt")
for i in range(3):
    stamp = datetime.datetime.now().isoformat()
    ask(f"Current time: {stamp}\n" + RULES, f"Question {i}: how do I export a note?")

print("C. same stable prompt, timestamp moved into the user turn")
for i in range(3):
    stamp = datetime.datetime.now().isoformat()
    ask(RULES, f"(Current time: {stamp}) Question {i}: how do I export a note?")

Output on the test box (8 CPU cores, Qwen3 1.7B Q8_0):

A. stable system prompt, changing part last
  wall=11.14s  cache_n=    0  prompt_n= 1539  prompt_ms=   9966
  wall= 1.52s  cache_n= 1521  prompt_n=   18  prompt_ms=    256
  wall= 1.55s  cache_n= 1521  prompt_n=   18  prompt_ms=    237
B. timestamp at the top of the system prompt
  wall= 9.83s  cache_n=    3  prompt_n= 1567  prompt_ms=   7027
  wall= 8.17s  cache_n=   24  prompt_n= 1546  prompt_ms=   7078
  wall=10.00s  cache_n=   24  prompt_n= 1546  prompt_ms=   8484
C. same stable prompt, timestamp moved into the user turn
  wall= 2.21s  cache_n= 1519  prompt_n=   51  prompt_ms=    582
  wall= 1.12s  cache_n= 1541  prompt_n=   29  prompt_ms=    290
  wall= 1.31s  cache_n= 1541  prompt_n=   29  prompt_ms=    297

Case B reads about 1,550 tokens every time. Cases A and C read only the new 18 to 51 tokens. The time to the first token drops from about 7 to 10 seconds to under 1 second. Same model, same question. Only the order changed.

Check the server log

The log tells you the same thing. Look for these lines:

slot get_availabl: id  0 | task -1 | selected slot by LCP similarity, f_sim_best = 0.988 (> 0.100 thold), f_keep = 0.976
slot print_timing: id  0 | task 21 | prompt eval time =     255.88 ms /    18 tokens

A high f_keep and a small token count on prompt eval time mean the cache worked. A token count close to your full prompt on every turn means it did not.

Common things that break the cache

Hybrid models need a recent build

Models that mix attention with recurrent or sliding-window layers (Qwen 3.5 and 3.6, Gemma 4) do not use the cache in the same simple way. llama.cpp keeps "context checkpoints" for them. Several bugs caused full re-reads on every turn: llama.cpp #20225 (Qwen 3.5, closed), llama.cpp #21912 (Gemma 4 and Qwen 3.5 in coding agents, closed), and llama.cpp #24055 (checkpoints always invalidated with --checkpoint-min-step, still open). If your log shows a full re-read on every turn with a hybrid model, update llama.cpp first, then run the script above again.

Rule: things that never change go first. Things that change every turn go last.

Key concepts

  • The prompt cache reuses tokens that match from the start of the prompt.
  • timings.cache_n is tokens reused. timings.prompt_n is tokens read again.
  • One changed token near the top forces a re-read of everything after it.
  • Hybrid models use context checkpoints, and some llama.cpp builds had bugs there.

Takeaways

  • Put instructions and tools first. Put time, state, and the new turn last.
  • Run the three-case script and compare prompt_n before you blame the model.
  • If a hybrid model re-reads on every turn, update llama.cpp and test again.