01One object, two bills
Five words first.
Token — the unit a language model reads and writes. Roughly 3–4 English characters, one Chinese/Japanese/Korean character. The unit of both pricing (dollars per million tokens) and work (one token per forward pass).
Cache — a store for expensive work’s result, so you don’t redo it. A website caches a rendered page; an LLM caches the attention state for tokens already read.
KV cache — the key/value cache. In attention each token becomes a query (Q), a key (K) and a value (V). A new token looks back at earlier tokens’ K and V, which never change on append, so compute each once and store it. That store is the KV cache. KV = “keys and values”.
Prefix — the start of a request. A request runs system prompt, tools, memory, history, then the new user message; everything before the new message is the prefix. Caching works only on prefixes, and only on exact matches (section 3).
Prefill and decode — a request’s two phases. Prefill processes the whole prompt at once (parallel, compute-heavy); a cache hit saves money and time-to-first-token here. Decode emits one token at a time (sequential, memory-bandwidth-heavy) and caching does not help: those tokens are new.
With those five words, the whole unit collapses into one table:
| Hosted API (DeepSeek, Anthropic, OpenAI) | Your own box / phone (llama.cpp, Ollama) | |
|---|---|---|
| Who holds the KV cache | The provider, on their disk and GPU memory | You, in VRAM or system RAM |
| What you pay | Dollars per token: a hit (reused prefix) vs a miss (recomputed prefix) | Memory bandwidth per generated token + RAM capacity for the cache |
| What limits you | The hit rate you can achieve | GB/s of memory bandwidth; how much context fits |
| What breaks it | Any change in the prefix, a model switch, expiry (TTL) | Longer context, bigger KV dtype, an OS that reclaims your RAM |
Gap 1 is the left column: an accounting question with a 50× price difference. Gap 2 is the right: a physics question about how fast bytes move. Same cache, two bills.
PrefixPrefix02Why a cache exists at all
The plain explanation
A transformer emits one token at a time. Token 500 needs attention over tokens 1–499; recomputing all of it every step costs on the order of 500 × 500 operations, the quadratic blow-up. Earlier tokens’ K and V vectors never change, so compute each token’s K and V once, keep them, and let attention read the whole table.
Attention — how each token decides how much to “look at” every other token. Practically, a weighted average of past value vectors, weighted by the new token’s query against each past key.
The HF Transformers docs: KV scores “are calculated every time because the model predicts one token at a time... A KV cache stores these calculations so they can be reused without recomputing them.”
It is not free — it is RAM that grows with your conversation
The cache copies part of your state and grows linearly with context length. Meta’s 2026 on-device survey[4]: it “grows linearly with sequence length and can dominate memory usage during long-context inference, often exceeding the model weights themselves.”
The arithmetic, from Qwen3-4B. Its published config.json says 36 layers, 8 key/value heads (grouped-query attention), head dimension 128:
KV cache bytes per token = 2 × layers × kv_heads × head_dim × bytes_per_element
= 2 × 36 × 8 × 128 × 2 bytes (f16)
≈ 144 KiB per token
| Context length | f16 KV cache | q8_0 KV cache |
|---|---|---|
| 4,096 tokens | ~0.60 GB | ~0.30 GB |
| 32,768 tokens | ~4.8 GB | ~2.4 GB |
Cache-only numbers. On top sit the weights (2.5 GB for Qwen3-4B at Q4_K_M) and runtime overhead. Hence Apple’s 4,096-token on-device window (section 6), and why your phone’s “15.5 GB of RAM” behaves like 5 GB (section 6).
Where you meet it in practice
- llama.cpp:
-c, --ctx-size Nis literally “how big is the KV cache you’re willing to allocate”. Default 0 means “take it from the model”.-ctk / --cache-type-kand-ctv / --cache-type-vchoose the cache’s data type;f16is the default, andq8_0,q4_0and friends are quantization formats for the cache itself. - Ollama: same idea, different names. Ollama’s default context window is 4096 tokens (
OLLAMA_CONTEXT_LENGTHoverrides it,/set parameter num_ctxper run).OLLAMA_KV_CACHE_TYPEselectsf16(default),q8_0(~1/2 the memory, “very small loss in precision”) orq4_0(~1/4 the memory, more noticeable quality loss at high context). - Hugging Face Transformers: the cache is an explicit object you choose among —
DynamicCache(default, grows),StaticCache(pre-allocated, fortorch.compile),QuantizedCache(smaller), and offloaded variants that keep only the current layer’s cache on the GPU. The doc’s framing: the KV cache “can occupy a significant portion of memory and become a bottleneck for long-context generation.”
Why it matters: the KV cache ties together three things you usually treat separately: your API bill, your time-to-first-token, and your maximum usable context. Tuning one touches the others.
BillBill03Prompt caching on the API: hits, misses and “cache prefix units”
The plain explanation
A hosted model is stateless: you resend the whole conversation each turn. The provider is not. It keeps your last request’s KV cache on disk; if the next request starts with identical tokens it reads that state instead of recomputing. Identical prefix → cache hit. Any difference → cache miss (full price, full prefill).
Three load-bearing properties, from DeepSeek’s Context Caching guide[1]:
- Prefix only. Caching covers the beginning of the request, not the middle or the end.
- Exact match. A hit requires that the request fully matches a persisted “cache prefix unit”. Not “mostly matches”. Not “semantically similar”. The same tokens, in the same order, byte for byte.
- Best effort. “The cache system works on a ‘best-effort’ basis and does not guarantee a 100% cache hit rate.” Nor is it instant: “Cache construction takes seconds”, and an unused cache clears “usually within a few hours to a few days.”
What counts as a cacheable unit
DeepSeek persists cache prefix units at:
- Request boundaries — the end of your input and the end of the model’s output. In
A + B(round 1) thenA + B + C(round 2), round 2 fully matchesA + Band hits. - Detected common prefixes — if round 1 is
A + Band round 2 isA + C, round 2 does not hit (A + Cnever matchesA + B). The system notices the sharedA, persists it as its own unit, and a third requestA + Dhits onA. Hits take a request or two to “warm up”. - Fixed token intervals — for long inputs and outputs, units are carved out at intervals so a very long prefix isn't uncacheable forever just because it never reaches an end position.
Reading the hit rate off the response
DeepSeek exposes the accounting in the usage object:
{
"id": "chatcmpl-...",
"model": "deepseek-flash",
"usage": {
"prompt_tokens": 61248,
"completion_tokens": 1842,
"total_tokens": 63090,
"prompt_cache_hit_tokens": 58752,
"prompt_cache_miss_tokens": 2496,
"prompt_tokens_details": { "cached_tokens": 58752 }
}
}
Read it like a bill: prompt_cache_hit_tokens is input served from the provider’s disk cache (cheap tier); prompt_cache_miss_tokens is what it computed (full price). Here 96% of a 61K-token prompt was a hit. Others name the pair differently: Anthropic’s cache_creation_input_tokens / cache_read_input_tokens; OpenAI’s usage.input_tokens_details.cached_tokens. Whichever field you read is your cost lever’s dashboard.
The 50× lever, verified
DeepSeek’s live price page[2] (fetched ; the model is now deepseek-flash, i.e. DeepSeek-V4.1-Flash):
| Input tier (per 1M tokens) | Off-peak | Peak |
|---|---|---|
| Cache hit | $0.003 | $0.006 |
| Cache miss | $0.15 | $0.30 |
| Output | $0.60 | $1.20 |
That is a 50× spread between hit and miss on input, in both windows (off-peak is half of peak; peak is 01:00–04:00 and 06:00–10:00 UTC, Mon–Fri). The audit’s anchor — “a cache hit costs roughly 1/50th of a miss ($0.003 vs $0.15 per 1M tokens)” — reproduces exactly.
Two caveats, since prices move fastest here:
- DeepSeek retired and repriced on with V4.1; some mirrors still serve the old tier (V4-Flash at $0.007 hit / $0.22 miss, ~31× spread). The lesson holds either way; the numbers must come from the live page.
- The spread is so large the multiplier barely matters: 50× or 31× on input is “budget for it” vs “don’t think about it”.
Why so cheap? A hit is a disk read, not a GPU forward pass. DeepSeek’s V4.1 announcement[3]: “Compared with the previous generation, V4.1-Flash’s KV cache needs just 1/4 the HBM and 1/8 the SSD storage. Cache-hit charges often account for a large share of agent costs. Compressing the cache cuts those costs significantly.” Pushing the KV cache down the memory hierarchy (GPU memory → host RAM → SSD) is how agent work gets affordable: section 5’s physics, monetized.
The same mechanism, three vendors
| DeepSeek | Anthropic (Claude) | OpenAI | |
|---|---|---|---|
| How you opt in | Automatic, on by default | cache_control (automatic or explicit breakpoints) | Automatic (prompt_cache_options to tune) |
| Unit of caching | Prefix; persisted “cache prefix units” | Prefix; cache breakpoints, hierarchy tools → system → messages | Prefix; implicit or explicit breakpoints (prompt_cache_key) |
| Write cost | (bundled — no separate write tier) | 1.25× base input for 5m TTL; 2× for 1h | 1.25× base input |
| Read (hit) cost | 1/50 of miss on input | 0.1× base input (0.025× on the newest flagships) | 0.1× base input |
| Lifetime | Hours to days, best effort | 5 min default, refreshed on use; 1h optional | 30 min default (ttl: "30m"), refreshed on use |
| Minimum cacheable prefix | Not documented as a token floor; short prefixes just may not persist | Model-specific minimum; check your model's row | 1,024 tokens (GPT-5.6+), 2,048 for older |
| Where the numbers show up | prompt_cache_hit_tokens / prompt_cache_miss_tokens | cache_creation_input_tokens / cache_read_input_tokens | input_tokens_details.cached_tokens |
Worked example. A 60K-token agent turn (system prompt + memory + skills + tools + history) with a 2K-token answer, on deepseek-flash off-peak:
- Miss on the whole input: 0.06M × $0.15 = $0.00900 input + 0.002M × $0.60 = $0.00120 output → $0.0102
- Hit on the whole input: 0.06M × $0.003 = $0.00018 input + $0.00120 output → $0.00138
Per turn that is 7.4× cheaper; over 50 turns, $0.51 vs $0.069. It isn’t 50× because output tokens are never cached; they are new work. Hence the next two sections: caching discounts reading the past, never producing the future.
Bill04Your stack's prefix: what is stable, what is volatile
Now the part that changes your behaviour. Hermes builds its system prompt in three ordered tiers — stable → context → volatile — with the stated intent: “This ordering matters... This separation keeps the stable prefix stable for caching.”
Walking the layers in the order the API sees them:
| Prompt layer | Source | Stability | Cache consequence |
|---|---|---|---|
| Agent identity | SOUL.md | Stable across turns | Part of the cacheable prefix — edit it between sessions, not mid-run |
| Tool-aware behaviour guidance, skills index | code + skills/ | Stable if the skill set is stable | Skills index is inside the cached prompt; loading a different set of skills mid-run changes it |
| Project context | .hermes.md, AGENTS.md, CLAUDE.md | Stable per project | Rebuilding it (new file, changed file) invalidates from that point |
| Memory snapshot | MEMORY.md | Frozen for the session | Mid-session writes update disk but “do not mutate the already-built cached system prompt until a rebuild path runs” |
| User profile | USER.md | Frozen for the session | Same as above |
| API-call-time layers | ephemeral_system_prompt, pre_llm_call plugin context | Changes every call | Deliberately kept out of the cached prompt — appended to the current user message instead |
The last row is the design lesson: Hermes keeps volatile context out of the prefix by placing it after the stable content. Do the same by hand in your prompts, cron job bodies, and skill text.
Hermes also manages the provider cache. Prompt caching is automatic for Anthropic-family models (native API or via OpenRouter); the TTL is set in config.yaml (prompt_caching.cache_ttl: “5m” or “1h”), and the CLI announces it at startup: Prompt caching: ENABLED (Claude via OpenRouter, 5m TTL). If Claude sessions feel expensive per turn, check that TTL line: a stop-start rhythm and a 5-minute cache is the common silent miss.
What the tool definitions cost you
The cache hierarchy is ordered tools → system → messages, and Anthropic’s invalidation table is brutal at the top. Changing tool definitions (names, descriptions, parameters) invalidates the entire cache: tools, system, messages. Changing tool_choice invalidates only messages; the system prompt invalidates system and messages.
Your stack maps onto this: a large tool surface behind progressive disclosure (tool_search / tool_describe / tool_call, 298 tools not all resident). Progressive disclosure is cheaper because a smaller, more stable tool array keeps the top of the cache hierarchy constant. Reshuffling loaded tools mid-run — enabling a skill, attaching an MCP server, switching tool sets — is a cache event, not just a context-length event.
The hit/miss table for your daily actions
| Action | Hit or miss? | Why |
|---|---|---|
| Another turn in the same session, same model, nothing edited | Hit on everything before the new message | The prefix is byte-identical; the reused part is served from cache |
Editing MEMORY.md mid-session | Hit for this session, miss next session/rebuild | The snapshot is frozen until the prompt is rebuilt — you pay at the rebuild, not immediately |
/model switch, provider fallback, or credential-pool rotation | Miss — the whole prompt | Hermes' own docs warn this: “means the next request gets zero cache hits and re-reads the full conversation at undiscounted input price. This is inherent to how provider caches work” |
| Loading different skills mid-run | Miss from the skills-index layer down | Skills index sits in the cached system prompt |
| Attaching an MCP server / changing the tool set | Miss — entire cache | Tool definitions are first in the hierarchy; a change there invalidates everything below |
| Context compaction fires (default at 50% of the window) | Miss once, then re-warm | Compaction rewrites the live message list; OpenAI says it plainly: “the first request after compaction may reuse less of the previous cache even when the conversation is logically the same” |
| A cron job with a fixed prompt | Hit across runs while the cache survives | Identical prefix → identical unit; DeepSeek's cache lives hours-to-days |
| A cron job that interpolates a timestamp at the top of its prompt | Miss, every run | You changed the prefix before anything stable |
| Retrieved chunks (RAG / Mnemosyne lookup) injected in the middle | Miss from the insertion point | Caching is prefix-based; material inserted mid-prompt truncates the reusable region |
| A 5-minute gap and a short turn, then the next call | Hit (Anthropic TTL refreshed on use) | But a 5-minute TTL with a long gap goes cold — a 4-minute streaming response already eats most of it |
Two habits fall out of it:
- Stable content first, volatile content last. Identity, tools, memory, skills, project context → front; today’s date, current query, retrieved snippets, per-run IDs → back. Inject volatile context at the end of the user message, as Hermes’ API-call-time layer does.
- Don’t change the middle of a run. Model switches, skill churn, tool-set changes, memory rewrites are fine — do them between runs. The cache is prefix-exact, so everything after a change is a miss.
TTL — “time to live”: how long a cached entry survives unused. DeepSeek: hours to days, best effort. Anthropic: 5 minutes default, refreshed free on every read; 1 hour for a 2× write. OpenAI: 30 minutes. A TTL is why “the same prompt, run tomorrow” is not automatically a hit.
WallWall05Memory bandwidth: why decode is not a compute problem
Switch columns from section 1. Locally nobody charges per token, so why is a 4B model on a phone stuck at single-digit tokens per second?
The plain explanation
Generating one token runs the whole model once: each layer’s weights are read from memory, plus the KV cache for the current context, then a tiny amount of arithmetic. That ratio of arithmetic to bytes moved, or arithmetic intensity, is low, so the chip waits on memory, not computing.
So TOPS (trillions of operations per second) is the wrong number to judge a device by. Meta’s on-device survey[4]: mobile NPUs “now deliver serious TOPS, getting close to the capability of data-center GPUs in 2017 (for example, V100 is 125 TOPS)”: Apple’s A19 Pro Neural Engine ~35 TOPS, Snapdragon 8 Elite Gen 5 ~60 TOPS, Dimensity 9400+ ~50 TOPS. “But TOPS alone doesn’t tell you much.” Then the decisive sentence:
“The deeper constraint is memory bandwidth. Mobile devices have 50–90 GB/s; data center GPUs have 2–3 TB/s. That's a 30–50x gap. For LLM inference, this gap is decisive because decode is memory-bound: you load the entire model weights for each token generated, so the compute units sit idle waiting for memory.”Chandra & Krishnamoorthi — On-Device LLMs: State of the Union, 2026 [4]
Memory bandwidth — how many bytes per second the processor can pull from RAM. Measured in GB/s (gigabytes) or TB/s (terabytes). Not to be confused with storage speed (SSD) or capacity (how many GB you have).
The napkin formula
tokens per second ≈ memory bandwidth ÷ bytes read per token
“Bytes read per token” is dominated by the model weights, plus the KV cache you carry. Two real datapoints.
Datacenter (official llama-bench README[5]). Qwen2.5-7B-Instruct-Q4_K_M, 4.677 GB of weights, on an RTX 4080:
- 4080 memory bandwidth: 716.8 GB/s (Wikipedia's RTX 40-series desktop table)
- Ceiling: 716.8 ÷ 4.677 ≈ 153 tokens/s
- Measured:
tg128= 120.6 tokens/s → 79% of the theoretical ceiling
Phone (the Sep 13 audit’s own bench). Qwen3-4B-Q4_K_M, ~2.5 GB of weights, Termux + llama.cpp on a Pixel-class device:
- Measured:
llama-benchpp512 / tg128= 9.28 tokens/s for generation - Implied achieved bandwidth: 2.5 GB × 9.28 ≈ 23 GB/s
- Against the survey's 50–90 GB/s range, that is 26–46% of the ceiling — while the 4080 sits at 79%
Same formula both times. The phone is further from its ceiling for reasons visible in section 6: KV-cache reads on top of weights, CPU overhead in Termux, thermal throttling, and an OS that reclaims memory behind your back.
| Class of device | Typical memory bandwidth | What that means for a 2.5 GB Q4 model |
|---|---|---|
| Phone / tablet SoC | ~50–90 GB/s | 20–36 tok/s ceiling; realistically single digits to low teens |
| Datacenter GPU (current) | ~2–3 TB/s | 800–1,200 tok/s ceiling on the same bytes |
| Consumer GPU (RTX 4080, 2022) | 716.8 GB/s | ~153 tok/s ceiling |
Quantization — storing model weights (and, optionally, KV cache entries) in fewer bits. Q4_K_M ≈ 4 bits per weight; f16 = 16 bits. Quantization speeds up generation not because the math is cheaper, but because fewer bytes cross the memory bus per token, the actual bottleneck.
Hence 16-bit to 4-bit weights is roughly a 4× throughput win, not a 4× size-only win. And speculative decoding (a small draft model proposes tokens; the big model verifies them in one pass) is nearly “free”: one memory-bound pass is amortized over several accepted tokens.
Where it shows up in practice
llama-bench measures this on your own hardware. From the official README:
# prompt processing (-p) vs text generation (-n); -ngl = layers offloaded to GPU
./llama-bench -m qwen3-4b-q4_k_m.gguf -ngl 99 -p 512 -n 128
# how much does context depth cost you? prefill the KV cache to 0, 512, 4096 tokens
./llama-bench -m qwen3-4b-q4_k_m.gguf -d 0,512,4096 -p 512 -n 128
# how much does the KV cache data type cost? f16 vs q8_0
./llama-bench -m qwen3-4b-q4_k_m.gguf -ctk q8_0 -ctv q8_0 -n 128
Reading the output (the README's own example, retitled for clarity):
| model | size | params | backend | ngl | test | t/s |
| qwen2 7B Q4_K - Medium | 4.36 GiB | 7.62 B | CUDA | -1 | pp512 | 7340.20 ± 23.45 |
| qwen2 7B Q4_K - Medium | 4.36 GiB | 7.62 B | CUDA | -1 | tg128 | 120.60 ± 0.59 |
pp512= prefill of 512 tokens: thousands of tokens/s, parallel and compute-bound.tg128= generation of 128 tokens: tens of tokens/s, sequential and memory-bandwidth-bound. The number a phone lives and dies by.-d Npre-fills the KV cache to N tokens, so you see context length costing speed instead of just believing it.- The README notes the measurements exclude tokenization and sampling: real-world numbers run a little lower.
One more trap, from llama.cpp’s completion docs. When the window fills, “some of the earlier tokens (half of the tokens after --keep) will be discarded. The context must then be re-evaluated before generation can resume. On large models and/or large context windows, this will result in a significant pause in output.” That pause is a cache miss felt through your chair, not on an invoice. The local “freeze your prefix” is --keep N (or --keep -1 to retain the initial prompt when the window rolls).
Wall06The phone case, concretely
The Sep 13 audit benched a Pixel-class Android device: Termux, llama.cpp, Qwen3-4B-Q4_K_M (~2.5 GB). The numbers, and what each says:
| Observation | Reading |
|---|---|
llama-bench pp512 / tg128 = 9.28 tok/s generation | ~23 GB/s achieved from a 50–90 GB/s memory system. Slow decode is a bandwidth/overhead story, not a “small chip” story |
-ngl 99 (offload all layers to the Adreno GPU via OpenCL) crashed on SET_ROWS with a q8_0 KV cache | Quantized KV cache is not universally supported by accelerator backends. Falling back to f16 KV fixed the crash — and doubled the cache size. Speed and correctness trade against each other at the backend's mercy |
| MemAvailable 5.2 GB against 15.5 GB headline RAM | Weights (2.5 GB) + KV cache (grows with context) + Android's own services + the GPU driver's allocations. “16 GB of RAM” never means 16 GB for your model |
| Context length was the knob that mattered | Per section 2's arithmetic: f16 KV at 32K context is ~4.8 GB for a 4B model. Long context is not free locally — it is a RAM bill paid up front |
SET_ROWS — a low-level array-writing operation in the compute backend. When a backend (here, the Adreno OpenCL path) hasn’t implemented an operation the quantized KV-cache path needs, the process doesn’t slow down — it crashes. Backend coverage, not raw FLOPS, decides which flags work on a phone.
-ngl 99 with -ctk q8_0 -ctv q8_0 crashing on SET_ROWS is a backend-coverage failure, not a memory failure. Fall back to f16 KV and accept the doubled cache, or drop the offload. Read a backend’s op coverage before its TOPS.
Apple's 4,096 tokens is a memory decision
Apple’s on-device Foundation Models framework[6] documents a 4096-token context window per session. Exceed it and it “throws a LanguageModelError.contextSizeExceeded error and the session stops responding.” The prescribed fixes are memory engineering, not “be less ambitious”:
- Split large tasks across multiple sessions, chaining summaries forward (their worked example summarizes a long article in chunks).
- Start a new session with a summary plus the first and last transcript entries: “The first transcript entry often contains important instructions and the last entry contains the most recent context.”
- Limit tool use: three to five tools per request, short descriptions — because “tool definitions and their input and output” consume the same context budget as your prompt.
- Keep prompts and instructions concise, and ask for shorter answers.
- Use
contextSize(the model's actual maximum) andtokenCount(for:)instead of hardcoding 4096.
Read that list as a KV-cache budget in English. A 4,096-token window is small because each token costs KV memory, on a device with 50–90 GB/s shared with the whole OS. Same 50–90 GB/s as section 5, same 144 KiB/token as section 2.
The local knobs, gathered
| Goal | llama.cpp | Ollama |
|---|---|---|
| Cap context (cap the KV cache) | -c, --ctx-size N | num_ctx option / OLLAMA_CONTEXT_LENGTH (default 4096) |
| Shrink the cache per token | -ctk q8_0 -ctv q8_0 (f16 default) | OLLAMA_KV_CACHE_TYPE=q8_0 or q4_0 (needs Flash Attention) |
| Enable Flash Attention (needed for quantized KV) | -fa on | OLLAMA_FLASH_ATTENTION=1 |
| Keep the cache on the CPU, not the GPU | -kvo / --no-kv-offload | — (managed automatically) |
| Keep the initial prefix when the window rolls | --keep N (-1 = keep all) | — |
| Cache prompt state to a file across runs | --prompt-cache FNAME (plus --prompt-cache-all, --prompt-cache-ro) | — (server keeps state while loaded) |
| Keep the model (and its KV cache) resident | --sleep-idle-seconds controls when it stops being resident | keep_alive (default ~5 min; ollama ps shows the countdown) |
| Restore saved cache state | --slot-save-path | — |
Two notes that save a debugging session. In Ollama the KV-cache quantization type is global — “all models will run with the specified quantization type” — and quantizing the cache “can harm latency if the context length is short and there is enough GPU memory”, so the win is context-length-dependent. And llama-server’s --sleep-idle-seconds unloads “the model and its associated memory (including the KV cache)” after idleness, so a warm local cache expires on your hardware’s clock.
CrossoverCrossover07The two halves are the same object
Side by side, the spine appears:
| Prompt caching (API) | KV cache on your hardware | |
|---|---|---|
| What is stored | The KV cache for a prefix of your request | The KV cache for your whole context |
| Who pays to store it | The provider (their HBM, their SSD) | You (your VRAM, your RAM) |
| What a “hit” costs | ~1/50 of a miss on input tokens | A read of N bytes per token at 50–90 GB/s |
| What a “miss” costs | Full input price + slower time-to-first-token | Re-prefill: the “significant pause” llama.cpp warns about |
| What invalidates it | Prefix edit, model change, tool-set change, TTL expiry | Context growth, cache dtype, OS memory pressure, backend limits |
| What you can control | Prompt layout, model stability, TTL strategy, watching hit-token counters | ctx-size, KV dtype, Flash Attention, quantization, context discipline |
| What stays constant | Output tokens are never cached | Decode is always memory-bound |
The join is the sentence to take away:
Prompt caching is renting somebody else's KV cache. Running a model locally is owning yours — and paying the bill in bandwidth and RAM instead of dollars.
Both obey the same two rules:
- The cache is prefix-exact. On the API, a changed byte at position 4,000 makes position 400,001 uncacheable. On your device, a cache dtype your backend can’t execute makes the whole thing nonexistent. Cacheability is a property of your input’s beginning and your hardware’s compatibility, not your intentions.
- You only ever discount the past. Prefill (reading the prompt) is what caching helps; decode (producing the answer) is what memory bandwidth limits, and it is never cached, not by DeepSeek’s disk tier or your phone’s LPDDR. Every cost or latency optimization must answer: how much of this request is re-reading, and how much is new work?
08Playbook
- Freeze your prefix. Put identity (SOUL/system prompt), tool definitions, memory, skills and project context at the front, in a stable order. Nothing volatile above them.
- Push volatility to the end. Current time, session IDs, retrieved chunks, per-run variables → end of the user message. Use an ephemeral-context hook rather than editing the system prompt.
- Stabilize the tool array. Keep the resident tool set (including progressive-disclosure/
tool_searchnames and descriptions) constant within a run. Tool definition changes invalidate the entire cache. - Don’t swap models, providers, or credentials mid-session. Hermes’ docs: this means zero cache hits on the next request. If a fallback is needed, expect a full-price turn.
- Edit memory between runs, not during them. A frozen
MEMORY.mdsnapshot is a feature: you keep the session’s cache while writes land on disk for the next one. - Measure the hit rate, don’t assume it. Log
prompt_cache_hit_tokens/prompt_cache_miss_tokens(DeepSeek),cache_read_input_tokens(Anthropic), orcached_tokens(OpenAI). A hit rate is a metric; a feeling is not. - Design cron prompts with a stable body. Fixed instructions first; interpolated values (dates, run ids) last. An identical prefix across runs is free money while the cache lives.
- Right-size context on local hardware.
ctx-size× KV dtype is a RAM allocation. Start at the smallest workable window, then reach forq8_0KV (≈ half) orq4_0KV (≈ a quarter) if supported. - Benchmark before optimizing.
llama-bench -p 512 -n 128shows prefill vs decode; add-d 0,512,4096to price context depth; add-ctk q8_0 -ctv q8_0to price cache quantization. Change one thing at a time. - Expect a prefill pause when the window rolls. In llama.cpp that is the
--keep/context-shift path; on a phone it’s the gap between a responsive assistant and a visible stall. Budget context accordingly.
09Key takeaway
One KV cache, two ways to meet it. On a hosted API it is the provider’s, kept on disk and charged by whether your request’s prefix matches. On DeepSeek today a hit costs $0.003 per 1M input tokens against $0.15 for a miss: a 50× lever, measurable (prompt_cache_hit_tokens / prompt_cache_miss_tokens) and controllable through prompt layout — stable first, volatile last, no mid-run model or tool-set changes, memory edits between sessions. On your own hardware the cache is yours, billed as memory bandwidth and RAM, because decode is memory-bound: tokens/s ≈ bandwidth ÷ bytes-read-per-token. That explains why a phone (50–90 GB/s) does ~9 tok/s on a 2.5 GB model while a datacenter GPU (2–3 TB/s) does hundreds, and why Apple’s 4,096-token window, Ollama’s 4,096-token default, and llama.cpp’s -c/-ctk/-ctv flags are one decision in different clothes. In both columns caching discounts only the past; the next tokens are new work. Freeze the prefix, keep volatile parts last, count your context.
10Go deeper
Gap 1 — prompt caching (the 1–3 best free references)-
[1]
api-docs.deepseek.com/guides/kv_cache
The primary source: request-boundary persistence, common-prefix detection, fixed token intervals, the “fully matches a cache prefix unit” rule, best-effort semantics, the
Cited in §03prompt_cache_hit_tokens/prompt_cache_miss_tokensfields, and the A+B / A+C / A+D examples. Read this one first. -
[2]
api-docs.deepseek.com/quick_start/pricing
The live hit/miss price table, the off-peak/peak split, and the model-rename footnote. Check it before quoting any number here.
Cited in §03 -
[3]
Introducing DeepSeek-V4.1-Flash
www.deepseek.com/en/news/deepseek-v4-1-flashThe vendor’s statement that “cache-hit charges often account for a large share of agent costs”, with the 1/4 HBM and 1/8 SSD figures. Pairs with the technical report PDF and the model card.
huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash/blob/main/DeepSeek_V41_Tech_Report.pdf · huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash Cited in §03
-
·
docs.anthropic.com/en/docs/build-with-claude/prompt-caching
Cited in §03, §04cache_control, thetools → system → messageshierarchy, 1.25×/2× write multipliers, 0.1× reads, the 20-block lookback window, and the single most useful table in the discipline: what invalidates the cache. Tool definition changes kill everything;tool_choicechanges kill only messages. - ·
-
[4]
Vikas Chandra & Raghuraman Krishnamoorthi (Meta) — “On-Device LLMs: State of the Union, 2026”
v-chandra.github.io/on-device-llmsThe 50–90 GB/s vs 2–3 TB/s comparison, “decode is memory-bound: you load the entire model weights for each token generated”, the mobile-NPU TOPS table, and a “KV Cache Management” section (KV quantization to ~3 bits, StreamingLLM, DuoAttention, ChunkKV, EvolKV).
Cited in §02, §05 -
[5]
llama.cpp —
github.com/ggml-org/llama.cpp/tree/master/tools/llama-benchllama-bench(tools/llama-bench README)The measurement tool, with real tables for
github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md · github.com/ggml-org/llama.cpp/blob/master/tools/completion/README.md Cited in §05pp/tg, threads, GPU layers and prefilled context depth. Paired with the server README (-ctk,-ctv,-kv-offload,--sleep-idle-seconds) and the completion README (--prompt-cache,--keep). -
[6]
Apple — Managing the context window
developer.apple.com/documentation/foundationmodels/managing-the-context-windowThe 4096-token window,
www.infoq.com/news/2026/03/apple-foundation-models-context Cited in §06contextSizeExceeded,tokenCount(for:), and the prescribed strategies (split tasks, fresh sessions, ≤3–5 tools) that are really a memory budget. Coverage: InfoQ.
-
·
Hugging Face — Cache strategies and Caching
huggingface.co/docs/transformers/en/kv_cache · huggingface.co/docs/transformers/en/cache_explanationThe cache as an explicit object: dynamic vs static vs quantized caches, offloading, and “prefill a cache (prefix caching)” as a first-class operation.
Cited in §02 -
·
Hugging Face — Llama 3.1 inference memory requirements (blog)
huggingface.co/blog/llama31The KV-cache sizing working shown end-to-end for a real model family.
-
·
vLLM — Automatic Prefix Caching and the deeper prefix caching design doc
docs.vllm.ai/en/latest/features/automatic_prefix_caching.html · docs.vllm.ai/en/latest/design/prefix_caching.htmlA self-hosted prefix cache:
enable_prefix_caching=True, block hashing, and the honesty that APC “only reduces the time of processing the queries (the prefilling phase) and does not reduce the time of generating new tokens”. -
·
Kwon et al., “Efficient Memory Management for Large Language Model Serving with PagedAttention” (arXiv:2309.06180, SOSP 2023)
arxiv.org/abs/2309.06180The paper behind vLLM; treating the KV cache as paged memory with explicit waste accounting, and why prefix sharing became practical at scale.
-
·
Dao, “FlashAttention-2” (arXiv:2307.08691) and Wikipedia — FlashAttention
arxiv.org/abs/2307.08691 · en.wikipedia.org/wiki/FlashAttentionWhy attention is written to minimize HBM↔SRAM traffic; “bandwidth is the bottleneck” one level down. (Flash Attention also makes quantized KV caches usable in Ollama.)
-
·
DeepSeek-V2 (arXiv:2405.04434)
arxiv.org/abs/2405.04434Multi-head Latent Attention, the lineage behind DeepSeek’s compressed KV cache; “how a vendor makes cache hits cheap by making the cache small”.
-
·
NVIDIA — Mastering LLM Techniques: Inference Optimization and Databricks — LLM Inference Performance Engineering
developer.nvidia.com/blog/mastering-llm-techniques-inference-optimization · www.databricks.com/blog/llm-inference-performance-engineering-best-practicesIndustry treatments of prefill vs decode, batching, and the memory-bandwidth ceiling.
- ·
-
·
Wikipedia — Memory bandwidth, Cache (computing), GeForce RTX 40 series
en.wikipedia.org/wiki/Memory_bandwidth · en.wikipedia.org/wiki/Cache_(computing) · en.wikipedia.org/wiki/GeForce_RTX_40_seriesThe 716.8 GB/s figure used in section 5.
Cited in §05 -
·
MIT HAN Lab — TinyML and Efficient Deep Learning Computing and StreamingLLM
hanlab.mit.edu/courses/2024-fall-65940 · github.com/mit-han-lab/streaming-llmKV-cache compression and attention sinks; “you don’t need to cache everything, you need to cache the right things”.
-
·
Stanford CS336 — Language Modeling from Scratch and CS224N
cs336.stanford.edu · web.stanford.edu/class/cs224nThe course-level treatment of attention and inference, if you want the derivation rather than the recipe.
-
·
Andrej Karpathy — “Let's build GPT: from scratch, in code, spelled out”
www.youtube.com/watch?v=kCc8FmEb1nYAttention line by line; the clearest way to see what K and V are. Follow with “Let’s reproduce GPT-2 (124M)” and “[1hr Talk] Intro to Large Language Models”.
www.youtube.com/watch?v=l8pRSuU81PU · www.youtube.com/watch?v=zjkBMFhNj_g -
·
3Blue1Brown — “Transformers, the tech behind LLMs” (Ch. 5) and “Attention in transformers, step-by-step” (Ch. 6)
www.youtube.com/watch?v=wjZofJX0v4M · www.youtube.com/watch?v=eMlx5fFNoYcThe visual intuition for queries, keys and values that section 2 assumes.
-
·
MIT 6.S191 — Introduction to Deep Learning
introtodeeplearning.comFree lecture series; useful scaffolding if you want a course rather than a lecture.
pp/tg split becomes a deployment decision).