Decoder-Only Transformers & KV Cache

Derive causal attention masking, Root Mean Square Normalization, Rotary Position Embeddings (RoPE), KV Cache memory bottlenecks, and zero-dependency implementations in Python and Rust.

04. Generative AI, Large Language Models & Diffusion9 min read

While the original 2017 Transformer was an encoder-decoder architecture designed for machine translation, modern foundation Large Language Models (LLMs) like GPT-4, LLaMA-3, and Mistral are strictly Decoder-Only.

Decoder-only architectures optimize auto-regressive next-token prediction by combining Causal Self-Attention, RMSNorm, Rotary Position Embeddings (RoPE), and inference-time Key-Value (KV) Caching.

KV Cache SpeedupInference Optimization
O(T) vs O(T^2)

Caching key-value tensors eliminates redundant historical projections during auto-regressive generation.

1. Causal Masking & Pre-LN Transformer Blocks#

Auto-regressive language modeling factorizes the joint probability of token sequence X=(x1,…,xT)X = (x_1, \dots, x_T) into conditional next-token probabilities:

P(x1,x2,…,xT)=∏t=1TP(xt∣x1,…,xt−1)P(x_1, x_2, \dots, x_T) = \prod_{t=1}^T P(x_t | x_1, \dots, x_{t-1})
Architecture Diagram
Rendering diagram...

To prevent token ii from attending to future tokens j>ij > i during parallel training, an upper-triangular causal mask M∈RT×TM \in \mathbb{R}^{T \times T} is added before softmax:

