Cross-Attention › The Cross-Attention Formulation

Michael BrenndoerferUpdated June 18, 202514 min read
Language AI HandbookAll chapters
Part 16 of 67Transformer Architectures
Chapter 114Cross-Attention › The Cross-Attention Formulation
Reading section 3 of 14 · The Cross-Attention Formulation · 14 minSection 3 of 14 · 14 min

Book contents

Section 3 of 14 · 14 min read

Cross-Attention › The Cross-Attention FormulationOne section · Sections in this chapter3 / 14 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

To understand cross-attention deeply, we need to think about what problem it solves. Imagine you're translating "The cat sat on the mat" into French. You've already generated "Le chat" (the cat), and now you need to produce the next word. Which part of the English sentence should you focus on? The answer is "sat," because that's the verb that follows the subject. The model doesn't know this in advance; it has to figure it out by examining the decoder's current state (which encodes the fact that you've generated a subject) and comparing it against every position in the encoder output to find the best match.

This is precisely what cross-attention computes: for each position in the target sequence, it determines which positions in the source sequence are most relevant, then gathers information from those positions. The mechanism needs to answer two questions simultaneously:

  1. Where should I look? Each decoder position needs to identify which encoder positions contain relevant information.
  2. What should I extract? Once the relevant positions are identified, the decoder needs to pull out the appropriate information.

The query-key-value framework elegantly separates these concerns. Queries encode what the decoder is looking for, keys encode what each encoder position offers, and values encode the actual information to transmit. The separation between keys and values is conceptually similar to the separation between an index and the data it points to: the key is a compact description used for matching, while the value is the full content that gets transmitted once a match is found.

Building the Formula Step by Step

Let's construct the cross-attention formula piece by piece, understanding why each component is necessary.

Step 1: Create queries from the decoder. Each decoder position needs to express what information it's seeking. We project the decoder representations into a "query space." Think of each query as a search query that the decoder sends out: "I'm looking for information about X." We compute this with a linear projection:

Q=XdecWQQ = X_{\text{dec}} W^Q

where Xdec∈Rndec×dX_{\text{dec}} \in \mathbb{R}^{n_{\text{dec}} \times d} contains the decoder's current representations and WQ∈Rd×dkW^Q \in \mathbb{R}^{d \times d_k} is a learned projection matrix. The resulting Q∈Rndec×dkQ \in \mathbb{R}^{n_{\text{dec}} \times d_k} has one row per decoder position, each encoding "what am I looking for?" The projection WQW^Q is learned: during training, the model learns to map decoder states into a query space where semantically or syntactically related queries and keys produce high dot products.

Step 2: Create keys from the encoder. Each encoder position needs to advertise what information it contains. We project the encoder output into a "key space" using a different projection:

K=XencWKK = X_{\text{enc}} W^K

where Xenc∈Rnenc×dX_{\text{enc}} \in \mathbb{R}^{n_{\text{enc}} \times d} is the encoder output and WK∈Rd×dkW^K \in \mathbb{R}^{d \times d_k} is another learned matrix. The resulting K∈Rnenc×dkK \in \mathbb{R}^{n_{\text{enc}} \times d_k} has one row per encoder position, each encoding "here's what I offer." The key space is designed to match the query space: a query that's looking for a verb should produce a high dot product with keys that correspond to verb positions in the source. Critically, the projection WKW^K is separate from WQW^Q: queries and keys live in the same dkd_k-dimensional space, but they are projected there by different matrices, giving the model flexibility to learn asymmetric matching.

Step 3: Create values from the encoder. When attention flows to an encoder position, we need to specify what information transfers. The value projection captures this:

V=XencWVV = X_{\text{enc}} W^V

where WV∈Rd×dvW^V \in \mathbb{R}^{d \times d_v} projects into a "value space." The resulting V∈Rnenc×dvV \in \mathbb{R}^{n_{\text{enc}} \times d_v} contains the content that will be aggregated. The value space is separate from the key space: the key is what you use to find a match, and the value is what you receive once the match is made. This separation allows the model to learn different representations for retrieval (keys) and content transmission (values).

Step 4: Compute similarity scores. Now we need to measure how well each query matches each key. The dot product is ideal for this: when two vectors point in similar directions (when they are highly correlated in the embedding space), their dot product is large. We compute all pairwise scores with a single matrix multiplication:

S=QKTS = Q K^T

The resulting score matrix S∈Rndec×nencS \in \mathbb{R}^{n_{\text{dec}} \times n_{\text{enc}}} has entry (i,j)(i, j) equal to the dot product between decoder query ii and encoder key jj. This is where the rectangular shape emerges: we're comparing ndecn_{\text{dec}} queries against nencn_{\text{enc}} keys. Entry (i,j)(i,j) being large means decoder position ii is likely to attend strongly to encoder position jj, because their query and key representations are well-aligned.

Step 5: Scale to prevent saturation. In high dimensions, dot products tend to have large magnitudes, which would push softmax into saturation. When a softmax input contains very large values, the output becomes nearly one-hot: almost all probability mass concentrates on the maximum entry, and gradients for non-maximum entries vanish. This makes learning very slow or unstable. Dividing by dk\sqrt{d_k} normalizes the scores:

