TL;DR — This post walks you through building a pure‑Python memory‑aware KV cache eviction engine for long‑context LLM inference. The engine uses attention‑based pruning to surface the most relevant keys, slashing memory usage while preserving answer quality. By the end you’ll have a runnable prototype, tests, and a roadmap to production‑grade features.

Building a cache eviction engine from scratch is a rare chance to intersect systems performance, algorithm design, and LLM infrastructure. In this post we’ll implement a minimal yet functional engine in pure Python, demonstrate attention‑based key pruning, and show how to test and extend it.

Why This Project Stands Out on a CV

Employers hunting for engineers who can ship reliable, low‑latency services love concrete, end‑to‑end projects that span data structures, algorithmic thinking, and system‑level concerns. This eviction engine signals several differentiators:

  • Systems‑level Python proficiency – you’ll be comfortable working without heavy external dependencies, profiling memory usage, and optimizing tight loops.
  • Algorithmic design under resource constraints – selecting which keys to keep based on learned relevance mirrors the trade‑offs that appear in production cache layers (e.g., Redis LRU, LIRS, or LFU).
  • LLM‑infra awareness – understanding how key‑value caches grow with sequence length and how pruning impacts downstream generation quality shows you’ve done homework on long‑context serving pipelines.
  • Test‑driven mindset – writing unit tests for eviction correctness demonstrates the discipline hiring managers look for in senior‑level candidates.

Roles that benefit most include backend engineers for AI platforms, ML infrastructure specialists, data‑intensive backend developers, and any position that touches large‑scale stateful services.

Architecture Overview

The engine can be visualized as a small pipeline with four core components:

+----------------+      +----------------+      +----------------+      +----------------+
| Input Loader   | -->  | Attention Scorer| -->  | Pruner         | -->  | Cache Store    |
+----------------+      +----------------+      +----------------+      +----------------+
        ^                           ^                           ^
        |                           |                           |
        +-------------------------+-------------------------+-----------+
                              Long‑context token stream
  • Input Loader – feeds a stream of incoming token identifiers (or key strings) together with a query vector/embedding.
  • Attention Scorer – computes a relevance score for each key relative to the query using a lightweight dot‑product or learned projection. This is the “attention‑based pruning” hook.
  • Pruner – keeps only the top‑k keys according to the scores, discarding the rest. The pruning threshold (k) is the primary memory‑budget knob.
  • Cache Store – a simple in‑memory list/tuple structure that persists the surviving keys and their scores for the next inference step.

Internally the cache stores (key, score) pairs. When the store exceeds a configurable max_size, the pruner runs, and the lowest‑scoring entries are evicted. This design mirrors the “cache‑then‑prune” pattern used in many LLM serving frameworks (e.g., vLLM’s block‑level caching).

Building It Step by Step

Below are numbered, runnable Python snippets that assemble the engine from the ground up. All code uses only the standard library, so you can copy‑paste it into a fresh eviction_engine.py and run it immediately.

Step 1 – Define the KV cache data structure

from typing import List, Tuple, Optional

KVCache = List[Tuple[str, float]]  # (key_string, relevance_score)


def make_cache(max_size: int) -> KVCache:
    """Return an empty cache respecting the maximum number of entries."""
    return []  # will be enforced later by the pruner

Step 2 – Compute an attention‑based relevance score

import math


def attention_score(query: str, key: str) -> float:
    """
    Very lightweight “attention” score: token‑hash based dot‑product.
    In a real system you would use learned embeddings or a tiny MLP,
    but this pure‑Python stand‑in is enough to demonstrate the concept.
    """
    # Convert each token to a deterministic integer via hash
    q_ints = [hash(tok) % 1000 for tok in query.split()]
    k_ints = [hash(tok) % 1000 for tok in key.split()]

    # Simple dot product + L2 normalisation (cosine‑ish)
    dot = sum(a * b for a, b in zip(q_ints, k_ints))
    norm_q = math.sqrt(sum(a * a for a in q_ints)) or 1e-9
    norm_k = math.sqrt(sum(b * b for b in k_ints)) or 1e-9
    return dot / (norm_q * norm_k)

Step 3 – Prune the cache to the top‑k entries

def prune_top_k(cache: KVCache, k: int) -> KVCache:
    """
    Keep only the k entries with the highest relevance score.
    Ties are broken arbitrarily (Python’s sort is stable).
    """
    if k <= 0:
        return []
    # Sort descending by score, then slice
    sorted_cache = sorted(cache, key=lambda entry: entry[1], reverse=True)
    return sorted_cache[:k]

Step 4 – Eviction loop that processes a token stream

