Heavy Hitter Oracle: Dynamic KV Cache Eviction Guide

The Problem: The Memory Wall and the Pitfalls of Blind Token Eviction
As Large Language Models (LLMs) scale to handle multi-document extraction, code repository synthesis, and multi-turn agent interactions in tools like AiFetchly, the Key-Value (KV) cache becomes the primary bottleneck in generative serving.
During autoregressive generation, storing Key and Value states across 32k, 64k, or 128k context lengths consumes massive GPU memory:
$$\text{KV Memory (Bytes)} = 2 \times 2 \times L \times H \times D \times T$$
For a 70B parameter model serving a 128k-token sequence in 16-bit precision, the KV cache alone consumes over 80 gigabytes of VRAM per concurrent user.
Why Simple Sliding Windows Break Down
To prevent out-of-memory (OOM) crashes, early long-context optimizations deployed sliding window attention or simple attention-sink buffers (e.g., StreamingLLM). These approaches maintain a fixed-size buffer by retaining the first few tokens and a rolling window of the most recent $W$ tokens, permanently discarding everything in between.
While sliding windows maintain basic linguistic fluency, they catastrophically fail on real-world reasoning tasks:
- Destruction of Associative Recall: Discarding middle tokens wipes out factual evidence, function declarations, user constraints, and data tables.
- 0% Needle-in-a-Haystack Accuracy: If an analytical query depends on a specific factual datum located in the middle of a 50,000-token document, sliding window eviction discards the needle before the model reaches the answer generation phase.
- Compound Reasoning Failures: Multi-step agentic workflows that branch across previous execution steps collapse because intermediate tool outputs and variables are prematurely evicted.
The Architecture: Power-Law Attention Sparsity and the H2O Algorithm
To compress the KV cache without losing historical reasoning capability, researchers introduced H2O (Heavy Hitter Oracle).
H2O is built on a fundamental empirical discovery: multi-head self-attention exhibits extreme power-law sparsity.
Across diverse LLM architectures, only a small fraction of historical tokens—termed Heavy Hitters ($H_2$)—consistently attract over 90% of the total cumulative attention weight during generation. The remaining 80% to 90% of tokens receive negligible attention and can be safely evicted with near-zero impact on downstream accuracy.

