Skip to content

Repository files navigation

cache-worthy

A prefix-cache advisor for LLM serving. Point it at your own request log and it tells you how much KV cache memory to allocate, what that memory actually buys you, and which eviction policy to run at that size.

Underneath it is a heavily verified simulator for dependency-constrained, cost-aware cache eviction, plus an open question in competitive analysis. The simulator is what makes the advisor's numbers trustworthy. The theory is a real second track and is not something you need to care about to use the tool.


Contents

For users

  1. What this is, and the one number that matters
  2. Quick start
  3. Reading the output
  4. What it is not
  5. Status: what is built, what is verified

For everyone

  1. The problem in depth
  2. What was measured
  3. Why you should believe the numbers

For researchers

  1. The formal model
  2. The open question and its branches
  3. Repository architecture
  4. Using the library
  5. Reproducing everything
  6. Development
  7. Claims discipline

1. What this is, and the one number that matters

If you serve LLM traffic with prefix caching turned on — SGLang's RadixCache, vLLM's block pool — you have made two configuration decisions, probably without much data:

  • How much GPU memory to give the KV cache. In SGLang this is --mem-fraction-static; in vLLM it is --gpu-memory-utilization.
  • Which eviction policy to run. In SGLang, --radix-eviction-policy, with seven options and no published guidance on choosing between them.

This tool replays your own traffic through a verified simulator and answers both. And the first thing it will tell you is which of the two questions is worth your time.

Measured on the public Mooncake production traces, cost expressed as a multiple of the compulsory minimum — what an infinite cache would still have to compute:

Lever conversation trace toolagent trace
Capacity, 2 percent to 25 percent of working set 1.4413 to 1.0224 1.4178 to 1.0036
Policy, best available versus LRU at 2 percent 1.4413 to 1.4113 1.4178 to 1.3823

Your memory allocation is worth about 20 percent of total prefill recomputation. Your policy choice is worth 1 to 2 percent.

That ratio is the tool's central point. Almost all published work on prefix-cache eviction — including, for a long time, this project — optimises the 1 percent axis. The advisor leads with the 20 percent one, then tells you honestly how small the remaining lever is.

Capacity curve for all three traces, with the recommended allocation marked

The dot on each line is where LRU's own curve flattens — the point section 3 calls the knee. Below it, capacity is doing the work; past it, only a policy change can move the line at all, and by the margin in the table above. Regenerated by python scripts/run_bench.py, off the same CapacityCurve the advisor's own capacity section renders — no second sweep to drift out of sync.


2. Quick start

Three commands from nothing to a capacity recommendation, on a sample log that ships with the repository:

git clone <this repo> && cd cache-worthy
python -m venv .venv && . .venv/bin/activate     # or .venv\Scripts\activate
pip install . && cache-worthy analyze examples/sample_log.jsonl

Then point it at your own traffic:

cache-worthy analyze yourlog.jsonl
cache-worthy analyze yourlog.jsonl --capacity-gib 40      # you know your size
cache-worthy analyze yourlog.jsonl --json                 # the whole result

examples/sample_log.jsonl is constructed, not production data — it exists so a first run needs no download. Every number quoted in this README comes from the Mooncake production traces instead, fetched by python scripts/fetch_traces.py.

The script path this repository has documented since the beginning still works and prints exactly the same bytes, which is checked on every test run:

python scripts/will_this_help_me.py yourlog.jsonl

Accepted input

One JSON object per line. Two shapes are auto-detected per file, and a file has to be internally consistent:

Shape Detected by How paths are derived
native a hash_ids list used verbatim; this is the Mooncake schema
text the first present of prompt, text, input, or an OpenAI-style messages list chunked and cumulatively hashed

The text path is the one place the tool is approximate, and it says so on every run. A real prefix cache hashes (parent_hash, block_tokens). With no tokenizer in the dependency list, this hashes (parent_hash, block_chars) using chars_per_block = round(512 * chars_per_token), where --chars-per-token defaults to 4.0.

The sharing structure is faithful — two prompts share a block exactly when they share that character prefix, which is when they share the token prefix up to tokenizer boundary effects. Only the scale rides on the ratio: how many blocks a prompt occupies, and therefore its size and cost. The tool prints the ratio it used and the flag to change it.

Hashing is blake2b truncated to 63 bits, deliberately not Python's hash(), which is salted per process for strings and would break determinism.

If you have no log

You need a request log with prompts or prefix hashes. If your gateway does not keep one, a capture helper is planned (section 5). Failing that, the three Mooncake traces are fetched by python scripts/fetch_traces.py and are a reasonable stand-in for conversational and tool-agent traffic.


3. Reading the output

The advisor answers six questions, in decreasing order of what they are worth. Each one gates the next, and any of them can end the run with a useful answer.

# Question What it tells you Worth
1 Is prefix caching worth enabling at all? If your reuse is near zero, stop here stop or continue
2 What is the least memory that can serve my traffic? A hard floor. Below it, requests cannot be served a constraint
3 How much should I allocate? The cost-versus-capacity curve and where the knee is about 20 percent
4 Am I capacity-bound or policy-bound? Whether to spend your next hour on memory or on a flag decides 3 vs 5
5 Which eviction policy? Which of the seven to run, honestly including "LRU already wins" 1 to 2 percent
6 What can I change today? The shared prefixes worth pinning, via retention APIs that already exist free

Questions 1, 2, 3, 5 and 6 are answered today. Question 4 needs the LP lower bound, which is not built; until it is, the tool reports what each policy costs but not how much of the remaining gap any policy could recover.

The capacity floor is a real constraint, not a formality

A request pins its own path while it is being served, so your cache must be at least as large as your deepest single request. This is not theoretical: on a constructed agent workload, the deepest request was 1.19 GiB of KV against a 50.81 GiB working set — 2.3 percent, which is above the 2 percent capacity point that this project had been using as its headline regime.

So "2 percent capacity" is not automatically a runnable configuration. It is on Mooncake, whose requests are short relative to its working set. It may not be on a workload with long conversations. The tool computes this floor exactly, always includes it as a sweep point, and names it in the output. There is no other way to discover it except by crashing.

