← Writing

A rotating KV cache saves 36% of your memory and 100% of your recall

Capping a model’s KV cache instead of letting it grow without bound saves 36% of your peak memory at 32k context. It also drops recall of five facts I’d planted in the prompt from 5 out of 5 to 0 out of 5, the instant the conversation outgrows the cap. Not gradually. Not “loses one or two.” All five, every single trial, eight recorded runs with zero exceptions — eleven if you count the warmup passes too.

This is the follow-up to the KV-cache tax post — same model, same laptop, same four context lengths. That post asked what long context costs. This one asks what you get back if you refuse to pay the full price.

Two panels. Left: peak memory in GB for full KV cache vs a rotating window, both starting near 5.3GB but the full cache climbing to 9.3GB at 32k while the rotating window plateaus near 6GB. Right: facts recalled out of 5, the full cache holding at 5 across all lengths while the rotating window collapses to 0 the instant context crosses the configured window.

What the window buys and what it costs: peak memory plateaus at a fixed ceiling instead of growing with context (left) — while recall doesn’t degrade, it collapses from 5/5 to 0/5 the instant the window is crossed, with attention sinks providing no protection because the planted facts live in the document body, not the first four tokens (right).

The setup

Same Llama-3.1-8B, 4-bit, same M3 16 GB. Same growing document with five “access codes” and the same end-of-prompt question. The only change: instead of the default cache, which remembers every token, I built one that caps out at 4096 tokens — a RotatingKVCache, the same mechanism behind “sliding window attention” in a lot of production serving stacks. I also kept the first 4 tokens pinned regardless of window position, since that’s the standard trick (called an “attention sink”) for keeping eviction from making the model’s output degenerate into garbage.

One methodology note worth stating up front: MLX’s rotating cache enforces that 4096-token cap exactly during decode, one token at a time. During prefill — the initial pass that processes a long prompt in chunks rather than token by token — it can transiently hold more than the cap before trimming back down, since a whole chunk lands before the trim happens. With mlx-lm’s default prefill chunk size (2048 tokens), that transient peak works out to roughly 6,100 tokens’ worth of cache, not a hard 4,096. The cap is still what makes the memory story below true — it’s a fixed ceiling regardless of how long the conversation gets — the ceiling itself just sits a bit higher than the configured number alone would suggest.

contextfull KV peakwindowed peakfull KV recallwindowed recall
2k5.3 GB5.3 GB5/55/5
8k6.1 GB5.9 GB5/50/5
16k7.2 GB6.0 GB5/50/5
32k9.3 GB6.0 GB5/50/5

The memory story is exactly what you’d hope

Peak memory stops growing. Once context passes the window, the cache simply can’t get any bigger — old tokens fall off the back as new ones come in. At 32k that’s the difference between 9.3 GB and 6.0 GB, more than a third of the memory bill, gone. If all you cared about was “does it fit,” a rotating window is a clean win.

The recall story is not a curve, it’s a cliff

I expected some degradation — maybe the model starts losing the earliest fact first, then the next, a gentle slide as the window eats further into the document. That’s not what happened. Recall is 5/5 right up until the window is crossed, then it’s 0/5, and it stays 0/5 all the way out to 32k. There’s no partial credit. The facts are either in the window or they’re gone, and once the conversation has grown past it, they’re gone.

That makes sense once you think about why: a rotating cache keeps the most recent tokens. My facts are near the front of the document. The moment the document outgrows the window, the front — where the facts live — is exactly the part that’s been evicted. This isn’t a subtle failure mode. It’s the window doing precisely what it’s built to do, applied to content that happened to be in the wrong place.

The sink tokens don’t help, and that’s the actual finding

Here’s the part I didn’t expect going in. I kept the first 4 tokens pinned — the standard “attention sink” setup, the same default MLX’s own RotatingKVCache ships with. Attention sinks exist because dropping the very first tokens of a sequence can make eviction-based generation degenerate into repetition or garbage; keeping a small anchor at the start fixes that. It’s a real mechanism and it works for what it’s for.

It did nothing for recall here, because it was never going to. The sink protects positions 0 through 3. My facts aren’t at positions 0 through 3 — they’re scattered through the body of a document that’s thousands of tokens long. Attention sinks keep a model’s output coherent under eviction. They don’t keep the model’s memory of arbitrary content intact. Those are two different jobs, and it’s easy to read “keeps the model from breaking” as “keeps the model from forgetting” when they’re not the same claim. This data is what that difference looks like when you measure it instead of assuming it.

Why I care about this

Every serving stack that offers a memory-saving KV policy — sliding windows, StreamingLLM-style attention sinks, sequence-length caps — is making the same trade this experiment makes explicit: memory for retention. The sink-token default makes the trade look safer than it is, because it solves the visible failure mode (garbled output) and leaves the invisible one (silent factual amnesia) completely unaddressed. If you’re running long-context agents on constrained hardware and reaching for a rotating cache to make things fit, this is the cost you’re not being warned about by the fact that generation still looks fluent.

Caveats, read before you quote this

Code and raw data: github.com/robertlangdonn/kv-cache-tax — see the “Eviction leg” section of the README.

Subscribe