Building Self-Attention and Transformers Under the Hood: A Step-by-Step Walkthrough

Language Models & Compilers

Building Self-Attention and Transformers Under the Hood: A Step-by-Step Walkthrough

In 2017, a team of researchers at Google Brain published a paper titled "Attention Is All You Need." It discarded decades of established Recurrent Neural Network (RNN) and Long Short-Term Memory (LSTM) architecture in favor of a devastatingly simple mechanism: the Scaled Dot-Product Attention. This single algorithmic leap birthed the Transformer, the engine powering every modern Large Language Model (LLM) from GPT-4 to Claude.

Yet, for many software engineers, the Transformer remains shrouded in mathematical mysticism. We use high-level APIs like HuggingFace without truly understanding the matrix multiplications occurring underneath. How does a computer actually "understand" context? How does the word "bank" change meaning when surrounded by "river" instead of "money"? In this comprehensive masterclass, we will construct a Self-Attention block from scratch using raw NumPy. We will trace a sentence through the Queries, Keys, and Values matrices, decode the famous attention formula, and expose the most common developer pitfalls when implementing these architectures.


1. The Problem with RNNs and the Birth of Attention

1.1 The Sequential Bottleneck

Before Transformers, the industry standard for processing text was the Recurrent Neural Network (RNN). RNNs read a sentence word-by-word from left to right. When processing the 10th word, the network relied on a "hidden state" that summarized the previous 9 words. This sequential design suffered from two fatal flaws.

First, it suffered from catastrophic forgetting. By the time an RNN reached the end of a long paragraph, the hidden state had often "forgotten" the critical context from the first sentence. Second, because it processed words sequentially, it could not leverage modern GPU parallelization. You cannot process word 10 until you finish word 9.

1.2 The Transformer Solution

Transformers discard sequence entirely. They ingest the entire sentence at once. To figure out how words relate to each other, the Transformer uses Self-Attention. Every single word in the sentence looks at every other word in the sentence simultaneously, calculates a "relevance score," and blends their meanings together. Because there is no sequential dependence, we can calculate these scores for all words at the exact same time using massive matrix multiplications on a GPU.

Developer Pitfall — Positional Ignorance:

Because Transformers ingest all words simultaneously, the architecture inherently has no concept of word order. The sentence "The dog bit the man" looks identical to "The man bit the dog" to a raw attention mechanism. To fix this, we must explicitly inject Positional Encodings (sine/cosine waves or learned vectors) into the initial word embeddings so the network knows where each word is located before the attention mechanism starts.


2. The Cocktail Party: Queries, Keys, and Values

The genius of Self-Attention is its use of a database retrieval analogy. Imagine you are at a crowded cocktail party (the sentence). You are a specific word, and you want to find other words to talk to in order to understand your own context.

  • Query (Q): What you are looking for. (e.g., "I am an adjective looking for the noun I modify.")
  • Key (K): What other words advertise about themselves. (e.g., "I am a singular, animate noun.")
  • Value (V): The actual substantive meaning or context the word provides once a match is found.

To create these Q, K, and V vectors, the model maintains three learnable weight matrices ($W_Q$, $W_K$, $W_V$). We take our initial word embedding vector and multiply it by these three matrices to generate the word's Query, Key, and Value.

import numpy as np

# Let's say we have 3 words, each represented by an embedding of size 4
X = np.array([
    [1.0, 0.0, 1.0, 0.0],  # "The"
    [0.0, 2.0, 0.0, 2.0],  # "cat"
    [1.0, 1.0, 1.0, 1.0]   # "sat"
])

# Initialize random weight matrices (size 4x3) to project into dimension d_k = 3
W_Q = np.random.randn(4, 3)
W_K = np.random.randn(4, 3)
W_V = np.random.randn(4, 3)

# Project the input to get Q, K, V
Q = np.dot(X, W_Q)  # Shape: (3, 3)
K = np.dot(X, W_K)  # Shape: (3, 3)
V = np.dot(X, W_V)  # Shape: (3, 3)

3. The Attention Formula and The Worked Trace

3.1 Calculating Relevance Scores

To figure out how much the word "cat" should pay attention to "sat", we take the Query vector of "cat" and calculate the dot product with the Key vector of "sat". The dot product is a mathematical measure of similarity. A high dot product means the Query and Key align perfectly; the words are highly relevant to each other.

Because we want to do this for all words simultaneously, we multiply the entire Query matrix by the transpose of the Key matrix: $Q \times K^T$. This results in a square "Attention Score" matrix where row $i$ and column $j$ represent how much word $i$ attends to word $j$.

3.2 The Worked Trace

