TAI BUI
Back to curriculum
CapstonePython

Vision Transformer Encoder

Patches alone do not see. A 12-layer pre-LN transformer with 12 attention heads turns the sequence of patch tokens into a sequence of contextual tokens, and the CLS token's final hidden state pools features from the whole image. This is the engine room of every modern vision-language model.

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

Learning Objectives

  • Implement a pre-LN transformer block with multi-head self-attention and a feed-forward sub-layer.
  • Stack 12 blocks with 12 heads to form a ViT-Base encoder.
  • Wire the patch front end from lesson 58 into the encoder and run a forward pass.
  • Verify that the CLS token aggregates information from every patch.

The Problem

A Vision Transformer encoder is a stack of transformer blocks that turns a sequence of patch tokens into a sequence of contextual tokens. The patch embedding hands it 197 tokens, each a vector with no awareness of any other patch. To recognize a cat, every patch has to learn which patches hold whiskers, which hold background, and which hold the eye. Attention builds that awareness one layer at a time. Without it, the patch front end is a clever tokenizer and nothing more.

The standard recipe at ViT-Base scale is twelve blocks deep and twelve heads wide, with pre-LayerNorm placement, GELU activation, and a 4x feed-forward expansion. That block shape (scaled in width and depth) is the spine of CLIP ViT-L, SigLIP, DINOv2, the Qwen-VL family, InternVL, and the other open-weight vision encoders of 2025-2026. It is stable enough that you can read any of those papers and assume this block shape unless they say otherwise.

Warm-up0/3
1. In a transformer, which mechanism lets one token's vector take in information from the other tokens in the sequence?Warm-up
2. In a deep network, what does a residual (skip) connection do?Warm-up
3. Layer normalization computes its mean and variance over which values?Warm-up
Check: The Problem0/1
1. Why does the encoder need attention layers after the patch embedding?Explain

The Concept

Mermaid diagram

Mermaid diagram

Pre-LN vs post-LN

Pre-LN means LayerNorm runs before each sub-layer, inside the residual branch. Post-LN, the original Transformer placement, runs it after the residual add. Modern vision-language models use pre-LN because it trains stably without learning-rate warm-up tricks. The difference is one line in the forward pass, and the gradient flow at depth 12+ is night and day.

Multi-head self-attention

Multi-head self-attention runs several attention heads in parallel, and each head projects the token vector to its own (query, key, value) triple of dimension head_dim = hidden / num_heads. With hidden = 768 and heads = 12, each head has dim = 64. The 12 heads attend in parallel, their outputs concatenate back to dimension 768, and an output projection mixes them. Splitting the work this way lets one head learn "attend to the cat eye" while another learns "attend to the background gradient" without interference.

Why the 4x feed-forward expansion

The feed-forward sub-layer (FFN) goes hidden -> 4 * hidden -> hidden with GELU in the middle. The factor 4 is empirical and has held across language and vision transformers since 2017. Smaller (2x) underfits, and larger (8x) overfits at a fixed data budget. The MLP is where the model stores most of its learned facts, and the wider middle is where they sit.

ComponentParameters at ViT-Base scale
qkv projection per block3 * 768 * 768 = 1.77M
output projection per block768 * 768 = 590K
FFN per block (4x expansion)2 * 768 * 4 * 768 = 4.72M
LayerNorm per block4 * 768 = 3K
Total per blockabout 7.1M
12 blocksabout 85M
Plus front endabout 86M total

The table counts weight matrices only. Biases and the front end's patch projection and position table add a small remainder, which is why the demo's printed total lands near, not exactly on, 86M.

ViT-Base is an 86M-parameter encoder. That is small by 2026 standards (SigLIP-So400M is 400M, the Qwen-VL ViT is 675M), but the architecture is the same up to width and depth.

Causal mask or not?

Vision Transformers are encoder-only and bidirectional: token i may attend to token j for any pair, so there is no mask. The decoder-side cross-attention in lesson 61 uses a causal mask, but inside the vision encoder attention is fully connected.

What the CLS token learns

The CLS token is a learned parameter with no patch content of its own. It picks up information through attention across every block, and by the final layer its row is a vector summary of the whole image. Downstream heads project that single vector into class logits, contrastive embeddings, or cross-attention keys for a text decoder.

