Skip to content

Self-Attention: Q, K, V from First Principles

The highlighted query mixes six value vectors using the constructed attention weights shown on the arcs.

Each token builds a query, scores it against every other token's key, and mixes their values in proportion to the match.

You have a sequence of token embeddings, each a vector in Rdmodel\mathbb{R}^{d_\text{model}}. At this stage, an embedding depends on its own token ID, not its neighbors. The token "bank" has the same embedding whether followed by "account" or "river."

Self-attention is the mechanism that lets tokens talk to each other. It's how "bank" figures out which other tokens in the sequence are relevant to it, and then gathers information from them. Scaled dot-product attention, introduced in the 2017 transformer, remains a core operation in models such as Llama. Later architectures change its projections, masks, and execution.

Q and K determine the weights; V supplies the information to mix. First, we need to see where those inputs come from and where the result goes: the residual stream.


The residual stream

A useful way to picture a transformer is as a residual stream. There's a single vector at each token position, of dimension dmodeld_\text{model}, that flows through the entire network from the input embedding to the final unembedding. In the pre-norm layout we'll use here, attention and feed-forward blocks update this vector through residual additions. They read from it, compute something, and add their result back.

Each sub-layer applies x←x+f(x)x \leftarrow x + f(x). The +f(x)+f(x) is the "residual connection" inherited from ResNet (He et al., 2015), which helped train deep networks by adding an identity term to each block’s derivative. In a transformer it does more than just stabilize training: it sets up a shared bus that every block reads from and writes to. An attention head in layer 7 can, in principle, pick up information that an attention head in layer 1 wrote, because layer 1's contribution is still in the sum.

This framing comes from Elhage et al.'s mathematical framework for transformer circuits. It helps explain how blocks communicate:

  • The token embedding remains a term in the residual sum. Later updates can reinforce or cancel its features; an additive path does not guarantee that all input information stays recoverable.
  • The stream is a communication channel between blocks. Heads that write specific patterns into specific dimensions can be "read" by later FFN or attention blocks that learned to pay attention to those dimensions. That's what people mean by a "circuit."
  • Each token position has its own residual stream. Attention is what lets positions exchange information; FFN blocks are position-independent.

The soft lookup analogy

I find it helpful to think of self-attention as a database lookup, but a soft one.

In a regular hash table, you have a query and you get back one exact match. Self-attention does something similar but fuzzier. Every token simultaneously plays three roles:

  • Query (Q): "What am I looking for?"
  • Key (K): "What do I advertise to others?"
  • Value (V): "What information do I carry?"

When token ii wants to gather information, it broadcasts its query, compares it against every other token's key, and then takes a weighted average of all the value vectors. Tokens whose keys match the query strongly contribute more. Tokens whose keys don't match still contribute, just very little.

Every unmasked key with a finite score participates in the idealized softmax result. The softmax function (which we covered in Post 1.3) turns the raw similarity scores into a probability distribution. So each token's output is a convex combination of all value vectors in the sequence.

In Bahdanau attention (Post 2.7), queries come from the decoder and keys/values come from the encoder. In self-attention, all three come from the same sequence.


Parameters vs. activations

Q, K, and V are not parameters. They are activations.

The parameters are three weight matrices: WQW_Q, WKW_K, and WVW_V. These get learned during training and stay fixed at inference time. They live inside the model's checkpoint file.

The activations Q, K, and V are computed fresh for every single input:

Q=XWQ,K=XWK,V=XWVQ = X W_Q, \quad K = X W_K, \quad V = X W_V

where XX is the input matrix (one row per token). Changing the input can change Q, K, and V. Identical prefixes in a causal model retain the same prefix activations; the weight matrices remain fixed.

WQW_Q is the model's learned concept of "what makes a good query." When multiplied by a specific token embedding, it produces that token's query vector. Same idea for WKW_K and WVW_V.

X (tokens)
0.3
-0.1
0.5
0.2
0.7
0.3
-0.4
0.5
2×4
×
W_Q
0.6
0.1
-0.2
0.8
0.4
-0.3
0.1
0.5
4×2
=
Q
0.42
-0.1
0.25
0.68
2×2
Tap or hover an output cell to see the dot product

The same W_Q matrix (parameters, fixed) produces different query vectors for different token embeddings (activations, per-input). Click an output cell to see the dot product.


The math, step by step