1
Dot Product: We calculate $Scores = Q \cdot K^T$. Let's assume the score of "cat" looking at "sat" is 112, and "cat" looking at "The" is 12.
2
Scaling: We divide the scores by $\sqrt{d_k}$ (the square root of the dimension size of the key vectors). If $d_k = 64$, we divide by 8. So 112 becomes 14, and 12 becomes 1.5.
3
Softmax: We apply the Softmax function to each row. Softmax turns raw scores into probabilities that sum to 1.0. The score of 14 dominates 1.5 exponentially. "cat" ends up with a 99.9% probability weight on "sat", and 0.1% on "The".
4
Value Blending: We multiply these probability weights by the Value matrix ($V$). The new context-aware embedding for "cat" becomes $0.999 imes V_{sat} + 0.001 imes V_{The}$. The meaning of "sat" has literally bled into the embedding of "cat".

$$Attention(Q, K, V) = ext{softmax}\left( rac{Q K^T}{\sqrt{d_k}} ight) V$$

import math

# Step 1 & 2: Dot product and Scale
d_k = Q.shape[1]
scores = np.dot(Q, K.T) / math.sqrt(d_k)

# Step 3: Softmax (row-wise)
exp_scores = np.exp(scores - np.max(scores, axis=1, keepdims=True)) # stability trick
attention_weights = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)

# Step 4: Multiply by Values
output = np.dot(attention_weights, V)

4. Why Divide by the Square Root? (The Scaling Factor)

One of the most common questions is why the original Google researchers included the $ rac{1}{\sqrt{d_k}}$ scaling factor. Why not just take the raw dot product?

Assume the elements in the $Q$ and $K$ vectors are independent random variables with a mean of 0 and a variance of 1. When you take the dot product of two vectors of dimension $d_k$, you are summing $d_k$ products. The mean of this sum is still 0, but the variance grows to $d_k$. A large variance means the dot products will produce extremely large positive and negative numbers.

The Softmax function uses exponentials ($e^x$). If you feed Softmax extremely large numbers (like 112), $e^{112}$ becomes astronomically larger than $e^{12}$. The Softmax output will slam to 1.0 for the highest value and 0.0 for everything else. This is a disaster for neural networks because the gradient (the derivative used for backpropagation) in these "flat" regions of the Softmax curve approaches exactly zero. The network completely stops learning—a phenomenon known as the vanishing gradient problem.

By dividing by $\sqrt{d_k}$, we reduce the variance back to 1. The numbers stay small, the Softmax curve remains gentle, and gradients flow smoothly during training.


5. Multi-Head Attention: Multiple Perspectives

If a single Attention block is good, multiple blocks are better. When reading the sentence "The bank of the river," the word "bank" needs to understand its syntactic role (it is a noun), its semantic meaning (related to water), and its grammatical relations. A single set of $Q, K, V$ matrices might struggle to capture all these distinct concepts simultaneously.

Instead of one massive attention operation, we split our embeddings into multiple "Heads." We project the input into $h$ different, smaller sets of $Q, K, V$ matrices. Head 1 might learn to attend strictly to adjectives. Head 2 might learn to look backwards at previous verbs. Each head performs the scaled dot-product attention completely independently in parallel.

Finally, we concatenate the outputs of all $h$ heads together, multiply them by a final output weight matrix ($W_O$), and pass them forward. This allows the model to jointly attend to information from different representation subspaces at different positions.

Developer Pitfall — Implementing Masking Incorrectly:

In decoder models like GPT, the model generates text autoregressively (one word at a time). When predicting word 5, it cannot be allowed to "look into the future" at word 6. To prevent this, we must apply a Causal Mask. Before applying Softmax, we replace the upper-triangle of the $Q K^T$ score matrix with negative infinity ($-1e9$). When Softmax processes $-\infty$, it outputs $0$, guaranteeing that future words receive exactly $0\%$ attention weight.


6. Frequently Asked Questions

Q1: What is the difference between Self-Attention and Cross-Attention?

In Self-Attention, the Queries, Keys, and Values all come from the exact same input sentence. In Cross-Attention (used in encoder-decoder architectures like translation models), the Queries come from the currently generating output sentence, but the Keys and Values come from the original input sentence. The model is literally querying the source text to find the translation context.

Q2: Why are Transformers so memory-intensive for long documents?

The matrix multiplication $Q imes K^T$ calculates a score for every word against every other word. If your sequence length is $N$, the resulting attention matrix is of size $N imes N$. The memory and compute complexity scales quadratically ($O(N^2)$). A 1,000-token sequence takes 1 million operations, but a 100,000-token context window requires 10 billion operations per layer, vastly exceeding GPU VRAM limits. This is why techniques like FlashAttention are critical today.

Q3: What are the Key-Value (KV) Caches used in inference?

