Cross-Attention
One decoder token scores its query against every encoder key, then blends encoder values by those weights, so generation reads a different sequence without merging the two.
Why Does This Exist?
Self-attention pins queries, keys, and values to the same sequence. Every token reads the sequence it belongs to. That is exactly right when you build a picture of one passage. It is the wrong tool when a French decoder must read an English source sentence, a genuinely different sequence with its own length and its own job.
The older fix for this sat inside sequence-to-sequence models. An encoder squeezed a whole sentence into one fixed vector, and quality fell as sentences grew because one container cannot hold an unbounded sentence. The attention mechanism stopped the single squeeze and let the decoder score every encoder state at every step. Cross-attention is that same idea rebuilt on query-key-value machinery, so it slots straight into transformer blocks.
This page covers the split-source version: where each matrix comes from, how scaling and softmax turn scores into shares, and one hand-worked blend you can check. Self-attention is the sibling contrast with all three matrices from one sequence. Attention mechanism is the prerequisite score, weigh, and blend idea. Transformers show the full stack where cross-attention lives. If the bottleneck is new to you, read sequence-to-sequence first.
Think of It Like This
Describing a photo while you write
You write a caption with the photo propped beside the page. For each word you glance at the patch that word needs, linger where the detail sits, skim the rest, and write. Each glance differs because each word asks something different.
Your half-written line is the query, each photo patch is a key plus the detail it holds, your linger time is the weight, and the word you write is the blend. The page and the photo never merge into one object.
Where it stops: looking cannot add what the photo lacks. If the lens blurred a street sign, attention faithfully reads the blur. It routes the right patch to the right word, it does not repair the picture.
How It Actually Works
Cross-attention runs the familiar attention math with an unfamiliar split. The query asks from the target side. The keys and values answer from the source side.
Where do Q, K, and V come from?
Call the decoder states , with rows for target positions, and the encoder outputs , with rows for source positions. Here a row means one token vector, and and rarely match in translation. Three learned matrices , , and project both sides into one shared width , where (read dee-kay) is the key dimension:
So holds queries from the decoder, holds keys from the encoder, and holds values from the encoder. A query says what the current target position needs. A key says what a source position offers. A value carries the source content that flows forward when its key matches. Because and come from fully computed encoder output, the decoder reads that output without editing it in place.
Score, scale, soften, blend
Every query scores against every key in one matrix multiply, then scales, softens, and blends:
is by , one row per decoder position and one column per encoder position, so the table need not be square. Dividing by keeps the scores near unit variance when query and key parts start with zero mean and unit variance, which stops early softmax spikes. Softmax turns each row into weights that sit between 0 and 1 and sum to exactly 1. Multiplying by mixes the encoder values by those shares. The diagram above shows this for one row: one query, three keys, three weights, one blended output.
Worked example: one query against three keys
Take , so , with decoder query and three encoder keys , , and . Raw dot products give:
Scale by 2:
Exponentiate for softmax: , , . Add them: . Divide each by the sum:
The shares sum to . The leader takes most of the mass without taking all of it, which is why the output stays a blend.
Give those positions scalar stand-in values and mix:
One number out, , leaning hard toward the first encoder state yet still touched by the second. The picture draws these same numbers: scaled scores , weights , values , output .
Code
Same step, run for real: soften the scaled scores, check they sum to one, mix the values.
import numpy as np
def softmax(z: np.ndarray) -> np.ndarray: e: np.ndarray = np.exp(z - np.max(z)) return e / np.sum(e)
scores: np.ndarray = np.array([2.0, 0.5, -1.0])weights: np.ndarray = softmax(scores)values: np.ndarray = np.array([4.0, 2.0, 0.0])context: float = float(weights @ values)
print(f"{weights[0]:.3f} {weights[1]:.3f} {weights[2]:.3f}") # -> 0.786 0.175 0.039print(f"{float(np.sum(weights)):.1f}") # -> 1.0print(f"{context:.2f}") # -> 3.49The printout matches the hand computation: weights , , summing to , and a blended context of .
Watch Out For
Letting padding soak up the weights
Source batches pad short sentences to equal length, and those pad positions still score unless you stop them. Our would flatten toward the pads, and the would drift toward whatever vector the pads carry, often zeros that drag every context down.
Symptom: short sentences translate worse than long ones in the same batch, and weight plots show mass sitting on trailing pad columns. Fix: add a pad mask of negative infinity to padded scores before the softmax, so pads get exactly and the live weights still sum to .
Building the query from the wrong side
In custom code it is easy to wire from the encoder and , from the decoder, since the call looks symmetric. The shapes still multiply when lengths differ, but the output then has one row per source token instead of one per target token, and generation conditions on itself while ignoring the source it should read.
Symptom: output length tracks the source length, or loss drops while translations read fluently yet miss source content. Fix: keep the direction fixed, always from the querying target sequence and , always from the read source sequence, and assert the output has rows for decoder positions.
The Quick Version
- Cross-attention keeps the score, softmax, and blend formula but takes queries from the decoder while keys and values come from the encoder.
- Raw dots [4, 1, -2] scale by 2 into [2.0, 0.5, -1.0], soften into [0.786, 0.175, 0.039], and blend values [4.0, 2.0, 0.0] into 3.49.
- The score table spans m target rows by n source columns, so source and target lengths never need to match.
- Encoder keys and values get built once and reused, while each new decoder token builds a fresh query.
- Padding positions need a mask before the softmax, or the shares leak onto tokens that carry no meaning.