The verdict can say the tool does not help

If plain LRU is already the cheapest policy at your capacity, the tool says so. On the real traces that is exactly what happens at 25 percent capacity. A tool that could only report a win would be reporting a number it cannot support.

What a run actually prints

Real output, abridged, from cache-worthy analyze examples/sample_log.jsonl — the sample log this repository ships, which is constructed and not production data. Question 4's headroom column is the one part not built yet.

Lines marked [...] are the only edits: twelve of the twenty curve rows and some explanatory prose are cut for length. Every number below is what the command prints.

  WORKLOAD
  --------------------------------------------------------------------
  read as          a text log
  chunking         2048 chars/block (--chars-per-token; structure is exact, ...)
  model            Llama-3.1-8B
  records          113
  blocks           429 references, 200 distinct  (53.4% reuse)
  re-referenced    30.5% of distinct blocks are ever asked for twice
  reuse distance   p50 3, p90 5, p99 16 requests   (<=10: 99%, <=100: 100%, ...)
  request depth    p50 4, p90 5, max 6 blocks of 512 tokens
  radix nodes      173   size p50 1, p90 2, max 2 blocks
  largest subtree  93 blocks must be evicted before its root can be
  cost spread      2.0x p90/p10 per node, but only 1.1x per byte  (Llama-3.1-8B)

  working set      12.50 GiB   (a cache this big never evicts)
  minimum cache    0.38 GiB = 3.0% of it   (your deepest request; below this ...)

  QUESTION 1       WORTH CACHING
    With unlimited memory and a perfect policy, prefix caching removes at most
    52.7% of this log's prefill recompute. That is a ceiling: no capacity
    and no policy can beat it, and a real cache lands under it.

  CAPACITY   under lru, the deployed default
  --------------------------------------------------------------------
  QUESTION 2       minimum cache 0.38 GiB = 3.00% of the working set.
    [...]
     capacity                     cost  elasticity
       3.00%        0.38 GiB   1.7810          --   floor: your deepest request
       3.61%        0.45 GiB   1.7134       0.209
       4.34%        0.54 GiB   1.6892       0.077
       5.22%        0.65 GiB   1.4663       0.767
       6.28%        0.78 GiB   1.3144       0.593
       7.55%        0.94 GiB   1.1766       0.600
       9.08%        1.13 GiB   1.0587       0.572
      10.92%        1.36 GiB   1.0144       0.232   recommended
      13.13%        1.64 GiB   1.0144      -0.000
      15.79%        1.97 GiB   1.0096       0.026
      [...]
     100.00%       12.50 GiB   1.0000      -0.000   working set: never evicts

  QUESTION 3       ALLOCATE 10.92% = 1.36 GiB
    Costs 1.0144x the compulsory minimum. Past it, the remaining
    11.14 GiB of KV buys 1.4% of the recompute you would
    still be paying there.

  POLICY
  --------------------------------------------------------------------
  compulsory cost  1.508e+15 FLOPs  (any cache pays this)

  Total recompute cost, as a multiple of the compulsory minimum.
  Lower is better; 1.0000 would mean nothing was ever re-prefilled.

  capacity              cost_only freq_landlord     gdsf  landlord      lru  ...
    3.0% =   0.38 GiB      1.7858       *1.7235   1.7235    1.7810   1.7810
    5.0% =   0.62 GiB      1.4699       *1.4429   1.4429    1.4703   1.4663
   10.0% =   1.25 GiB      1.0290        1.0788   1.0788    1.0436  *1.0243
   10.9% =   1.36 GiB      1.0339        1.0642   1.0642    1.0339  *1.0144
   25.0% =   3.12 GiB     *1.0000        1.0000   1.0000    1.0000   1.0000

  * = cheapest at that capacity.

  Verdict
  -------
    3.0%  freq_landlord saves 3.23% of total recompute vs lru (8.672e+13 FLOPs).
    5.0%  freq_landlord saves 1.59% of total recompute vs lru (3.525e+13 FLOPs).
   10.0%  no policy here beats lru, your current default. Changing it would not
          help.
   25.0%  every policy costs exactly the same, and all of it is unavoidable.
    [...]

  PIN   available at your current capacity, under your current policy
  --------------------------------------------------------------------
  QUESTION 6       5 prefixes protect at most 50.8% of what caching can save

              refs   depth   blocks    subtree       pin KV   protects
    #0          51       0        1         93     0.06 GiB      21.5%
    #1          31       0        1         52     0.06 GiB      12.9%
    #2          31       0        1         55     0.06 GiB      12.9%
    #3           5       1        1          8     0.12 GiB       1.8%
    #4           5       1        1          9     0.12 GiB       1.8%

    Together: 0.31 GiB held resident protects at most
    50.8% of avoidable recompute = 26.8% of the whole prefill bill.

  These are recomputation FLOPs under the analytic cost model in
  src/cache_worthy/cost.py, not measured latency. This repo has not
  timed a GPU, so it does not translate them into seconds or dollars.
  Nothing here is a proven bound.

Two things in that output are worth pointing at, because they are the tool working rather than the tool failing. At 10 percent and above, plain LRU is already the cheapest policy and the verdict says so. And the three top pins are the three tenants' shared preambles — 51, 31 and 31 requests — which between them protect half of everything caching could ever save on this log, for 0.31 GiB and no restart.

The elasticity column, and where the recommendation comes from

Elasticity is the percent of recompute removed per percent of memory added, as a local log-log slope, so it is unit-free. The recommendation is the smallest capacity past which no further step on the curve reaches 0.10 — the point where another ten percent of KV buys under one percent of recompute, which is about what changing the eviction policy is worth at all.

The whole column is printed so you can apply a different threshold without re-running anything. The curve is measured under LRU, the policy your server already runs, so that the 1-percent policy axis can never move the 20-percent capacity recommendation.

The pin list is the one answer that costs nothing

Question 6 ranks shared prefixes by the recompute that holding them resident would protect: (references - 1) x cost. It assumes no capacity and no policy, so it is advice you can act on today through a retention API, without a restart.