The Three Operational Mechanics of H2O
- Cumulative Attention Tracking: Rather than evaluating token importance at a single instant, H2O tracks the running cumulative attention score $s_j$ for every token $j$ in the sequence. At generation step $t$, as the current query token attends across all cached keys, the score accumulator updates: $$s_j^{(t)} = s_j^{(t-1)} + \sum_{h=1}^H A_{h, t, j}$$ Where $A_{h, t, j}$ is the attention probability assigned to token $j$ by attention head $h$. Tokens representing pivotal entities, prompt constraints, and structural syntax delimiters continuously accumulate high scores.
- Dual-Budget Cache Partitioning:
H2O dynamically divides the total KV cache memory budget $B$ into two complementary pools:
- Recent Local Window ($W_{\text{local}}$): Keeps the last $W$ tokens (e.g., 64 to 128 tokens) unconditionally in memory to ensure immediate conversational coherence and next-token fluency.
- Heavy Hitter Pool ($W_{\text{H2}}$): Retains the top-$K$ historical tokens with the highest cumulative scores $s_j$.
- Dynamic Eviction Policy: When the total token count exceeds budget $B$, H2O inspects the cumulative score accumulator. Tokens outside the local window with the lowest cumulative attention scores are permanently pruned from GPU VRAM, recycling memory pages while strictly preserving the structural $H_2$ backbone.
Comparing Long-Context KV Cache Strategies
| Strategy | Memory Footprint (128k Context) | Retention Mechanism | Needle Retrieval (NIAH) | PPL Preservation |
|---|---|---|---|---|
| Dense Full Cache | ~80 GB (100% Baseline) | Retains all tokens | 100% | 100% |
| Sliding Window (StreamingLLM) | ~4 GB (Fixed window) | First $N$ + Last $W$ tokens | < 5% (Fails on middle context) | High fluency, poor recall |
| KV Cache Quantization (KIVI) | ~10 GB – 20 GB (2-bit/4-bit) | Compresses values in-place | 98% – 100% | Lossless |
| Heavy Hitter Oracle (H2O) | ~12 GB – 16 GB (80% cut) | Top-$K$ $H_2$ + Local window | 95% – 99% | Lossless (< 0.1 PPL drop) |
Implementation: Building an H2O Dynamic KV Cache Manager in PyTorch
Let us implement a complete, modular Python implementation of an H2O KV cache manager demonstrating cumulative attention score tracking, dual-budget partitioning, and dynamic token eviction.
Step 1: Define the H2O Cache State and Token Storage
import torch
import dataclasses
from typing import List, Tuple, Optional
@dataclasses.dataclass
class H2OCacheConfig:
max_budget: int = 128 # Total allowable KV cache tokens per head
recent_window_size: int = 32 # Number of trailing tokens always preserved
num_heads: int = 4
head_dim: int = 64
class H2OKVCacheManager:
def __init__(self, config: H2OCacheConfig):
self.config = config
self.heavy_hitter_budget = config.max_budget - config.recent_window_size
# In-memory storage buffers: Shape [batch_size, num_heads, seq_len, head_dim]
self.cached_keys: Optional[torch.Tensor] = None
self.cached_values: Optional[torch.Tensor] = None
# Cumulative attention scores per token: Shape [batch_size, seq_len]
self.cumulative_scores: Optional[torch.Tensor] = None
self.token_indices: List[int] = [] # Original token position tracking Step 2: Implement Attention Score Accumulation and Dynamic Eviction
def step_and_evict(
self,
new_key: torch.Tensor, # [batch, heads, 1, dim]
new_value: torch.Tensor, # [batch, heads, 1, dim]
current_step_attention_probs: torch.Tensor # [batch, heads, 1, current_seq_len]
):
batch_size = new_key.size(0)
# 1. Append newly generated token representation to cache
if self.cached_keys is None:
self.cached_keys = new_key
self.cached_values = new_value
self.cumulative_scores = torch.zeros((batch_size, 1), device=new_key.device)
self.token_indices = [0]
return
else:
self.cached_keys = torch.cat([self.cached_keys, new_key], dim=2)
self.cached_values = torch.cat([self.cached_values, new_value], dim=2)
new_zero_score = torch.zeros((batch_size, 1), device=new_key.device)
self.cumulative_scores = torch.cat([self.cumulative_scores, new_zero_score], dim=1)
self.token_indices.append(len(self.token_indices))
# 2. Accumulate attention weights across heads
# Sum across heads: [batch, 1, current_seq_len] -> [batch, current_seq_len]
step_scores = current_step_attention_probs.sum(dim=1).squeeze(1)
self.cumulative_scores += step_scores
current_len = self.cached_keys.size(2)
# 3. Check if eviction is triggered
if current_len > self.config.max_budget:
self._evict_non_heavy_hitters()
def _evict_non_heavy_hitters(self):
current_len = self.cached_keys.size(2)
local_window_start = current_len - self.config.recent_window_size
# Split sequence into candidate historical tokens vs protected local window
candidate_scores = self.cumulative_scores[:, :local_window_start]
# Identify Top-K Heavy Hitters in the candidate pool
_, topk_relative_indices = torch.topk(
candidate_scores,
k=self.heavy_hitter_budget,
dim=-1,
largest=True
)
# Build index mask of tokens to keep: Top-K Heavy Hitters + Entire Local Window
keep_indices = topk_relative_indices[0].sort()[0] # For single-batch demonstration
local_indices = torch.arange(local_window_start, current_len, device=self.cached_keys.device)
surviving_indices = torch.cat([keep_indices, local_indices])
# Prune Tensors
self.cached_keys = self.cached_keys[:, :, surviving_indices, :]
self.cached_values = self.cached_values[:, :, surviving_indices, :]
self.cumulative_scores = self.cumulative_scores[:, surviving_indices]
self.token_indices = [self.token_indices[i] for i in surviving_indices.tolist()] Step 3: Run the Generative Simulation and Needle Retention Test
def run_h2o_simulation():
torch.manual_seed(42)
config = H2OCacheConfig(max_budget=80, recent_window_size=20, num_heads=2, head_dim=32)
manager = H2OKVCacheManager(config)
print(f"[H2O Simulation Start] Initializing Cache with Max Budget: {config.max_budget} tokens")
# Simulate generating 200 sequential tokens (Exceeding budget by 2.5x)
total_steps = 200
needle_position = 25 # Critical factual needle injected early in sequence
for step in range(total_steps):
new_k = torch.randn(1, config.num_heads, 1, config.head_dim)
new_v = torch.randn(1, config.num_heads, 1, config.head_dim)
# Generate mock attention probabilities
current_cache_size = step + 1 if manager.cached_keys is None else manager.cached_keys.size(2) + 1
probs = torch.rand(1, config.num_heads, 1, current_cache_size)
# If needle is in cache, give it consistently high attention weight (Heavy Hitter behavior)
if needle_position in manager.token_indices:
idx_in_cache = manager.token_indices.index(needle_position)
probs[:, :, :, idx_in_cache] += 5.0
# Normalize attention probabilities
probs = probs / probs.sum(dim=-1, keepdim=True)
manager.step_and_evict(new_k, new_v, probs)
print(f"\n[Generation Complete]")
print(f" -> Total Steps Executed: {total_steps} tokens")
print(f" -> Final Cached Tokens in GPU Memory: {manager.cached_keys.size(2)} (Strictly Capped at Budget)")
print(f" -> Total VRAM Reduction: {(1.0 - (manager.cached_keys.size(2) / total_steps)) * 100:.1f}%")
is_needle_retained = needle_position in manager.token_indices
print(f" -> Critical Needle (Token #{needle_position}) Retained: {is_needle_retained} (Heavy Hitter Preserved!)")
run_h2o_simulation() Production Serving and Deployment Trade-offs in 2026
When operating H2O dynamic eviction in production inference runtimes (such as vLLM, SGLang, and TensorRT-LLM):
- Layer-Specific Attention Concentration: Empirical profiling reveals that upper transformer layers exhibit far stronger attention sparsity than early layers. In production, engineering teams apply heterogeneous budgeting: shallow layers maintain larger local windows, while deeper reasoning layers apply aggressive 90% Heavy Hitter pruning.
- Block-Level Eviction in PagedAttention: Standard PagedAttention manages memory in discrete blocks (typically 16 or 32 tokens). Rather than evicting isolated individual tokens (which fragments memory pages), production frameworks like PagedEviction compute average block-level cumulative scores and evict whole physical pages at a time.
- Synergy with Low-Bit Quantization: H2O can be directly combined with KIVI 2-bit/4-bit KV quantization. Applying 4-bit quantization reduces memory per token by 4x, while H2O evicts 75% of tokens. Together, they achieve an astonishing 16x net reduction in KV cache memory, allowing single GPUs to serve 500k-token agent sessions effortlessly.
Conclusion
The Heavy Hitter Oracle demonstrates that infinite KV cache expansion is neither necessary nor optimal for generative intelligence. By tracking cumulative attention scores to distinguish high-impact tokens from ephemeral filler, H2O cuts long-context memory demands by 80% while safeguarding associative recall and multi-hop reasoning in production AI infrastructure.