When a model like ChatGPT generates text, it produces one token at a time. To generate word 100, it needs the Keys and Values for words 1 through 99. Recomputing the $K$ and $V$ vectors for the previous 99 words on every single step is incredibly wasteful. Instead, inference engines cache the $K$ and $V$ tensors in GPU memory. The bottleneck in modern LLM serving is almost entirely memory bandwidth related to reading this massive KV cache.

Q4: Why do we need a Feed-Forward network after the Attention block?

Attention is purely a routing and blending mechanism; it mixes existing values together using linear operations. Without non-linear activation functions (like ReLU or GELU), a deep neural network collapses into a single linear transformation capable of learning very little. The Feed-Forward Network (FFN) applied after the attention block provides the critical non-linearity, allowing the model to learn complex, complex linguistic representations.


Written by Professor Pixel · CodingPancake · Language Models & Compilers Series

1. The Problem with RNNs and the Birth of Attention

1.1 The Sequential Bottleneck

Before Transformers, the industry standard for processing text was the Recurrent Neural Network (RNN). RNNs read a sentence word-by-word from left to right. When processing the 10th word, the network relied on a "hidden state" that summarized the previous 9 words. This sequential design suffered from two fatal flaws.

First, it suffered from catastrophic forgetting. By the time an RNN reached the end of a long paragraph, the hidden state had often "forgotten" the critical context from the first sentence. Second, because it processed words sequentially, it could not leverage modern GPU parallelization. You cannot process word 10 until you finish word 9.

1.2 The Transformer Solution

Transformers discard sequence entirely. They ingest the entire sentence at once. To figure out how words relate to each other, the Transformer uses Self-Attention. Every single word in the sentence looks at every other word in the sentence simultaneously, calculates a "relevance score," and blends their meanings together. Because there is no sequential dependence, we can calculate these scores for all words at the exact same time using massive matrix multiplications on a GPU.

Developer Pitfall — Positional Ignorance:

Because Transformers ingest all words simultaneously, the architecture inherently has no concept of word order. The sentence "The dog bit the man" looks identical to "The man bit the dog" to a raw attention mechanism. To fix this, we must explicitly inject Positional Encodings (sine/cosine waves or learned vectors) into the initial word embeddings so the network knows where each word is located before the attention mechanism starts.


2. The Cocktail Party: Queries, Keys, and Values

The genius of Self-Attention is its use of a database retrieval analogy. Imagine you are at a crowded cocktail party (the sentence). You are a specific word, and you want to find other words to talk to in order to understand your own context.

  • Query (Q): What you are looking for. (e.g., "I am an adjective looking for the noun I modify.")
  • Key (K): What other words advertise about themselves. (e.g., "I am a singular, animate noun.")
  • Value (V): The actual substantive meaning or context the word provides once a match is found.

To create these Q, K, and V vectors, the model maintains three learnable weight matrices ($W_Q$, $W_K$, $W_V$). We take our initial word embedding vector and multiply it by these three matrices to generate the word's Query, Key, and Value.

import numpy as np

# Let's say we have 3 words, each represented by an embedding of size 4
X = np.array([
    [1.0, 0.0, 1.0, 0.0],  # "The"
    [0.0, 2.0, 0.0, 2.0],  # "cat"
    [1.0, 1.0, 1.0, 1.0]   # "sat"
])

# Initialize random weight matrices (size 4x3) to project into dimension d_k = 3
W_Q = np.random.randn(4, 3)
W_K = np.random.randn(4, 3)
W_V = np.random.randn(4, 3)

# Project the input to get Q, K, V
Q = np.dot(X, W_Q)  # Shape: (3, 3)
K = np.dot(X, W_K)  # Shape: (3, 3)
V = np.dot(X, W_V)  # Shape: (3, 3)

3. The Attention Formula and The Worked Trace

3.1 Calculating Relevance Scores

To figure out how much the word "cat" should pay attention to "sat", we take the Query vector of "cat" and calculate the dot product with the Key vector of "sat". The dot product is a mathematical measure of similarity. A high dot product means the Query and Key align perfectly; the words are highly relevant to each other.

Because we want to do this for all words simultaneously, we multiply the entire Query matrix by the transpose of the Key matrix: $Q \times K^T$. This results in a square "Attention Score" matrix where row $i$ and column $j$ represent how much word $i$ attends to word $j$.

3.2 The Worked Trace

