How a photograph becomes a sequence of patch tokens, why a model with almost no built-in knowledge of images beats the convolutional networks that had it, and how the same tokens are trained, made hierarchical, and turned into boxes and masks. From the first strided convolution to SAM 3, at interview depth.
Images as sequences of patches
This chapter is about the Vision Transformer, or ViT, a model that reads an image the way a language model reads a sentence. It cuts the image into small squares, turns each square into a vector called a token, and runs an ordinary Transformer over the sequence. That one design now sits inside most of vision. The image encoders of CLIP and of vision-language models are ViTs, and so are the encoder of Segment Anything and the diffusion Transformers that generate images.
Two earlier ideas carry the chapter. A Transformer is a stack of layers that updates a sequence of vectors through attention, which compares every token with every other token (Chapter 5). A convolutional network, or CNN, slides small filters over the image, so each layer looks only at a small neighborhood and applies the same weights at every position (§0.8). Those two assumptions, locality and weight sharing, are much of why a CNN learns from fewer images. A ViT drops locality, since any patch can attend to any other from the first layer. It keeps weight sharing only in a weak form, since every patch passes through the same weights but also carries its own learned position. §27.3 measures what that trade costs and when it pays.
This section covers the first step, turning pixels into tokens. Nearly every detail of it is a standard interview question, from the order of the pixels inside a patch to what happens when the image changes size. The obvious way fails. Give a Transformer one token per pixel, and a photograph becomes tokens. Each head of each layer then computes scores, one for every pair of tokens. Stored as 16-bit numbers, that is about 5 GB for one head of one layer.
The ViT of Dosovitskiy et al. (2021) fixes this with one decision. It cuts the image into squares, called patches, and makes each patch one token. A image then has tokens and scores per head, which is times fewer. A completely standard Transformer can read that. §1.8 sketched this step, and its Exercise 1.8a cut an image into patches with one reshape and one transpose.
Filling in the details means answering five questions. In what exact sense is the patch embedding "the same as a convolution"? What does the patch size buy, and what does it cost? Can you change the patch size after training? How does a set of tokens become one vector that describes the image? And since attention cannot see order, how does the model know where each patch came from, and what happens to that knowledge when the image changes size?
How Vision Transformers are named
A ViT's name gives its size and its patch side. ViT-B/16 is the Base size with patches, and ViT-L/14 is the Large size with patches. When a resolution is added, as in "ViT-L/14 at 336" or "ViT-L/14@336px", it is the side of the square input image in pixels. The Base, Large, and Huge sizes come from the ViT paper, which took the first two from the BERT text encoder. The Tiny and Small sizes come from DeiT, a recipe for training ViTs on ImageNet alone (Touvron et al. 2021).
| Size | Layers | Width | Heads | MLP width |
|---|---|---|---|---|
| Ti (Tiny) | 12 | 192 | 3 | 768 |
| S (Small) | 12 | 384 | 6 | 1,536 |
| B (Base) | 12 | 768 | 12 | 3,072 |
| L (Large) | 24 | 1,024 | 16 | 4,096 |
| H (Huge) | 32 | 1,280 | 16 | 5,120 |
Here is the number of Transformer blocks, the width of every token, the number of attention heads, and the width of the hidden layer of each block's two-layer network (its MLP). §27.2 counts the parameters of every size, along with the giant sizes g and G and the 22-billion-parameter ViT-22B.
Why pixels cannot be tokens
A Transformer layer does two kinds of work on tokens, each a vector of numbers. The linear layers, meaning the attention projections and the MLP, transform each token on its own, so their work grows like . Attention compares every pair of tokens, so its work grows like . Plain attention also stores the scores during training, and that memory grows like for heads (§4.7). With one token per pixel, is the number of pixels, and the term is fatal at any useful resolution.
The field tried three ways out. The first shrinks the image until the pixels are few. Image GPT (Chen et al. 2020) made each pixel a token, but only on images of at most pixels, with the colors clustered into a palette of 512. The second keeps every pixel but lets a small, fixed set of learned vectors read them through cross-attention, in which the queries come from one set and the keys and values from another. The cost then grows with the number of pixels times the number of learned vectors, not with the square of the pixel count. This is the design of the Perceiver (Jaegle et al. 2021), which §4.6 covers. The third groups pixels into patches before the Transformer sees anything.
Patches won. They keep every pixel value in the input, cost one matrix multiply, and leave an ordinary Transformer behind them.
A patch is a vector the model projects
In a language model a token is an integer, and the model turns it into a vector by reading one row of an embedding table (§1.2). A patch has no integer. It arrives as a vector already, the raw numbers of a square with channels. A color image has three channels, one each for red, green, and blue. So the table lookup becomes a matrix multiply. Every patch is multiplied by the same learned matrix. Each column of that matrix is a pattern detector, and each number of the resulting token is the dot product of the patch with one detector.
A tiny case with four pixels per patch and two detectors shows everything that happens.
That last patch shows, in miniature, what a patch embedding can lose. The null space of the map is the set of patches it sends to zero. This map takes four numbers to two, so its null space is two-dimensional. It contains the vertical edge , the horizontal edge , and every mix of the two. In general, a matrix that maps numbers to numbers must send some patterns to zero. One with can be injective, meaning different patches always get different tokens, and then nothing is lost at this step. We come back to this with real model sizes below.
The patch embedding, stated exactly
Now the general case. An image is , with height , width , and channels. In the math the channels come last. PyTorch stores images as , and the code always says which layout it uses. The patch side is pixels, and for now divides both and . The patches form a grid of rows and columns, so there are
patches. We write rather than the ViT paper's , because counts parameters in this book and the next section needs both.
In plain words, cut the image into a grid, flatten each square, multiply every square by the same matrix, and add the same bias.
A note on conventions. As everywhere from Chapter 4 on, a weight matrix has shape (input, output) and multiplies token row vectors from the right. §1.8 wrote the same map in column form, with , so . The ViT paper calls this matrix . We rename it because is the token embedding table everywhere else in the book.
For ViT-B/16 (, , ) each patch holds numbers, and the patch embedding has parameters. That is under 0.7 percent of the model's 86.6 million. The first layer of a ViT is tiny. Almost all of the cost lies in the Transformer behind it.
When the image does not divide
So far divided both sides. When it does not, there are three options, and libraries differ on which they take.
- Crop. Keep rows of whole patches and drop the last rows of pixels. The strided convolution that most libraries use for the patch embedding (the next subsection) does exactly this, without a warning. Its output has rows.
- Pad. Pad the image up to the next multiple of . That gives rows, and the last patches are partly empty. timm, the PyTorch Image Models library that most vision code builds on, does this when built with
dynamic_img_pad=True. - Resize. Resample the image to a multiple of before cutting it into patches. This changes the apparent size of objects.
By default, timm's patch embedding accepts only the image size the model was built for and raises an error otherwise. That is the safest behavior of all.
The Transformer also reads a few learned tokens that are not patches, placed in front of them and called prefix tokens. Most ViTs use a class token, a learned vector whose final value summarizes the image (defined later in this section). Some also add register tokens, extra learned slots that §27.4 explains. With prefix tokens, the sequence length is . For ViT-B/16 at 224 with one class token, .
Count tokens for any image
Implement patch_token_count(H, W, P, n_prefix=1, mode="crop"). The image has height H and width W, the patch side is P, and the model uses n_prefix prefix tokens. Return the tuple (H_p, W_p, N_p, T), the rows and columns of the patch grid, the number of patches, and the sequence length . With mode="crop", partial patches are dropped, as Conv2d does. With mode="pad", the image is padded up to whole patches. Any other mode raises ValueError. Use Python integers.
Three ways to compute the same map
The definition can be computed three ways, and production code uses all of them.
- Reshape, then multiply. Cut the image into patches with a reshape and a transpose, as in Exercise 1.8a, then compute with one matrix multiply.
- A linear layer per patch. Apply
nn.Linear(P*P*C, d)to every patch vector. This is the same arithmetic. PyTorch stores the weight as , with shape . - A strided convolution. Apply
nn.Conv2d(C, d, kernel_size=P, stride=P)to the image and read the output grid in raster order. The original ViT code, timm, and most libraries implement it this way.
The third surprises people, so here it is exactly. A convolution slides a small grid of weights, the kernel, across the image. At each stop it multiplies the kernel with the pixels under it and adds up the products. That gives one output number per filter, where a filter is one kernel with its own weights. The stride is how many pixels the kernel moves between stops. §0.8 builds convolutions, strides, and output sizes from scratch. In symbols, take a convolution with filters (PyTorch's layout), stride , and no padding. For output channel at output position it computes
and the output has rows. With that is rows, and likewise columns.
In plain words, a convolution applies one small dense layer to every window it visits. When the stride equals the kernel size, the windows tile the image with no overlaps and no gaps, so each pixel is used exactly once. Then "a dense layer on every window" is exactly "a dense layer on every patch".
This also explains why the patch embedding is cheap. Fast convolution code often uses im2col, short for "image to columns". It copies every window the kernel visits into a row of a matrix and then does one matrix multiply (§0.8). For a kernel at stride 1, the copies take about times the memory of the input. With kernel equal to stride, the windows do not overlap, so im2col copies nothing. It is a reshape and a transpose, the same patch cutting as in Exercise 1.8a.
The one trap is the order in which a patch is flattened. Every wrong order still produces tensors of the right shape, so nothing crashes. The definition above flattens channels last, so pixel in channel goes to index . PyTorch's Conv2d weight and F.unfold flatten channels first, to index .
Here is one pixel in both orders. Take , , and pixel in channel 1. It sits at index in the channels-last order and at index in the channels-first order. In NumPy, the matrix for channels-last patches is W_conv.transpose(2, 3, 1, 0).reshape(P*P*C, d). The matrix for channels-first patches is W_conv.reshape(d, C*P*P).T. An interviewer who hands you a checkpoint is testing exactly this.
The cell checks the hand example. Then it checks the theorem on a random color image, computing the convolution the slow way, window by window.
Both forms give the four tokens of the hand example, , , , and . On the random image they agree to about , which is rounding error in 64-bit floats.
Convert convolution weights to a patch embedding
You are handed a pretrained PyTorch checkpoint. Its patch embedding is a Conv2d with weight W_conv of shape (d, C, P, P) and bias b of shape (d,). You must reproduce it without a convolution. Implement two functions.
patch_embed_conv(img_chw, W_conv, b)takes an image of shape(C, H, W), PyTorch's layout, where divides and . It returns the tokens, shape(N_p, d), in raster order, equal to the convolution's output flattened row by row. Use reshapes, a transpose, and one matrix multiply, with no Python loops.conv_weight_to_linear(W_conv)returns , shape(P*P*C, d), such thatpatchify(img_hwc, P) @ W_E + bgives the same tokens. Herepatchifytakes an image of shape(H, W, C)and returns its patches in raster order, each flattened channels last as in the definition above.
Overlapping patches and convolutional stems
Kernel equal to stride is a choice, not a law. A convolution whose kernel is larger than its stride gives overlapping patches. Neighboring tokens then share pixels, so an edge that falls on a patch border is seen whole by some token. Xiao et al. (2021) went further. They replaced ViT's single stride-16 convolution with a few stacked stride-2 convolutions, called a convolutional stem, that reach the same total stride of 16. The token count is unchanged, and they removed one Transformer block so that the total compute matches. Yet training became much less sensitive to the optimizer and its settings, and ImageNet-1k accuracy rose by about 1 to 2 points.
These designs put back some of the machinery of convolutional networks, whose built-in assumptions §27.3 examines. The ViT paper itself tried a hybrid, in which each "patch" is one cell of the feature map of a convolutional network (a ResNet). The hierarchical backbones of §27.5 start from small patches. That way they can produce the fine feature maps, one vector per 4 pixels, that detection and segmentation need.
In an interview, the patch embedding is the place to say that a ViT is not free of image-specific assumptions. It assumes that squares are meaningful units.
What the patch size trades
The patch side is the most consequential choice in a ViT. Everything the model costs follows from its token count, and the token count follows from .
The cell sweeps the patch sizes in use today at two resolutions, for a model of width with one class token. "Pixels per token" is . The last column is the attention-score work relative to ViT-B/16 at 224.
Three things stand out. First, the attention work spans a factor of about 34,000 across sizes that all appear in real models. It runs from 2,500 scores per head at and 224 pixels to 85 million at and 384 pixels. Second, the patch embedding gets smaller as patches shrink, since it has parameters. The extra cost of small patches is all in the Transformer that reads them. Third, does not divide 384, since . By the crop rule above, a convolution reads only the top-left pixels and silently drops the rest.
The widget shows the same trade on a picture. Each patch on the right is replaced by its average color. That is the crudest summary a token could carry, and it shows how much detail must survive inside one token.
What is lost inside a patch
Go back to the null space of the hand example. The rank of a matrix is the number of independent directions it can output. A patch embedding has rank at most . So when , at least directions of pixel space map to zero. They are gone before the first attention layer. When , a learned can be injective, and nothing is lost at this step.
The standard models sit on both sides of that line.
- ViT-B/16 maps numbers to . The matrix is square and can be invertible.
- ViT-L/16 maps 768 numbers to 1,024, and ViT-H/14 maps numbers to 1,280. Both have room to spare.
- ViT-B/32 maps 3,072 numbers to 768, so at least 2,304 directions of each patch are discarded. ViT-S/16 maps 768 numbers to 384.
What does a model short of dimensions keep? The ViT paper plotted the top principal components of ViT-L/32's learned first-layer filters, the directions that carry most of their variation (their Figure 7, left). They are smooth patterns rather than noise, plausible building blocks for the fine structure inside a patch. Smooth patterns are what natural image patches mostly contain, so they are the right directions to keep.
The larger loss is not the projection. It is what the model can afford to compute on. Each token must summarize pixels, and attention mixes information only between whole tokens. So detail finer than a patch must survive inside the numbers of a single token. Wang et al. (2025) measured what happens when you stop compressing.
In practice this is the main design question about patches. An interviewer who asks "why not use smaller patches?" wants three things. Smaller patches help. They cost about in attention per halving. And the choice is set by the compute you have.
Changing the patch size of a trained model
A ViT trained with cannot be run with by changing a number. Two parts of the model depend on , and nothing else does. The patch embedding is a bank of filters, and the position table has one row per patch of a grid. Beyer et al. (2023) asked how to resize the patch-embedding filters so that the resized model behaves like the original. Their answer, part of a model they call FlexiViT, is short enough to prove here.
Think of each column of as a filter , a small image of weights the same size as a patch. The token is the dot product of the patch with the filter. Suppose you want to run the model on an image twice as large, with patches twice as large, so that each patch still covers the same part of the scene. The obvious idea is to resize the filters like images. It fails, and the smallest case shows why.
The general case works the same way. Resizing a patch with bilinear interpolation is a linear map. So for single-channel patches there is a matrix with , where is the new side. A color patch resizes each channel separately with the same . In the example, is the column .
If you resize both the patch and the filter, the token becomes . Each original pixel now covers about new pixels, and each of them adds its product with the filter to the token. So the token grows by about , and doubling the patch side quadruples it. What we want instead is a filter that gives the same token on the resized patch, , for every patch .
The tool is the Moore-Penrose pseudoinverse of a matrix . It gives the least-squares solution of as . Among all least-squares solutions it picks the shortest (§0.10). FlexiViT's rule applies it to , and the paper calls it PI-resize, for pseudoinverse resize.
In the one-pixel example, and , the filter we found by hand.
∂ Proof of PI-resize
Part 1. If has full column rank, then is invertible, and the pseudoinverse of the full-row-rank matrix is . Then for every ,
Full column rank needs at least as many rows as columns, , which is why exactness is possible only when upsampling.
Part 2. Write the token error as with . Then
using for a standard Gaussian. This is an ordinary least-squares problem in with design matrix and target . The pseudoinverse solution is its minimum-norm minimizer, the standard property of the pseudoinverse (§0.10). When has full row rank, as in downsampling, is invertible and the minimizer is unique, .
With a general patch covariance , the same steps give the weighted problem , which FlexiViT's appendix solves too. The paper reports no clear benefit from using the covariance of real images.
In plain words, when the new patch has at least as many pixels as the old one, some filter undoes the resize exactly, and the pseudoinverse finds it. When it has fewer, some information is gone, and the pseudoinverse gives the best linear compromise for patches that look like random noise.
The cell builds the bilinear resize matrix with half-pixel centers. Each pixel is treated as the center of a small square cell, and the resized grid covers the same squares. PyTorch calls this align_corners=False, and the last part of this section defines it precisely. The cell resizes filters up and down and measures the token error on random smooth patches.
The patches and filters here are smooth, built by upsampling random grids. Natural image patches and learned filters are also dominated by slowly varying patterns. The cell shows four things.
- Naive resizing scales the tokens by close to , by 3.77 for against the predicted 4.
- Dividing by fixes the scale but still leaves errors of about 3 to 9 percent.
- PI-resize reproduces the original tokens to rounding error whenever .
- Going down from 16 to 12 pixels, no filter can be exact for every patch. Yet on smooth patches PI-resize misses by only about 0.1 percent, since almost nothing they contain is lost in the resize.
One special case is worth knowing. Downsampling by exactly 2 with half-pixel bilinear resizing averages each block, so . PI-resize then reduces to resizing the filter and multiplying it by 4.
FlexiViT then trains one model for many patch sizes at once. It stores the patch embedding as filters and the positions as a table. At every training step it draws a patch size uniformly from , every divisor of its 240-pixel images in that range. It then resizes both on the fly, the filters with PI-resize and the positions bilinearly. At deployment the trained model can trade accuracy for speed by choosing its patch size, with no retraining.
PI-resize a patch embedding
The setup gives bilinear_resize_matrix(p, q), the matrix that resizes a patch to . Patches are flattened row by row, and the resize is half-pixel bilinear. Implement pi_resize(w, B). The input w has shape (p*p, k) and holds k flattened single-channel filters as columns. A real patch embedding has of them, one per pair of output channel and input channel. Return the resized filters, shape (q*q, k), that minimize the expected squared token error of the theorem and, among all minimizers, have the smallest norm. Then tokens are preserved exactly when upsampling.
From many tokens to one vector
A Transformer returns one output vector per input token. After Transformer blocks (§27.2 builds them), the model holds output vectors, the rows of . But a classifier, a retrieval index, or a contrastive loss needs one vector per image. Turning many vectors into one is called pooling. Three ways of doing it are in use, and they are worth knowing precisely, because codebases mix them up.
The first comes from text. The BERT encoder puts a special [CLS] token in front of the sentence and reads a summary of the whole sentence off its final vector (§7.1). ViT copies this with a learned class token placed in front of the patch tokens. Attention lets this token read every patch in every layer. It starts as the same vector for every image, so its final value is whatever summary of the patches the loss rewards.
The second is the habit of convolutional networks. It averages the outputs at every position and is called global average pooling (GAP). The third, multihead attention pooling (MAP), lets a learned query vector decide how much each patch contributes. It is the pooling with one learned query of §4.6, a single query that cross-attends to the set, here with several heads.
Rows of are numbered from 0. Row 0 is the class token if there is one, the next rows are any register tokens, and the patch tokens follow. So patch sits in row , where counts the prefix tokens.
The two orders of GAP are not a detail. Two codebases average first and normalize after. One is timm. The other is the fine-tuning code of the masked autoencoder (MAE) of §28.5, a ViT pretrained by filling in hidden patches. Google's big_vision codebase, which trained many of the large ViTs, does the opposite. It applies its final LayerNorm to every token and then averages. A checkpoint trained one way and evaluated the other way gets the wrong features, and nothing crashes.
Mean pooling has one property the class token lacks. It does not depend on the order of the patches, since a sum does not care about order. Attention pooling with a learned query has the same property, because cross-attention gives the same output however its keys and values are ordered (§4.6). The cell checks the first claim and measures how far apart the two GAP orders are.
The tokens in this cell share an offset, as real final-layer tokens do. Four of them have a very large norm, like the artifact tokens that §27.4 studies. The two orders disagree in direction, with a cosine of about 0.95, and in length, 8.00 against 7.09. Averaging first lets the four large tokens pull the mean toward themselves. Normalizing first caps each token's influence.
The lengths differ for a simple reason. With gain 1 and shift 0, a normalized vector has mean 0 and variance 1 over its entries, so its norm is , up to the that the cell adds to the variance. The mean-then-normalize vector is one normalized vector, so its norm is 8. The normalize-then-mean vector averages 196 vectors of norm 8 that point in different directions, which makes it shorter. A linear head trained on one sees the wrong scale and the wrong direction on the other.
Which head is best was settled by careful experiments, not by argument.
| Head | What it computes | Strengths | Weaknesses | Used by |
|---|---|---|---|---|
| Class token | the final vector of an extra learned token | lets attention choose the summary, matches BERT, a natural target for distillation and self-supervised heads | one more token (padding on TPUs), and the summary is only as good as the attention that fills it | the original ViT, DeiT, DINO, CLIP |
| Global average pooling | the mean of the patch outputs | no extra token, independent of patch order, every patch gets gradient | weights all patches equally, wants its own learning rate, LayerNorm order differs between codebases | MAE fine-tuning, the big_vision ViT baselines |
| Multihead attention pooling | a learned query attending to the patch outputs, then an MLP | input-dependent weights, most expressive, no extra token in the encoder | extra parameters, one more attention to implement | ViT-G and ViT-22B, the SigLIP 2 image-text encoders |
In an interview, "class token or mean pooling?" has a precise answer. They perform about the same once each has its own learning rate. Mean pooling avoids an extra token. And the LayerNorm order must match the checkpoint.
The multihead attention pooling head
Exercise 4.6c pooled a set with one learned query and one head. Here you build the full head, with several heads and a residual MLP, laid out as in big_vision but without the projection biases. Interviewers like it because it combines multi-head attention, the prefix bookkeeping, and a residual MLP in a few lines. Implement map_head(X, n_prefix, params) for final-layer tokens X of shape (B, T, d), returning (B, d).
- Keep the patch tokens only, rows
n_prefixonward. - Use
h = params["h"]heads of width . Form the query fromparams["probe"]of shape(d,), the keys , and the values . All weights are(d, d), with no biases. Split each into heads of consecutive columns, and for each head compute over the patch tokens, with a stable softmax. - Concatenate the heads in order and multiply by to get , shape
(B, d). - Return . Here
ln_gln_bover the last axis, and , with the exact GELU fromscipy.special.erf.
No Python loops over the batch or the heads.
Telling the model where each patch came from
Self-attention cannot see order. §4.6 proved that an encoder with no position information is permutation equivariant. If you shuffle its input tokens, its output tokens shuffle the same way, and a mean-pooled vector is unchanged. So a ViT without positions reads a bag of patches, a collection with no arrangement. It can still tell a cat's fur from a dog's. But it cannot tell an eye above a mouth from an eye below one.
So we add to each token a vector that says where it is. For the patch at grid row and column , write this vector . Stacked over the whole sequence, with rows for the prefix tokens too, these vectors form the position table . It is the position table of §5.4 with a subscript, since italic is the patch size here.
How much do positions matter for images? The ViT paper measured it with a cheap test that it uses for quick comparisons.
The test measures the features alone, since nothing in the network is trained. It is far cheaper than fine-tuning, which is why the paper uses it for ablations, experiments that remove or swap one part to see what it contributes. With only five examples per class its numbers sit well below fine-tuned accuracy, so they are compared only with each other.
Why does a plain 1-D table do as well as a 2-D one? Because it learns the 2-D structure by itself. Figure 7 (center) of the ViT paper takes each patch of ViT-L/32's grid and shows the cosine similarity of its learned position vector with all the others. Nearby patches have similar vectors. Patches in the same row or the same column are more similar than others. All of this is learned from the raster index and the data.
The paper's own guess is that a grid is small, so any reasonable scheme learns it equally easily. At higher resolutions, at many resolutions, and at many aspect ratios, the choice starts to matter. The rest of this part covers the three schemes in use.
Learned tables, one-dimensional and factorized
The default ViT learns outright, one free vector per token, initialized with small random values. With a class token that is parameters. The ViT paper's "2-D" variant factorizes the table. It learns a row table and a column table, each with vectors of width , and concatenates them, . That needs parameters instead of . It can also cover a larger grid by extending each table. In the ablation it made no difference. NaViT (Dehghani et al. 2023), which reads images at their native size, factorizes the same way but adds two full-width vectors instead of concatenating them.
Fixed two-dimensional sines and cosines
The second scheme learns nothing. §5.4 built the sinusoidal encoding for text. It gives position the sines and cosines of times a ladder of frequencies, from fast to slow. The grid version gives half of the channels to the column index and half to the row index.
Beyer et al. (2022) found it better than a learned table in their ImageNet-1k recipe, 76.5 against 75.0 percent top-1 at 90 epochs. The gap shrinks to 80.0 against 79.6 at 300 epochs. The masked autoencoder, MAE (He et al. 2022), uses it in both its encoder and its decoder.
The layout is easiest to see on one row with very few channels. With there are only two frequencies, one fast and one slow, so every number can be checked by hand.
The squared length came out as for this row, and that is no accident. The dot product of any two rows of the table has a simple form, and it depends only on how far apart the two patches are.
In plain words, the similarity of two positions is a row term plus a column term, and each depends only on how far apart the patches are. Every patch sees the same similarity pattern, centered on itself. Patches in the same row or column are especially similar. That is the structure learned tables discover on their own. The cell draws the cosine-similarity map for three patches of a grid at .
The plus-shaped pattern is the row term plus the column term. Notice also how high the similarities are everywhere, never below about 0.71 for patch (5, 9). Most of the 192 frequencies are so slow that they barely change across 14 patches. They add a nearly constant amount to every dot product. The position signal is carried by the few fast frequencies, and that matters when we resize the table below.
Build the 2-D sine-cosine table
Implement sincos_2d(H_p, W_p, d, base=10000.0), returning the table of shape (H_p * W_p, d) in the MAE convention of the definition. Row is , with for . Raise ValueError if d is not a multiple of 4. No Python loops.
Rotary positions on a grid
The third scheme moves positions out of the input and into attention. Rotary position embedding, RoPE for short, splits each query and key into pairs of numbers. It rotates each pair by an angle proportional to the token's position (§5.5). The dot product of two rotated pairs depends only on the difference of their angles. So every attention score depends on position only through the offset between the query and the key. The grid version does this once per axis.
In plain words, attention between two patches can depend on what they contain and on how far apart they are vertically and horizontally. It never depends on where they sit in absolute terms. That is the right assumption for an image that might be any size.
The cell checks it. Two pairs of patches with the same offset get the same score, wherever they are. A different offset gives a different score.
The payoff shows up when the resolution changes, and one experiment measures it.
In RoPE-Mixed a pair can rotate by , so it can represent diagonal directions. Heo et al. use base rather than . Their reason is that a grid's side is only the square root of its token count, so they take the square root of the base too. A smaller base raises every frequency, so fewer channels are spent on angles that barely move across a grid only a few dozen patches wide. DINOv3 (Siméoni et al. 2025) uses RoPE with patch coordinates scaled into the box . During training it rescales that box by a random factor in , so the model learns to handle many resolutions and aspect ratios.
Every implementation has to decide what to do with the prefix tokens. The class token and the registers have no place on the grid. So timm's version of EVA-02, a family of ViTs that use axial RoPE, rotates only the patch tokens, and so does DINOv3. They leave the first queries and keys as they are. A rotation by angle zero changes nothing, so an unrotated token behaves exactly like a token at grid position .
By the theorem, the score between the class token and a patch then depends on where that patch is. Scores between two patches stay purely relative. With DINOv3's coordinates in , that silent origin is the center of the image. And with no prefix tokens at all, shifting every patch by the same offset leaves every score unchanged, and so the whole attention output too.
Axial 2-D RoPE attention with prefix tokens
You are moving a ViT from a learned position table to axial RoPE, as DINOv3 and the vision encoder of the Qwen2-VL vision-language model did. Implement two functions, with no Python loops over heads or tokens.
rope_2d_axial(x, rows, cols, n_prefix=0, base=100.0)takes queries or keysxof shape(h, T, d_k), where the firstn_prefixtokens are prefix tokens.rowsandcolsare integer arrays of lengthT - n_prefix. They hold the grid row and column of each patch token, in whatever order the tokens come. Return the rotated array, with the same shape, as the definition states. The firstd_k/2channels are rotated by the column index and the lastd_k/2by the row index. Pair of each half is its channels , rotated counterclockwise by the position times for . Prefix tokens pass through unchanged. RaiseValueErrorifd_kis not a multiple of 4.rope_2d_attention(Q, K, V, rows, cols, n_prefix=0, base=100.0)takesQ,K,Vof shape(h, T, d_k), rotates the queries and keys (not the values), and returns for every head, shape(h, T, d_k). Every token attends to every token, with a stable softmax.
The table sums up the five ways to give a ViT positions.
| Scheme | Parameters | A new grid size | Relative positions | Used by |
|---|---|---|---|---|
| Learned 1-D table | interpolate the table | learned from data, approximately | ViT, DeiT, CLIP, DINOv2 | |
| Learned factorized row and column tables | concatenated, or summed | interpolate or extend each table | approximately | the ViT paper's 2-D ablation (concatenated), NaViT (summed) |
| Fixed 2-D sine-cosine | none | recompute at the new grid, or interpolate | exactly in the dot products of the table, not in attention | MAE, the big_vision ViT baselines |
| Axial 2-D RoPE | none | recompute angles for any grid | exactly, in every score between patches | EVA-02, DINOv3, the Qwen2-VL vision encoder, and RoPE-Mixed variants |
| Relative bias inside windows | a small table per head | interpolate the bias table | exactly, within a window | Swin (§27.5) |
Some models pack images of many sizes and aspect ratios into one batch, each at its native resolution. NaViT and its successors push this furthest, and they are covered with vision-language models at native resolution.
In an interview, the position question is usually "how would you serve a ViT at a resolution it was not trained at?" The answer depends on the scheme, and the next part covers the most common case, a learned table.
New resolution, same model
Fine-tuning at a higher resolution than in pretraining usually improves accuracy (§27.4 explains why). A ViT handles the change gracefully in every part but one. Keep fixed, and a image becomes a grid of 576 patches instead of . The patch embedding, the attention, and the MLPs work on any number of tokens. But a learned position table has exactly rows.
The ViT paper's fix, in its section on fine-tuning, is to treat the table as a image with channels and resize it. It uses 2-D interpolation "according to their location in the original image". The class token's row is not part of the grid and is kept as it is. The paper notes that two steps put knowledge of the 2-D structure of images into the model by hand, this resizing and the cutting into patches. Nothing else does.
Resizing a grid has one subtle convention, and it is the source of many silent bugs. Which old coordinate does each new grid point read from? A tiny case shows the usual answer.
In plain words, each new cell reads the old grid at its own center. With this rule a table resized to comes back unchanged, and the grid covers the same image at every size.
What does interpolation do to a table? For the sine-cosine table we can check exactly, since we know the formula at any fractional position. The cell resizes the table to . It compares the result with the formula evaluated at the very coordinates that the new grid reads.
The fastest frequency, one radian per patch, comes back with errors of about 0.12. The slow ones are reproduced almost perfectly. The similarity maps of the interpolated and exact tables still have a correlation above 0.999. Linear interpolation is a low-pass filter, meaning it keeps slowly varying patterns and damps fast ones. So it keeps the coarse layout and blurs the finest positional detail, and the finest detail is what tells neighboring patches apart.
In practice there are two cases. For the sine-cosine table you do not need to interpolate at all, since you can recompute it at the new coordinates. For a learned table there is no formula, so interpolation followed by brief fine-tuning is the standard fix. Language models face the same problem when a context grows past its training length. Position interpolation for long contexts is the one-dimensional version of this step.
Resize a position table to a new grid
Implement resize_pos_embed(P_pos, n_prefix, old_grid, new_grid). The table P_pos has shape (n_prefix + H * W, d). Its first n_prefix rows belong to the prefix tokens, the class token and any registers. The patch rows of an old_grid = (H, W) grid follow in raster order. Return the table for new_grid = (H2, W2), with shape (n_prefix + H2 * W2, d). Copy the prefix rows unchanged. Resize the patch rows as a 2-D grid by bilinear interpolation with half-pixel centers, exactly as the definition above states. Grids need not be square, and H is the number of rows.
The ViT input sequence
All the pieces now fit together. The class token goes first, then any registers, then the embedded patches in raster order, and the position table is added to the whole sequence.
Registers carry no position because they do not belong anywhere in the image. In the code of DINOv2, and in timm's version of it, they are inserted after the position embedding has been added.
The Transformer that reads is the subject of §27.2.
In PyTorch
The block checks the three forms of the patch embedding against each other. They are the convolution, F.unfold with channels-first weights, and the channels-last reshape with the permuted weight. It then assembles with a class token, four registers, and 2-D sine-cosine positions. Finally, it confirms that F.interpolate with align_corners=False computes the half-pixel formula.
In practice you rarely write these by hand. timm loads a checkpoint at a new resolution and resizes its positions for you. It also ships PI-resize for patch embeddings, with bicubic resizing and antialiasing by default rather than the bilinear resize of our derivation. The calls below are shown, not run.
Read more
- Dosovitskiy et al. (2021). An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale. The Vision Transformer. Read Section 3 for the patch embedding and the resolution change, and Appendix D.3 and D.4 for the pooling and position ablations.
- Beyer, Zhai & Kolesnikov (2022). Better plain ViT baselines for ImageNet-1k. A short note whose Table 1 isolates the effect of GAP and fixed 2-D sine-cosine positions.
- Beyer et al. (2023). FlexiViT: One Model for All Patch Sizes. Section 3.4 and Appendix A.2 derive PI-resize, and the rest shows one model serving every patch size.
- Zhai et al. (2022). Scaling Vision Transformers. Section 3.2 compares the class token with GAP and MAP heads and explains the TPU padding argument.
- Xiao et al. (2021). Early Convolutions Help Transformers See Better. Replacing the patchify stem with a few small convolutions, and why it stabilizes training.
- Heo et al. (2024). Rotary Position Embedding for Vision Transformer. Axial and mixed-frequency 2-D RoPE, with multi-resolution results in Appendix A.2.
- Wang et al. (2025). Scaling Laws in Patchification: An Image Is Worth 50,176 Tokens And More. What happens to accuracy as patches shrink all the way to single pixels.
- He et al. (2022). Masked Autoencoders Are Scalable Vision Learners. The source of the 2-D sine-cosine convention used here, in its released code's
util/pos_embed.py.