Mi,j={0if j≤i−∞if j>iM_{i,j} = \begin{cases} 0 & \text{if } j \le i \\ -\infty & \text{if } j > i \end{cases}
CausalAttention(Q,K,V)=softmax(QKTdk+M)V\text{CausalAttention}(Q, K, V) = \text{softmax}\left( \frac{Q K^T}{\sqrt{d_k}} + M \right) V

Because exp⁡(−∞)=0\exp(-\infty) = 0, attention weights for all future positions evaluate strictly to zero.

Root Mean Square Normalization (RMSNorm)#

Zhang and Sennrich (2019) discovered that the scaling property of LayerNorm dominates its stabilizing effect, whereas mean-centering is computationally redundant. RMSNorm discards mean calculation:

RMS(x)=1d∑j=1dxj2+ϵ\text{RMS}(x) = \sqrt{\frac{1}{d} \sum_{j=1}^d x_j^2 + \epsilon}
xˉj=xjRMS(x)⋅γj\bar{x}_j = \frac{x_j}{\text{RMS}(x)} \cdot \gamma_j

Where γ∈Rd\gamma \in \mathbb{R}^d is a learnable gain parameter. RMSNorm reduces normalization runtime by 7% to 10% across deep architectures.

2. Rotary Position Embeddings (RoPE)#

Su et al. (2021) introduced Rotary Position Embeddings (RoPE) to encode relative positional geometry directly into Query and Key inner products.

We seek an embedding function f(x,m)f(x, m) such that the inner product between query qq at position mm and key kk at position nn depends purely on relative offset m−nm - n:

⟨f(q,m),f(k,n)⟩=g(q,k,m−n)\langle f(q, m), f(k, n) \rangle = g(q, k, m - n)
Architecture Diagram
Rendering diagram...

Derivation in the Complex 2D Plane#

Treating a 2D coordinate pair (q1,q2)(q_1, q_2) as a complex number z=q1+iq2∈Cz = q_1 + i q_2 \in \mathbb{C}:

Multiplying by eimθ=cos⁡(mθ)+isin⁡(mθ)e^{i m \theta} = \cos(m \theta) + i \sin(m \theta) rotates the vector by angle mθm \theta:

RΘ,m[q1q2]=[cos⁡(mθ)−sin⁡(mθ)sin⁡(mθ)cos⁡(mθ)][q1q2]R_{\Theta, m} \begin{bmatrix} q_1 \\ q_2 \end{bmatrix} = \begin{bmatrix} \cos(m \theta) & -\sin(m \theta) \\ \sin(m \theta) & \cos(m \theta) \end{bmatrix} \begin{bmatrix} q_1 \\ q_2 \end{bmatrix}

Now, compute the inner product of rotated query and rotated key:

⟨RΘ,mq,RΘ,nk⟩=(RΘ,mq)T(RΘ,nk)=qTRΘ,mTRΘ,nk\langle R_{\Theta, m} q, R_{\Theta, n} k \rangle = (R_{\Theta, m} q)^T (R_{\Theta, n} k) = q^T R_{\Theta, m}^T R_{\Theta, n} k

Because rotation matrices are orthogonal and commutative in 2D:

RΘ,mTRΘ,n=RΘ,−mRΘ,n=RΘ,n−mR_{\Theta, m}^T R_{\Theta, n} = R_{\Theta, -m} R_{\Theta, n} = R_{\Theta, n - m}

Therefore:

⟨RΘ,mq,RΘ,nk⟩=qTRΘ,n−mk\langle R_{\Theta, m} q, R_{\Theta, n} k \rangle = q^T R_{\Theta, n - m} k

The inner product is mathematically determined solely by relative distance m−nm - n, enabling natural length generalization.

3. KV Cache Architecture and Arithmetic Intensity#

During inference generation, tokens are emitted auto-regressively one token at a time.

Architecture Diagram
Rendering diagram...

The KV Cache Principle#

At timestep tt, historical tokens x1,…,xt−1x_1, \dots, x_{t-1} have already been processed. Their Key and Value projections do not change:

  1. Cache previous key tensors Kcache∈R(t−1)×dkK_{\text{cache}} \in \mathbb{R}^{(t-1) \times d_k} and value tensors Vcache∈R(t−1)×dvV_{\text{cache}} \in \mathbb{R}^{(t-1) \times d_v} in GPU memory.
  2. At step tt, compute Query qt∈R1×dkq_t \in \mathbb{R}^{1 \times d_k}, new Key kt∈R1×dkk_t \in \mathbb{R}^{1 \times d_k}, and new Value vt∈R1×dvv_t \in \mathbb{R}^{1 \times d_v} for the current token only.
  3. Append ktk_t to KcacheK_{\text{cache}} and vtv_t to VcacheV_{\text{cache}}.
  4. Compute attention vector:
Attentiont=softmax(qtK≤tTdk)V≤t∈R1×dv\text{Attention}_t = \text{softmax}\left( \frac{q_t K_{\le t}^T}{\sqrt{d_k}} \right) V_{\le t} \in \mathbb{R}^{1 \times d_v}

KV Cache Memory Footprint Formula#

For a model with LL layers, HH attention heads, head dimension dkd_k, context length TT, batch size BB, and 16-bit floating point precision (22 bytes):

MemoryKV=2×2×L×H×dk×T×Bbytes\text{Memory}_{\text{KV}} = 2 \times 2 \times L \times H \times d_k \times T \times B \quad \text{bytes}

For LLaMA-3-70B (L=80,H=64,dk=128L = 80, H = 64, d_k = 128) with context length T=8192T = 8192 and batch size B=4B = 4:

MemoryKV=4×80×64×128×8192×4≈85.9 GB\text{Memory}_{\text{KV}} = 4 \times 80 \times 64 \times 128 \times 8192 \times 4 \approx 85.9 \text{ GB}

This proves why LLM token generation is memory-bandwidth bound (high memory transfer, low arithmetic compute) rather than compute-bound.

4. From-Scratch Implementation: Zero Dependencies#

The following implementations construct an Autoregressive Causal Attention Layer with an Explicit KV Cache from scratch in pure Python and pure Rust.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import math from typing import List, Tuple, Optional class CausalKVCacheAttention: def __init__(self, d_model: int = 4, d_k: int = 2): self.d_model = d_model self.d_k = d_k self.scale = 1.0 / math.sqrt(d_k) # Cache storage for keys and values across timesteps self.k_cache: List[List[float]] = [] self.v_cache: List[List[float]] = [] def reset_cache(self): self.k_cache.clear() self.v_cache.clear() @staticmethod def _dot(a: List[float], b: List[float]) -> float: return sum(x * y for x, y in zip(a, b)) @staticmethod def _softmax(logits: List[float]) -> List[float]: max_val = max(logits) exps = [math.exp(x - max_val) for x in logits] sum_exp = sum(exps) return [e / sum_exp for e in exps] def forward_step( self, q_t: List[float], k_t: List[float], v_t: List[float] ) -> List[float]: # 1. Append current token key and value to cache self.k_cache.append(k_t) self.v_cache.append(v_t) seq_len = len(self.k_cache) # 2. Compute attention scores: q_t * K_cache^T / sqrt(d_k) scores = [ self._dot(q_t, self.k_cache[pos]) * self.scale for pos in range(seq_len) ] # 3. Softmax over all past positions (causal by construction) weights = self._softmax(scores) # 4. Multiply weights by V_cache output = [0.0] * len(v_t) for pos in range(seq_len): for col in range(len(v_t)): output[col] += weights[pos] * self.v_cache[pos][col] return output if __name__ == "__main__": attn = CausalKVCacheAttention(d_model=4, d_k=2) # Simulate 3 autoregressive generation steps steps = [ ([1.0, 0.5], [1.0, 0.5], [10.0, 1.0]), # Token 1 ([0.8, 1.2], [0.8, 1.2], [20.0, 2.0]), # Token 2 ([0.2, 0.9], [0.2, 0.9], [30.0, 3.0]), # Token 3 ] print("Autoregressive Generation with KV Cache:") for step_idx, (q, k, v) in enumerate(steps): out_t = attn.forward_step(q, k, v) print(f"Step {step_idx + 1} (Cache Size: {len(attn.k_cache)}):") print(" Emitted Representation:", [round(x, 4) for x in out_t])

5. Active Knowledge Checks#

Concept check

Why does Rotary Position Embedding (RoPE) guarantee that query-key attention scores depend purely on relative token distance m - n?

Concept check

What computational bottleneck shift occurs when deploying a KV Cache during Large Language Model token generation?