Two things about that column are load-bearing. It is a ceiling — your cache already retains some of it for free, so the real gain is smaller. And pin KV includes the ancestors a pin implies, because a cached prefix whose own prefix has been evicted is unreachable.

Machine-readable output

--json emits the whole result: every workload field, every curve point with its elasticity, every policy cell, and every pin. The table is a second rendering of the same measurement, and a test reparses the table and compares it against the JSON cell by cell, so the two cannot drift.

cache-worthy analyze yourlog.jsonl --json | jq '.capacity.recommended_fraction'

4. What it is not

  • Not a policy you install. SGLang's eviction-strategy registry is a hardcoded module-level dict with no registration hook, and an unknown policy name raises. Selecting a third-party policy today means patching SGLang. An upstream patch to add a registration hook is being written; until it lands, the advisor's value does not depend on it, because it needs nothing from any serving stack.
  • Not a latency or TTFT predictor. It reports prefill recomputation in FLOPs. This repository has measured FLOPs, not wall-clock, and converting one to the other would be a claim it cannot support. Treat the output as a proxy for prefill work avoided.
  • Not a claim that any policy here beats the deployed default. At large capacity on the measured traces, none of them does, and the tool reports that.
  • Not a model of what cache memory costs you. KV memory trades against batch size and concurrency. The advisor gives you the benefit curve; the trade is yours to make.
  • Not a proof of anything. See section 15.

5. Status: what is built, what is verified

Two words are used throughout this document, and they mean specific things:

  • verified — its behaviour has been confirmed against at least one oracle that exists independently of the implementation. For a mathematical result it means every automated check that exists for it passes. It does not mean proven.
  • unverified — not yet checked, or no oracle above self-written unit tests exists yet. Stated explicitly rather than quietly downgraded.

Built and verified

Component Verified by
Trace pipeline Reproduces the published Mooncake figures exactly — all twelve cells. Mutation-tested: every one-unit perturbation is caught
Static prefix forest Every one of 409,914 distinct blocks has exactly one parent and one depth. Node sizes sum exactly to the distinct-block count
Cost model Derived non-embedding parameter counts sum to the published 8.03 B and 70.55 B model sizes
Online simulator and radix tree Leaf-only eviction, byte conservation, capacity and determinism enforced in-loop as raises. Heap and linear-scan eviction agree byte-for-byte
Seven eviction policies Landlord obeys the known competitive bound on every flat sequence where that theorem applies. Each diagnostic policy replays byte-identically to its reference under the conditions where they must coincide
Exhaustive true-OPT solver Matches Belady exactly on every short sequence where Belady is optimal; is at most every policy's cost, always
Potential-function harness Every row of the published theorem holds across 4,784 replays on flat instances
Adversarial construction LRU's cost matches a closed form derived before the code ran
The advisor (log ingest and replay) Reproduces the reference replay field-for-field on 12,031 real requests across every policy and capacity; text ingest and native ingest agree byte-for-byte on isomorphic logs
The capacity curve and the knee The reported minimum cache serves the log and one block less does not; at a capacity equal to the working set all seven policies pay exactly the compulsory cost with zero evictions; the knee is checked against curves whose answer was computed by hand first. The benchmark and the command-line tool render the same curve byte-for-byte
The pin list The per-prefix protections sum exactly to the log's total avoidable recompute, against a total computed two other ways, on all three traces, with no block counted twice. On the conversation trace the top prefix is the depth-0 preamble shared by all 12,031 requests. A three-request toy reproduces a ceiling that was derived by hand for a different oracle
JSON output The rendered table is parsed back and compared with the JSON cell by cell, section by section, positionally, on a real trace and again under a non-default model. The document is strict JSON: undefined quantities are null, never NaN
The packaged CLI cache-worthy analyze and the original script path print byte-identical output. The shipped example log is regenerated from its script and compared byte for byte
The LRU-unbounded result Four independent automated checks, including against brute-force true OPT

Built, unverified

  • Continuous integration. The workflow is committed but has never actually run on a runner, so "a stranger can reproduce this on a clean non-Windows machine" is unproven. What has been done: the hermetic job's exact commands — pip install -e ".[dev]", then make check and make proofs — were run against a fresh virtualenv on a tree containing only committed files, and both passed. The trace-fetching job runs green against the real traces, but not yet from a fresh virtualenv. Ubuntu and the runner itself remain untested.

Not built

The LP lower bound Question 4. Without it the tool reports what each policy costs but not how much is recoverable
Sensitivity band Every printed number depends on the cost model's coefficients; the band is a gate, not a nicety
Tail statistics and cold start p50/p95/p99 per request, and time-to-steady-state from an empty cache
Log adapters and capture helper vLLM, SGLang and OpenAI-style logs behind the existing auto-detect seam, and a recorder for users with no log at all
The main theoretical result Whether the standard guarantee survives these constraints. Three outcomes are pre-registered as acceptable, including that it is false
SGLang integration Both the registration-hook patch and any policy contribution

6. The problem in depth

6.1 In plain terms

An LLM must process ("prefill") your entire prompt before it emits a single token, and prefill cost grows superlinearly with prompt length. When two requests share a prefix — a system prompt, a conversation history, a tool preamble — the shared part only has to be computed once. Those intermediate results, the KV blocks, are cached in GPU memory. Evicting a KV block means paying its recomputation cost again the next time that prefix appears.

That makes it a caching problem, but an unusual one in two specific ways.

Cached items form a tree, and only leaves are evictable. A cache holds system prompt, then system prompt + turn 1, then system prompt + turn 1 + turn 2. Evicting an interior node would orphan everything built on top of it, so it is forbidden. To free an interior node's space you must first evict its entire cached subtree. Eviction cost is therefore not local, and an adversary can exploit that.

Recomputation costs are wildly non-uniform. A short recent turn costs almost nothing to redo. A long shared prefix costs a great deal, and that cost grows with depth because attention is quadratic.

The policy deployed almost everywhere is LRU, which is blind to both.

6.2 Why LRU fails, concretely