1
Dot Product: We calculate $Scores = Q \cdot K^T$. Let's assume the score of "cat" looking at "sat" is 112, and "cat" looking at "The" is 12.
2
Scaling: We divide the scores by $\sqrt{d_k}$ (the square root of the dimension size of the key vectors). If $d_k = 64$, we divide by 8. So 112 becomes 14, and 12 becomes 1.5.
3
Softmax: We apply the Softmax function to each row. Softmax turns raw scores into probabilities that sum to 1.0. The score of 14 dominates 1.5 exponentially. "cat" ends up with a 99.9% probability weight on "sat", and 0.1% on "The".
4
Value Blending: We multiply these probability weights by the Value matrix ($V$). The new context-aware embedding for "cat" becomes $0.999 imes V_{sat} + 0.001 imes V_{The}$. The meaning of "sat" has literally bled into the embedding of "cat".

$$Attention(Q, K, V) = ext{softmax}\left( rac{Q K^T}{\sqrt{d_k}} ight) V$$

import math

# Step 1 & 2: Dot product and Scale
d_k = Q.shape[1]
scores = np.dot(Q, K.T) / math.sqrt(d_k)

# Step 3: Softmax (row-wise)
exp_scores = np.exp(scores - np.max(scores, axis=1, keepdims=True)) # stability trick
attention_weights = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)

# Step 4: Multiply by Values
output = np.dot(attention_weights, V)

4. Why Divide by the Square Root? (The Scaling Factor)

One of the most common questions is why the original Google researchers included the $ rac{1}{\sqrt{d_k}}$ scaling factor. Why not just take the raw dot product?

Assume the elements in the $Q$ and $K$ vectors are independent random variables with a mean of 0 and a variance of 1. When you take the dot product of two vectors of dimension $d_k$, you are summing $d_k$ products. The mean of this sum is still 0, but the variance grows to $d_k$. A large variance means the dot products will produce extremely large positive and negative numbers.

The Softmax function uses exponentials ($e^x$). If you feed Softmax extremely large numbers (like 112), $e^{112}$ becomes astronomically larger than $e^{12}$. The Softmax output will slam to 1.0 for the highest value and 0.0 for everything else. This is a disaster for neural networks because the gradient (the derivative used for backpropagation) in these "flat" regions of the Softmax curve approaches exactly zero. The network completely stops learning—a phenomenon known as the vanishing gradient problem.

By dividing by $\sqrt{d_k}$, we reduce the variance back to 1. The numbers stay small, the Softmax curve remains gentle, and gradients flow smoothly during training.


5. Multi-Head Attention: Multiple Perspectives

If a single Attention block is good, multiple blocks are better. When reading the sentence "The bank of the river," the word "bank" needs to understand its syntactic role (it is a noun), its semantic meaning (related to water), and its grammatical relations. A single set of $Q, K, V$ matrices might struggle to capture all these distinct concepts simultaneously.

Instead of one massive attention operation, we split our embeddings into multiple "Heads." We project the input into $h$ different, smaller sets of $Q, K, V$ matrices. Head 1 might learn to attend strictly to adjectives. Head 2 might learn to look backwards at previous verbs. Each head performs the scaled dot-product attention completely independently in parallel.

Finally, we concatenate the outputs of all $h$ heads together, multiply them by a final output weight matrix ($W_O$), and pass them forward. This allows the model to jointly attend to information from different representation subspaces at different positions.

Developer Pitfall — Implementing Masking Incorrectly:

In decoder models like GPT, the model generates text autoregressively (one word at a time). When predicting word 5, it cannot be allowed to "look into the future" at word 6. To prevent this, we must apply a Causal Mask. Before applying Softmax, we replace the upper-triangle of the $Q K^T$ score matrix with negative infinity ($-1e9$). When Softmax processes $-\infty$, it outputs $0$, guaranteeing that future words receive exactly $0\%$ attention weight.


6. Frequently Asked Questions

Q1: What is the difference between Self-Attention and Cross-Attention?

In Self-Attention, the Queries, Keys, and Values all come from the exact same input sentence. In Cross-Attention (used in encoder-decoder architectures like translation models), the Queries come from the currently generating output sentence, but the Keys and Values come from the original input sentence. The model is literally querying the source text to find the translation context.

Q2: Why are Transformers so memory-intensive for long documents?

The matrix multiplication $Q imes K^T$ calculates a score for every word against every other word. If your sequence length is $N$, the resulting attention matrix is of size $N imes N$. The memory and compute complexity scales quadratically ($O(N^2)$). A 1,000-token sequence takes 1 million operations, but a 100,000-token context window requires 10 billion operations per layer, vastly exceeding GPU VRAM limits. This is why techniques like FlashAttention are critical today.

Q3: What are the Key-Value (KV) Caches used in inference?

When a model like ChatGPT generates text, it produces one token at a time. To generate word 100, it needs the Keys and Values for words 1 through 99. Recomputing the $K$ and $V$ vectors for the previous 99 words on every single step is incredibly wasteful. Instead, inference engines cache the $K$ and $V$ tensors in GPU memory. The bottleneck in modern LLM serving is almost entirely memory bandwidth related to reading this massive KV cache.

