TAI BUI
Back to curriculum
CapstonePython

Cross-Attention Fusion

Cross-attention is attention whose queries come from one stream and whose keys and values come from another. A projection layer aligns one image vector with one caption vector, but a real vision-language decoder needs every text token to attend to every patch token, so the model can ground each word in a region. The text queries; the vision keys and values answer. This lesson builds the cross-attention block, the causal text self-attention, and the mask shapes that keep both legal.

TIME: ~90 minutesPREREQUISITES: Phase 19 lessons 30-37 (Track B foundations)

Learning Objectives

  • Implement multi-head cross-attention where the query stream is text and the key/value stream is vision.
  • Compose a decoder block: causal self-attention + cross-attention + feed-forward.
  • Get the mask shapes right: causal mask for self-attention, no mask for cross-attention.
  • Run a forward pass with batched text tokens and a fixed pool of image tokens.
  • Explain how a KV cache lets the image keys and values be projected once and reused.

The Problem

Early fusion concatenates image tokens and text tokens into one sequence. Chameleon and Emu3 take this path. Late fusion keeps the two streams separate: the text decoder runs on text-only tokens and reaches into the image stream through cross-attention. This lesson's code adds it to every block; Flamingo inserts it every K blocks of a frozen language model, and later Flamingo-shaped decoders copied the design.

Late fusion has two advantages. The text stream stays clean, so the model preserves text-only capabilities. And the image stream is computed once per image and reused for every decode step, which keeps generation cheap even for long captions. The cost is one extra attention sub-layer per block.

Warm-up0/3
1. In standard attention, softmax(QK^T / sqrt(d)) is computed for each query position. What does the result give you?Warm-up
2. A decoder generates caption tokens one at a time. What must its self-attention mask prevent?Warm-up
3. A transformer sub-layer returns x + f(x) instead of just f(x). What does that residual connection give the layer?Warm-up
Check: The Problem0/5
1. What does early fusion do with image tokens and text tokens?Define
2. What is late fusion in a vision-language decoder?Define
3. Which pair of advantages does late fusion offer over early fusion?Explain
4. What is the cost of adding late fusion to a decoder?Explain
5. A depth-4 decoder adds cross-attention to every block, as this lesson's code does. How many cross-attention sub-layers does it have, and how many would a Flamingo-style K=2 layout have?Apply

The Concept

Mermaid diagram

Mermaid diagram

Mask shapes

The two attentions inside a decoder block need different masks:

AttentionQuery lengthKey lengthMaskWhy
Self-attentionNt (text)Nt (text)Causal: lower-triangular (Nt, Nt)Text tokens may not look ahead during autoregression
Cross-attentionNt (text)Nv (vision)No maskThe whole image is visible to every text position

The self-attention path validates its mask shape, so passing the wrong mask there surfaces as a ValueError instead of a silently broken loss curve. Cross-attention takes no mask argument at all.

Why no mask on cross-attention

The image is fully observed before any text is generated, and image patches have no temporal order. Token t of the caption may attend to any patch. Some Flamingo variants add a per-sample masking pattern when interleaving multiple images and text segments, but for a single image plus a caption, cross-attention sees everything.

Key/value caching

A KV cache holds the image keys and values, computed once at the start of the decode. Each new text token reads from the cache without recomputing them. This is what makes captioning fast at inference: the heavy ViT runs once, and the cross-attention reuses its keys and values at every step. The code exposes the cache, and the tests cover the cache-hit path. One limit to know: VisionLanguageDecoder.forward builds the cache inside a single call, so the demo proves the cached and uncached logits match but does not carry a cache across separate decode steps. A real generation loop would call build_kv_cache once and reuse it.

Block composition

A decoder block runs pre-LN -> self-attention -> residual -> pre-LN -> cross-attention -> residual -> pre-LN -> feed-forward -> residual. That is three sub-layers, each with its own LayerNorm. The Flamingo paper added a learned gate on cross-attention so the model could opt out of the image path and start training from text-only behavior, which helps stability. The canonical baseline, which this code uses, has no gate.

python
class DecoderBlock(nn.Module):
    def forward(self, x, memory, text_mask, kv_cache=None):
        x = x + self.self_attn(self.ln1(x), mask=text_mask)
        x = x + self.cross_attn(self.ln2(x), memory, kv_cache=kv_cache)
        x = x + self.ffn(self.ln3(x))
        return x
Check: The Concept0/15
1. What is cross-attention?Define
2. What does a causal mask do?Define
3. In this lesson, what does the KV cache hold?Define
4. What are memory tokens in this decoder?Define
5. Why can the cross-attention keys and values be computed once for an image?Explain
6. Why does cross-attention take no mask when there is a single image and a caption?Explain
7. What is the point of validating the mask shape in self-attention with a ValueError?Explain
8. What work does the KV cache remove at each decode step?Explain
9. Which extension recovers Flamingo-style stability by starting training from text-only behavior?Explain
10. In DecoderBlock.forward, where do the queries and the keys and values of the second sub-layer come from?Apply
11. You call a block on a 12-token caption but pass a (576, 576) mask as the self-attention mask. What happens?Apply
12. A teammate swaps the two masks: self-attention gets no mask and cross-attention gets a causal-style mask. What is the main failure?Analyze
13. A team calls decoder(text_ids, memory, use_cache=True) at each of 20 decode steps but still sees the image keys and values projected at every step. Why?Analyze
14. A decoder must read several images interleaved with text, and caption segment 2 must not attend to image 1. Which change fits this lesson's design?Design
15. You will generate a 30-token caption for one image step by step and want the image keys and values projected only once. Which design fits?Design