Sscaled=QKTdkS_{\text{scaled}} = \frac{Q K^T}{\sqrt{d_k}}

Why dk\sqrt{d_k} specifically? The reasoning comes from the expected magnitude of a dot product between two random unit vectors in dkd_k dimensions. If the components of QQ and KK are independent with mean zero and unit variance, the dot product Qi⋅KjQ_i \cdot K_j has variance equal to dkd_k, so its standard deviation is dk\sqrt{d_k}. Dividing by dk\sqrt{d_k} normalizes this to unit variance, keeping the scores in a range where softmax gradients remain healthy throughout training. The original "Attention Is All You Need" paper noted this instability explicitly and proposed the scaling factor as the fix.

Out[3]:
Visualization
Histogram of raw attention scores showing wide spread from -4 to +4.
Raw dot product scores before scaling. In high dimensions, scores can have large magnitudes (here from roughly -4 to +4 with d=64).
Histogram of scaled attention scores showing compressed spread from -0.5 to +0.5.
Scores after dividing by sqrt(d_k). Scaling compresses the range, keeping softmax in its sensitive region where gradients flow well.

The histograms illustrate why scaling matters. With dk=64d_k = 64, raw dot products have a standard deviation around 8, producing scores that can easily reach ±20\pm 20 or more. After scaling by 64=8\sqrt{64} = 8, the standard deviation drops to approximately 1, keeping scores in a range where softmax produces meaningful gradients. This is not a minor detail: without the scaling factor, training deep transformer models becomes significantly more difficult. The gradients from the softmax layer become nearly zero for most attention entries, meaning the model barely learns to adjust its attention patterns. The fix is elegantly simple, and it works.

Step 6: Convert scores to attention weights. We apply softmax row-wise to convert each row of scores into a probability distribution over encoder positions. Each row of the score matrix represents one decoder position asking questions of all encoder positions. The softmax converts these raw scores into a probability distribution, interpreting the scores as un-normalized log-probabilities:

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

Each row of AA sums to 1.0, representing how one decoder position distributes its attention across all encoder positions. An entry Aij=0.7A_{ij} = 0.7 means decoder position ii is directing 70% of its "attention budget" to encoder position jj. The softmax is applied row-wise, so each decoder position independently computes its own distribution over the encoder. This means different decoder positions can attend to completely different parts of the encoder simultaneously.

Step 7: Aggregate values. Finally, we use the attention weights to compute a weighted sum of encoder values for each decoder position. This is the step where information flows from encoder to decoder:

Output=AV\text{Output} = A V

The output has shape (ndec×dv)(n_{\text{dec}} \times d_v): one row per decoder position, each containing information gathered from the encoder. If decoder position ii attends strongly to encoder position jj, then the value vector VjV_j contributes heavily to the output for position ii. The weighted average captures the decoder's selective reading of the encoder: the output is not a copy of any single encoder representation, but a blend computed according to the decoder's current needs.

Why does this formula make sense? Notice that the softmax attention weights AA are non-negative and sum to 1 along each row, so the output AVAV is literally a convex combination of the encoder's value vectors. Position ii in the decoder output is a weighted mixture of all encoder values, with weights determined by how well each encoder key matched that decoder's query. The model learns to assign high weights to relevant encoder positions and low weights to irrelevant ones, entirely through gradient descent on the end task.

The Complete Formula

Combining all steps, we arrive at the cross-attention formula:

CrossAttention(Q,K,V)=softmax(QKTdk)V\text{CrossAttention}(Q, K, V) = \text{softmax}\left(\frac{Q K^T}{\sqrt{d_k}}\right) V

where:

  • Q=XdecWQ∈Rndec×dkQ = X_{\text{dec}} W^Q \in \mathbb{R}^{n_{\text{dec}} \times d_k}: queries from the decoder, encoding "what information am I looking for?"
  • K=XencWK∈Rnenc×dkK = X_{\text{enc}} W^K \in \mathbb{R}^{n_{\text{enc}} \times d_k}: keys from the encoder, encoding "what information do I have to offer?"
  • V=XencWV∈Rnenc×dvV = X_{\text{enc}} W^V \in \mathbb{R}^{n_{\text{enc}} \times d_v}: values from the encoder, encoding "what content should I contribute?"
  • QKT∈Rndec×nencQ K^T \in \mathbb{R}^{n_{\text{dec}} \times n_{\text{enc}}}: raw similarity scores measuring alignment between decoder queries and encoder keys
  • dk\sqrt{d_k}: scaling factor that maintains healthy gradients during training
  • softmax(⋅)\text{softmax}(\cdot): applied row-wise to produce probability distributions
  • dkd_k: query/key dimension (must match for the dot product)
  • dvd_v: value dimension (determines output size)