Let's trace the full computation. We have nn tokens, each with a dmodeld_\text{model}-dimensional embedding. Stack them into a matrix XX of shape (n,dmodel)(n, d_\text{model}).

Step 1: Project into Q, K, V.

Q=XWQ∈Rn×dk,K=XWK∈Rn×dk,V=XWV∈Rn×dvQ = X W_Q \in \mathbb{R}^{n \times d_k}, \quad K = X W_K \in \mathbb{R}^{n \times d_k}, \quad V = X W_V \in \mathbb{R}^{n \times d_v}

The projection dimensions dkd_k and dvd_v are typically equal and often smaller than dmodeld_\text{model}. In the original transformer, dmodel=512d_\text{model} = 512 and dk=dv=64d_k = d_v = 64. We'll talk about why they're smaller when we get to multi-head attention in Post 4.2.

Step 2: Compute raw attention scores.

S=QK⊤∈Rn×nS = Q K^\top \in \mathbb{R}^{n \times n}

This is a matrix of dot products. Entry SijS_{ij} is the dot product of token ii's query with token jj's key. High dot product means strong alignment between what token ii is looking for and what token jj is advertising.

The result is an n×nn \times n matrix. Every token has a score for every other token. This is where the quadratic cost of attention comes from.

Here's what that multiplication looks like with real (small) numbers. Two query vectors times two key vectors, transposed, producing a 2x2 score matrix.

Q
0.4
0.7
-0.2
-0.1
0.5
0.6
2×3
×
Kᵀ
0.4
-0.1
0.7
0.5
-0.2
0.6
3×2
=
S (scores)
0.69
0.19
0.19
0.62
2×2
Tap or hover an output cell to see the dot product

Each cell in the score matrix S is a dot product between one token's query and another token's key. Click a cell to see which query-key pair produced it.

Step 3: Scale.

Sscaled=SdkS_\text{scaled} = \frac{S}{\sqrt{d_k}}

The division by dk\sqrt{d_k} controls how score magnitude grows with dimension; the next section derives it.

Step 4: Softmax.

A=softmax(Sscaled)A = \text{softmax}(S_\text{scaled})

Softmax is applied row-wise. Each row of AA is a probability distribution: it sums to 1, every entry is non-negative. Row ii of AA is the attention pattern for token ii, telling us how much weight to give each token's value vector.

Step 5: Weighted sum of values.

Output=AV∈Rn×dv\text{Output} = A V \in \mathbb{R}^{n \times d_v}

Each output row is a weighted average of the value vectors, where the weights come from the attention pattern. Token ii's output is ∑jAijVj\sum_j A_{ij} V_j.

Or, all at once:

Attention(Q,K,V)=softmax ⁣(QK⊤dk)V\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{Q K^\top}{\sqrt{d_k}}\right) V

Why divide by dk\sqrt{d_k}?

Consider the dot product of two random vectors qq and kk, each with dkd_k components drawn independently from a standard normal distribution. The dot product is:

q⋅k=∑i=1dkqikiq \cdot k = \sum_{i=1}^{d_k} q_i k_i

