Cross-Attention › The Cross-Attention Formulation
Language AI HandbookAll chapters 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:
- Where should I look? Each decoder position needs to identify which encoder positions contain relevant information.
- 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:
where contains the decoder's current representations and is a learned projection matrix. The resulting has one row per decoder position, each encoding "what am I looking for?" The projection 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:
where is the encoder output and is another learned matrix. The resulting 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 is separate from : queries and keys live in the same -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:
where projects into a "value space." The resulting 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:
The resulting score matrix has entry equal to the dot product between decoder query and encoder key . This is where the rectangular shape emerges: we're comparing queries against keys. Entry being large means decoder position is likely to attend strongly to encoder position , 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 normalizes the scores:
Why specifically? The reasoning comes from the expected magnitude of a dot product between two random unit vectors in dimensions. If the components of and are independent with mean zero and unit variance, the dot product has variance equal to , so its standard deviation is . Dividing by 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.


The histograms illustrate why scaling matters. With , raw dot products have a standard deviation around 8, producing scores that can easily reach or more. After scaling by , 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:
Each row of sums to 1.0, representing how one decoder position distributes its attention across all encoder positions. An entry means decoder position is directing 70% of its "attention budget" to encoder position . 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:
The output has shape : one row per decoder position, each containing information gathered from the encoder. If decoder position attends strongly to encoder position , then the value vector contributes heavily to the output for position . 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 are non-negative and sum to 1 along each row, so the output is literally a convex combination of the encoder's value vectors. Position 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:
where:
- : queries from the decoder, encoding "what information am I looking for?"
- : keys from the encoder, encoding "what information do I have to offer?"
- : values from the encoder, encoding "what content should I contribute?"
- : raw similarity scores measuring alignment between decoder queries and encoder keys
- : scaling factor that maintains healthy gradients during training
- : applied row-wise to produce probability distributions
- : query/key dimension (must match for the dot product)
- : value dimension (determines output size)
The attention weight matrix has shape , fundamentally different from self-attention's square matrix. This rectangular shape reflects the asymmetry of sequence-to-sequence tasks: we have positions that need to attend to 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 : 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.
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))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 matrix. The decoder has generated 4 tokens so far, giving us a 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:
# 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)Projection shapes: Q (from decoder): (4, 8) K (from encoder): (6, 8) V (from encoder): (6, 8)
Notice the asymmetry: has 4 rows (one per target token) while and 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 , we multiply a matrix by a matrix, yielding a score matrix. Each of the 4 decoder positions gets a similarity score against each of the 6 encoder positions.
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 informationAttention 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 , 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.

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.