Q4: Why do we need a Feed-Forward network after the Attention block?

Attention is purely a routing and blending mechanism; it mixes existing values together using linear operations. Without non-linear activation functions (like ReLU or GELU), a deep neural network collapses into a single linear transformation capable of learning very little. The Feed-Forward Network (FFN) applied after the attention block provides the critical non-linearity, allowing the model to learn complex, complex linguistic representations.


Written by Professor Pixel · CodingPancake · Language Models & Compilers Series

1. The Problem with RNNs and the Birth of Attention

1.1 The Sequential Bottleneck

Before Transformers, the industry standard for processing text was the Recurrent Neural Network (RNN). RNNs read a sentence word-by-word from left to right. When processing the 10th word, the network relied on a "hidden state" that summarized the previous 9 words. This sequential design suffered from two fatal flaws.

First, it suffered from catastrophic forgetting. By the time an RNN reached the end of a long paragraph, the hidden state had often "forgotten" the critical context from the first sentence. Second, because it processed words sequentially, it could not leverage modern GPU parallelization. You cannot process word 10 until you finish word 9.

1.2 The Transformer Solution

Transformers discard sequence entirely. They ingest the entire sentence at once. To figure out how words relate to each other, the Transformer uses Self-Attention. Every single word in the sentence looks at every other word in the sentence simultaneously, calculates a "relevance score," and blends their meanings together. Because there is no sequential dependence, we can calculate these scores for all words at the exact same time using massive matrix multiplications on a GPU.

Developer Pitfall — Positional Ignorance:

Because Transformers ingest all words simultaneously, the architecture inherently has no concept of word order. The sentence "The dog bit the man" looks identical to "The man bit the dog" to a raw attention mechanism. To fix this, we must explicitly inject Positional Encodings (sine/cosine waves or learned vectors) into the initial word embeddings so the network knows where each word is located before the attention mechanism starts.


2. The Cocktail Party: Queries, Keys, and Values

The genius of Self-Attention is its use of a database retrieval analogy. Imagine you are at a crowded cocktail party (the sentence). You are a specific word, and you want to find other words to talk to in order to understand your own context.

  • Query (Q): What you are looking for. (e.g., "I am an adjective looking for the noun I modify.")
  • Key (K): What other words advertise about themselves. (e.g., "I am a singular, animate noun.")
  • Value (V): The actual substantive meaning or context the word provides once a match is found.

To create these Q, K, and V vectors, the model maintains three learnable weight matrices ($W_Q$, $W_K$, $W_V$). We take our initial word embedding vector and multiply it by these three matrices to generate the word's Query, Key, and Value.

import numpy as np

# Let's say we have 3 words, each represented by an embedding of size 4
X = np.array([
    [1.0, 0.0, 1.0, 0.0],  # "The"
    [0.0, 2.0, 0.0, 2.0],  # "cat"
    [1.0, 1.0, 1.0, 1.0]   # "sat"
])

# Initialize random weight matrices (size 4x3) to project into dimension d_k = 3
W_Q = np.random.randn(4, 3)
W_K = np.random.randn(4, 3)
W_V = np.random.randn(4, 3)

# Project the input to get Q, K, V
Q = np.dot(X, W_Q)  # Shape: (3, 3)
K = np.dot(X, W_K)  # Shape: (3, 3)
V = np.dot(X, W_V)  # Shape: (3, 3)

3. The Attention Formula and The Worked Trace

3.1 Calculating Relevance Scores

To figure out how much the word "cat" should pay attention to "sat", we take the Query vector of "cat" and calculate the dot product with the Key vector of "sat". The dot product is a mathematical measure of similarity. A high dot product means the Query and Key align perfectly; the words are highly relevant to each other.

Because we want to do this for all words simultaneously, we multiply the entire Query matrix by the transpose of the Key matrix: $Q \times K^T$. This results in a square "Attention Score" matrix where row $i$ and column $j$ represent how much word $i$ attends to word $j$.

3.2 The Worked Trace

1
Dot Product: We calculate $Scores = Q \cdot K^T$. Let's assume the score of "cat" looking at "sat" is 112, and "cat" looking at "The" is 12.
2
Scaling: We divide the scores by $\sqrt{d_k}$ (the square root of the dimension size of the key vectors). If $d_k = 64$, we divide by 8. So 112 becomes 14, and 12 becomes 1.5.
3
Softmax: We apply the Softmax function to each row. Softmax turns raw scores into probabilities that sum to 1.0. The score of 14 dominates 1.5 exponentially. "cat" ends up with a 99.9% probability weight on "sat", and 0.1% on "The".
4
Value Blending: We multiply these probability weights by the Value matrix ($V$). The new context-aware embedding for "cat" becomes $0.999 imes V_{sat} + 0.001 imes V_{The}$. The meaning of "sat" has literally bled into the embedding of "cat".