Each term qikiq_i k_i has mean 0 and variance 1 (since each factor has variance 1 and they're independent). The sum of dkd_k such terms has variance dkd_k, which means the standard deviation is dk\sqrt{d_k}.

The viz below samples 240 random query/key pairs at each dd and plots the dot products. Notice how the cloud of red dots widens as dd steps up. The blue panel applies the 1/d1/\sqrt{d} rescaling and keeps its spread roughly constant as dd grows.

dot products of random unit-variance vectorsd = 4 (√d ≈ 2.0)
d sweep:
raw q · kσ_theory = √d = 2.0+σ−σraw dot product (clipped at ±40)σ ≈ 1.87(q · k) / √dσ_theory = 1+σ−σscaled dot product (range ±4)σ ≈ 0.93

raw: raw dot products spread out as d grows. For the independent, zero-mean, unit-variance components sampled here, their variance is d.

scaled: dividing each score by √d makes the variance one under these assumptions. Finite samples still vary; this does not bound individual scores.

For independent, zero-mean, unit-variance coordinates, dividing by √d_k gives the dot product unit variance. The simulated distributions still differ at small dimensions; learned queries and keys need not satisfy these assumptions.

Under these assumptions, when dk=64d_k = 64, the dot products will typically land somewhere in the range [−16,16][-16, 16] (within two standard deviations). That's a problem for softmax. When score gaps become large, softmax saturates: one element gets pushed toward 1 and everything else toward 0. The softmax Jacobian becomes small, which can make it harder to change how the head mixes values.

Under the same independence assumptions, dividing by dk\sqrt{d_k} restores unit variance, reducing this source of softmax saturation. This is what Vaswani et al. call "scaled dot-product attention."

Suppose dk=64d_k = 64 and one row of your raw score matrix is:

[12.3,  1.5,  −4.2,  0.8][12.3, \; 1.5, \; -4.2, \; 0.8]

The softmax of that is approximately [0.999970,  0.000020,  0.000000,  0.000010][0.999970, \; 0.000020, \; 0.000000, \; 0.000010]. Almost a one-hot distribution, with little mixing of the other values.

After scaling by 64=8\sqrt{64} = 8:

[1.54,  0.19,  −0.53,  0.10][1.54, \; 0.19, \; -0.53, \; 0.10]

Softmax of that is roughly [0.616,  0.160,  0.078,  0.146][0.616, \; 0.160, \; 0.078, \; 0.146]. Much softer. The softmax is less saturated, so small score changes have a larger effect on the mixture.

softmax saturation with and without 1/√d_kd_k = 64 (√d_k ≈ 8.0)
481632641282565121024
without scaling (score range ±30)entropy 4%
input to softmax
7.2
2.4
-1.6
1.2
-4.4
softmax output
99%
1%
0%
0%
0%
t₁
t₂
t₃
t₄
t₅

This constructed score vector grows with √d_k. As gaps widen, softmax concentrates more probability on the largest score.

scaled scores (range ±1.8)entropy 92%
input to softmax
0.9
0.3
-0.2
0.1
-0.6
softmax output
39%
21%
13%
18%
9%
t₁
t₂
t₃
t₄
t₅

Dividing by √d_k cancels that chosen growth. Real Q/K vectors need not be independent or have unit variance; this illustrates the scaling rationale.

Compare the largest probability in the two panels as d_k increases.


Worked example

We have four tokens: "The", "cat", "sat", "down". I'll use dmodel=8d_\text{model} = 8 and dk=dv=4d_k = d_v = 4, with small hand-picked numbers so the patterns are visible.

Step 1: Project Q, K, V — W_Q, W_K, and W_V project the same input X into query, key, and value vectors.

tap a token to highlight its row
X (input)
The
1.00
0.20
-0.50
0.30
0.80
-0.10
0.40
0.60
cat
0.30
1.20
0.70
-0.40
0.10
0.90
-0.20
0.50
sat
-0.20
0.50
1.00
0.80
-0.30
0.40
0.70
-0.10
down
0.60
-0.30
0.40
1.10
0.50
-0.60
0.20
0.80
(4 × 8)
↓ three separate projections
Q = X @ W_Q
The
0.72
-0.31
0.45
0.18
cat
-0.15
0.88
0.22
-0.41
sat
0.33
0.51
-0.67
0.29
down
0.56
-0.22
0.81
0.14
(4 × 4)
K = X @ W_K
The
0.41
0.63
-0.28
0.55
cat
0.82
-0.19
0.47
0.31
sat
-0.36
0.74
0.58
-0.12
down
0.29
0.45
-0.63
0.77
(4 × 4)
V = X @ W_V
The
0.55
-0.22
0.38
0.71
cat
-0.31
0.67
0.14
0.43
sat
0.48
0.29
-0.55
0.16
down
0.12
0.81
0.62
-0.34
(4 × 4)

Single-head self-attention on four tokens. Click a token to trace its attention pattern through each step.

In Step 4, look at how each row sums to 1.00. Each token distributes its attention budget across all four tokens, and the weights have to add up.

In Step 5, when you click on a specific token, you can see exactly how its output is assembled. If "cat" attends strongly to "sat" (weight 0.35, say), then the sum contains 0.35 times "sat"'s value vector. The other coefficients sum to 0.65. Those coefficients do not describe fractions of the output’s norm: vector contributions can reinforce or cancel.


Implementation

Here's single-head scaled dot-product attention in PyTorch.

import torch
import torch.nn as nn
import torch.nn.functional as F
import math
 
class SingleHeadAttention(nn.Module):
    def __init__(self, d_model: int, d_k: int):
        super().__init__()
        self.d_k = d_k
        # These are the PARAMETERS — learned during training
        self.W_Q = nn.Linear(d_model, d_k, bias=False)
        self.W_K = nn.Linear(d_model, d_k, bias=False)
        self.W_V = nn.Linear(d_model, d_k, bias=False)
 
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # x: (batch, seq_len, d_model)
 
        # These are the ACTIVATIONS — different for every input
        Q = self.W_Q(x)  # (batch, seq_len, d_k)
        K = self.W_K(x)  # (batch, seq_len, d_k)
        V = self.W_V(x)  # (batch, seq_len, d_k)
 
        # Scaled dot-product attention
        scores = Q @ K.transpose(-2, -1)  # (batch, seq_len, seq_len)
        scores = scores / math.sqrt(self.d_k)
        weights = F.softmax(scores, dim=-1)
        output = weights @ V  # (batch, seq_len, d_k)
 
        return output

A few implementation notes. The nn.Linear modules are the weight matrices. PyTorch's nn.Linear(d_model, d_k, bias=False) stores a matrix of shape (dk,dmodel)(d_k, d_\text{model}) internally and computes xW⊤x W^\top, which is equivalent to our XWX W formulation. I've set bias=False to keep the example focused on the matrices. Bias conventions vary by implementation and model.

The @ operator is matrix multiplication. K.transpose(-2, -1) transposes the last two dimensions, giving us K⊤K^\top while preserving the batch dimension. The softmax is applied along dim=-1, meaning each row independently becomes a probability distribution.

In a real transformer, you'd add causal masking (which we'll cover in Post 4.3) and wrap this in a multi-head structure (Post 4.2). The scaled dot-product operation stays the same.


