teaching_llm_applications

K-V Cache

image

    token 1
       โ†“
    token 2
       โ†“
    token 3
       โ†“
    token 4
       โ†“
      ...

KV cache grows โ†’

Concept ๐Ÿงฉ ๐Ÿš€

KV cache:

Memory โ†’ O(T)

Attention per new token:

Computation โ†’ O(T)

Total decode attention:

Computation โ†’ O(Tยฒ)

import numpy as np
import matplotlib.pyplot as plt

# Context lengths
tokens = np.arange(0, 100_001, 1_000)

# Assume one KV entry per token
kv_size = tokens

plt.figure(figsize=(8, 5))
plt.plot(tokens, kv_size)

plt.xlabel("Number of tokens in context")
plt.ylabel("Relative KV cache size")
plt.title("KV Cache Size Grows Linearly with Context Length")
plt.grid(True)

plt.show()

MGQ, GQA

MHA โ†’ very large GQA โ†’ much smaller MQA โ†’ dramatically smaller

Suppose we have:

32 attention heads

32 layers

head dimension 128

100,000 tokens

Compare:

Multi-Head Attention H KV =32 Grouped-Query Attention H KV =8 Multi-Query Attention HKV =1

models = {
    "MHA": 32,
    "GQA": 8,
    "MQA": 1
}

for name, kv_heads in models.items():

    memory = kv_cache_memory(
        layers=32,
        kv_heads=kv_heads,
        head_dim=128,
        sequence_length=100_000,
        bytes_per_element=2
    )

    print(
        name,
        f"{memory / 1e9:.2f} GB"
    )

Q1 โ†’ K1 V1 Q2 โ†’ K2 V2 Q3 โ†’ K3 V3 Q4 โ†’ K4 V4

GQA

Q1 โ”€โ” Q2 โ”€โ”ค โ”œโ†’ K1 V1 Q3 โ”€โ”ค Q4 โ”€โ”˜

MQA

Q1 โ”€โ” Q2 โ”€โ”ค Q3 โ”€โ”ผโ†’ K1 V1 Q4 โ”€โ”˜

Memory โ†’ O(T)

Attention per new token:

Computation โ†’ O(T)

Total decode attention:

Computation โ†’ O(Tยฒ)

Practical

Can we reproduce the intuition behind KV caching without running a 70-billion-parameter LLM?

import numpy as np

sequence_length = 10

# Simulate key and value vectors
keys = []
values = []

for t in range(sequence_length):

    key = np.random.randn(4)
    value = np.random.randn(4)

    keys.append(key)
    values.append(value)

    print(
        f"Token {t+1}: "
        f"KV cache contains {len(keys)} tokens"
    )
import numpy as np

def attention(query, keys, values):

    keys = np.array(keys)
    values = np.array(values)

    # Query-key similarity
    scores = keys @ query

    # Softmax
    weights = np.exp(scores)
    weights = weights / weights.sum()

    # Weighted sum of values
    output = weights @ values

    return output, weights

keys = []
values = []

for t in range(10):

    key = np.random.randn(4)
    value = np.random.randn(4)

    keys.append(key)
    values.append(value)

    query = np.random.randn(4)

    output, weights = attention(
        query,
        keys,
        values
    )

    print(
        f"Step {t+1}: "
        f"attending over {len(keys)} tokens"
    )

New query โ”‚ โ–ผ Compare against K1 K2 K3 โ€ฆ Kt โ”‚ โ–ผ Attention weights โ”‚ โ–ผ Retrieve from V1 V2 V3 โ€ฆ Vt

Discussion question

If the KV cache saves us from recomputing the past, why doesnโ€™t generation become constant-time?

Because the new token still has to attend over an increasingly large set of cached keys and values.

sparse attention

local attention

sliding-window attention

recurrent architectures

state-space models

linear attention

memory compression

retrieval-based architectures

recurrent memory

hierarchical memory

The KV cache is a compromise between remembering everything and recomputing everything.

WITHOUT KV CACHE

Long history โ†“ Repeated recomputation โ†“ Huge computation cost

WITH KV CACHE

Long history โ†“ Store K and V โ†“ Avoid recomputation โ†“ But cache grows โ†“ Every new query sees more history

THE FUNDAMENTAL TRADE-OFF

Memory โ†” Computation

blindly sending entire chat histories,

excessive RAG retrieval,

unnecessarily large system prompts,

conversation summarization,

context pruning,

caching strategies,

and choosing the right context window.

A good LLM application is not necessarily the one that gives the model the most context. It is the one that gives the model the most useful context per unit of compute and memory.

KV caching eliminates the need to recompute the past, but it does not eliminate the cost of having a large past.

๐ŸŽฎ Practicals