$$Attention(Q, K, V) = ext{softmax}\left( rac{Q K^T}{\sqrt{d_k}} ight) V$$

import math

# Step 1 & 2: Dot product and Scale
d_k = Q.shape[1]
scores = np.dot(Q, K.T) / math.sqrt(d_k)

# Step 3: Softmax (row-wise)
exp_scores = np.exp(scores - np.max(scores, axis=1, keepdims=True)) # stability trick
attention_weights = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)

# Step 4: Multiply by Values
output = np.dot(attention_weights, V)

4. Why Divide by the Square Root? (The Scaling Factor)

One of the most common questions is why the original Google researchers included the $ rac{1}{\sqrt{d_k}}$ scaling factor. Why not just take the raw dot product?

Assume the elements in the $Q$ and $K$ vectors are independent random variables with a mean of 0 and a variance of 1. When you take the dot product of two vectors of dimension $d_k$, you are summing $d_k$ products. The mean of this sum is still 0, but the variance grows to $d_k$. A large variance means the dot products will produce extremely large positive and negative numbers.

The Softmax function uses exponentials ($e^x$). If you feed Softmax extremely large numbers (like 112), $e^{112}$ becomes astronomically larger than $e^{12}$. The Softmax output will slam to 1.0 for the highest value and 0.0 for everything else. This is a disaster for neural networks because the gradient (the derivative used for backpropagation) in these "flat" regions of the Softmax curve approaches exactly zero. The network completely stops learning—a phenomenon known as the vanishing gradient problem.

By dividing by $\sqrt{d_k}$, we reduce the variance back to 1. The numbers stay small, the Softmax curve remains gentle, and gradients flow smoothly during training.


5. Multi-Head Attention: Multiple Perspectives

If a single Attention block is good, multiple blocks are better. When reading the sentence "The bank of the river," the word "bank" needs to understand its syntactic role (it is a noun), its semantic meaning (related to water), and its grammatical relations. A single set of $Q, K, V$ matrices might struggle to capture all these distinct concepts simultaneously.

Instead of one massive attention operation, we split our embeddings into multiple "Heads." We project the input into $h$ different, smaller sets of $Q, K, V$ matrices. Head 1 might learn to attend strictly to adjectives. Head 2 might learn to look backwards at previous verbs. Each head performs the scaled dot-product attention completely independently in parallel.

Finally, we concatenate the outputs of all $h$ heads together, multiply them by a final output weight matrix ($W_O$), and pass them forward. This allows the model to jointly attend to information from different representation subspaces at different positions.

Developer Pitfall — Implementing Masking Incorrectly:

In decoder models like GPT, the model generates text autoregressively (one word at a time). When predicting word 5, it cannot be allowed to "look into the future" at word 6. To prevent this, we must apply a Causal Mask. Before applying Softmax, we replace the upper-triangle of the $Q K^T$ score matrix with negative infinity ($-1e9$). When Softmax processes $-\infty$, it outputs $0$, guaranteeing that future words receive exactly $0\%$ attention weight.


6. Frequently Asked Questions

Q1: What is the difference between Self-Attention and Cross-Attention?

In Self-Attention, the Queries, Keys, and Values all come from the exact same input sentence. In Cross-Attention (used in encoder-decoder architectures like translation models), the Queries come from the currently generating output sentence, but the Keys and Values come from the original input sentence. The model is literally querying the source text to find the translation context.

Q2: Why are Transformers so memory-intensive for long documents?

The matrix multiplication $Q imes K^T$ calculates a score for every word against every other word. If your sequence length is $N$, the resulting attention matrix is of size $N imes N$. The memory and compute complexity scales quadratically ($O(N^2)$). A 1,000-token sequence takes 1 million operations, but a 100,000-token context window requires 10 billion operations per layer, vastly exceeding GPU VRAM limits. This is why techniques like FlashAttention are critical today.

Q3: What are the Key-Value (KV) Caches used in inference?

When a model like ChatGPT generates text, it produces one token at a time. To generate word 100, it needs the Keys and Values for words 1 through 99. Recomputing the $K$ and $V$ vectors for the previous 99 words on every single step is incredibly wasteful. Instead, inference engines cache the $K$ and $V$ tensors in GPU memory. The bottleneck in modern LLM serving is almost entirely memory bandwidth related to reading this massive KV cache.

Q4: Why do we need a Feed-Forward network after the Attention block?