Take a cache holding k items: one expensive item E and k cheap ones. Requests cycle E, c1, ..., ck forever. The cycle touches k+1 distinct items through a k-slot cache, so LRU misses on every request — and being cost-blind, it discards E as readily as anything else, paying E's cost every cycle. A cost-aware policy pins E and rotates the cheap items through the remaining k-1 slots.

LRU's cost grows without bound as E gets more expensive: there is no constant, and no function of the cache size, that bounds it. The construction is implemented and runnable, and the write-up passes four independent automated checks — including its per-phase optimum verified against a brute-force true optimum rather than against the prose, which is what caught an arithmetic error in an earlier draft of it.

Run it yourself:

python scripts/run_adversary.py

6.3 Why "just use a cost-aware policy" is not the answer

Cost-aware prefix-cache eviction already exists. RAGCache ships a GreedyDual-Size-Frequency variant over a knowledge tree, evicting the least-priority leaf. Two gaps remain:

  • It carries no guarantee. The string competitive appears zero times in the RAGCache paper. "Works on the traces tested" is not a bound.
  • It ignores the tree in its analysis. GreedyDual-family policies were designed for flat caches. Whether their guarantees survive leaf-only eviction is an assumption, not a fact, and it is exactly what this project tests.

And, as section 7 shows, the empirical gap between all these policies on real traffic is 1 to 2 percent — which is why the tool leads with capacity instead.


7. What was measured

Everything in this section is a measurement regenerated by a committed script. None of it is a claim about a proven result, and none of it was tuned toward. Where an expectation was written down in advance and turned out wrong, that is stated.

7.1 The traces

Three production traces from Mooncake FAST'25, Apache-2.0. Reproduced exactly against Mooncake's own published figures:

Trace Requests Span Blocks total / distinct Block reuse Mean input
conversation_trace 12,031 0.98 h 288,500 / 182,790 36.6 percent 12,035
toolagent_trace 23,608 0.98 h 409,616 / 183,300 55.3 percent 8,596
synthetic_trace 3,993 0.28 h 121,877 / 43,924 64.0 percent 15,325

Caveats stated openly. Timestamps are quantized to about 1,180 distinct values, roughly 3-second buckets. The span is about one hour, so there are no diurnal effects. synthetic_trace is constructed with Poisson arrivals and must never be presented as production data; it is reported alongside the others and excluded from any verdict.

7.2 Structure and cost spread

conversation toolagent synthetic
radix nodes 15,787 27,253 4,177
node size p50 / p90 / max, in blocks 2 / 31 / 246 2 / 17 / 246 1 / 37 / 373
cost per node, p90/p10 41.3x 19.8x 49.7x
cost per node, max/min 825x 825x 1,695x
cost per byte, max/min (8B model) 5.6x 5.6x 8.1x
cost per byte, max/min (70B model) 3.3x 3.3x 4.6x

Prefix depth and radix node size

Sizes and costs really are non-uniform — recomputation cost varies by nearly three orders of magnitude within a single trace.

But that spread is dominated by size, not depth. The quantity a GreedyDual-family policy actually ranks on is cost per byte, and that spans under one order of magnitude — and narrows on bigger models, because the linear term grows faster than the attention term. There is much less for a cost-aware policy to exploit here than the raw 825x suggests, and the measured 1 to 2 percent margins are the direct consequence.

Cost spread per node and per byte

Subtree sizes are extremely skewed. The conversation_trace root — a system prompt present in all 12,031 requests — sits above all 182,790 blocks. Under leaf-only eviction, reaching that node means first evicting essentially the entire cache. That asymmetry is present in production data at extreme scale, not just in constructed adversaries.

7.3 Which policy wins, and by how little

Cost as a multiple of the compulsory minimum. recursive_lru is omitted because it is byte-identical to lru everywhere (see 7.4). cost_only and size_only are Landlord with exactly one ranking term flattened — diagnostics, not proposals — so each neighbouring pair differs in one term.

Trace Capacity cost_only gdsf landlord lru size_only
conversation 2 percent 1.4113 1.4197 1.4288 1.4413 1.4368
conversation 10 percent 1.2027 1.1192 1.1259 1.1432 1.2451
conversation 25 percent 1.0779 1.0263 1.0232 1.0224 1.0946
toolagent 2 percent 1.3823 1.3894 1.3943 1.4178 1.4374
toolagent 10 percent 1.1635 1.0799 1.0855 1.1049 1.2519
toolagent 25 percent 1.0538 1.0090 1.0078 1.0036 1.1123
synthetic 2 percent 2.6513 2.6879 2.7014 2.7302 2.8373
synthetic 25 percent 1.4706 1.5022 1.4926 1.5621 2.0462

No policy wins everywhere. Cost-ranked eviction is cheapest at the tightest capacity on all three traces; a frequency-aware policy takes over in the middle; plain LRU is cheapest at 25 percent on both real traces. The whole spread is 0.5 to 1.8 percent of total cost.

Ranking on size alone is harmful here, and that reversed a prior prediction. Before these variants existed, the expectation was written down that size_only would track landlord and both would lead cost_only and lru, reasoning from the narrow cost-per-byte spread and from the fact that none of SGLang's seven strategies ranks on size at all. The measured order is the reverse. size_only is the worst policy on the board at almost every capacity on every trace. The inference that "nobody ranks on size" identified a gap worth filling did not survive contact with the data.

The likely mechanism, stated as inference and not as a result: because the cost range is driven almost entirely by node size, cost and size are strongly correlated here, so "evict the cheapest leaf" and "evict the largest leaf" are closer to opposites than to independent knobs. What the traces say is keep the large expensive nodes, and dividing by size partially cancels that signal.

7.4 Dallot's algorithm is already what you deploy

Recursive LRU — proven k-competitive under DAG dependencies in published work — is byte-identical to plain LRU on all three Mooncake traces, every field of every replay at every capacity, and also on a fourth constructed non-Mooncake log. There is an argument for why:

A request in a prefix cache is a single root-path. Every matched node except the deepest has a child on that same path, so at most one node per request is a leaf, and the newly admitted node makes the previously-deepest node interior anyway. Two distinct evictable leaves therefore always carry timestamps from two different requests, so the clock alone orders them, and the ancestor-ordering discipline never gets to break a tie.

