Cross-Attention

Cross-attention is attention in which the queries come from one sequence and the keys and values come from another. Self-attention lets the positions of a sequence read each other; cross-attention lets the positions of one sequence read a different sequence. It is the standard way to condition one stream on another: a decoder reading an encoder’s output in translation, and a diffusion denoiser reading a text prompt when it generates an image.

The computation

Let \(X\in\mathbb{R}^{N_q\times d_x}\) be the sequence that is being updated (for a diffusion model, one row per spatial position of the latent image) and \(C\in\mathbb{R}^{N_c\times d_c}\) the sequence that is read (one row per text token). Three learned projections give

\[ Q = XW_Q,\qquad K = CW_K,\qquad V = CW_V,\qquad \text{out} = \mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d}}\right)V , \]

with \(Q\in\mathbb{R}^{N_q\times d}\), \(K,V\in\mathbb{R}^{N_c\times d}\) and \(d\) the head dimension. The score matrix \(QK^\top\) has one row per query position and one column per token. The softmax runs along each row, so every position receives a convex combination of the token values, and its weights say which words matter at that position. The input and output lengths match the updated sequence: \(N_q\) rows in, \(N_q\) rows out. The conditioning length \(N_c\) disappears in the product. In self-attention the same formula applies with \(C=X\).

Shapes in a diffusion model

The program below uses a 32×32 latent grid (1,024 positions), a prompt padded to 77 tokens, and made-up feature widths of 320 for the image and 768 for the text. Weights are random, so it checks shapes and cost, not meaning.

import numpy as np
rng = np.random.default_rng(0)
H = W = 32; Nq = H * W; Nt = 77; d_img = 320; d_txt = 768; d = 64
x = rng.normal(size=(Nq, d_img)); c = rng.normal(size=(Nt, d_txt))
Wq = rng.normal(size=(d_img, d)) / np.sqrt(d_img)
Wk = rng.normal(size=(d_txt, d)) / np.sqrt(d_txt)
Wv = rng.normal(size=(d_txt, d)) / np.sqrt(d_txt)
Q, K, V = x @ Wq, c @ Wk, c @ Wv
S = Q @ K.T / np.sqrt(d)
A = np.exp(S - S.max(1, keepdims=True)); A /= A.sum(1, keepdims=True)
out = A @ V
print("Q", Q.shape, "K", K.shape, "V", V.shape, "A", A.shape, "out", out.shape)
print("row sums min/max:", A.sum(1).min().round(6), A.sum(1).max().round(6))
for res in (32, 64, 128):
    n = res * res
    print(f"latent {res}x{res}: self-attn pairs {n*n:>12,}  cross-attn pairs (77 tokens) {n*77:>10,}")
Q (1024, 64) K (77, 64) V (77, 64) A (1024, 77) out (1024, 64)
row sums min/max: 1.0 1.0
latent 32x32: self-attn pairs    1,048,576  cross-attn pairs (77 tokens)     78,848
latent 64x64: self-attn pairs   16,777,216  cross-attn pairs (77 tokens)    315,392
latent 128x128: self-attn pairs  268,435,456  cross-attn pairs (77 tokens) 1,261,568

Three consequences follow from the shapes.

  • The attention matrix is 1,024×77, one distribution over the tokens for each position. Reshaping a column back to the 32×32 grid gives a map of where one word acts, which is why cross-attention maps are used to inspect and edit generations.
  • The cost grows with \(N_qN_c\), not \(N_q^2\). With 77 tokens, text conditioning at a 32×32 latent needs about 13 times fewer score entries than self-attention over the image, and the gap grows with resolution (about 213 times at 128×128).
  • Text enters only through \(K\) and \(V\). Every position reads the same token matrix, with different weights, so the text is a shared memory that the image consults, not a signal added to a fixed place.

Why it conditions well, and where it falls short

Because each position chooses its own weights, a prompt such as “a red cube and a blue sphere” can send the left part of the image toward “red cube” and the right toward “blue sphere”. Nothing forces this: which attribute attaches to which object is learned, and a prompt with several objects often swaps or blends attributes. Keys and values also depend on the text encoder, so a token that the encoder represents poorly is poorly available to every position.

The softmax requires each position to spend its full weight of 1 on some token, even where the prompt says nothing about that region, so models tend to park the weight on a few uninformative tokens such as the start token. Cross-attention weights therefore show where the model reads, not a proof that a word caused the pixels there.

Neighbors

Self-attention uses one sequence for queries, keys and values. Concatenating text tokens with image tokens and applying self-attention to the joint sequence has the same expressive power for reading text, but costs \((N_q+N_c)^2\) and lets text tokens be updated too, which is how some newer diffusion architectures work. Channel concatenation and feature modulation (a scale and shift computed from a pooled vector) condition on a single vector or an aligned map and cannot let different positions read different tokens.


Discover more from Insightful Data Lab

Subscribe to get the latest posts sent to your email.

Similar Posts

Questions, corrections, or additional insights?

This site uses Akismet to reduce spam. Learn how your comment data is processed.