Navigation
🏭 Platform & Infrastructure· 10 min read

Caching Layers

Exact match, semantic, and KV prefix caching — three levels of cache that reduce LLM inference cost by 20-60%.

cachingsemantic-cacheprefix-cachekv-cachecost-reduction

LLM inference is expensive — $1-5 per million tokens. Caching avoids redundant computation by reusing results from identical or similar previous requests. Three cache layers stack together: exact match (cheapest), semantic (broader hits), and KV prefix (deepest, inside the engine).


1. Three Cache Levels

LevelWhat’s cachedHit conditionTypical hit rateSavings per hit
L1: Exact match(prompt hash → response)Identical prompt15-40% (FAQ, system prompts)Skip entire LLM call
L2: Semantic(similar prompt → response)Semantically equivalent prompt5-15% (paraphrased queries)Skip entire LLM call
L3: KV prefixKV cache blocks for shared prefixCommon prefix tokens30-80% (shared system prompt)Skip prefill computation
Request → L1 exact cache → L2 semantic cache → L3 KV prefix cache → Full inference
           15-40% hit        5-15% hit           30-80% partial hit

2. L1: Exact Match Cache

Cache the full response for identical prompts. The simplest and most reliable layer:

class ExactMatchCache:
    def __init__(self, redis: Redis, ttl=3600):
        self.redis = redis
        self.ttl = ttl
    
    def _cache_key(self, messages: list[dict], model: str, params: dict) -> str:
        """Hash the full request — messages + model + sampling params."""
        content = json.dumps({
            "messages": messages,
            "model": model,
            "temperature": params.get("temperature", 1.0),
            "max_tokens": params.get("max_tokens"),
        }, sort_keys=True)
        return f"llm:exact:{hashlib.sha256(content.encode()).hexdigest()}"
    
    async def get(self, messages, model, params) -> str | None:
        key = self._cache_key(messages, model, params)
        cached = await self.redis.get(key)
        if cached:
            metrics.increment("cache.exact.hit")
            return json.loads(cached)
        metrics.increment("cache.exact.miss")
        return None
    
    async def put(self, messages, model, params, response: str):
        key = self._cache_key(messages, model, params)
        await self.redis.setex(key, self.ttl, json.dumps(response))

When Exact Match Works Well

  • System prompts: Same system prompt across all users → cache warmed quickly
  • FAQ patterns: Customer support gets the same questions repeatedly
  • Structured queries: “Convert 5 USD to EUR” → deterministic answer
  • Temperature=0: Deterministic output makes caching valid

When It Doesn’t Work

  • Temperature > 0: Same prompt gives different outputs (cache returns stale variety)
  • Personalized context: Each user has unique context → no overlap
  • Long conversations: Multi-turn conversations rarely repeat exactly

3. L2: Semantic Cache

Match queries that are semantically equivalent even if worded differently:

class SemanticCache:
    def __init__(self, embedder, vector_db, similarity_threshold=0.95, ttl=3600):
        self.embedder = embedder    # e.g., all-MiniLM-L6-v2
        self.vector_db = vector_db  # e.g., Qdrant
        self.threshold = similarity_threshold
        self.ttl = ttl
    
    async def get(self, messages: list[dict], model: str) -> str | None:
        # Embed the user query (last user message)
        user_query = messages[-1]["content"]
        query_vector = self.embedder.encode(user_query)
        
        # Search for similar cached queries
        results = self.vector_db.search(
            query_vector,
            filter={"model": model},
            top_k=1,
        )
        
        if results and results[0].score >= self.threshold:
            metrics.increment("cache.semantic.hit")
            return results[0].payload["response"]
        
        metrics.increment("cache.semantic.miss")
        return None
    
    async def put(self, messages, model, response: str):
        user_query = messages[-1]["content"]
        query_vector = self.embedder.encode(user_query)
        
        self.vector_db.upsert(
            vector=query_vector,
            payload={
                "query": user_query,
                "model": model,
                "response": response,
                "created_at": time.time(),
            }
        )

Similarity Threshold Tuning