This is measured and argued, not proven, and it is flagged for human review — the argument is short enough to be checked and should be.

7.5 A pre-registered hypothesis was tested and found false

Written into the plan long before the policies existed:

H1: Landlord's advantage on these traces survives because its credit mechanism carries no frequency term, so the short-lifespan critique of arXiv:2506.02634 does not apply to it.

Verdict: false, on the causal clause. Adding a frequency term to Landlord helps at 12 of the 14 real-trace capacity points where Landlord beats LRU.

The useful part is the attribution. GDSF and Landlord differ in two places at once, so their measured gap could not be assigned to either:

gdsf            key = clock + freq * cost/size    clock = key of last evicted
landlord        key = rent  +        cost/size    rent  = max(rent, key of last evicted)
freq_landlord   key = rent  + freq * cost/size    rent  = max(rent, key of last evicted)

Inserting the missing middle row splits one unattributable gap into two exact one-term steps. Across all 16 real-trace points, the frequency step reaches 0.0177 while the aging-rule step never exceeds 0.0018. The reason Landlord trails GDSF is the one term it deliberately omits, and Landlord's max() — the place the dependency constraint enters the aging rule — buys essentially nothing on this workload. That is a fact about Mooncake, not about the model: on constructed forests the two aging rules genuinely diverge.

The critique's premise holds and its conclusion does not follow. Only 24.2 percent (conversation) and 21.5 percent (toolagent) of distinct blocks are ever referenced more than once, and reuse is close-range — median distance 4 requests on toolagent. So for roughly four blocks in five a frequency term is definitionally uninformative. It still helps.

One measurement that is easy to misread. At 2 percent capacity, 97.6 percent and 98.5 percent of evicted nodes leave having been hit exactly once. The tempting reading is that the frequency term is idle. That reading is wrong: the evicted population is the complement of the protected one, so a frequency term that works produces exactly this histogram. This is an inference, not a result.

Two predictions were registered in advance. One was right — that the frequency term would help at 2 percent capacity. One was wrong — the LRU crossover was predicted between 2 and 10 percent capacity, and it is between 15 and 25 percent. A pre-registration reported only when it wins is not a pre-registration.

python scripts/h1_check.py

8. Why you should believe the numbers

This is the part of the repository most worth reading if you are deciding whether to trust it.

8.1 Green tests are not the bar

When the same author writes the code and its tests, passing tests can encode one misunderstanding twice. A component is done when its behaviour is confirmed against an oracle — an expectation that exists independently of the implementation — and the expectation is written down before the code.

Rank Oracle Example here
1 External published ground truth The trace table reproduces Mooncake's own published figures exactly
2 Mathematical invariant that cannot hold by coincidence The optimum is at most every policy's cost; bytes conserved; capacity never exceeded; every block has exactly one parent
3 Established theory as a test On flat traces, where the published theorem applies, the measured ratio must obey it. A violation is a bug in this implementation, never the theory's
4 Hand-computed known answer A toy trace with per-policy cost worked out on paper before the code ran
5 Differential Heap and linear-scan eviction agree byte-for-byte; the advisor reproduces the reference replay field-for-field
6 Self-written unit tests Lowest rank. Necessary, never sufficient for a correctness-critical component

No correctness-critical component is complete on rank-6 evidence alone. Where no oracle above rank 6 exists, that is stated rather than quietly downgraded.

8.2 Invariants are assertions, not tests

Four invariants run inside the replay loop, raising rather than asserting so that python -O cannot disable them: leaf-only eviction, byte conservation, capacity, and determinism. Reaching the end of a replay is the assertion. A violation is a bug in the simulator, never a curiosity to work around.

8.3 A worked example of the discipline catching something

The dependency-constrained survey of the potential harness passed on its first draft. It passed because four of its twelve instances had capacity above their working set and never evicted at all — a check that passes because it never ran. That is recorded here rather than quietly fixed, and every survey case now reports its eviction count and is flagged as vacuous if it never evicts.

Separately, an arithmetic claim inside an otherwise correct argument — "the optimum pays one miss per phase" — was wrong by a factor that is 2x at the smallest cache size. It was caught by differencing the brute-force true optimum, not by re-reading the prose.

8.4 The limits of all this

These checks catch wrong constants, wrong potentials and false theorems. They do not catch an unstated assumption, a flawed asymptotic argument, or a proof that is correct but proves something weaker than claimed. Verified means "survived the checks that exist", never "is true".


9. The formal model

Full statement in proofs/model.md. An instance is (F, size, cost, sigma, k, h).

  • Forest F = (V, parent). parent(u) = v means u's KV blocks were computed with v's resident, so u is meaningless without v. The published dependency-caching work allows an arbitrary DAG; the model here specializes to a forest because that is what a prefix cache is — and this was measured to hold on all 409,914 distinct blocks rather than assumed.
  • size(v), a positive integer. Integrality is the one restriction the underlying theorem requires, and KV blocks satisfy it naturally.
  • cost(v), the price of recomputing v given its ancestors are resident — not the price of recomputing it from nothing.
  • k is the online capacity and h <= k the offline optimum's, both in size units, not item counts.

A cache state must satisfy ancestor-closure (if a node is resident, its parent is) and capacity. Ancestor-closure has an operational form that is the reason this is not the classical problem:

Leaf-only eviction. From an ancestor-closed state, the single-node removals preserving ancestor-closure are exactly those of nodes with no resident children.

So the evictable set is the leaf set of the resident subforest, and it changes as eviction proceeds — evicting a node can make its parent evictable. This is exactly what SGLang's evict() does when it re-pushes a parent onto the heap.

Five assumptions are named explicitly: demand paging with no bypassing, the in-service path is pinned, integer sizes, one request at a time, and capacity at least the deepest requested path — which is the floor the tool reports.

Two degeneracies, asserted as tests rather than claimed in prose: a flat forest recovers classical file caching, and unit sizes with unit costs recovers published dependency-aware caching. The first is the strongest available check on this implementation; the second keeps notation compatible and prevents an accidental scoop.

