LLM KV Cache Eviction: Protect Latency Under Memory Pressure
Your GPU can have free compute and still stall because the KV cache is full. A practical eviction policy keeps long contexts from owning every byte.
An overloaded LLM server often runs out of memory before it runs out of math. Every active sequence holds key and value tensors for its previous tokens. Without an explicit LLM KV cache eviction policy, one long conversation can crowd out dozens of short requests and turn stable latency into a queue.
This tutorial builds the policy as a control system: measure pressure, classify cache blocks, choose victims, and verify that the cure does not create more recomputation than it saves.
Prerequisites
You should know basic transformer inference and have metrics for GPU memory, request state and token counts. The examples use Python-like pseudocode because vLLM, TensorRT-LLM and custom runtimes expose different hooks.
Before changing policy, record p50, p95 and p99 time-to-first-token, inter-token latency, cache hit rate, preemption count and tokens recomputed. A lower memory watermark is not a win if p99 latency doubles.
Step 1: calculate the real cache budget
Reserve memory for model weights, activations, CUDA graphs and a safety margin. The remainder is your usable KV budget.
usable_bytes = total_vram - weights - runtime_reserve - safety_margin
high_watermark = int(usable_bytes * 0.92)
low_watermark = int(usable_bytes * 0.82)
Use two thresholds. Eviction begins at the high watermark and stops below the low watermark. This hysteresis prevents the scheduler from evicting one block on every allocation.
Expected result: normal traffic stays below the high watermark; bursts cause one bounded cleanup cycle instead of continuous churn.
Step 2: track blocks, not whole conversations
Paged-attention runtimes divide KV data into blocks. Track each block’s bytes, last access, owning request, recomputation cost and whether it is shared by prefix caching.
class CacheBlock:
bytes: int
last_used_ms: int
owner: str
tokens: int
shared_refs: int
priority: int
Whole-sequence eviction is simple but wasteful. A 100,000-token chat may have a cold middle, a reusable system prefix and a hot recent window. Block metadata gives the scheduler options.
Step 3: score eviction candidates
Pure least-recently-used behavior is a reasonable baseline, but production workloads need cost awareness. Shared prefixes and expensive-to-recompute blocks should be harder to evict.
def victim_score(block, now_ms):
age = now_ms - block.last_used_ms
reuse_penalty = 4_000 * block.shared_refs
recompute_penalty = 2 * block.tokens
priority_penalty = 10_000 * block.priority
return age - reuse_penalty - recompute_penalty - priority_penalty
Evict the highest scores until memory falls below the low watermark. Keep priority coarse—perhaps zero for batch, one for interactive and two for protected system work. Too many tiers make capacity planning opaque.
Step 4: decide what preemption means
When an active request loses cache, you can recompute tokens later, swap blocks to CPU memory, or reject lower-priority work. Each option shifts cost.
- Recomputation avoids transfer overhead but consumes GPU time.
- CPU swap preserves work but can be limited by PCIe bandwidth and host memory.
- Admission control protects existing requests but produces explicit errors.
A sensible default is recomputation for short batch prompts, host swap for moderately sized paused requests, and admission control when both GPU and host watermarks are exceeded.
Step 5: protect the decoding set
Requests emitting tokens now are latency-sensitive. Prefer evicting queued or paused sequences before active decoders. Do not make the rule absolute: one runaway decoder with a huge context still needs a cap.
if request.state == "DECODING":
block.priority += 1
if request.context_tokens > tenant_limit:
block.priority -= 1
Per-tenant limits prevent a single customer from consuming the shared cache. Charge reserved cache bytes to the same quota system used for tokens or concurrent requests.
Step 6: test with adversarial traffic
Replay at least four shapes: many short chats, a few maximum-context requests, mixed batch and interactive traffic, and repeated shared prefixes. Increase arrival rate until the system crosses both watermarks.
Success means:
- no out-of-memory crashes;
- bounded preemption per minute;
- interactive p99 within the service objective;
- recomputed tokens remain an understood fraction of throughput;
- one tenant cannot starve others.
Common mistakes
Evicting shared prefixes too early
A prefix with many references may look old while still saving large amounts of prefill work. Count references and recent downstream hits.
Measuring only cache hit rate
A high hit rate can coexist with bad tail latency if large requests monopolize memory. Pair hit rate with bytes, recomputation and latency by request class.
Waiting for allocation failure
Emergency cleanup is already late. Start at a high watermark and keep a reserve for kernels and temporary buffers.
Ignoring cancellation
Disconnected clients and timed-out jobs must release blocks quickly. A cancellation leak looks exactly like organic memory pressure.
A practical starting policy
Start with paged blocks, high/low watermarks, LRU scoring, protection for active decoding and shared prefixes, and hard per-tenant context limits. Add swapping only after measuring transfer cost on your hardware.
The takeaway is simple: KV cache is a scheduled resource, not leftover memory. Treating eviction as an explicit policy turns overload from a GPU crash into a predictable tradeoff you can monitor and improve.
Sources
> Want more like this?
Get the best AI insights delivered weekly.
By subscribing, you agree to our Privacy Policy. You can unsubscribe at any time.
> Related Articles
Context Window Truncation Safeguards: Stop AI Agents From Forgetting Critical Instructions
Long-running agents eventually exceed a model's context window. Use explicit budgets, pinned invariants, summaries, and regression tests to prevent silent instruction loss.
MCP OAuth Security for AI Agents: Scope Tokens, Bind Resources, and Block Confused Deputies
Connecting agents to remote MCP servers expands the authorization surface. Use resource-bound tokens, PKCE, audience checks, and per-tool policy enforcement.
Structured Output Repair Loops: Recover Invalid LLM JSON Without Hiding Failures
Schema-constrained generation still fails at integrations and edge cases. Build bounded repair loops that preserve evidence, validate semantics, and never duplicate side effects.
Tags
> Stay in the loop
Weekly AI tools & insights.