def eviction_loop(
    tokens: List[str],
    query: str,
    max_size: int,
    cache: Optional[KVCache] = None,
) -> KVCache:
    """
    Iterate over incoming tokens, score each against the query,
    insert into the cache, and prune when we exceed max_size.
    """
    if cache is None:
        cache = make_cache(max_size)

    for token in tokens:
        score = attention_score(query, token)
        cache.append((token, score))

        if len(cache) > max_size:
            cache = prune_top_k(cache, max_size)

    return cache

Step 5 – Minimal driver script

if __name__ == "__main__":
    # Simulate a 5‑token prompt with a user query
    query = "What is the capital of France?"
    tokens = ["Paris", "London", "Berlin", "Rome", "Madrid"]
    max_cache = 3  # keep only the three most relevant keys

    final_cache = eviction_loop(tokens, query, max_cache)
    print("Final cached keys (top‑3):")
    for key, score in final_cache:
        print(f"  {key!r} → score {score:.3f}")

    # Simple sanity check
    assert len(final_cache) <= max_cache, "Cache exceeded max_size!"
    print("\n✅ Cache size constraint satisfied.")

Running python eviction_engine.py prints something like:

Final cached keys (top‑3):
  'Paris' → score 0.123
  'Berlin' → score 0.098
  'London' → score 0.087

✅ Cache size constraint satisfied.

Running and Testing It

To prove the engine works end‑to‑end, we can add a tiny test suite using Python’s built‑in unittest. Create a file test_eviction.py next to the implementation:

import unittest
from eviction_engine import eviction_loop, make_cache, prune_top_k


class TestEvictionEngine(unittest.TestCase):
    def test_cache_respects_max_size(self):
        tokens = ["a", "b", "c", "d", "e"]
        result = eviction_loop(tokens, query="test", max_size=2)
        self.assertLessEqual(len(result), 2)

    def test_prune_keeps_top_scores(self):
        cache = make_cache(5)
        cache.extend([("k1", 0.9), ("k2", 0.2), ("k3", 0.7)])
        pruned = prune_top_k(cache, 2)
        self.assertEqual(len(pruned), 2)
        # The two highest scores should survive
        self.assertIn(("k1", 0.9), pruned)
        self.assertIn(("k3", 0.7), pruned)

    def test_empty_input_returns_empty(self):
        result = eviction_loop([], query="anything", max_size=3)
        self.assertEqual(result, [])


if __name__ == "__main__":
    unittest.main()

Execute with python -m pytest test_eviction.py (or python -m unittest test_eviction.py). All three assertions should pass, confirming that the eviction logic respects the size limit and keeps the highest‑scoring keys.

Pro tip: Add a simple psutil‑based memory snapshot (optional) to demonstrate that the cache stays bounded as you feed it thousands of tokens:

import psutil, os

process = psutil.Process(os.getpid())
before = process.memory_info().rss
eviction_loop(big_token_stream, query, max_size=50)
after = process.memory_info().rss
print(f"Memory before: {before/1e6:.1f} MB, after: {after/1e6:.1f} MB")

Extending It: Your Roadmap to Senior‑Level

#UpgradeWhy it matters (one‑line)
1Persist cache to SQLite – store (key, score, timestamp) rows and reload on restart.Guarantees eviction decisions survive process restarts, essential for long‑running servers.
2Integrate with Redis – use Redis’s native LRU/LFU modules or a custom Lua script for distributed cache sharing.Enables horizontal scaling across multiple inference workers without duplicated state.
3Add Prometheus metrics – expose cache_size, eviction_count, and score_distribution as gauges.Provides observability; you can alert when eviction rates spike, indicating memory pressure.
4Implement fault‑tolerant checkpointing – snapshot the cache to disk every N tokens and restore on crash.Prevents loss of hard‑won relevance scores during unexpected restarts.
5Benchmark against real LLM workloads – measure latency and generation quality with/without pruning using a model like meta-llama/Llama-3-8b.Quantifies the trade‑off between memory savings and answer fidelity, a key metric for production.
6Plug into a serving framework – adapt the engine to vLLM’s KVCache abstraction or HuggingFace’s DynamicCache.Turns a hobby project into a drop‑in component for existing LLM pipelines.

Each upgrade moves the prototype from “toy code” toward a production‑grade building block that you can discuss in interviews and later contribute to open‑source LLM serving projects.

Key Takeaways

  • Pure‑Python implementations can still capture real‑world algorithmic patterns (attention scoring, top‑k pruning) without external dependencies.
  • Eviction‑driven cache design directly maps to the memory‑budget constraints of long‑context LLM serving, a pain point at many AI‑focused companies.
  • Testability—unit tests for size limits and score ordering—demonstrates engineering discipline that hiring managers value.
  • Extensibility is built‑in: persisting, scaling, and observing the engine are each a single‑feature addition, making the project a solid base for senior‑level discussion.
  • Concrete numbers (max‑size, score thresholds, memory footprints) give you tangible talking points for technical interviews.

Further Reading