Quiz 2

Self-Attention and QKV Computation

2409 words
12 min read
Python Week 1: the first filter for runtime behavior
Visual companion
Python
Type and operator map

Python Week 1: the first filter for runtime behavior

View
Revision summary

What this note is really saying

Short form

# Self-Attention and QKV Computation ## 🎯 Learning Objectives - Explain why Q, K, V matrices are necessary and how they differ - Derive Q, K, V from input embeddings through linear projections - Interpret attention weight matrices and visualize attention patterns - Distinguish between self-attention, cross-attentio...

Self-Attention and QKV Computation

🎯 Learning Objectives

  • Explain why Q, K, V matrices are necessary and how they differ
  • Derive Q, K, V from input embeddings through linear projections
  • Interpret attention weight matrices and visualize attention patterns
  • Distinguish between self-attention, cross-attention, and bidirectional attention
  • Compute attention outputs manually for small examples

📋 Prerequisites

  • Transformer Architecture (previous topic) — high-level understanding of the architecture
  • Matrix multiplication — required for QKV projections
  • Softmax function — normalizing attention weights

1. 📖 Core Content

1.1 Intuition: Why Q, K, V?

The Query-Key-Value (QKV) framework is borrowed from information retrieval systems. Imagine searching for a document in a database:
  • Query (Q): Your search query — "What I'm looking for"
  • Key (K): The titles/index of each document — "What each document is about"
  • Value (V): The actual content of each document — "The information itself"
  • Attention: The process of matching your query against all keys to retrieve relevant values In self-attention, every token simultaneously acts as a query, a key, and a value. Each token asks: "What should I pay attention to?" (Query), each token announces: "Here's what I contain" (Key), and each token offers: "Here's my contribution" (Value). Why can't we use the embeddings directly as Q, K, V? Because the model needs to learn different representations for these three roles. The same word used as a query might need different features than when used as a key.

1.2 Linear Projections: Computing Q, K, V

Given input embeddings XRn×dmodelX \in \mathbb{R}^{n \times d_{model}}:
Q=XWQwhereWQRdmodel×dkQ = X \cdot W^Q \quad \text{where} \quad W^Q \in \mathbb{R}^{d_{model} \times d_k} K=XWKwhereWKRdmodel×dkK = X \cdot W^K \quad \text{where} \quad W^K \in \mathbb{R}^{d_{model} \times d_k} V=XWVwhereWVRdmodel×dvV = X \cdot W^V \quad \text{where} \quad W^V \in \mathbb{R}^{d_{model} \times d_v}
Key points:
  • WQW^Q, WKW^K, WVW^V are learned weight matrices (different for each head)
  • dkd_k is typically dmodel/hd_{model} / h (e.g., 512/8 = 64)
  • In the original Transformer, dk=dvd_k = d_v (both = 64 per head)

1.2.1 Worked Example: QKV Projection