9.1 The algorithm under study

Landlord (Young 2002, Figure 1, transcribed verbatim):

Maintain a real value credit[f] with each file f in the cache.
When a file g is requested:
1.  if g is not in the cache then
2.      until there is room for g in the cache:
3.          For each file f in the cache, decrease credit[f] by delta * size[f],
4.          where delta = min over f in cache of credit[f] / size[f].
5.          Evict any subset of the files f such that credit[f] = 0.
6.      Bring g into the cache and set credit[g] to cost(g).
7.  else reset credit[g] to any value between its current value and cost(g).

The transfer this project studies changes exactly one line: line 4's minimum ranges over the evictable nodes only, because line 5 can only evict a leaf. Line 3 keeps charging rent to the whole cache. That leaves one genuine degree of freedom, and it is flagged rather than settled:

Variant Line 3 charges What breaks
all-resident (implemented) every resident node credit can go negative for interior nodes, since the minimum is over a strict subset. The published proof needs non-negative credit
leaf-only evictable nodes only credits stay in range, but the rent base shrinks, and the published proof needs the larger one

Neither is claimed to work.

An implementation note that is a fact about the algorithm, not an optimization. Rent decreases credit/size by the same amount for every resident node at once, so the ranking by credit/size is invariant under line 3. Landlord is therefore exactly "evict the minimum credit/size", implementable with a global aging term and key L + cost/size. That is what makes a heap admissible: no node's key moves when a different node is evicted. The equivalence is asserted in the tests against a direct transcription of the rent loop, so the shortcut is verified rather than argued.


10. The open question and its branches

Two lines of published work, neither sufficient alone. Every row was verified against primary source text, not a search summary.

Work What it proves What it assumes away
Young 2002, On-Line File Caching (Landlord) k/(k-h+1)-competitive for arbitrary integer sizes and arbitrary costs; best possible for a deterministic algorithm No dependencies. Any item is evictable at any time
Dallot et al. 2024, Dependency-Aware Online Caching k-competitive under arbitrary DAG dependencies, with matching randomized bounds Unit size, unit cost
Bienkowski et al. 2017, Online Tree Caching A tree-constrained bound Unit cost, and the bound carries a height factor
Wu, Silwal and Zhang, ICLR 2026 Leaf-LRU on a RadixAttention tree is Theta(B - L)-competitive; a randomized leaf policy is Theta(log(B - L))-competitive with a matching lower bound Unit token size, unit per-token miss cost
Cao and Irani 1997, GreedyDual-Size k-competitive, k = cache size over smallest item No dependencies; a different theorem from Young's — the two must not be merged
RAGCache 2024 Ships GreedyDual for KV caching over a knowledge tree, evicting the least-priority leaf No competitive analysis of any kind

Two details that are commonly misstated and are load-bearing: in Young's bound k and h are capacities in size units, not item counts, and arbitrary sizes together with arbitrary costs are fine for deterministic Landlord — the unit-cost restriction people remember attaches to a different, randomized line of work.

The gap is the conjunction. Dallot's own conclusion names it:

"it will be interesting to study dependency aspects in more general variants of caching, such as weighted caching or file caching"

where the cited reference is Young, On-line file caching. Dependencies without costs exists. Costs without dependencies exists. Prefix-tree leaf eviction under unit sizes and costs now exists too. The conjunction does not.

10.1 Three admissible outcomes, pre-registered

Written down before any proof was attempted, so that a negative result reads as a result:

Branch Trigger Deliverable
A The potential argument goes through, possibly with a worse constant (a height factor is expected; the tree-caching literature carries one) The positive theorem
B The dependency constraint turns out harmless and the published proof survives unchanged Report honestly; the LRU-unbounded result becomes the headline
C An adversary exploits forced-subtree-eviction to defeat every online policy Impossibility plus a matching lower bound

Branch C is a success, arguably the better paper. It closes the open problem just as definitively and is more surprising. This is written in advance specifically so that finding it is not mistaken for failure.

None of the three changes the tool. The advisor, the capacity curve, the lower bound and the pin list are identical under all of them. A branch-A theorem adds a sentence to the policy recommendation; branches B and C cost the product nothing.

10.2 What the harness has found so far

Young's potential is implemented as code and checked after every event of the amortised argument, not per request, against a true-optimum schedule.

On flat instances, where the dependency constraint is vacuous and the published theorem applies verbatim, the harness is green across 4,784 replays with zero violations. That is the gate: it reproduces a known bound before being trusted on new mathematics.

On dependency-constrained forests, exactly one row breaks — and it is not the one the analysis leads with. The credit-floor row breaks on 8 of 12 surveyed cases across 34 events; the rent row did not break on any surveyed case.

Both halves matter. The obstruction is genuine and reproduces on demand: the rent minimum is taken over evictable nodes only, so an interior node gets charged past zero, measured at a raw credit of -1 on the smallest probe. The clamp that restores non-negativity is ours, not Young's, so every downstream row rests on a step the published proof never took. But twelve instances of at most eight nodes is a survey, not a search, and the surviving rent row is not evidence that the positive result holds. It only says where to look first.

python scripts/potential_check.py

11. Repository architecture