Check: The Concept0/18
1. What is pre-LN in a transformer block?Define
2. What is self-attention?Define
3. What does multi-head attention do with the hidden dimension?Define
4. What is FFN expansion?Define
5. What is CLS pooling?Define
6. Does the ViT encoder need a causal attention mask?Explain
7. The CLS token starts as a learned parameter with no patch content. Why can its final hidden state still summarize the whole image?Explain
8. Why does the encoder split attention across several heads instead of using a single head?Explain
9. What role does the lesson give the feed-forward sub-layer, as opposed to attention?Explain
10. What reason does the lesson give for using a 4x feed-forward expansion?Explain
11. A 768-dimensional vector is split evenly across 12 parallel attention heads. How many dimensions does each head get?Apply
12. SigLIP So400M uses hidden=1152 and heads=16. How many dimensions does each head get?Apply
13. A ViT-Base block has about 7.1M parameters, the encoder stacks 12 of them, and the front end adds a small remainder. About how many parameters does the whole encoder have?Apply
14. In the per-block table, the FFN has 4.72M of a block's roughly 7.1M parameters. About what share of the block is that?Apply
15. You swap pre-LN for post-LN in a 12-block ViT and training diverges at your usual learning rate with no warm-up. Which explanation fits the lesson?Analyze
16. Someone adds a lower-triangular mask to every attention layer of the ViT, so token i attends only to tokens j <= i. The CLS token sits at position 0. What goes wrong for the CLS summary?Analyze
17. You want to reuse the Block class as a decoder block in a text model. Which change fits the lesson's design?Design
18. You want a narrower encoder with hidden=512 that keeps the 64-dimension heads used in ViT-Base. Which configuration is valid?Design

Build It

code/main.py implements:

  • MultiHeadSelfAttention, with qkv and output projections, the scaled-dot-product attention math, and a (B, N, D) input check that raises ValueError on any other rank.
  • FeedForward, the 4x-expansion GELU MLP.
  • Block, a pre-LN block that composes the attention and feed-forward sub-layers with residuals.
  • ViT, a stack of 12 blocks with a final LayerNorm.
  • VisionEncoder, which wires VisionFrontEnd from lesson 58 to the ViT stack. Its forward() returns the contextual sequence and the pooled CLS vector.
  • A demo that runs a synthesized 224x224 fixture image through the full encoder and prints the config, front-end, ViT and total parameter counts, the input and output shapes, and the CLS norm at every other layer. It also prints a softmax row-sum check on block 0 and the gradient norms reaching the patch projection and the CLS token.

Run it:

bash
python3 code/main.py

The fixture encodes to a (1, 197, 768) tensor. The CLS norm drifts upward as the layers compose, then stabilizes at the final LayerNorm. Total parameters come to about 86M.

Check: Build It0/6
1. A FeedForward is built from ViTConfig(hidden=768, mlp_ratio=4.0). What are fc1.out_features and fc2.out_features?Apply
2. MultiHeadSelfAttention is called on a tensor of shape (5, 64), with no batch axis. What happens?Apply
3. A small config (image_size=64, patch_size=16, hidden=64, depth=1, heads=4) encodes one image of shape (1, 3, 64, 64). What is the shape of the output tokens?Apply
4. The demo prints CLS norm 35.802 after block 12 and 27.713 on the final line of the trace. Which step produces that drop?Apply
5. The encoder's VisionEncoder.forward returns two values. What are they, for a batch of B images and hidden width D?Apply
6. A teammate says they built the lesson's pre-LN block, but their forward is x = self.ln1(x + self.attn(x)) followed by x = self.ln2(x + self.ffn(x)). What is wrong?Analyze

Use It

Up to width and depth, the encoder defined here is the same block stack that ships inside every open-weight VLM in 2025-2026. The differences live in:

  • Width and depth. ViT-Large is hidden=1024, depth=24, heads=16; SigLIP So400M is hidden=1152, depth=27, heads=16. Same block.
  • Pooling head. CLS pooling (this lesson) vs average pooling (SigLIP) vs attention pooling (later VLMs).
  • Position handling. Fixed sinusoidal (lesson 58) vs learned 1D vs ALiBi vs 2D RoPE. The block math is unchanged.
  • Register tokens. DINOv2 prepends 4 extra learned tokens. One line of code.