Let's trace QKV computation for a tiny example. Input: 3 tokens, d_model = 4
X=[101001011100]X = \begin{bmatrix} 1 & 0 & 1 & 0 \\ 0 & 1 & 0 & 1 \\ 1 & 1 & 0 & 0 \end{bmatrix}
Weight matrices (randomly initialized, d_k = 2):
WQ=[0.10.20.30.40.50.60.70.8]WK=[0.20.10.40.30.60.50.80.7]WV=[0.50.50.10.90.80.20.30.7]W^Q = \begin{bmatrix} 0.1 & 0.2 \\ 0.3 & 0.4 \\ 0.5 & 0.6 \\ 0.7 & 0.8 \end{bmatrix} \quad W^K = \begin{bmatrix} 0.2 & 0.1 \\ 0.4 & 0.3 \\ 0.6 & 0.5 \\ 0.8 & 0.7 \end{bmatrix} \quad W^V = \begin{bmatrix} 0.5 & 0.5 \\ 0.1 & 0.9 \\ 0.8 & 0.2 \\ 0.3 & 0.7 \end{bmatrix}
Compute Q:
Q=XWQ=[10.1+00.3+10.5+00.710.2+00.4+10.6+00.800.1+10.3+00.5+10.700.2+10.4+00.6+10.810.1+10.3+00.5+00.710.2+10.4+00.6+00.8]Q = X \cdot W^Q = \begin{bmatrix} 1\cdot0.1 + 0\cdot0.3 + 1\cdot0.5 + 0\cdot0.7 & 1\cdot0.2 + 0\cdot0.4 + 1\cdot0.6 + 0\cdot0.8 \\ 0\cdot0.1 + 1\cdot0.3 + 0\cdot0.5 + 1\cdot0.7 & 0\cdot0.2 + 1\cdot0.4 + 0\cdot0.6 + 1\cdot0.8 \\ 1\cdot0.1 + 1\cdot0.3 + 0\cdot0.5 + 0\cdot0.7 & 1\cdot0.2 + 1\cdot0.4 + 0\cdot0.6 + 0\cdot0.8 \end{bmatrix} Q=[0.60.81.01.20.40.6]Q = \begin{bmatrix} 0.6 & 0.8 \\ 1.0 & 1.2 \\ 0.4 & 0.6 \end{bmatrix}
Compute K:
K=XWK=[10.2+00.4+10.6+00.810.1+00.3+10.5+00.700.2+10.4+00.6+10.800.1+10.3+00.5+10.710.2+10.4+00.6+00.810.1+10.3+00.5+00.7]K = X \cdot W^K = \begin{bmatrix} 1\cdot0.2 + 0\cdot0.4 + 1\cdot0.6 + 0\cdot0.8 & 1\cdot0.1 + 0\cdot0.3 + 1\cdot0.5 + 0\cdot0.7 \\ 0\cdot0.2 + 1\cdot0.4 + 0\cdot0.6 + 1\cdot0.8 & 0\cdot0.1 + 1\cdot0.3 + 0\cdot0.5 + 1\cdot0.7 \\ 1\cdot0.2 + 1\cdot0.4 + 0\cdot0.6 + 0\cdot0.8 & 1\cdot0.1 + 1\cdot0.3 + 0\cdot0.5 + 0\cdot0.7 \end{bmatrix} K=[0.80.61.21.00.60.4]K = \begin{bmatrix} 0.8 & 0.6 \\ 1.2 & 1.0 \\ 0.6 & 0.4 \end{bmatrix}
Compute V:
V=XWV=[10.5+00.1+10.8+00.310.5+00.9+10.2+00.700.5+10.1+00.8+10.300.5+10.9+00.2+10.710.5+10.1+00.8+00.310.5+10.9+00.2+00.7]V = X \cdot W^V = \begin{bmatrix} 1\cdot0.5 + 0\cdot0.1 + 1\cdot0.8 + 0\cdot0.3 & 1\cdot0.5 + 0\cdot0.9 + 1\cdot0.2 + 0\cdot0.7 \\ 0\cdot0.5 + 1\cdot0.1 + 0\cdot0.8 + 1\cdot0.3 & 0\cdot0.5 + 1\cdot0.9 + 0\cdot0.2 + 1\cdot0.7 \\ 1\cdot0.5 + 1\cdot0.1 + 0\cdot0.8 + 0\cdot0.3 & 1\cdot0.5 + 1\cdot0.9 + 0\cdot0.2 + 0\cdot0.7 \end{bmatrix} V=[1.30.70.41.60.61.4]V = \begin{bmatrix} 1.3 & 0.7 \\ 0.4 & 1.6 \\ 0.6 & 1.4 \end{bmatrix}
Now attention can be computed: Attention(Q,K,V)=softmax(QKT/2)V\text{Attention}(Q, K, V) = \text{softmax}(QK^T/\sqrt{2})V

1.3 Self-Attention vs Cross-Attention