cache-worthy/
  plan.md                      what is being built and why
  CLAUDE.md                    how: verification discipline, tiering, gates
  README.md                    this file
  LICENSE                      Apache-2.0
  Makefile                     check / verify / proofs / bench
  .github/workflows/ci.yml     hermetic check; verify with fetched traces
  data/
    raw/                       Mooncake JSONL, gitignored, fetched by script
    checksums.json             committed; a fresh clone verifies rather than trusts
    ATTRIBUTION.md             provenance and privacy caveats
  examples/
    sample_log.jsonl           constructed; the quickstart's first run
    README.md                  says so, at length
  src/cache_worthy/
    cli.py                     `cache-worthy analyze`, argument parsing only
    ingest.py                  a user's log to prefix paths, either shape
    advisor.py                 the six-question funnel; measure, render, document
    result.py                  the JSON document, and the table-vs-JSON reparse
    workload.py                question 1: the caching ceiling
    capacity.py                questions 2 and 3: the floor, the curve, the knee
    pins.py                    question 6: shared prefixes worth holding resident
    trace.py                   JSONL to Request
    forest.py                  static offline prefix forest, radix-compressed
    cost.py                    THE cost model, one definition
    tree.py             [1]    online mutable radix forest; leaf-only eviction
    simulator.py        [1]    the replay loop; invariants enforced with raises
    policies/
      base.py                  EvictionPolicy ABC, mirrors SGLang's interface
      lru.py                   the deployed default
      gdsf.py                  RAGCache's variant
      recursive_lru.py         Dallot
      landlord.py       [1]    Young 2002 Fig. 1 under leaf-only eviction
      ablations.py             size-only / cost-only: diagnostics, not proposals
      frequency.py             freq-landlord: a diagnostic, not a proposal
    bruteforce.py       [1]    exhaustive true optimum plus Belady
    potential.py        [1]    Young's potential as an event-level checker
    adversary.py               the LRU-unbounded construction, runnable
  scripts/
    fetch_traces.py            download and checksum
    characterize.py            reproduces the published trace table
    plot_distributions.py      structure and cost spread
    replay_check.py            real-trace simulator oracles
    potential_check.py         flat gate plus dependency survey
    run_adversary.py           the construction's checks, one command
    h1_check.py                the hypothesis test and its verdict
    make_example_log.py        regenerates examples/sample_log.jsonl
    run_bench.py               the advisor pointed at this project's own three
                                traces; also writes figs/capacity_curve.png
    will_this_help_me.py       the documented script path, plus its oracles
  proofs/
    model.md                   the formal model
    related_work.md            each work quoted from fetched source text
  figs/                        committed; regenerated by plot_distributions.py
                                and run_bench.py
  tests/

[1] marks correctness-critical components: full typing, property-based tests, and at least one rank-3-or-better oracle. A subtly wrong simulator produces plausible wrong numbers that survive all the way into someone's capacity decision.

11.1 The policy interface

Deliberately mirrors SGLang's EvictionStrategy, so an upstream patch is one new file implementing an existing interface rather than a new subsystem.

class EvictionPolicy(ABC):
    @abstractmethod
    def priority(self, node: Node, clock: float) -> float:
        """Lower is evicted first."""

    def on_hit(self, node: Node, clock: float) -> None: ...
    def on_insert(self, node: Node, clock: float) -> None: ...
    def on_evict(self, node: Node, clock: float) -> None: ...
    def on_split(self, upper: Node, lower: Node, clock: float) -> None: ...

on_split is the one method added beyond SGLang's interface. A radix cache splits nodes, and a split must not look like an eviction plus an insertion, or a node's age resets for a reason the workload never caused. The default copies the parent's state to the new child, matching SGLang's handling of last_access_time; for Landlord that is provably the right rule, since copying the GreedyDual key is identical to splitting the credit in proportion to size. GDSF takes the same rule deliberately, so a modelling choice made in one policy and not another cannot surface as a measured difference.

You can drop in your own policy and have the advisor score it against the seven shipped ones. This is deliberate: an upstream patch to give SGLang a registration hook is being written, and it would be incoherent to reproduce the limitation that patch is meant to fix.

11.2 One cost model, defined once

The cost function used in the proofs and the one used in the simulator are the same object. Theory-implementation drift is a standard failure mode in theory-plus-systems work and it invalidates results silently.

Prefilling n tokens through a decoder-only transformer, per layer, counting multiply and add separately:

projections and FFN      2 FLOPs per parameter per token       -> 2*N*n
attention scores         heads * (n^2/2 causal pairs) * 2 * d  -> n^2 * d_model
attention-weighted V     same shape                            -> n^2 * d_model

cost(n) = a*n + b*n^2    with  a = 2N,  b = 2 * n_layers * d_model

a and b are derived from published architecture constants, not fitted, and the derived non-embedding parameter counts are checked against the published model sizes, so a mistyped constant cannot pass silently. A node's cost is the difference form prefill(end) - prefill(start), which is exact rather than approximate: the causal pairs whose query lies in the range number exactly (end^2 - start^2)/2.

No theorem depends on a or b. The advisor's output does — which is why both target models are reported and why a sensitivity band is a gate rather than a nicety.

11.3 How the offline optimum will be bounded

Not built yet, and the method is chosen for a reason. The standard offline 4-approximation for weighted caching is proven without dependencies, and there is no reason to assume it transfers. Rather than assume it:

  1. Formulate dependency-constrained offline caching as an integer program.
  2. Drop the dependency constraints — valid, because the constrained feasible set is a subset, so the unconstrained optimum is at most the true optimum.
  3. Relax integrality — valid, because the LP optimum is at most the integer optimum.

The result is a provable lower bound. Reporting a policy's cost against it overstates the true ratio, which is the conservative direction: it lets the tool say "at most this much is recoverable" and be right. For policy-versus-policy comparison the bound cancels, so relative comparisons stay exact.


12. Using the library

Replaying a trace under a policy

from cache_worthy.cost import DEFAULT_MODEL, kv_bytes, segment_cost
from cache_worthy.policies import POLICIES
from cache_worthy.simulator import replay
from cache_worthy.trace import BLOCK_SIZE_TOKENS, load_trace

requests = load_trace("data/raw/conversation_trace.jsonl")
paths = tuple(r.hash_ids for r in requests)

def size(block_ids, depth):
    return kv_bytes(len(block_ids) * BLOCK_SIZE_TOKENS, DEFAULT_MODEL)

def cost(block_ids, depth):
    start = depth * BLOCK_SIZE_TOKENS
    return segment_cost(start, start + len(block_ids) * BLOCK_SIZE_TOKENS, DEFAULT_MODEL)

result = replay(paths, POLICIES["landlord"](), capacity=200 * 2**30,
                size_fn=size, cost_fn=cost)
print(result.total_cost, result.compulsory_cost, result.block_hit_rate)

replay enforces every invariant as it goes, so reaching the end of a replay is the assertion. Heap and linear-scan eviction are byte-for-byte equivalent: the scan is the obvious-correctness reference, the heap is SGLang's structure.