Attention is purely a routing and blending mechanism; it mixes existing values together using linear operations. Without non-linear activation functions (like ReLU or GELU), a deep neural network collapses into a single linear transformation capable of learning very little. The Feed-Forward Network (FFN) applied after the attention block provides the critical non-linearity, allowing the model to learn complex, complex linguistic representations.


Written by Professor Pixel · CodingPancake · Language Models & Compilers Series

1. The Problem with RNNs and the Birth of Attention

1.1 The Sequential Bottleneck

Before Transformers, the industry standard for processing text was the Recurrent Neural Network (RNN). RNNs read a sentence word-by-word from left to right. When processing the 10th word, the network relied on a "hidden state" that summarized the previous 9 words. This sequential design suffered from two fatal flaws.

First, it suffered from catastrophic forgetting. By the time an RNN reached the end of a long paragraph, the hidden state had often "forgotten" the critical context from the first sentence. Second, because it processed words sequentially, it could not leverage modern GPU parallelization. You cannot process word 10 until you finish word 9.

1.2 The Transformer Solution

Transformers discard sequence entirely. They ingest the entire sentence at once. To figure out how words relate to each other, the Transformer uses Self-Attention. Every single word in the sentence looks at every other word in the sentence simultaneously, calculates a "relevance score," and blends their meanings together. Because there is no sequential dependence, we can calculate these scores for all words at the exact same time using massive matrix multiplications on a GPU.

Developer Pitfall — Positional Ignorance:

Because Transformers ingest all words simultaneously, the architecture inherently has no concept of word order. The sentence "The dog bit the man" looks identical to "The man bit the dog" to a raw attention mechanism. To fix this, we must explicitly inject Positional Encodings (sine/cosine waves or learned vectors) into the initial word embeddings so the network knows where each word is located before the attention mechanism starts.


2. The Cocktail Party: Queries, Keys, and Values

The genius of Self-Attention is its use of a database retrieval analogy. Imagine you are at a crowded cocktail party (the sentence). You are a specific word, and you want to find other words to talk to in order to understand your own context.

  • Query (Q): What you are looking for. (e.g., "I am an adjective looking for the noun I modify.")
  • Key (K): What other words advertise about themselves. (e.g., "I am a singular, animate noun.")
  • Value (V): The actual substantive meaning or context the word provides once a match is found.

To create these Q, K, and V vectors, the model maintains three learnable weight matrices ($W_Q$, $W_K$, $W_V$). We take our initial word embedding vector and multiply it by these three matrices to generate the word's Query, Key, and Value.

import numpy as np

# Let's say we have 3 words, each represented by an embedding of size 4
X = np.array([
    [1.0, 0.0, 1.0, 0.0],  # "The"
    [0.0, 2.0, 0.0, 2.0],  # "cat"
    [1.0, 1.0, 1.0, 1.0]   # "sat"
])

# Initialize random weight matrices (size 4x3) to project into dimension d_k = 3
W_Q = np.random.randn(4, 3)
W_K = np.random.randn(4, 3)
W_V = np.random.randn(4, 3)

# Project the input to get Q, K, V
Q = np.dot(X, W_Q)  # Shape: (3, 3)
K = np.dot(X, W_K)  # Shape: (3, 3)
V = np.dot(X, W_V)  # Shape: (3, 3)

3. The Attention Formula and The Worked Trace

3.1 Calculating Relevance Scores

To figure out how much the word "cat" should pay attention to "sat", we take the Query vector of "cat" and calculate the dot product with the Key vector of "sat". The dot product is a mathematical measure of similarity. A high dot product means the Query and Key align perfectly; the words are highly relevant to each other.

Because we want to do this for all words simultaneously, we multiply the entire Query matrix by the transpose of the Key matrix: $Q \times K^T$. This results in a square "Attention Score" matrix where row $i$ and column $j$ represent how much word $i$ attends to word $j$.

3.2 The Worked Trace

1
Dot Product: We calculate $Scores = Q \cdot K^T$. Let's assume the score of "cat" looking at "sat" is 112, and "cat" looking at "The" is 12.
2
Scaling: We divide the scores by $\sqrt{d_k}$ (the square root of the dimension size of the key vectors). If $d_k = 64$, we divide by 8. So 112 becomes 14, and 12 becomes 1.5.
3
Softmax: We apply the Softmax function to each row. Softmax turns raw scores into probabilities that sum to 1.0. The score of 14 dominates 1.5 exponentially. "cat" ends up with a 99.9% probability weight on "sat", and 0.1% on "The".
4
Value Blending: We multiply these probability weights by the Value matrix ($V$). The new context-aware embedding for "cat" becomes $0.999 imes V_{sat} + 0.001 imes V_{The}$. The meaning of "sat" has literally bled into the embedding of "cat".

$$Attention(Q, K, V) = ext{softmax}\left( rac{Q K^T}{\sqrt{d_k}} ight) V$$

