Takeaways first
How to win the local KV cache
One study, two runtimes, same mental model. Nine Ollama configurations across seven scenarios;
eight llama-server GGUFs on the shared-root suite with a direct cache_n hit
counter. Design the prompt so the expensive part stays identical; size the server so a slot is free
to hold it.
One strip below = one request’s context window. Segments are the things you actually put in it; the cache line marks the first byte that differs from the resident prefix. Everything left of the line is nearly free; everything right of it is evaluated from scratch. Toggle the layout and watch where the line lands.
System, tools, and briefing are byte-identical at the front of every branch. Only the short task tail is new. This is the layout that produced 0.04–0.05× warm ratios.
Rules for local agent systems
- Stabilize the root. System prompt + shared briefing must be byte-for-byte identical at the front of every branch.
- Warm once, then fork. Pay for the long root on the first request; send short lane-specific tails afterward.
- Never put uniqueness first. Lane IDs, timestamps, and tool noise belong after the shared material.
- Size slots to the fan-out. If five agents arrive together, you need enough parallel slots—or accept a queue.
- Context after slots. Reserve KV for the windows you will actually keep warm; huge unused context is a tax.
- Reducers are new prompts. Branch outputs must be pasted in explicitly; caches do not merge.
- Trust wall time and cache hits. Client parallel ≠ server parallel. On llama.cpp, read
cache_n; on Ollama, compare against a unique-prefix control. - Doubt the runtime before the weights. Nemotron looked cache-blind on Ollama and reused normally on
llama-server.
Best practical balance under Ollama (warm parallel continuations ~2.3s at 128K) and still a clean llama.cpp profile.
Gemma 4 26B (Ollama) and GLM 4.7 Flash (llama.cpp, 0.08× prompt ratio) crush after the root is resident.
Gemma 4 31B at 256K fits; five copies of that window do not. Prefer depth over fan-out.
Do not treat an Ollama serial/no-reuse result as a model property until a second runtime agrees.
Evidence · Interactive 01 · Root anatomy
Follow one root as it branches, continues, and reduces
The harness sent the full chat history on every request—no conversation ID. Blue segments below are token prefixes available for reuse; red segments are newly evaluated. Step through the topology to see when the match holds and when a reducer becomes a new prompt.
The first request pays for the root. Keeping the model loaded makes this prefix available to later requests.
Interactive 02 · Prefix microscope
Where uniqueness sits in the prompt decides your bill
The server already holds one warm prompt in a slot (top row). Every other row is a new request. Read left → right as start → end of that prompt. Blue means “still identical to the warm copy, so skip the work.” The first token that differs turns the rest red—even if the later text looks the same to you.
A lane ID, a timestamp, or extra whitespace at the front is enough. The cache does not know your intent; it knows bytes from the left.
Evidence · Scenario map
Seven tests, three pressures
Every scenario sends the full prompt history. The difference is what stays identical at the front, whether requests arrive one-at-a-time or together, and whether a later reducer starts a brand-new prompt. Pick a pressure to see the request shape.
Shared root
Can later forks skip the briefing?
Warm one large root, then diverge. Sequential isolates pure prefix reuse; parallel asks whether slots and bandwidth keep up when five forks arrive together.
Blue is the reusable prefix. Red is newly evaluated work. Left → right is time.
Evidence · Measured results
Prefix reuse and elapsed time are different questions
Prompt-evaluation ratio compares prompt processing with that model’s unique-prefix control
(lower is better). Wall time is what the client waited for. On llama.cpp we also have
cache_n; on Ollama the ratio is the reuse signal. Same scenarios, both stacks.
On Ollama: 128K context, 12.09 GiB resident, warm parallel continuations in 2.34s. On llama.cpp: still a clean sequential profile (0.12× · 100% cache).
Gemma 4 26B hit 0.05× on Ollama; every llama.cpp GGUF reported 100% cache_n on shared sequential forks. Same mechanism, both stacks.
Oversized Ollama slots and -np 2 with three llama.cpp agents both show the same failure: cold forks re-pay the root while siblings hit cache.
Ollama forced one slot and ~0.82–1.37× ratios. The same Nano 4B on llama-server posted 0.10× sequential reuse. Doubt the serving path before the model.
Interactive 05 · Experiment tree
Roots are expensive. Branches are nearly free.
Warm one 2,700-token root, then fork K short tails—ask the same context K different questions. The naive plan pays for the root K times; the shared-root plan pays once. This is the measured mechanism behind the 0.04–0.05× ratios: it turns “one careful question” into “a sweep of experiments” at almost the same price.
Sequential forks reused ~100% of the root on every llama.cpp model tested (0.08–0.15× prompt ratio). The catch: parallel arrivals need slots—fan out wider than your slots and cold forks recompute the root anyway.
Interactive 06 · Slot theater
“Parallel” has four meanings on one Mac
Promise.all only submits requests together. Actual concurrency depends on loaded
slots (OLLAMA_NUM_PARALLEL or llama-server -np), the model architecture,
and whether memory bandwidth can feed them.
Five requests arrive together, but a single inference slot serves them one after another. Client wall time accumulates even when prefix reuse is excellent.
Interactive 07 · Compaction
Compression is buying a new root. Time the purchase.
A long agent session, turn by turn. The shaded area is total context (climbing toward the window). Bars are tokens evaluated per request—tiny while the prefix holds. Compacting shrinks the context but rewrites the prefix: one full re-evaluation spike, then cheap turns resume from the new, smaller root. Compact at a natural boundary—ideally right before a fan-out—never in the middle of one.
Without compaction the session dies at the window edge—and every turn keeps re-reserving the largest KV allocation of the run.
Modeled illustration (8K window, 2,700-token root, 350-token turns, 1,100-token summary)—drawn to scale of the measured runs, not itself a measurement.
Interactive 08 · Memory budget
Context length is a reservation, not a badge
Our prompts were only about 2,500–2,900 tokens. Reserving 256K across five slots therefore bought no useful benchmark capacity while multiplying KV memory. The sharpest example was dense Gemma 4 31B: five 256K slots projected 108.4 GiB of KV cache before model weights and compute buffers. Pick a model, a context length, and parallel slots: black is weights, each blue block is one slot’s KV reservation, and the double line is the machine.
Reservations are modeled from the run’s anchor points (illustrative, not remeasured). The lesson survives the error bars: unused context is a tax, and it compounds per slot.
Choose which scarce resource you are preserving: large context or parallel lanes. Maximizing both is usually an expensive way to process a short prompt.
Complete measurements
Same harness, both runtimes
Absolute wall times are not cross-runtime comparable—the llama.cpp sweep used a shorter prefix and three agents. Compare shape: sequential reuse, parallel contention, and whether a model that fails on one stack recovers on the other. Pick a view.
Shared-root sequential prompt cost vs that model’s cold control. Squares are Ollama; circles are llama.cpp. Left is better. Nemotron Nano 4B is the outlined row—the only clear verdict flip.
Rule 8, drawn: doubt the runtime before the weights. An Ollama no-reuse result is not a model property until a second runtime agrees.
Times are client wall time; ratios are prompt-evaluation time relative to each model’s unique-prefix control. Scroll horizontally on a narrow screen.
| Configuration | Ctx | Resident | Unique control | Shared seq | Shared parallel | Round robin | Indep. parallel | Fork seq | Fork parallel |
|---|---|---|---|---|---|---|---|---|---|
| Gemma 4 12B · 1 slot | 128K | 8.87 GiB | 5.45s · 1.00× | 1.47s · 0.04× | 3.54s · 0.04× | 1.79s · 0.10× | 4.58s · 0.09× | 1.24s · 0.04× | 3.17s · 0.05× |
| Gemma 4 12B · 5 slots | 256K | 9.74 GiB* | 6.62s · 1.00× | 1.47s · 0.03× | 31.39s · 1.71× | 2.18s · 0.08× | 8.74s · 0.37× | 1.68s · 0.04× | 37.99s · 2.04× |
| Qwen 3.6 27B · 5 slots | 256K | 31.47 GiB | 15.55s · 1.00× | 5.78s · 0.26× | 17.97s · 0.26× | 6.91s · 0.31× | 21.83s · 0.35× | 6.71s · 0.31× | 22.89s · 0.36× |
| GPT-OSS 20B · 5 requested | 128K† | 12.09 GiB | 3.35s · 1.00× | 0.87s · 0.04× | 10.99s · 1.38× | 0.96s · 0.06× | 2.34s · 0.12× | 0.90s · 0.05× | 11.08s · 1.39× |
| Gemma 4 26B · 5 slots | 256K | 17.71 GiB | 3.17s · 1.00× | 0.70s · 0.05× | 15.92s · 2.17× | 1.08s · 0.08× | 4.85s · 0.26× | 0.63s · 0.05× | 12.77s · 1.70× |
| Gemma 4 31B · 1 slot | 256K | 19.13 GiB | 18.32s · 1.00× | 3.21s · 0.04× | 10.47s · 0.05× | 5.05s · 0.10× | 13.20s · 0.09× | 4.07s · 0.04× | 10.90s · 0.04× |
| Nemotron 3 Nano 30B | 64K | 23.34 GiB | 2.99s · 1.00× | 2.86s · 0.93× | 9.64s · 1.04× | 3.44s · 1.04× | 12.76s · 1.34× | 4.26s · 1.31× | 12.29s · 1.37× |
| Nemotron 3 Nano 4B | 256K | 7.80 GiB | 2.93s · 1.00× | 2.68s · 0.86× | 8.55s · 0.89× | 2.88s · 0.86× | 9.58s · 1.02× | 3.54s · 1.15× | 10.83s · 1.19× |
| Nemotron Cascade 2 30B | 64K | 23.34 GiB | 3.32s · 1.00× | 2.95s · 0.82× | 9.17s · 0.90× | 3.19s · 0.85× | 9.74s · 0.91× | 3.48s · 1.01× | 11.18s · 1.11× |
* Ollama’s process view reported 9.74 GiB; server startup projected about 30.4 GiB including model, context, and compute allocation. † Requested 256K; model/runtime clamped actual context to 128K.
llama-server -c 8192 -np 2 --cache-reuse 256, three agents, ~4K-character prefix.
Ratios are median prompt ms vs unique control; cache is median cache_n / prompt_n.
| GGUF · llama-server | Unique control | Shared seq · wall / prompt / cache | Shared parallel · wall / prompt / cache |
|---|---|---|---|
| Nemotron 3 Nano 4B | 1.20s · 1.00× · 0% | 0.37s · 0.10× · 100% | 2.47s · 2.08× · 8% |
| Gemma 4 E2B | 0.61s · 1.00× · 0% | 0.22s · 0.11× · 100% | 0.81s · 1.03× · 100%* |
| Gemma 4 E4B | 1.14s · 1.00× · 0% | 0.37s · 0.10× · 100% | 1.49s · 1.08× · 100%* |
| GPT-OSS 20B | 1.02s · 1.00× · 0% | 0.30s · 0.12× · 100% | 1.41s · 0.67× · 100%* |
| Gemma 4 26B A4B | 1.29s · 1.00× · 0% | 0.40s · 0.15× · 100% | 2.33s · 1.97× · 8% |
| Qwen 3.6 27B | 9.81s · 1.00× · 0% | 2.33s · 0.09× · 100% | 17.73s · 1.72× · 8% |
| GLM 4.7 Flash | 1.34s · 1.00× · 0% | 0.39s · 0.08× · 100% | 1.76s · 1.10× · 100%* |
| Gemma 4 31B | 7.98s · 1.00× · 0% | 2.02s · 0.11× · 100% | 16.38s · 2.16× · 8% |
*Median cache can read 100% even when one of three parallel forks misses and recomputes—inspect per-request rows before celebrating concurrency.
Method and limits
What this benchmark does—and does not—say
Machine
Apple M4 Max with 64 GiB unified memory. Ollama 0.32.x and Homebrew llama-server (b9860) on the same box, same harness.
Prompts
Ollama matrix: ~2.5–2.9K tokens × seven scenarios. llama.cpp sweep: shorter ~4K-character prefixes, three agents, unique / shared-seq / shared-parallel.
Metrics
Client wall time and server prompt-evaluation time. llama.cpp adds timings.cache_n; Ollama reuse is inferred from prompt cost vs the unique-prefix control.
Limits
No answer-quality judging. Parallel contention can inflate prompt time. Do not rank absolute wall times across runtimes—prompt budgets differ.
Under Ollama, keep_alive retained the model, not a permanent cache namespace. Under llama.cpp,
each sweep run injected a fresh nonce so prior KV state could not fake the unique-prefix control.
Both stacks sent complete histories. Results are about resident prefix matching and
scheduling on this machine, not a universal ranking of model intelligence.