Checking an instance against the true optimum

from cache_worthy.bruteforce import Instance, optimal_cost

instance = Instance(
    parent=(None, 0, 0, 1),      # 0 -> 1 -> 3, and 0 -> 2
    size=(1, 2, 1, 3),
    cost=(4.0, 1.0, 7.0, 2.0),
    requests=(3, 2, 3, 2),
)
print(optimal_cost(instance, capacity=6))   # exact, not an approximation

Running the potential-function checker

from cache_worthy.potential import check_potential

report = check_potential(instance, k=6, h=6, keep_events=True)
print(report.summary())
for violation in report.violations:
    print(violation.describe())    # which row broke, at which event, and by how much

13. Reproducing everything

git clone <this repo> && cd cache-worthy
python -m venv .venv && . .venv/bin/activate    # or .venv/Scripts/activate
pip install -e ".[dev]"

Four gates, each meaning something different:

make check      # lint, types, tests. Hermetic: no network, no data.
make verify     # cumulative milestone oracles. Needs the real traces.
make proofs     # mathematical oracles.
make bench      # headline numbers: the advisor, pointed at the three traces.

make verify is cumulative: it re-runs every completed milestone's oracles, so a later change that breaks an earlier guarantee fails immediately. It fails loudly rather than skipping if the trace data is absent.

Individual steps:

python scripts/fetch_traces.py            # 3 traces, about 8.5 MB, records SHA-256
python scripts/characterize.py --check    # against Mooncake's published table
python scripts/plot_distributions.py      # regenerates figs/, prints section 7.2
python scripts/replay_check.py --check    # simulator oracles on real traces
python scripts/potential_check.py         # flat gate plus dependency survey
python scripts/h1_check.py                # the section 7.5 hypothesis test
python scripts/run_adversary.py           # the LRU-unbounded construction
python scripts/make_example_log.py        # regenerate examples/sample_log.jsonl
python scripts/run_bench.py               # headline numbers, figs/capacity_curve.png
cache-worthy analyze LOG                  # the advisor, on your own log

Everything runs on a laptop CPU. Expected cloud spend: zero.

Environment note. make is not on PATH on every Windows machine; MSYS2's mingw32-make works and is what this repository is developed against. Makefile recipes are written shell-agnostically, one command per line with no POSIX-only constructs, so they run identically under mingw32-make, GNU make in CI, and any contributor's shell.


14. Development

  • Python 3.13. ruff for lint and format, default rules, not hand-tuned. mypy --strict on src/ and scripts/.
  • Full type annotations in src/. Frozen slotted dataclasses for value types.
  • Docstrings state why, and cite the paper and section for anything implementing a published algorithm.
  • Determinism: seed everything; no wall-clock or dict-ordering dependence in any result.
  • Simplicity is the default and needs no justification; complexity must be triggered in writing at the point of introduction. No abstraction before the second use case. Dependencies are a fixed short list. No Rust until a measured wall-clock trigger fires, and never for the simulator loop.
  • Rigid core, flexible edges. The simulator, tree, cost model and shipped policies are fixed and oracle-covered. Log adapters, user-supplied cost constants and third-party policies are injected through existing interfaces, validated at the boundary, and explicitly outside oracle coverage — and the output says which numbers came from which.
  • Git hooks (core.hooksPath = .githooks): commit runs make check, push runs make verify. Committed, so they survive a fresh clone.
  • No emoji anywhere in the repository.

Every numeric claim in this file or in proofs/ traces to a committed script that regenerates it.


15. Claims discipline

This section exists so that nothing above it gets quoted out of context.

About the theory

  • No theorem in this repository has been proven. "Verified" means it survived the automated checks that exist. It never means "is true".
  • The positive result is human-gated. It may not be described as proven anywhere — including in this file — until a human mathematician has read it and agreed. That gate is not a formality: the specific failure mode it guards against is a confident, locally plausible, globally wrong amortised argument, and this project's contribution is exactly the kind of result that failure mode produces.
  • What may be claimed, once earned: a competitive-ratio guarantee for dependency-constrained caching with non-uniform sizes and costs. That qualifier is load-bearing and may not be dropped — an ICLR 2026 result already holds the unqualified ground for prefix-cache eviction under unit sizes and costs.
  • What may never be claimed: "provably optimal" (the offline optimum here is NP-hard and every online policy has a lower bound against it); "first cost-aware KV eviction" (RAGCache); "first leaf-constrained competitive analysis" (Bienkowski, Dallot); "first guarantee for a deployed policy" (nothing here is deployed); or any merging of Young's bound with Cao and Irani's, which are different theorems.

About the measurements

  • Numbers here are external ground truth, measurements of the input data, or measurements of code behaviour — never a project result dressed as a proof.
  • The cost coefficients are analytic, not measured on hardware. They come from a FLOPs derivation over published architecture constants. A GPU validation run is optional and gates nothing. No theorem depends on them; the advisor's output does, which is why both target models are reported.
  • The tool reports prefill FLOPs, not latency. Do not convert.
  • Constructed data is never presented as production data. synthetic_trace is reported and excluded from every verdict.
  • The measured policy margins are 0.5 to 1.8 percent of total cost, on two real traces from one source spanning one hour. That is why the tool leads with capacity.
  • Predicting your own negative results is a credibility signal. A pre-registered hypothesis was tested and found false (section 7.5); a pre-registered expectation about the size channel was reversed by the data (section 7.3); and one of two registered predictions was wrong and is reported as wrong.

Refused outright

Learned or predictive policies, which destroy the guarantee that is the point. Multi-replica and distributed caching. CPU and disk tiering. Online self-tuning.


License

The cache-worthy code is Apache-2.0 — see LICENSE, matching the declaration in pyproject.toml. The Mooncake trace data is separately licensed Apache-2.0 by its authors — see data/ATTRIBUTION.md for full provenance, including the privacy and representativeness caveats that apply to it.

About

A prefix-cache advisor for LLM serving infrastructure that recommends KV-cache capacity and eviction policies from your request traces/logs.

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages