Home / Articles

KV cache is the unseen database eating GPU memory in LLM decode

The KV cache quietly eats GPU memory during LLM inference: prefill vs decode, KV math, GQA and MQA, PagedAttention, quantization and eviction policies.

Publisher: REACTAPP.TOP9 min read2109 words

This article is published in English.

Sources: The KV Cache: AI’s Unseen Database Dominating GPU Memory

The real bottleneck in LLM inference

During inference, GPU compute is often idle while memory bandwidth feeds an ever-growing key–value cache. Every new token appends K/V tensors that must be reread on later steps. Deployments that ignore KV growth hit context and latency walls long before FLOPs run out.

Under the hood: how the KV cache works

Attention needs keys and values for every prior token. Caching them avoids recomputing the whole prefix each step. The tradeoff is memory linear in sequence length × layers × heads × dim.

Prefill vs decoding

Prefill processes the prompt and fills the cache. Decoding appends one token at a time, reading the full cache each step. Prefill is compute-heavy; decoding is bandwidth-heavy.

import torch
import torch.nn as nn

# Configuration representing a simplified LLM layer
batch_size = 1
num_heads = 8
head_dim = 64
hidden_dim = 512

# Projection layers for Keys and Values
W_k = nn.Linear(hidden_dim, num_heads * head_dim)
W_v = nn.Linear(hidden_dim, num_heads * head_dim)

# --- 1. PREFILL PHASE ---
# The prompt has 5 tokens. We process them simultaneously.
prompt_len = 5
prompt_embeddings = torch.randn(batch_size, prompt_len, hidden_dim)

# Project to K and V, then reshape to: [Batch, Heads, SeqLen, HeadDim]
kv_cache_k = W_k(prompt_embeddings).view(batch_size, prompt_len, num_heads, head_dim).transpose(1, 2)
kv_cache_v = W_v(prompt_embeddings).view(batch_size, prompt_len, num_heads, head_dim).transpose(1, 2)

print(f"Prefill complete. Cache K shape: {kv_cache_k.shape}")
# Output: [1, 8, 5, 64] (5 tokens cached)

# --- 2. DECODING PHASE ---
# We generate 3 subsequent tokens, one by one.
for step in range(3):
    # Input is only the single, newly generated token (SeqLen = 1)
    new_token_embedding = torch.randn(batch_size, 1, hidden_dim)

    # Project only the new token's K and V
    new_k = W_k(new_token_embedding).view(batch_size, 1, num_heads, head_dim).transpose(1, 2)
    new_v = W_v(new_token_embedding).view(batch_size, 1, num_heads, head_dim).transpose(1, 2)

    # Append the new token's representations to our historical cache
    # This prevents recalculating the past prompt_len + step tokens
    kv_cache_k = torch.cat([kv_cache_k, new_k], dim=2)
    kv_cache_v = torch.cat([kv_cache_v, new_v], dim=2)

    print(f"Decoding Step {step + 1} | Cache K shape: {kv_cache_k.shape}")
    # Step 1 Output: [1, 8, 6, 64]
    # Step 2 Output: [1, 8, 7, 64]
    # Step 3 Output: [1, 8, 8, 64]
def calculate_kv_cache_gb(batch_size: int, seq_len: int, layers: int, hidden_dim: int, precision_bytes: int = 2) -> float:
    # 2 represents the key and value states
    total_bytes = batch_size * seq_len * layers * hidden_dim * 2 * precision_bytes
    return total_bytes / (1024 ** 3)  # Convert to Gigabytes

# Llama 2 70B architecture parameters (assuming standard MHA)
llama_70b_gb = calculate_kv_cache_gb(
    batch_size=32,
    seq_len=8192,
    layers=80,
    hidden_dim=8192
)

print(f"KV Cache Size: {llama_70b_gb:.2f} GB")
# Output: KV Cache Size: 320.00 GB
+---------------------------------------------------------------+
|                      GPU VRAM (80 GB)                         |
+------------------------------+--------------------------------+
| Static Model Weights (Fixed) |  KV Cache (Dynamic / Variable) |
| [==========================] | [=======================>    ] |
| FP16 Weights (e.g., ~35GB/GPU| Grows linearly with:           |
| across split tensor setups)  | Batch Size * Sequence Length   |
+------------------------------+--------------------------------+
MHA (1:1 Ratio)       GQA (Grouped Ratio)     MQA (Many:1 Ratio)
[Q1][Q2][Q3][Q4]      [Q1][Q2]  [Q3][Q4]      [Q1][Q2][Q3][Q4]
  |   |   |   |         \   /     \   /         \   |   /   /
[K1][K2][K3][K4]        [ K1 ]   [ K2 ]          [   K1    ]
[V1][V2][V3][V4]        [ V1 ]   [ V2 ]          [   V1    ]
# A simple simulation of PagedAttention block mapping
class VirtualPageTable:
    def __init__(self, block_size: int):
        self.block_size = block_size
        self.logical_to_physical = {} # Maps (request_id, block_index) -> physical_block_id
        self.free_blocks = list(range(1000)) # Pool of physical memory blocks

    def allocate_block_for_token(self, request_id: int, token_index: int) -> int:
        # Determine which logical block the token falls into
        logical_block_idx = token_index // self.block_size
        key = (request_id, logical_block_idx)

        # Allocate a new physical block only when the previous one is full
        if key not in self.logical_to_physical:
            physical_block = self.free_blocks.pop(0)
            self.logical_to_physical[key] = physical_block

        return self.logical_to_physical[key]

# Instantiating a table with a block size of 4 tokens
table = VirtualPageTable(block_size=4)
# Token 5 belongs to logical block 1 (tokens 4-7). We allocate physical space on demand.
physical_id = table.allocate_block_for_token(request_id=42, token_index=5)
print(f"Logical token mapped to physical block: {physical_id}")
FP16/BF16 Value: [0.12345678]  (16 bits of precision)
     ↓ Quantization via Scaling Factor (e.g., scale = 127.0)
INT8 Value:      [15]          (8 bits of precision, 50% memory saved)
Sequence: [T1] [T2] [T3] [T4] [T5] [T6] [T7]
          └───── Window Size (4) ─────┘
          (T1 and T2 are evicted from active memory)
# Example: Initializing a vLLM engine with memory optimizations
from vllm import LLMEngine, EngineArgs

# Define engine arguments optimized for memory management
engine_args = EngineArgs(
    model="meta-llama/Llama-3-8B-Instruct",
    # 1. Quantization: Reduces KV cache footprint
    quantization="fp8",
    # 2. Paged KV Cache: Eliminates physical fragmentation
    gpu_memory_utilization=0.90,
    # 3. Dynamic Batching: Prevents static padding overhead
    max_num_seqs=256
)

# Initialize the engine with built-in memory guardrails
engine = LLMEngine.from_engine_args(engine_args)

Appending mechanics

Implementations store per-layer K/V buffers and write the new row each step. Bugs here corrupt attention silently—prefer battle-tested kernels.

The sobering memory math

Bytes ≈ 2 (K and V) × layers × kv_heads × head_dim × seq_len × bytes_per_element. Long context on large models can consume more memory than weights.

Optimization techniques

Architectural redesign: MHA, GQA, MQA

Grouped-query and multi-query attention shrink KV heads so the cache is smaller for the same sequence length.

PagedAttention

Treat KV like paged virtual memory: allocate blocks on demand, reduce fragmentation, enable higher batching.

KV quantization

Store cache entries at lower precision when quality budgets allow.

Sliding-window / eviction

Drop or compress distant tokens when the task tolerates limited attention span.

Production decision matrix

Pick GQA/MQA models when memory-bound; enable paged attention for high concurrency; quantize KV only after eval; use windows for chat with local context needs.

Closing

The KV cache is the unseen database of autoregressive decoding. Optimize it explicitly—architecture, paging, precision, eviction—or pay in GPUs and latency.

Measure tokens/second and KV bytes/token together. A “faster” kernel that doubles cache footprint can lose under multi-tenant load.

Document max context per SKU so product cannot silently request 128k on a card provisioned for 8k.

When batching, remember decode steps synchronize on the longest sequence; padding policy and continuous batching matter as much as raw bandwidth.

Measure tokens/second and KV bytes/token together. A “faster” kernel that doubles cache footprint can lose under multi-tenant load.

Document max context per SKU so product cannot silently request 128k on a card provisioned for 8k.

When batching, remember decode steps synchronize on the longest sequence; padding policy and continuous batching matter as much as raw bandwidth.

Measure tokens/second and KV bytes/token together. A “faster” kernel that doubles cache footprint can lose under multi-tenant load.

Document max context per SKU so product cannot silently request 128k on a card provisioned for 8k.

When batching, remember decode steps synchronize on the longest sequence; padding policy and continuous batching matter as much as raw bandwidth.

Measure tokens/second and KV bytes/token together. A “faster” kernel that doubles cache footprint can lose under multi-tenant load.

Document max context per SKU so product cannot silently request 128k on a card provisioned for 8k.

When batching, remember decode steps synchronize on the longest sequence; padding policy and continuous batching matter as much as raw bandwidth.

Measure tokens/second and KV bytes/token together. A “faster” kernel that doubles cache footprint can lose under multi-tenant load.

Document max context per SKU so product cannot silently request 128k on a card provisioned for 8k.

When batching, remember decode steps synchronize on the longest sequence; padding policy and continuous batching matter as much as raw bandwidth.

Measure tokens/second and KV bytes/token together. A “faster” kernel that doubles cache footprint can lose under multi-tenant load.

Document max context per SKU so product cannot silently request 128k on a card provisioned for 8k.

When batching, remember decode steps synchronize on the longest sequence; padding policy and continuous batching matter as much as raw bandwidth.

Measure tokens/second and KV bytes/token together. A “faster” kernel that doubles cache footprint can lose under multi-tenant load.

Document max context per SKU so product cannot silently request 128k on a card provisioned for 8k.

When batching, remember decode steps synchronize on the longest sequence; padding policy and continuous batching matter as much as raw bandwidth.

Measure tokens/second and KV bytes/token together. A “faster” kernel that doubles cache footprint can lose under multi-tenant load.

Document max context per SKU so product cannot silently request 128k on a card provisioned for 8k.

When batching, remember decode steps synchronize on the longest sequence; padding policy and continuous batching matter as much as raw bandwidth.

Measure tokens/second and KV bytes/token together. A “faster” kernel that doubles cache footprint can lose under multi-tenant load.

Document max context per SKU so product cannot silently request 128k on a card provisioned for 8k.

When batching, remember decode steps synchronize on the longest sequence; padding policy and continuous batching matter as much as raw bandwidth.

Measure tokens/second and KV bytes/token together. A “faster” kernel that doubles cache footprint can lose under multi-tenant load.

Document max context per SKU so product cannot silently request 128k on a card provisioned for 8k.

When batching, remember decode steps synchronize on the longest sequence; padding policy and continuous batching matter as much as raw bandwidth.

Measure tokens/second and KV bytes/token together. A “faster” kernel that doubles cache footprint can lose under multi-tenant load.

Document max context per SKU so product cannot silently request 128k on a card provisioned for 8k.

When batching, remember decode steps synchronize on the longest sequence; padding policy and continuous batching matter as much as raw bandwidth.

Measure tokens/second and KV bytes/token together. A “faster” kernel that doubles cache footprint can lose under multi-tenant load.

Document max context per SKU so product cannot silently request 128k on a card provisioned for 8k.

When batching, remember decode steps synchronize on the longest sequence; padding policy and continuous batching matter as much as raw bandwidth.

Measure tokens/second and KV bytes/token together. A “faster” kernel that doubles cache footprint can lose under multi-tenant load.

Document max context per SKU so product cannot silently request 128k on a card provisioned for 8k.

When batching, remember decode steps synchronize on the longest sequence; padding policy and continuous batching matter as much as raw bandwidth.

Measure tokens/second and KV bytes/token together. A “faster” kernel that doubles cache footprint can lose under multi-tenant load.

Prefill/decoding asymmetry also affects autoscaling: spikes in prompt length hammer prefill compute, while long generations hammer bandwidth. Size pools for both shapes.

Compare vendor claims on “long context” by asking for KV bytes at max length with your dtype and batch size—not marketing tokens.

Eviction policies must be product-aware: summarizing dropped spans beats silent forgetting for support agents that cite earlier turns.

Continuous batching schedulers that ignore KV fragmentation still OOM; paged layouts and defrag hooks belong in the same rollout.

Prefill/decoding asymmetry also affects autoscaling: spikes in prompt length hammer prefill compute, while long generations hammer bandwidth. Size pools for both shapes.

Compare vendor claims on “long context” by asking for KV bytes at max length with your dtype and batch size—not marketing tokens.

Eviction policies must be product-aware: summarizing dropped spans beats silent forgetting for support agents that cite earlier turns.

Continuous batching schedulers that ignore KV fragmentation still OOM; paged layouts and defrag hooks belong in the same rollout.

Prefill/decoding asymmetry also affects autoscaling: spikes in prompt length hammer prefill compute, while long generations hammer bandwidth. Size pools for both shapes.

Compare vendor claims on “long context” by asking for KV bytes at max length with your dtype and batch size—not marketing tokens.

Eviction policies must be product-aware: summarizing dropped spans beats silent forgetting for support agents that cite earlier turns.

Continuous batching schedulers that ignore KV fragmentation still OOM; paged layouts and defrag hooks belong in the same rollout.

Prefill/decoding asymmetry also affects autoscaling: spikes in prompt length hammer prefill compute, while long generations hammer bandwidth. Size pools for both shapes.

Compare vendor claims on “long context” by asking for KV bytes at max length with your dtype and batch size—not marketing tokens.

Eviction policies must be product-aware: summarizing dropped spans beats silent forgetting for support agents that cite earlier turns.