SelfAttention: The Core Pattern
Introduction
Attention is the mechanism that lets each token in a sequence "look at" every other token and decide how much to integrate from each one. Self-attention is the case where all the look directions (queries), lookup keys, and values come from the same input sequence. This is the building block for every attention variant that follows.
In this post, we implement a concrete C++ LibTorch class for self-attention: define three learnable projections (Q, K, V), compute scaled dot-product similarity, apply softmax to get weights, and combine values according to those weights.
Operations Pipeline
Here is the complete computation, from input embeddings to attended output:
In many designs, $d_k = d_v = d_{model}$ (or a fraction of it per head). We compute similarity between every query and every key, scale to stabilize gradient flow, convert to probabilities, and use those to mix value vectors.
The C++ Class
Q, K, V Projections
All three projections start from the same input $x$. No interaction between samples yet; this is purely per-token dimension reduction.
| Operation | Input Shape | Output Shape | Interpretation |
|---|---|---|---|
| $Q = W_Q \cdot x$ | (batch, T, d_model) | (batch, T, d_k) | What each token is looking for |
| $K = W_K \cdot x$ | (batch, T, d_model) | (batch, T, d_k) | What each token represents |
| $V = W_V \cdot x$ | (batch, T, d_model) | (batch, T, d_k) | What each token contributes if attended |
The three weight matrices are independent. This means each token can be "viewed" differently through each lens: what it looks for, how it matches, and what it provides are learned separately.
Scaled Dot-Product Scores
Once we have Q and K, the core operation is:
Each entry $\text{scores}[i,j]$ is the dot product of token $i$'s query with token $j$'s key, normalized by $\sqrt{d_k}$. Without this scaling, when $d_k$ is large (say, 512), the dot products grow large, and softmax becomes nearly one-hot, killing gradient flow during backprop. Dividing by $\sqrt{d_k}$ brings the average magnitude back down to a reasonable range.
Softmax and Weighted Sum
After scaling, apply softmax to each row independently:
Then multiply by value vectors:
Tokens with high attention weight contribute more to the output; those with low weight contribute less. The softmax ensures weights are positive and sum to 1, making the combination interpretable as a distribution.
Complete Shape Trace
| Step | Variable | Shape | Notes |
|---|---|---|---|
| Input | x | (batch, T, d_model) | Batch of token embeddings |
| Project Q | Q | (batch, T, d_k) | Linear wq applied |
| Project K | K | (batch, T, d_k) | Linear wk applied |
| Project V | V | (batch, T, d_k) | Linear wv applied |
| Transpose K | K^T | (batch, d_k, T) | Last two dims swapped |
| Scores | scores | (batch, T, T) | Q @ K^T; all (query, key) pairs |
| Scale | scores | (batch, T, T) | Divide by sqrt(d_k) |
| Softmax | attn_weights | (batch, T, T) | Each row sums to 1 |
| Output | output | (batch, T, d_k) | Weighted sum of values |
Interactive Pipeline Animation
▶ Shape Transformation through Self-Attention
Step through the tensor shapes and operations as input flows through the attention module.
Key Insights
What Comes Next
Self-attention assumes every token can attend to every other token. But in many settings, this is too much: the attention matrix becomes huge, or certain attention patterns shouldn't be allowed. The next posts explore variants that modify or restrict this behavior:
- Softmax Attention — break down softmax into its three stable components.
- Causal Attention — add a triangular mask to prevent tokens from seeing the future.
- Cross-Attention — separate keys/values from queries (encoder-decoder pattern).
- Sliding-Window Attention — limit the attention span to a band around each token.
- Global Attention — combine sliding-window with a few "global" tokens that see all.
- Linear Attention — replace softmax with a positive feature map to reduce complexity from $O(T^2)$ to $O(T \cdot d^2)$.