import math

# Step 1 & 2: Dot product and Scale
d_k = Q.shape[1]
scores = np.dot(Q, K.T) / math.sqrt(d_k)

# Step 3: Softmax (row-wise)
exp_scores = np.exp(scores - np.max(scores, axis=1, keepdims=True)) # stability trick
attention_weights = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)

# Step 4: Multiply by Values
output = np.dot(attention_weights, V)

4. Why Divide by the Square Root? (The Scaling Factor)

One of the most common questions is why the original Google researchers included the $ rac{1}{\sqrt{d_k}}$ scaling factor. Why not just take the raw dot product?

Assume the elements in the $Q$ and $K$ vectors are independent random variables with a mean of 0 and a variance of 1. When you take the dot product of two vectors of dimension $d_k$, you are summing $d_k$ products. The mean of this sum is still 0, but the variance grows to $d_k$. A large variance means the dot products will produce extremely large positive and negative numbers.

The Softmax function uses exponentials ($e^x$). If you feed Softmax extremely large numbers (like 112), $e^{112}$ becomes astronomically larger than $e^{12}$. The Softmax output will slam to 1.0 for the highest value and 0.0 for everything else. This is a disaster for neural networks because the gradient (the derivative used for backpropagation) in these "flat" regions of the Softmax curve approaches exactly zero. The network completely stops learning—a phenomenon known as the vanishing gradient problem.

By dividing by $\sqrt{d_k}$, we reduce the variance back to 1. The numbers stay small, the Softmax curve remains gentle, and gradients flow smoothly during training.


5. Multi-Head Attention: Multiple Perspectives

If a single Attention block is good, multiple blocks are better. When reading the sentence "The bank of the river," the word "bank" needs to understand its syntactic role (it is a noun), its semantic meaning (related to water), and its grammatical relations. A single set of $Q, K, V$ matrices might struggle to capture all these distinct concepts simultaneously.

Instead of one massive attention operation, we split our embeddings into multiple "Heads." We project the input into $h$ different, smaller sets of $Q, K, V$ matrices. Head 1 might learn to attend strictly to adjectives. Head 2 might learn to look backwards at previous verbs. Each head performs the scaled dot-product attention completely independently in parallel.

Finally, we concatenate the outputs of all $h$ heads together, multiply them by a final output weight matrix ($W_O$), and pass them forward. This allows the model to jointly attend to information from different representation subspaces at different positions.

Developer Pitfall — Implementing Masking Incorrectly:

In decoder models like GPT, the model generates text autoregressively (one word at a time). When predicting word 5, it cannot be allowed to "look into the future" at word 6. To prevent this, we must apply a Causal Mask. Before applying Softmax, we replace the upper-triangle of the $Q K^T$ score matrix with negative infinity ($-1e9$). When Softmax processes $-\infty$, it outputs $0$, guaranteeing that future words receive exactly $0\%$ attention weight.


6. Frequently Asked Questions

Q1: What is the difference between Self-Attention and Cross-Attention?

In Self-Attention, the Queries, Keys, and Values all come from the exact same input sentence. In Cross-Attention (used in encoder-decoder architectures like translation models), the Queries come from the currently generating output sentence, but the Keys and Values come from the original input sentence. The model is literally querying the source text to find the translation context.

Q2: Why are Transformers so memory-intensive for long documents?

The matrix multiplication $Q imes K^T$ calculates a score for every word against every other word. If your sequence length is $N$, the resulting attention matrix is of size $N imes N$. The memory and compute complexity scales quadratically ($O(N^2)$). A 1,000-token sequence takes 1 million operations, but a 100,000-token context window requires 10 billion operations per layer, vastly exceeding GPU VRAM limits. This is why techniques like FlashAttention are critical today.

Q3: What are the Key-Value (KV) Caches used in inference?

When a model like ChatGPT generates text, it produces one token at a time. To generate word 100, it needs the Keys and Values for words 1 through 99. Recomputing the $K$ and $V$ vectors for the previous 99 words on every single step is incredibly wasteful. Instead, inference engines cache the $K$ and $V$ tensors in GPU memory. The bottleneck in modern LLM serving is almost entirely memory bandwidth related to reading this massive KV cache.

Q4: Why do we need a Feed-Forward network after the Attention block?

Attention is purely a routing and blending mechanism; it mixes existing values together using linear operations. Without non-linear activation functions (like ReLU or GELU), a deep neural network collapses into a single linear transformation capable of learning very little. The Feed-Forward Network (FFN) applied after the attention block provides the critical non-linearity, allowing the model to learn complex, complex linguistic representations.


Written by Professor Pixel · CodingPancake · Language Models & Compilers Series

Post a Comment

Previous Post Next Post