Prompt caching is a crucial optimization technique for AI applications leveraging Large Language Models (LLMs), significantly reducing inference costs and latency. This article explores two primary architectural approaches: provider-native model-level caching and application-level caching, detailing their mechanisms, storage considerations, and implementation best practices. It emphasizes how strategic prompt design and appropriate cache storage solutions enhance efficiency and user experience in generative AI systems.
Read original on DZone MicroservicesAs Large Language Models (LLMs) become central to enterprise applications, managing their operational costs and ensuring low latency are paramount. Prompt caching addresses these challenges by reusing previously processed segments of prompts. This technique avoids redundant computation for identical or semantically similar prompt prefixes, leading to faster inference, reduced API costs, and improved application responsiveness. It is especially beneficial in scenarios with repetitive system instructions or recurring contextual information, such as AI assistants, chatbots, and RAG systems.
Prompt caching can be implemented at different layers within an LLM-powered application's architecture, each with distinct characteristics regarding where the cache resides, what it stores, and who manages it.
Many LLM providers (e.g., OpenAI, Anthropic, Google) offer built-in prompt caching. At this level, the cache is managed directly within the provider's cloud infrastructure. It stores Key-Value (KV) Tensors, which represent the raw, mathematical attention states calculated during the 'prefill' phase of a prompt. These tensors reside in high-speed volatile memory like GPU VRAM or ultra-fast host system RAM for instant access and minimal latency. Providers employ advanced, proprietary cache-eviction systems (e.g., based on Time-to-Live) to manage the constrained and expensive GPU memory. Users typically have no direct access to inspect or manage these cached tensors; the system automatically applies discounts if a match is found during API calls.
For greater control and additional cost savings, developers can implement their own caching layer in front of LLM APIs. This allows for bypassing the LLM entirely for repeat queries and offers flexibility in storage solutions:
A semantic cache extends traditional exact-match caching by using vector-based similarity. When a new prompt arrives, it's converted into an embedding. A vector database (like Redis Stack) then searches for the 'nearest neighbor' (most similar prompt) among stored embeddings. If the similarity score exceeds a defined threshold, a cache hit occurs, returning the cached response. Otherwise, the prompt is sent to the LLM, and the new prompt and response are stored in the cache.
import redis
import numpy as np
from openai import OpenAI
from redis.commands.search.query import Query
# 1. Initialize Clients
redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
openai_client = OpenAI(api_key="YOUR_API_KEY")
# Configuration
THRESHOLD = 0.95 # 95% similarity required for a cache hit
INDEX_NAME = "prompt_cache_idx"
def get_embedding(text):
"""Convert text to an embedding vector."""
response = openai_client.embeddings.create(
input=text,
model="text-embedding-3-small"
)
return np.array(response.data[0].embedding, dtype=np.float32).tobytes()
def check_semantic_cache(prompt_text):
"""Search Redis for a semantically similar prompt."""
query_vector = get_embedding(prompt_text)
# Construct a KNN Vector Search Query in Redis
q = Query(f"*=>[KNN 1 @prompt_vector $vec AS score]")\
.return_fields("response", "score")\
.sort_by("score")\
.dialect(2)
res = redis_client.ft(INDEX_NAME).search(
q,
query_params={"vec": query_vector}
)
if res.docs:
# Redis returns distance (0 is perfect match). Convert to similarity.
similarity = 1 - float(res.docs[0].score)
if similarity >= THRESHOLD:
print(f"✅ Cache Hit! (Similarity: {similarity:.2f})")
return res.docs[0].response
print("❌ Cache Miss.")
return None
def store_in_cache(prompt_text, llm_response):
"""Store the new prompt and response in Redis."""
prompt_vector = get_embedding(prompt_text)
# Store as a Redis Hash
doc_id = f"cache:{hash(prompt_text)}"
redis_client.hset(doc_id, mapping={
"prompt": prompt_text,
"response": llm_response,
"prompt_vector": prompt_vector
})
# Optional: Set a Time-To-Live (TTL) so the cache clears old entries
redis_client.expire(doc_id, 86400) # 24 hours