This block stack is the substrate, and lessons 60-63 build on top of it.

Check: Use It0/5
1. Which extension adds learned tokens to the sequence while leaving the block math unchanged?Define
2. How do ViT-Large and SigLIP So400M relate to the ViT-Base recipe in this lesson?Explain
3. A vision-language model wants one image vector without relying on the CLS token. Which pooling does the lesson name?Explain
4. Why can encoders swap the fixed sinusoidal position table for learned 1D embeddings, ALiBi or 2D RoPE?Explain
5. You want four DINOv2-style register tokens on this encoder. Which change is enough?Design

Tests

code/test_main.py has eight tests:

  • attention preserves the input shape, and its attention rows sum to one along the key axis (softmax sanity)
  • the feed-forward layer widens to hidden * mlp_ratio and contracts back to hidden
  • a block's residual path is wired: the output keeps the input shape and differs from the input
  • a 4-layer VisionEncoder forward pass returns the right shapes for the token sequence and the CLS vector
  • gradients flow to the patch projection from a loss on the CLS output
  • head_dim divides hidden, and a hidden that heads do not divide is rejected with ValueError

The gradient test backprops (cls * cls).sum(), not cls.sum(). The encoder ends in a LayerNorm with weight 1 and bias 0, so each output vector sums to zero for any input, and the gradient of a plain sum is exactly zero even when the wiring is fine. Squaring first makes the loss depend on the values.

Nothing tests invariance to batch size, a zero input, or the ValueError on a non-(B, N, D) input. Treat those as unchecked.

Run them:

bash
python3 -m unittest code/test_main.py
Check: Tests0/3
1. A block's attention runs with store_attn=True on a (2, 5, 64) input with 4 heads. What is the shape of the stored attention weights, and along which axis do they sum to one?Apply
2. Your shape checks pass, but you suspect the patch front end is disconnected from the encoder. Which check from the lesson best confirms the wiring?Analyze
3. A test backprops cls.sum() through the whole encoder, which ends in a LayerNorm with weight 1 and bias 0, and the patch projection's gradient norm is exactly 0.0. The demo backprops (c * c).sum() and gets a nonzero gradient. What explains the difference?Analyze

Exercises

  1. Add register tokens (4 learned vectors prepended after CLS) and rerun. Compare attention map smoothness via the entropy of the softmax distribution on the last layer.

  2. Swap pre-LN for post-LN and train for one epoch on a synthetic shape classifier. Observe which one trains stably without LR warm-up.

  3. Implement causal masking as an attn_mask argument so the same block can be reused as a decoder block. The mask shape is (seq, seq), lower-triangular.

  4. Profile a forward pass at batch sizes 1, 8, 64 with torch.profiler. Check which of the MLP and attention dominates wall time. The FFN holds about two thirds of each block's parameters, so it is the natural suspect, but let the profiler say.

  5. Replace one attention head's q-k-v projections with a low-rank LoRA adapter, freeze the rest, and verify the gradient only flows where you expect.

Key Terms

TermWhat it means
Pre-LNLayerNorm applied before each sub-layer instead of after
Self-attentionEach token attends to every other token in the same sequence
Multi-headThe hidden dim is split across H independent attention heads
FFN expansionThe feed-forward layer widens to 4 * hidden before contracting
CLS poolingUse the first token's final hidden state as the image summary
Author's Note

I'd run the post-LN swap from the exercises yourself instead of taking my word for the stability claim. The change is one line in the forward pass, so the experiment is cheap to set up, and seeing which version trains without warm-up will stick better than a sentence I wrote.

My bet is that the CLS token is the part worth staring at. It starts as a learned parameter with no patch content, and the image information it ends up holding arrives through attention. When you wire the front end in, I'd check that gradients reach the patch projection from the CLS output, as the tests do, before trusting the shapes alone.

Further Reading

  • An Image is Worth 16x16 Words (ViT, 2021) for the encoder recipe.
  • DINOv2 (2023) for register tokens and the self-supervised pretraining objective.
  • SigLIP (2023) for the average-pooling variant and the sigmoid contrastive loss used in lesson 62.

Code