The attention weight matrix has shape (ndec×nenc)(n_{\text{dec}} \times n_{\text{enc}}), fundamentally different from self-attention's square (n×n)(n \times n) matrix. This rectangular shape reflects the asymmetry of sequence-to-sequence tasks: we have ndecn_{\text{dec}} positions that need to attend to nencn_{\text{enc}} positions, and these lengths are typically different. For translating a 10-word English sentence into a 12-word French sentence when 8 French words have been generated, the attention matrix is 8×108 \times 10: eight rows for the eight generated target positions, ten columns for the ten source positions.

Tracing Through the Computation

Let's make this concrete by tracing the shapes through a realistic example. We'll simulate translating a 6-word English sentence into French, where we've generated 4 words so far.

In[4]:
Code
import numpy as np

# Example dimensions for translation
n_enc = 6  # Source: "The cat sat on the mat" (6 tokens)
n_dec = 4  # Target so far: "Le chat s'assit sur" (4 tokens)
d_model = 8  # Model dimension
d_k = 8  # Query/key dimension
d_v = 8  # Value dimension

# Encoder output (fixed representations of the source sentence)
encoder_output = np.random.randn(n_enc, d_model)

# Decoder state (evolving representations of generated target)
decoder_state = np.random.randn(n_dec, d_model)

# Learned projection matrices (Xavier initialization for realistic attention variation)
W_Q = np.random.randn(d_model, d_k) * np.sqrt(2.0 / (d_model + d_k))
W_K = np.random.randn(d_model, d_k) * np.sqrt(2.0 / (d_model + d_k))
W_V = np.random.randn(d_model, d_v) * np.sqrt(2.0 / (d_model + d_v))
Out[5]:
Console
Input shapes:
  Encoder output: (6, 8) (source_len × d_model)
  Decoder state:  (4, 8) (target_len × d_model)

The encoder has processed all 6 source tokens, producing a (6×8)(6 \times 8) matrix. The decoder has generated 4 tokens so far, giving us a (4×8)(4 \times 8) state matrix. These different sequence lengths are the essence of cross-attention: we are not comparing a sequence to itself, but bridging two sequences of different lengths with different purposes.

Now we project into QKV spaces:

In[6]:
Code
# Q from decoder, K and V from encoder
Q = decoder_state @ W_Q  # (n_dec, d_k) = (4, 8)
K = encoder_output @ W_K  # (n_enc, d_k) = (6, 8)
V = encoder_output @ W_V  # (n_enc, d_v) = (6, 8)
Out[7]:
Console
Projection shapes:
  Q (from decoder): (4, 8)
  K (from encoder): (6, 8)
  V (from encoder): (6, 8)

Notice the asymmetry: QQ has 4 rows (one per target token) while KK and VV have 6 rows (one per source token). This is exactly what we expect. The query matrix has one row per decoder position because each decoder position formulates its own search query. The key and value matrices have one row per encoder position because each encoder position advertises its content and provides its value. When we compute QKTQ K^T, we multiply a (4×8)(4 \times 8) matrix by a (8×6)(8 \times 6) matrix, yielding a (4×6)(4 \times 6) score matrix. Each of the 4 decoder positions gets a similarity score against each of the 6 encoder positions.

In[8]:
Code
def softmax(x):
    """Numerically stable softmax."""
    exp_x = np.exp(x - x.max(axis=-1, keepdims=True))
    return exp_x / exp_x.sum(axis=-1, keepdims=True)


# Step-by-step attention computation
scores = Q @ K.T  # (4, 6) raw similarity scores
scores_scaled = scores / np.sqrt(d_k)  # Scale to prevent saturation
attention_weights = softmax(scores_scaled)  # (4, 6) probability distributions
output = attention_weights @ V  # (4, 8) gathered information
Out[9]:
Console
Attention computation shapes:
  Raw scores:        (4, 6) (target_len × source_len)
  Scaled scores:     (4, 6)
  Attention weights: (4, 6)
  Output:            (4, 8) (target_len × d_v)

Each of the 4 decoder positions attends to all 6 encoder positions
Row sums (should be 1.0): [1. 1. 1. 1.]

The output has shape (4×8)(4 \times 8), matching the decoder's sequence length but with the value dimension. Each of the 4 decoder positions now contains a weighted mixture of information from the encoder, with the weights determined by query-key similarity. This enriched representation flows to the next stage of the decoder, helping predict the next target token. The row sums confirm that the attention weights form valid probability distributions over the source positions.

Out[10]:
Visualization
Heatmap showing a 4x6 attention weight matrix with decoder tokens on y-axis and encoder tokens on x-axis.
The rectangular shape of cross-attention weights. Unlike self-attention's square matrix where every token attends to every other token in the same sequence, cross-attention produces a rectangular matrix where each decoder position (rows) attends to all encoder positions (columns). The matrix has 4 rows (target tokens) and 6 columns (source tokens). This reflects the different lengths of the two sequences.

The heatmap reveals how each decoder token distributes its attention across the source sequence. Row sums equal 1.0 (each row is a probability distribution), but column sums can vary: some source tokens receive more total attention than others. In a trained model, you would often see diagonal-ish patterns for monotonic languages (where words tend to translate in order) or more complex patterns for languages with different word orders.