(Diagram)
TypeQ SourceK SourceV SourceUsed In
Self-attentionSame sequenceSame sequenceSame sequenceEncoder & Decoder
Cross-attentionDecoderEncoderEncoderDecoder only
Masked self-attentionSame sequence (masked)Same sequenceSame sequenceDecoder (first sublayer)

1.4 Attention Pattern Analysis

Different attention heads learn different patterns. Common patterns include:
  1. Diagonal attention: Tokens attend mostly to themselves and immediate neighbors
  2. Vertical attention: Certain tokens (like [CLS] or [SEP] in BERT) attend to many tokens
  3. Block diagonal: Within-phrase attention (tokens attend within their own clause)
  4. Long-range: Attending to distant but semantically related tokens

1.4.1 Visualizing Attention Patterns

python
# runnable
import numpy as np
import matplotlib.pyplot as plt
def visualize_attention(attention_weights, tokens):
    """
    Visualize attention weights as a heatmap
    Args:
        attention_weights: (n_tokens, n_tokens) matrix
        tokens: List of token strings
    """
    fig, ax = plt.subplots(figsize=(8, 6))
    im = ax.imshow(attention_weights, cmap='Blues')
    # Labels
    ax.set_xticks(range(len(tokens)))
    ax.set_yticks(range(len(tokens)))
    ax.set_xticklabels(tokens, rotation=45)
    ax.set_yticklabels(tokens)
    ax.set_xlabel('Keys')
    ax.set_ylabel('Queries')
    # Colorbar
    plt.colorbar(im)
    plt.title('Attention Weights')
    plt.tight_layout()
    return fig
# Example: Sentence with self-attention pattern
tokens = ['The', 'cat', 'sat', 'on', 'the', 'mat']
# Simulated attention: diagonal + long-range (cat→mat)
np.random.seed(42)
attn = np.random.rand(6, 6)
# Make it more interpretable
attn = attn / attn.sum(axis=-1, keepdims=True)  # Normalize rows
print("Simulated attention weights (6 tokens):")
print(np.round(attn, 2))
# Each row sums to 1.0
print("\nRow sums (should be all 1.0):", attn.sum(axis=-1))

1.5 Attention in Encoder vs Decoder

Encoder Self-Attention

  • Bidirectional: Each token can attend to ALL tokens (before and after)
  • No masking needed
  • Produces contextualized representations of the input

Decoder Masked Self-Attention

  • Unidirectional: Each token can only attend to itself and previous tokens
  • Uses a causal mask (upper triangular matrix of -\infty)
  • Prevents "cheating" during generation

Decoder Cross-Attention

  • Q comes from decoder, K and V come from encoder
  • Each decoder token can attend to all encoder tokens
  • Allows the decoder to "look at" the input

1.5.1 Causal Masking

python
# runnable
import numpy as np
def create_causal_mask(seq_len):
    """
    Create a causal attention mask (lower triangular)
    """
    mask = np.triu(np.ones((seq_len, seq_len)) * -1e9, k=1)
    return mask
seq_len = 5
mask = create_causal_mask(seq_len)
print("Causal Mask (showing -1e9 as -inf):")
print(np.where(mask < -1e8, float('-inf'), 0))
Effect: When computing attention for position 3, it can only attend to positions 0, 1, 2, and 3. Future tokens (4, 5+) have attention weights of 0.

1.5.2 Worked Example: Masked Attention