What each projection learns

So we have three weight matrices that each take the same input and produce different vectors. What do they actually learn to extract?

WQW_Q learns to produce queries: representations of what each token is looking for. For a verb, the query might encode "I need my subject." For an adjective, it might encode "I need the noun I'm modifying."

WKW_K learns to produce keys: representations of what each token has to offer. A noun might advertise "I'm an entity" or "I'm the subject of this clause." These keys don't have to be semantically meaningful to humans. They just need to produce high dot products with the right queries.

WVW_V learns to produce values: the information that actually gets passed along when a token is attended to. This is what gets blended into the output. A token's key says "I'm relevant," and its value says "here's my information."

A token's role as a query target (what matches against it?) can be completely different from the information it carries (what does it contribute?). The three projections give the model three independent degrees of freedom per token.

Mechanistic interpretability work, particularly the "mathematical framework for transformer circuits" from Anthropic, has shown that these projections often learn clean structure. Certain attention heads learn to implement specific operations: "attend to the previous token," "attend to tokens with matching syntactic role," "attend to the most recent proper noun." The QKV decomposition is what enables this specialization.

The heatmap below shows a hand-constructed self-attention pattern for the sentence "The cat sat down." Notice how each row (query token) distributes its attention budget differently. "cat" attends back to "The" (its determiner) and forward to "sat" (its verb). "sat" attends heavily to "cat" (its subject). A trained model can learn such relationships through its Q and K projections; these particular weights are illustrative.

Key (K)Query (Q)ThecatsatdownThecatsatdown.52.35.45.40.37dist.
Tap or hover a cell to inspect its weight. Darker cells have larger weights on a fixed 0–1 scale.
Tap or hover cells to inspect weights. Select a cell or row to see the weighted sum.

Constructed self-attention weights for one head. Each row sums to 1. Tap or hover over a cell to see exact weights. The Q and K projections learn which token pairs should exchange information.


Misconceptions

"Q, K, V are the model's weights." No. WQW_Q, WKW_K, WVW_V are the weights. Q, K, V are activations computed from the input.

"The attention matrix shows what the model is thinking about." The attention weights AA show how information flows between positions for that specific head. But the model has many heads (typically 12-96), and the actual computation depends on what's in the value vectors, not just the attention pattern. A head that attends strongly to position 5 but has near-zero values at position 5 effectively ignores it. Attention weights are one signal, not the whole picture.

"Softmax just normalizes the scores." Softmax exponentiates and then normalizes. Its weight ratio is Ai/Aj=esi−sjA_i/A_j=e^{s_i-s_j}, so large score gaps produce concentrated distributions. Dividing raw scores by their sum is a different operation and may not even produce probabilities when scores are negative. The 1/dk1/\sqrt{d_k} factor controls the scale of those gaps under the assumptions above.

What's next

This post covered a single attention head. The next post covers multi-head attention and representation subspaces, where the transformer runs many of these heads in parallel on different projections of the same stream.


Additional reading (and watching)