ThresholdBehaviorRisk
0.99Almost exact match onlyVery few hits, safe
0.95Close paraphrases matchedGood balance
0.90Broader matchingSome wrong cache returns
0.85AggressiveHigh risk of returning wrong answer

Danger: “How do I delete my account?” and “How do I create an account?” may have 0.92 similarity but completely different answers. The semantic cache can return catastrophically wrong results.

Mitigation: Use semantic cache only for narrow, well-defined domains where similar queries have similar answers (e.g., product documentation lookup).


4. L3: KV Prefix Cache

The deepest optimization — share computed KV cache blocks across requests with common prefixes. This happens inside the inference engine:

Request A: [system prompt: 2000 tokens] + [user A: 50 tokens]
Request B: [system prompt: 2000 tokens] + [user B: 80 tokens]
                                ↑
          KV cache for 2000-token prefix computed ONCE, shared

Prefill savings: 2000 tokens of KV computation saved per request
At 1000 req/s: 2M tokens/s of prefill avoided

How It Works (Engine Level)

# In vLLM/SGLang, prefix caching is automatic:
# 1. Engine hashes token blocks
# 2. On new request, check if prefix blocks already exist
# 3. If yes, skip prefill for cached portion
# 4. Only compute attention for new (uncached) tokens

# vLLM configuration
llm = LLM(
    model="meta-llama/Llama-3.1-70B",
    enable_prefix_caching=True,   # Enable automatic prefix sharing
)

# SGLang: RadixAttention (tree-based prefix caching)
# Automatically shares prefixes across all requests in a tree structure

For detailed prefix caching internals, see Prefix Caching.


5. Cache Invalidation

The hardest problem in caching — when should cached responses be invalidated?

TriggerWhat to invalidateMethod
Model updateAll responses for that modelFlush by model version tag
System prompt changeAll responses with old promptInclude prompt hash in cache key
TTL expiryStale entriesAutomatic (Redis TTL, vector DB TTL)
Quality issue detectedSpecific bad responsesManual purge + quality monitoring
RAG index updateResponses using outdated contextTag cache entries with index version
async def invalidate_on_model_update(model_name: str, new_version: str):
    """Flush all cache entries for a model when it's updated."""
    # Exact cache: delete by pattern
    keys = await redis.keys(f"llm:exact:*:{model_name}:*")
    if keys:
        await redis.delete(*keys)
    
    # Semantic cache: delete by metadata filter
    vector_db.delete(filter={"model": model_name})
    
    logger.info(f"Invalidated cache for {model_name} v{new_version}")

6. Cache Economics

# Cost-benefit analysis
def cache_savings(
    requests_per_day: int,
    avg_tokens_per_request: int,
    cost_per_m_tokens: float,
    hit_rate: float,
    cache_cost_per_day: float,  # Redis/vector DB cost
) -> dict:
    total_tokens = requests_per_day * avg_tokens_per_request
    inference_cost = total_tokens / 1_000_000 * cost_per_m_tokens
    
    saved = inference_cost * hit_rate
    net_savings = saved - cache_cost_per_day
    
    return {
        "daily_inference_cost_without_cache": inference_cost,
        "daily_savings": saved,
        "cache_cost": cache_cost_per_day,
        "net_daily_savings": net_savings,
        "roi": net_savings / cache_cost_per_day if cache_cost_per_day > 0 else float('inf'),
    }

# Example: 1M requests/day, 500 tokens avg, $1/M tokens, 30% hit rate
result = cache_savings(1_000_000, 500, 1.0, 0.30, 50.0)
# daily_inference_cost: $500
# daily_savings: $150
# cache_cost: $50 (Redis)
# net_daily_savings: $100
# ROI: 2.0× (200% return on cache investment)

Self-Check

  • Compare exact match, semantic, and KV prefix caching on hit rate, risk, and savings
  • Implement an exact match cache with model version in the cache key
  • Explain the danger of semantic caching and how threshold tuning mitigates it
  • Design a cache invalidation strategy for model updates and RAG index changes
  • Calculate the ROI of caching for a given request volume and hit rate

References