Let's trace attention with masking for 3 tokens. Scores (before masking):
S=[0.50.30.20.10.80.10.40.30.3]S = \begin{bmatrix} 0.5 & 0.3 & 0.2 \\ 0.1 & 0.8 & 0.1 \\ 0.4 & 0.3 & 0.3 \end{bmatrix}
Causal mask (upper triangular filled with -\infty):
M=[000000]M = \begin{bmatrix} 0 & -\infty & -\infty \\ 0 & 0 & -\infty \\ 0 & 0 & 0 \end{bmatrix}
Scores + Mask:
S+M=[0.50.10.80.40.30.3]S + M = \begin{bmatrix} 0.5 & -\infty & -\infty \\ 0.1 & 0.8 & -\infty \\ 0.4 & 0.3 & 0.3 \end{bmatrix}
After softmax (row-wise):
softmax(S+M)=[1.00.00.00.330.670.00.370.330.30]\text{softmax}(S+M) = \begin{bmatrix} 1.0 & 0.0 & 0.0 \\ 0.33 & 0.67 & 0.0 \\ 0.37 & 0.33 & 0.30 \end{bmatrix}
Token 0 only attends to itself. Token 1 attends to token 0 and itself. Token 2 attends to all three.

1.6 Attention Output Interpretation

The output of self-attention is a contextualized representation of each token. Unlike the input embeddings (which are fixed for each word regardless of context), the output embeddings change based on the surrounding words. Example: The word "bank" in two different sentences:
  • "I went to the bank to deposit money" → "bank" attends to "deposit", "money" → financial institution
  • "I sat on the river bank" → "bank" attends to "river" → river bank The same word gets different representations based on what it attends to.

1.7 Why This Matters

The QKV mechanism is the core innovation that makes Transformers work. Understanding it is essential for:
  • Debugging: When attention patterns go wrong, model behavior becomes unpredictable
  • Interpretability: Attention weights can be extracted to understand what the model focuses on
  • Architecture design: Many improvements (sparse attention, linear attention, Flash Attention) are modifications to the QKV computation
  • Fine-tuning: Methods like PEFT modify how attention is computed

4. 📐 Key Formulas / Concepts

ConceptFormulaPurpose
Query projectionQ=XWQQ = XW^QWhat each token is looking for
Key projectionK=XWKK = XW^KWhat each token contains
Value projectionV=XWVV = XW^VWhat each token contributes
Attention scoreS=QKTS = QK^TCompatibility between Q and K
Scaled attentionS/dkS/\sqrt{d_k}Prevent softmax saturation
Attention weightssoftmax(S/dk)\text{softmax}(S/\sqrt{d_k})Normalized attention distribution
Context outputAVAVWeighted sum of values
Causal maskMij=0M_{ij} = 0 if iji \ge j , else -\inftyPrevent future token access

5. ⚠️ Common Pitfalls

Pitfall 1: Thinking Q, K, V are arbitrary names

The mistake: Assuming Q, K, V are interchangeable or arbitrary labels. Why it happens: The same token generates all three, so they seem symmetric. Correction: Q, K, V roles are fundamentally different. Q is "what I need," K is "what I offer," V is "what I contribute." The learned weight matrices are different for each projection, and they encode different information. In cross-attention, Q comes from a different source than K and V.

Pitfall 2: Confusing attention dimension with model dimension

The mistake: Setting d_k = d_model and wondering why attention is slow. Why it happens: Many explanations just show d_model without explaining per-head dimensions. Correction: In multi-head attention, d_k = d_model / h. For d_model=512, h=8, d_k=64. Each head operates in a 64-dimensional space, not 512-dimensional. This is what makes multi-head attention computationally feasible.

Pitfall 3: Assuming attention weights sum to 1.0 for each column

The mistake: Thinking iAij=1.0\sum_i A_{ij} = 1.0 (columns sum to 1) instead of rows. Why it happens: Softmax is applied along each row, so each query's attention sums to 1, not each key's. Correction:
  • Row i (query i): jAij=1.0\sum_j A_{ij} = 1.0
  • Column j (key j): iAij\sum_i A_{ij} can be anything
  • Each query distributes 1 unit of attention across all keys

6. 📝 Practice Questions