Build It

code/main.py implements:

  • CrossAttention(cfg), taking a DecoderConfig (hidden size, heads, depth, vocab sizes), a multi-head cross-attention with separate q and kv projections.
  • CausalSelfAttention(cfg), the masked self-attention from a standard decoder.
  • DecoderBlock, composing the three sub-layers with pre-LN residuals.
  • VisionLanguageDecoder, four-layer decoder fed by a mock vision encoder output and a small text embedding table.
  • causal_mask(length) returning a (length, length) lower-triangular boolean tensor.
  • A demo that feeds a batch of two text sequences of length 10 with image memory of length 197 and prints output shape, the self-attention mask shape, and the cross-attention output norm per position.

Run it:

bash
python3 code/main.py

The decoder produces a (2, 10, text_vocab) logits tensor, and the mask shape is (10, 10). The KV-cache check confirms the cached and uncached logits agree to within 1e-4.

Check: Build It0/6
1. What is the output shape of cross-attention when text length is Nt=10 and image length is Nv=197 with hidden=256?Apply
2. The demo feeds text ids of shape (2, 10), 197 image tokens and text_vocab 1024. What shape are the logits?Apply
3. With the demo config (hidden 256, 8 heads, batch 2, 197 image tokens), what shape must each cached key tensor have in one layer?Apply
4. In the demo's causal_mask, which keys can the token at index 3 attend to?Apply
5. Cross-attention scores form an Nt x Nv grid per head. At Nt=64, you move from 196 image tokens to 576. Roughly how does the score cost change?Apply
6. A new vision encoder outputs 768-dimensional tokens while the decoder hidden size stays 256. Which part of cross-attention has to change?Design

Use It

Cross-attention shows up in two production families:

  • Flamingo and IDEFICS. Insert a cross-attention sub-layer every K language model blocks, with a frozen LM. The vision-language adapter is the cross-attention block plus its gate.
  • BLIP-2. The Q-Former uses cross-attention from a fixed set of 32 query tokens into the image features, then projects the queries into the LM embedding space.

The block built here maps directly onto both, and so does the mask discipline: causal on self-attention, none on cross-attention.

Check: Use It0/2
1. Where do Flamingo and IDEFICS place cross-attention sub-layers in the language model?Explain
2. How does BLIP-2's Q-Former use cross-attention?Explain

Tests

code/test_main.py covers:

  • causal mask is lower-triangular and matches expected boolean shape
  • cross-attention output shape is (B, Nt, hidden) regardless of key length
  • KV-cache path matches uncached path to float tolerance
  • a batch-size mismatch between text and image streams, or a wrong-shaped causal mask, raises a clear ValueError
  • a full decoder forward pass produces the right batch and sequence shape

Run them:

bash
python3 -m unittest code/test_main.py
Check: Tests0/4
1. The text batch has 2 sequences but the image memory has batch size 3. What does `CrossAttention` do?Apply
2. A test builds a decoder with max_text_len 16 and feeds it a batch of 17 token ids per sequence. What happens?Apply
3. test_lower_triangular compares causal_mask(5) with torch.tril of a 5x5 matrix of ones. How many True entries does causal_mask(5) have?Apply
4. After a refactor, test_kv_cache_matches_uncached fails while every shape test still passes. What does that point to?Analyze

Exercises

  1. Add a learned tanh gate to the cross-attention residual (the Flamingo trick) and verify training converges from a near-zero initial gate. The gate starts at 0; the model recovers text-only behavior before mixing the image stream in.

  2. Implement interleaved attention where the same decoder consumes multiple images plus multiple text segments. Build the per-sample cross-attention mask that prevents text segment 2 from attending to image 1.

  3. Profile the cross-attention vs the self-attention layer at Nt=64, Nv=576 (raise max_text_len in DecoderConfig above its default of 32 first) (a 24x24 grid at higher resolution). The cross-attention cost is Nt * Nv and dominates at high image resolution.

  4. Add a query-side dropout on the cross-attention map and measure caption diversity on the demo (caption sample variance increases with dropout in the cross map).

  5. Swap the cross-attention layer for a Q-Former-style attention block where a fixed 32-token query pool attends to image features once per layer.

Key Terms

TermWhat it means
Late fusionText and vision stay in separate streams; cross-attention bridges them (in every block in this lesson's code)
Cross-attentionQ comes from one stream, K and V from another
Causal maskLower-triangular boolean mask that prevents looking ahead during autoregression
KV cacheImage keys and values stored once and reused for every decode step
Memory tokensThe frozen image tokens that the decoder reaches into
Author's Note

I'd build the block without the gate first. The Flamingo gate lets the model opt out of the image path, and the baseline here skips it, so all three sub-layers stay plain residual blocks. Exercise 1 is where I'd add the gate, once the shapes and masks behave.

My bet is that the mask table deserves a read before the code does. Causal on self-attention and none on cross-attention is a two-row rule, and the ValueError on a mismatched mask shape exists so you hear about a mix-up at the first forward pass instead of from a broken loss curve.

Further Reading

  • Flamingo (2022) for the canonical late-fusion design with gated cross-attention.
  • BLIP-2 (2023) for the Q-Former, which is a cross-attention block dressed as a learned query pool.
  • IDEFICS (2023) for an open-weight reproduction of the Flamingo recipe.

Code