Q1: What is the shape of Q, K, V for a batch of 8 sequences, each of length 20, d_model=768, and 12 heads?
  • d_k = d_model / heads = 768 / 12 = 64
  • Q shape: (8, 20, 768) × (768, 64) = (8, 20, 64) per head → after splitting: (8, 12, 20, 64)
  • K shape: same as Q: (8, 12, 20, 64)
  • V shape: same: (8, 12, 20, 64)
  • Attention weights: (8, 12, 20, 20)
  • Output: (8, 12, 20, 64) → combined: (8, 20, 768) Q2: If d_model=256, h=8, what is d_k? What if d_model=1024, h=16?
Case 1: d_k = 256/8 = 32 Case 2: d_k = 1024/16 = 64
The per-head dimension typically stays between 32-128 in most architectures. Q3: Why can't the decoder use bidirectional attention during training?
If the decoder could attend to future tokens during training, it would "cheat" by looking at the correct next token instead of learning to predict it. For example, training to predict the third word "sat" in "The cat sat":
  • Without mask: The model sees "sat" as a key and uses its value directly → trivial prediction
  • With causal mask: The model must predict "sat" using only "The" and "cat" → actual learning
During inference, future tokens don't exist yet, so the mask naturally prevents looking at non-existent tokens. Q4: In cross-attention, why does Q come from the decoder while K and V come from the encoder?
Q represents "what the decoder needs to know next." The decoder asks: "Given what I've generated so far, what part of the input should I look at to generate the next token?"
K and V from the encoder represent "what the input contains." The encoder has already processed the full input and created rich representations.
If the roles were reversed (Q from encoder, K, V from decoder), the encoder would be trying to "query" the decoder, which doesn't make sense since the decoder is being built incrementally. Q5: Compute Q, K, V for X = [[2, 1], [0, 3]] with W^Q = [[1, 0], [0, 1]], W^K = [[0, 1], [1, 0]], W^V = [[0.5, 0.5], [0.5, 0.5]].
Q = X·W^Q = [[2·1+1·0, 2·0+1·1], [0·1+3·0, 0·0+3·1]] = [[2, 1], [0, 3]] K = X·W^K = [[2·0+1·1, 2·1+1·0], [0·0+3·1, 0·1+3·0]] = [[1, 2], [3, 0]] V = X·W^V = [[2·0.5+1·0.5, 2·0.5+1·0.5], [0·0.5+3·0.5, 0·0.5+3·0.5]] = [[1.5, 1.5], [1.5, 1.5]] Q6: What happens to attention weights if we add a constant C to all scores before softmax?
Nothing. Adding a constant to all elements of a row before softmax doesn't change the result because:
softmax(xi+C)=exi+Cjexj+C=exieCeCjexj=exijexj=softmax(xi)\text{softmax}(x_i + C) = \frac{e^{x_i + C}}{\sum_j e^{x_j + C}} = \frac{e^{x_i} \cdot e^C}{e^C \sum_j e^{x_j}} = \frac{e^{x_i}}{\sum_j e^{x_j}} = \text{softmax}(x_i)
But adding a constant to a single element (while keeping others unchanged) does change the distribution. Q7: For a sequence of length n, how many pairwise attention comparisons are made in self-attention?
n² comparisons. Every token (as query) compares against every token (as key). For n=100, that's 10,000 comparisons. This quadratic growth is the reason Transformers are computationally expensive for long sequences. Q8: What is the difference between "content-based" attention and "position-based" attention?
Content-based attention: The attention score depends on what the tokens represent (the QK dot product captures semantic similarity). "bank" attends to "money" because their representations are similar.
Position-based attention: The attention score depends on where tokens are in the sequence. The model might learn to always attend to the token at position i-1 (previous token) regardless of content.
Multi-head attention can capture both: some heads learn content-based patterns while others learn position-based patterns. The learned projections determine which type each head specializes in.

7. 🔗 Cross-References

Document outline

Keep your place and jump directly to a heading.

Table of Contents
System Normal // Awaiting Context

Intelligence Hub

Navigate the knowledge graph to generate context. The Hub adapts dynamically to surface backlinks, related notes, and metadata insights.