mariia

From CNNs to LLMs: How Vision Transformers Changed Everything

~35 min read

17 diagrams

LaTeX + PyTorch code

The trajectory from convolutional neural networks to large language models is not a random walk through research papers. It is a story about one idea — attention — escaping its original domain and rewriting the rules of deep learning. If you build ML systems in production, this arc directly shapes the infrastructure you need, the hardware you provision, and the trade-offs you make every day.

I have worked on both sides of this transition: building CNN-based anomaly detection and object tracking systems, and now deploying LLM inference infrastructure with vLLM. This post traces the architectural path between those worlds.

Table of Contents

Part I — The CNN Era

Part II — ViT: From Images to Sequences

Part III — Self-Attention: How Patches Talk

Part IV — From ViT to LLMs


The CNN Era: Locality as Inductive Bias

Convolutional neural networks dominated computer vision for nearly a decade. The core idea was elegant: spatial locality matters. A pixel's meaning depends on its neighbors, not on pixels across the image. The convolutional kernel encodes this assumption directly — it slides across the image, processing small local patches.

How Convolution Works

The kernel is a small matrix of learnable parameters (e.g. 3×3 = 9 weights). It slides across the input, computing a weighted sum at each position to produce one value in the output feature map.

3×3 KERNEL (learnable weights)w1w2w3w4w5w6w7w8w9slides overINPUT (e.g. 5×5)padding = 1 (dashed border)3×3receptivefieldOUTPUT (5×5)ΣΣ(kernel × patch)Stride = how far kernel movesstride 1 → moves 1 pixel → output ≈ inputstride 2 → moves 2 pixels → output = input/2s=1s=2

Output = ⌊(W - K + 2P) / S⌋ + 1

Each position: the kernel multiplies its 9 weights with the 9 input values under it, sums them up, and produces one output value. The kernel weights are learned during training — the network discovers which patterns (edges, textures, shapes) matter for the task.

Convolution output size:

O=⌊W−K+2PS⌋+1O = \left\lfloor \frac{W - K + 2P}{S} \right\rfloor + 1

where WW = input size, KK = kernel size, PP = padding, SS = stride.

Example: Input 224×224224 \times 224, kernel 3×33 \times 3, padding 1, stride 1:   ⌊(224−3+2)/1⌋+1=224\;\lfloor(224 - 3 + 2)/1\rfloor + 1 = 224

Max pooling: 2×22 \times 2 pool with stride 2 halves dimensions: 224→112→56→28→14→7224 \to 112 \to 56 \to 28 \to 14 \to 7

Hierarchical Features: Wider → Deeper

As you go deeper in the network, feature maps get spatially smaller (less width/height) but deeper (more channels). Each level captures increasingly abstract patterns:

🐱Input224×224×3large spatialfew channelsEdges112×112×64Textures56×56×128Parts14×14×256🐱Objects7×7×512← spatial size (width × height) decreaseschannel depth (# filters) increases →abstraction level increases →

Each pooling layer halves the spatial dimensions while each convolution layer can increase the number of channels. The network trades spatial resolution for semantic depth — from raw pixels to high-level concepts.

CNN Architecture

FEATURE EXTRACTIONCLASSIFICATIONInput🐱224×224×3Conv+ReLU224×224×64k:3×3 s:1 p:1Pool112×112×642×2 s:2Conv+ReLU112×112×128k:3×3 s:1Pool56×56×1282×2 s:2Conv+ReLU56×56×256k:3×3 s:1Pool7×7×2562×2 s:2Flatten12,544FC14,096FC24,096SoftmaxOutput ProbabilitiesCat 0.91Dog 0.05Bird 0.03

see convolution output formula below ↓

This inductive bias was a gift:

  • Parameter efficiency: A 3×3 kernel has 9 parameters regardless of image size. A fully connected layer over a 224×224 image would need ~150 million parameters for a single layer.
  • Translation equivariance: A cat in the top-left corner activates the same filters as a cat in the bottom-right. The network doesn't need to re-learn features at every position.
  • Hierarchical features: Stacked convolutional layers build increasingly abstract representations — edges → textures → parts → objects.

Architectures like ResNet, VGG, and EfficientNet refined this recipe. Residual connections solved gradient vanishing. Batch normalization stabilized training. Depthwise separable convolutions reduced compute. The field was optimizing within a well-understood paradigm.

But CNNs had a ceiling.

The local receptive field — the very thing that made CNNs efficient — also limited them. To understand relationships between distant parts of an image, you need the receptive field to grow large enough to cover both regions. With 3×3 kernels, that means stacking many layers:

The Receptive Field Problem

CNN: RECEPTIVE FIELD GROWS SLOWLYTRANSFORMER: GLOBAL FROM LAYER 1pixelL13×3Layer 1: sees 3×3 (9 pixels)Layer 2: sees 5×5 (25 pixels)Layer 10: sees 21×21Layer 50+: finally sees whole image👂🐾can't connectin early layers!token👂🐾connectedimmediately!Layer 1: sees ALL patches (64 patches)Every token attends to every tokenGlobal context from the very first layer

How Many Layers Until Ear Meets Paw?

Each 3×3 conv layer expands the receptive field by 1 pixel in each direction. Two features far apart need many stacked layers before their receptive fields overlap:

Layer 1RF: 3×3👂🐾isolated→Layer 3RF: 7×7👂🐾growing...→Layer 5RF: 11×11👂🐾almost...→Layer 7+RF: 15×15👂🐾OVERLAP!connectedvsTransformerLayer 1 — instant👂🐾ALL connectedin 1 layerCNN receptive field growth (3×3 kernel):L1: 3×3L3: 7×7L5: 11×11L10: 21×21L50: 101×101RF = 1 + 2 × num_layers224×224 image → ~112 layers neededTransformer → 1 layer. Done.

This is the fundamental limitation: depth is the price of global context in CNNs. A Transformer skips all of this — self-attention connects everything in one step.

A CNN needs 50+ layers of 3×3 convolutions just to let distant pixels "see" each other. A Transformer connects everything in one layer through self-attention. This is the fundamental trade-off: CNNs are efficient but local; Transformers are expensive but global.


ViT: The Bridge Between Worlds

In 2020, Google Research published "An Image Is Worth 16×16 Words" — the Vision Transformer (ViT). The idea was almost provocatively simple: treat image patches as tokens and feed them into a standard Transformer.

Patch Embedding: How Images Become Tokens

The key step is linear projection of flattened patches. Each 16×16 patch of the image is flattened into a vector and projected into the Transformer's embedding dimension through a learnable linear layer:

STEP 1: SPLIT🐱224×224×314×14 = 196 patcheseach 16×16×3STEP 2: FLATTENRGB16×16×3flatten768 values(16 × 16 × 3 = 768)STEP 3: LINEAR PROJECTION (e = Wx + b)e∈ ℝ^D=WD × 768learnable·x∈ ℝ^768+b∈ ℝ^Dembeddingoutputweight matrixD×768 paramsflattenedpatchbias+STEP 4: + POSITIONp∈ ℝ^Dpositionlearnable=t∈ ℝ^Dtoken→ TransformerAll 196 patches → 196 tokens (+ 1 [CLS] token = 197 total)[CLS]t1t2t3t4t5t6t7t8t9t10t11t12t13t14t15t16t17t18...
ti=W⏟∈ RD×768⋅xi⏟∈ R768+b⏟∈ RD+pi⏟∈ RD\mathbf{t}_i = \underbrace{\mathbf{W}}_{\in\, \mathbb{R}^{D \times 768}} \cdot \underbrace{\mathbf{x}_i}_{\in\, \mathbb{R}^{768}} + \underbrace{\mathbf{b}}_{\in\, \mathbb{R}^{D}} + \underbrace{\mathbf{p}_i}_{\in\, \mathbb{R}^{D}}

where xi\mathbf{x}_i is the flattened patch (16×16×3 = 768), W\mathbf{W} and b\mathbf{b} are learnable projection weights and bias, pi\mathbf{p}_i is a learnable position embedding, and DD is the embedding dimension: D=768D = 768 (Base), 10241024 (Large), 12801280 (Huge) — a hyperparameter, not derived from input size.

The CNN ↔ ViT Equivalence

Here's something elegant: a convolution with kernel size 16×16, stride 16, and D output filters is mathematically identical to ViT's linear projection. The two formulations produce exactly the same result.

For a single 16×16 RGB patch, one convolution filter computes:

y=b+∑u=015∑v=015∑c=02Wu,v,c⋅Xu,v,cy = b + \sum_{u=0}^{15}\sum_{v=0}^{15}\sum_{c=0}^{2} W_{u,v,c} \cdot X_{u,v,c}

Where:

  • Xu,v,cX_{u,v,c} — pixel value at row uu, column vv, channel cc
  • Wu,v,cW_{u,v,c} — corresponding learned weight (768 weights total)
  • bb — one learned bias

You multiply matching values, then add all 768 products together — the same as flattening and doing a dot product. Expanding across the 3 RGB channels:

y=b+∑u,vWu,v,RXu,v,R⏟red (256 weights)+∑u,vWu,v,GXu,v,G⏟green (256 weights)+∑u,vWu,v,BXu,v,B⏟blue (256 weights)y = b + \underbrace{\sum_{u,v} W_{u,v,R} X_{u,v,R}}_{\color{#ff6464}\text{red (256 weights)}} + \underbrace{\sum_{u,v} W_{u,v,G} X_{u,v,G}}_{\color{#64ff64}\text{green (256 weights)}} + \underbrace{\sum_{u,v} W_{u,v,B} X_{u,v,B}}_{\color{#8096ff}\text{blue (256 weights)}}

For the whole image with stride 16 and output filter jj:

Yp,q,j=bj+∑u=015∑v=015∑c=02Wj,u,v,c⋅X16p+u,  16q+v,  cY_{p,q,j} = b_j + \sum_{u=0}^{15}\sum_{v=0}^{15}\sum_{c=0}^{2} W_{j,u,v,c} \cdot X_{16p+u,\; 16q+v,\; c}

The 16p16p and 16q16q move the filter 16 pixels at a time (stride 16). Every patch uses the same weights WW — this IS the shared linear projection.

The equivalence:

CNN formulationConv2d(3, D, kernel=16, stride=16)D × 768 params + D biases≡identicalViT formulationLinear(768, D) → e = Wx + bW ∈ ℝ^(D×768) + b ∈ ℝ^D

Same computation, same parameters, same output.

ViT actually uses nn.Conv2d(3, D, 16, 16) in PyTorch — cleaner than manual flatten + linear.

This is why ViT is not a complete departure from CNNs — the patch embedding is a convolution. The difference is that ViT uses this single convolution only for tokenization, then switches entirely to attention for all feature learning.

Positional Embeddings: Teaching Position to a Sequence Model

In a CNN, pixels have spatial relationships because convolutions operate on a 2D grid — the kernel physically slides across spatial positions. But after flattening image patches into a sequence of vectors, the Transformer doesn't inherently know which patch came from which image location. Patch 1 (top-left) and patch 196 (bottom-right) are just vectors in a list — the model has no notion of "above," "below," "left," or "right."

Why Do We Need Positional Embeddings?

Imagine two images containing exactly the same patches, but arranged in different positions:

IMAGE AIMAGE B (same patches, different layout)🌳P1🏠P2☁️P3🌸P4🐶P5🌳P6🌊P7🌊P8🌸P9🌊🌸🏠☁️🌳🌊🐶🌸🌳WITHOUT positional embeddings:set of patches is IDENTICAL → same output!WITH positional embeddings:each patch knows its location → different output≟same patchesdifferent positions

Self-attention is permutation-invariant: it treats inputs as an unordered set.

Without position info, the Transformer CAN'T tell these images apart.

Without positional information, a standard Transformer has no built-in way to distinguish their spatial arrangement — self-attention is permutation-invariant, meaning it produces the same output regardless of input order. The model would classify both images identically, even though they represent completely different scenes.

We solve this by adding (not concatenating) a learned positional vector to each patch embedding:

Zi=Ei+PiZ_i = E_i + P_i

Where:

  • Ei∈RDE_i \in \mathbb{R}^D = patch embedding (from linear projection)
  • Pi∈RDP_i \in \mathbb{R}^D = positional embedding (learnable)
  • Zi∈RDZ_i \in \mathbb{R}^D = final input vector — same dimension as EiE_i

We add the vectors, we do not concatenate them. The embedding dimension remains DD (e.g. 768). Positional embeddings are like learnable location labels added to your patch features — they don't change the number of patches or the feature dimension.

ADDITION (✓ ViT uses this)CONCATENATION (✗ NOT used)E_i (768)+P_i (768)=Z_i (768) ✓dimension stays the same!E_i (768)||P_i (768)=Z_i (1536) ✗dimension doubles — breaks Transformer!
Image divided into fixed-size patches🐶P1P2P3P4P5P6P7P8P9Linear Projection of Flattened PatchesE1E2E3E4E5E6E7E8E9positional encodings(learnable)+P1+P2+P3+P4+P5+P6+P7+P8+P9[CLS]Extra learnableembedding(learnable)TransformerEncoder× L layersMLP HeadClassdog: 0.92cat: 0.05Transformer Encoder Block (×L)input: Z = [z_cls, z_1, z_2, ..., z_N]Layer NormMulti-Head Self-Attention++ residualLayer NormFeed Forward (MLP)++ residual → outputEmbedded Patchesdetail →

Why learned, not fixed? The original Transformer (for text) used fixed sinusoidal positional encodings. ViT uses learned positional embeddings — the model discovers the optimal spatial encoding during training. Interestingly, after training, the learned position embeddings show a 2D spatial pattern: nearby patches have similar embeddings, and the model recovers a grid-like structure even though it was never told the patches came from a 2D image.

The full ViT input can be written as:

Z0=[xclsE1+P1E2+P2⋮EN+PN]∈R(N+1)×D\mathbf{Z}_0 = \begin{bmatrix} \mathbf{x}_{\text{cls}} \\ E_1 + P_1 \\ E_2 + P_2 \\ \vdots \\ E_N + P_N \end{bmatrix} \in \mathbb{R}^{(N+1) \times D}

where N=196N = 196 patches, each Ei=W⋅flatten(patchi)+bE_i = \mathbf{W}\cdot\text{flatten}(\text{patch}_i) + \mathbf{b}, and xcls\mathbf{x}_{\text{cls}} is a special learnable class token whose output representation is used for classification.

ViT Architecture

🐎Image224×224split into16×16patchesP1P2P3P4P5...196 patches(14×14 grid)LinearProjection+ PositionEmbedding196 × 768TransformerEncoderMulti-Head AttnFeed Forward× 12 layers[CLS]tokenMLP HeadHorse: 0.94Dog: 0.03Key insight: image patches = tokens. Same Transformer, different input.No convolutions. No pooling. Just attention over patch embeddings.

ViT vs CNN: The Trade-Off

| | CNN | ViT | |---|---|---| | Inductive bias | Locality (hardcoded) | None (learned from data) | | Receptive field | Local → grows with depth | Global from layer 1 | | Data efficiency | Good (works on small datasets) | Poor (needs millions of images) | | Scaling | Diminishing returns | Near-linear improvement | | Parameters (large) | ~25M (ResNet-50) | ~307M (ViT-Large) | | Compute pattern | Compute-bound, regular memory | Memory-bandwidth bound |

ViT proved something profound: you don't need domain-specific architectural biases if you have enough data. With sufficient training data, the Transformer learned spatial relationships purely from data rather than having them hardwired.

This had immediate implications:

  • Unified architecture: The same Transformer block processes both images (as patch sequences) and text (as token sequences). One architecture, multiple modalities.
  • Scaling behavior: ViT scaled better than CNNs with increasing compute and data. Larger models consistently improved.
  • Transfer learning: Pre-trained ViT representations transferred more effectively than CNN features.

Self-Attention: How Patches Talk to Each Other

Now that we understand how ViT splits an image into 196 patch embeddings and feeds them as a sequence into a Transformer, let's look at the core mechanism inside the Transformer Encoder: self-attention. This is how each patch gathers information from every other patch to build a richer, context-aware representation.

The Transformer architecture (Vaswani et al., 2017 — "Attention Is All You Need") originally proposed self-attention for NLP, where it let every token in a sentence attend to every other token in parallel. ViT applies the exact same mechanism — but instead of words, the tokens are our 196 patch embeddings.

First, Understand the Goal (Without Q, K, V)

Before diving into the mechanics, let's understand what self-attention is trying to do. Imagine we have only 4 image patches:

One patch gathers information from other patches👂Ear👁️Eye🐕Body☁️Sky↓ Attention weights for the ear patchEar15%Eye35%Body45%Sky5%

Illustrative weights, not actual model outputs.

The ear patch attends more strongly to the body and eye — semantically related parts of the cat.

The new representation of the ear patch is a weighted combination of information from all patches:

hear=0.15⋅Vear+0.35⋅Veye+0.45⋅Vbody+0.05⋅Vskyh_{\text{ear}} = 0.15 \cdot V_{\text{ear}} + 0.35 \cdot V_{\text{eye}} + 0.45 \cdot V_{\text{body}} + 0.05 \cdot V_{\text{sky}}

After self-attention, the ear patch doesn't just contain "ear pixels" — it contains information about the eye, body, and their spatial relationships. It now understands context. The sky contributes almost nothing because it's semantically irrelevant to the ear.

This is the fundamental idea: every patch updates its representation by gathering relevant information from all other patches. The weights (15%, 35%, 45%, 5%) determine how much each patch contributes.

The question is: how does the model learn these weights? That's where Q, K, V come in.

Self-Attention: Step by Step (the Q, K, V mechanism)

After patch projection and positional encoding, we have:

X∈R196×768X \in \mathbb{R}^{196 \times 768}

That's 196 vectors (one per patch), each with 768 features. Now self-attention lets every patch look at every other patch to decide what's relevant.

Step 1: Project X into three different representations

Each patch vector gets projected through three separate learned weight matrices into Query, Key, and Value:

Q=XWQ,K=XWK,V=XWVQ = X W_Q, \quad K = X W_K, \quad V = X W_VWQ,WK∈R768×dk,WV∈R768×dvW_Q, W_K \in \mathbb{R}^{768 \times d_k}, \quad W_V \in \mathbb{R}^{768 \times d_v}

Think of it as asking each patch three questions:

  • Query (Q): "What am I looking for?" — what information does this patch need from others?
  • Key (K): "What do I contain?" — what information can this patch offer to others?
  • Value (V): "What information do I carry?" — the actual content to share

Step 2: Compute attention scores

How much should patch ii attend to patch jj? Take the dot product of patch ii's query with patch jj's key:

score(i,j)=qi⋅kj⊤dk\text{score}(i, j) = \frac{q_i \cdot k_j^\top}{\sqrt{d_k}}

What about the diagonal? Notice that when i=ji = j (a patch attending to itself), the same xix_i appears twice:

qi=xiWQ,ki=xiWKq_i = x_i W_Q, \quad k_i = x_i W_Kqi⋅ki⊤=xiWQ(xiWK)⊤=xiWQWK⊤xi⊤q_i \cdot k_i^\top = x_i W_Q (x_i W_K)^\top = x_i W_Q W_K^\top x_i^\top

This is not simply the square of xix_i. The matrix WQWK⊤W_Q W_K^\top sits between the two copies and can mix features with positive or negative weights. For a tiny example, suppose the projections produce:

qi=[1,0],ki=[0,1]⇒qi⋅ki⊤=0q_i = [1, 0], \quad k_i = [0, 1] \quad \Rightarrow \quad q_i \cdot k_i^\top = 0

Even though both came from the same patch, the score is zero! If another patch has kj=[1,0]k_j = [1, 0], then qi⋅kj=1q_i \cdot k_j = 1. So using the same input twice does not guarantee a larger self-score — the two transformations (WQW_Q and WKW_K) are different learned matrices.

Why divide by dk\sqrt{d_k}? Recall the dot product formula:

q⋅k=∥q∥∥k∥cos⁡(θ)q \cdot k = \|q\| \|k\| \cos(\theta)

where ∥q∥=q12+q22+⋯+qdk2\|q\| = \sqrt{q_1^2 + q_2^2 + \cdots + q_{d_k}^2} is the vector magnitude. As dkd_k grows, dot products tend to grow proportionally (more terms summed), pushing softmax into saturated regions with near-zero gradients. Dividing by dk\sqrt{d_k} normalizes this:

score(i,j)=qi⋅kjdk\text{score}(i,j) = \frac{q_i \cdot k_j}{\sqrt{d_k}}

What is dkd_k? It is the number of features in each key/query vector within one attention head — not the vector's magnitude:

  • With 1 head: dk=768d_k = 768, so we divide by 768≈27.7\sqrt{768} \approx 27.7
  • With 12 heads: dk=768/12=64d_k = 768 / 12 = 64, so we divide by 64=8\sqrt{64} = 8

Step 3: Convert scores to weights with softmax

The raw scores sijs_{ij} can be any real number — we need to convert them into weights that sum to 1 for each query patch. The division by dk\sqrt{d_k} helps keep the scores at a manageable scale. Then apply softmax:

aij=esij∑m=1196esima_{ij} = \frac{e^{s_{ij}}}{\sum_{m=1}^{196} e^{s_{im}}}

So for each query patch ii, its attention weights across all 196 patches sum to exactly 1:

∑j=1196aij=1\sum_{j=1}^{196} a_{ij} = 1

This is a probability distribution — the ear patch "spends" 100% of its attention budget across all other patches. Higher scores get exponentially more weight (that's what esije^{s_{ij}} does), making the distribution peaky: a few patches get most of the attention, the rest get almost none.

This gives us a 196×196196 \times 196 attention matrix — every patch's relationship to every other patch:

Attention Matrix (196 × 196)P1P4...P1P4...query (from)key (to)= high attention= low attentionExample: Row 4 (patch with 👂 ear)α(4,1) = 0.02 → barely looks at sky patchα(4,5) = 0.31 → strongly attends to eye patch!α(4,7) = 0.28 → strongly attends to nose patch!α(4,9) = 0.03 → ignores background patch

Step 4: Multiply by V — gather the actual information

Attention weights tell us where to gather information, but VV contains the information itself. Without VV, we only have relationships between patches — not their content.

For the ear patch, its attention row might be:

[0.15,  0.35,  0.45,  0.05][0.15,\; 0.35,\; 0.45,\; 0.05]

Those four numbers only say how much to take from each patch. They don't contain the eye's shape or the body's appearance. Multiplying by VV gathers that information:

oear=0.15⋅vear+0.35⋅veye+0.45⋅vbody+0.05⋅vskyo_{\text{ear}} = 0.15 \cdot v_{\text{ear}} + 0.35 \cdot v_{\text{eye}} + 0.45 \cdot v_{\text{body}} + 0.05 \cdot v_{\text{sky}}

The result is a new feature vector for the ear patch — it no longer contains just "ear pixels." It now holds a weighted mixture of eye features, body features, and spatial context. The ear patch now understands that it's part of a cat face.

For the full input:

A⏟196×196⋅V⏟196×768=O⏟196×768\underbrace{A}_{196 \times 196} \cdot \underbrace{V}_{196 \times 768} = \underbrace{O}_{196 \times 768}

Keeping only the weights (AA) gives relationships between patches. Multiplying by VV turns those relationships into updated patch features — each patch's representation is enriched with context from the entire image.

Putting it all together:

Attention(Q,K,V)=softmax ⁣(QK⊤dk)⏟A:  196×196⋅V⏟196×dv=O⏟196×dv\text{Attention}(Q, K, V) = \underbrace{\text{softmax}\!\left(\frac{Q K^\top}{\sqrt{d_k}}\right)}_{A:\; 196 \times 196} \cdot \underbrace{V}_{196 \times d_v} = \underbrace{O}_{196 \times d_v}
DIMENSION TRACKING THROUGH SELF-ATTENTIONX196 × 768→Q: 196×64K: 196×64V: 196×64→QK^T / √d196 × 196→softmax196 × 196→attn × V196 × 64→12 headsconcat196 × 768→Output196 × 768same shape as input!

Why Do We Need Multiple Heads?

Imagine a ViT processing a photograph of a dog. Each head has different trainable weights (WQh,WKh,WVhW_Q^h, W_K^h, W_V^h), allowing the model to learn different ways of comparing and aggregating patches. A single head produces one attention distribution per patch. Twelve heads produce twelve potentially different distributions.

Different attention heads could learn different relationships:

Different heads learn different relationships🐕body shape highlightedHead 1Local shape or texturerelationships🐶eyes, nose, ears connectedHead 2Relationships amongfacial features🌳 ☁️ 🌿🐕Head 3Relationships withthe background

Conceptual examples, not guaranteed head specializations. Real heads may learn overlapping or less interpretable patterns.

One big head (d_k = 768)12 small heads (d_k = 64 each)Pro:Larger feature space per comparisonPro:12 independent attention patternsPro:Simpler architecturePro:Each head uses different W_Q, W_K, W_VCon:All 768 dims share one attention patternCon:Each head has fewer features (64 vs 768)

What Happens After the 12 Heads?

Each head produces its own output Oh∈R196×64O_h \in \mathbb{R}^{196 \times 64}. We concatenate them:

O=Concat(O1,O2,…,O12)∈R196×768O = \text{Concat}(O_1, O_2, \ldots, O_{12}) \in \mathbb{R}^{196 \times 768}

Then we apply another fully connected layer (output projection):

Y=O⋅WO,WO∈R768×768Y = O \cdot W_O, \quad W_O \in \mathbb{R}^{768 \times 768}

Why this extra FC layer? Because concatenation only places the head outputs next to each other — head 1's 64 features, then head 2's 64 features, etc. The output projection WOW_O allows the model to learn combinations of information from all 12 heads. Without it, each head's features stay isolated.

This is analogous to a 1×11 \times 1 Conv2d in CNNs — it mixes channels from different convolution filters into richer representations.

CONCAT + OUTPUT PROJECTIONh1h2h3h4h5h6h7h8h9h10h11h1212 × [196×64] → concat → [196×768]→ × W_O →768×768Output: 196 × 768 (same as input!)heads mixed together → ready for next layer

The output has the same shape as the input (196×768196 \times 768) — this is critical because it means Transformer blocks can be stacked. The output of one block feeds directly into the next.

Single Head Alternative: A × X Then FC

With a single head, there's an interesting equivalence. Since matrix multiplication is associative:

A(XWV)=(AX)WVA(X W_V) = (AX) W_V

These give the same result:

  1. Project XX with WVW_V, then apply attention AA
  2. Apply attention AA to XX, then use a fully connected layer with the same WVW_V

So a single-head architecture can be written as: first mix patches (attention), then project features (linear layer):

class SingleHeadAttention(nn.Module):
    def __init__(self, dim=768):
        super().__init__()
        self.dim = dim
        self.q = nn.Linear(dim, dim)
        self.k = nn.Linear(dim, dim)
        self.v = nn.Linear(dim, dim)
        self.out = nn.Linear(dim, dim)

    def forward(self, x):
        # x: [B, N, 768]
        Q = self.q(x)
        K = self.k(x)

        # scores: [B, N, N]
        scores = Q @ K.transpose(-2, -1)
        scores = scores / (self.dim ** 0.5)
        weights = F.softmax(scores, dim=-1)

        # First mix patches, then project features
        mixed = weights @ x         # [B, N, 768]
        attended = self.v(mixed)     # [B, N, 768]
        return self.out(attended)    # [B, N, 768]

This works because every row of the attention weights sums to 1 (from softmax), which also makes the equivalence hold for bias terms in nn.Linear — apart from floating-point rounding.

Important: This fully connected layer (WVW_V) is a linear projection — not the full Transformer MLP, which includes a nonlinear activation (GELU). The distinction matters.

One Head vs Multi-Head: Full Comparison

One Head (d_k = 768)Pros:+ Larger feature space per comparison+ Each dot product uses all 768 features+ Simpler architecture, easier to reason about+ Can be written as A·X then FC (associative)+ Fewer weight matrices to manageCons:- All 768 value dims share ONE attention pattern- Can't attend to eyes AND body simultaneously- One set of Q/K weights = one "question" per patch- Must compromise between different relationshipsOne 196×196 attention matrixOne perspective on patch relationships12 Heads (d_k = 64 each)Pros:+ 12 independent attention patterns in parallel+ Head 1: shape, Head 2: face, Head 3: texture...+ Each head has different W_Q, W_K, W_V+ W_O mixes heads (like 1×1 Conv2d)+ Empirically better performanceCons:- Each head has only 64 features (not 768)- Too many heads → each too small, redundant- More weight matrices = more parameters- Needs concat + W_O projection afterTwelve 196×196 attention matrices12 perspectives on patch relationships

The winner: Multi-head attention is used in every modern Transformer (ViT, GPT, LLaMA) because the diversity of 12 parallel perspectives consistently outperforms one large perspective — even though each individual head sees fewer features. The sweet spot is typically dk=64d_k = 64 with h=D/64h = D/64 heads.

Complexity: Self-attention has O(n2)O(n^2) complexity — the 196×196196 \times 196 attention matrix means every patch attends to every other patch. For 196 patches it's fine. For 1024 patches (smaller patch size), it starts to hurt. This quadratic cost is why techniques like FlashAttention and sparse attention matter at scale.

The Complete Transformer Block: Residuals + MLP

Once attention produces an output of shape [B,196,768][B, 196, 768], the next steps are the same whether you use one or multiple heads.

Step 1: Add the residual connection

Add the attention output back to the block's original input:

Y=X+Attention(LayerNorm(X))Y = X + \text{Attention}(\text{LayerNorm}(X))

Both have the same shape, so this is element-by-element addition. It lets the block keep the original patch information while adding information gathered through attention.

Why residual? Without it, attention can dilute patch-specific information. Imagine the ear patch attends mostly to the body and eye:

oear=0.15vear+0.35veye+0.45vbody+0.05vskyo_{\text{ear}} = 0.15 v_{\text{ear}} + 0.35 v_{\text{eye}} + 0.45 v_{\text{body}} + 0.05 v_{\text{sky}}

Without the residual, the MLP receives only this mixture — it has no direct access to the original ear embedding. With the residual:

year=xear+oeary_{\text{ear}} = x_{\text{ear}} + o_{\text{ear}}

The original ear features contribute directly, alongside the gathered context. If the attention update is not useful, it can learn F(X)≈0F(X) \approx 0, and the input passes through unchanged:

F(X)≈0⇒Y≈XF(X) \approx 0 \quad \Rightarrow \quad Y \approx X

This also makes deep networks trainable — gradients have a direct path backward through the addition.

Why add, not concatenate?

Addition: x+o∈R768Concatenation: [x; o]∈R1536\text{Addition: } x + o \in \mathbb{R}^{768} \qquad \text{Concatenation: } [x;\, o] \in \mathbb{R}^{1536}

Concatenation would double the dimension at every block — after 12 blocks you'd have 768×212768 \times 2^{12}. Addition keeps the same size and provides a direct gradient path without a learned projection.

Step 2: Apply the MLP with another residual

Z=Y+MLP(LayerNorm(Y))Z = Y + \text{MLP}(\text{LayerNorm}(Y))

A typical ViT MLP is:

768  →Linear  3072  →GELU  3072  →Linear  768768 \;\xrightarrow{\text{Linear}}\; 3072 \;\xrightarrow{\text{GELU}}\; 3072 \;\xrightarrow{\text{Linear}}\; 768
mlp = nn.Sequential(
    nn.Linear(768, 3072),   # expand 4×
    nn.GELU(),              # nonlinear activation
    nn.Linear(3072, 768),   # project back
)

Why the activation matters: Without GELU, two linear layers collapse into one (W2W1=WcombinedW_2 W_1 = W_{combined}). The nonlinearity lets the MLP learn more complex transformations that a single FC layer cannot.

The two components have different jobs:

  • Attention: mixes information between patches (inter-patch)
  • MLP: transforms features within each patch (intra-patch), applied identically to every patch
COMPLETE TRANSFORMER BLOCK (Pre-Norm Layout)X196×768residualLNMulti-HeadAttention+YresidualLNMLP768 → 3072 → GELU → 768(per patch, same weights)+Z196×768× 12 blocks (ViT-Base) — output Z becomes input X of the next block

The complete block outputs [B,196,768][B, 196, 768] — same shape as input. This is the pre-normalization layout used in modern ViTs. Stack 12 of these blocks, and the final output goes to the classification head.

Key insight: If both transformations (FF) are zero, the data flows directly from initial patch embeddings to the final layer through the residual connections — the network has a direct path to the original information at every depth.

Notice: the MLP has its own separate residual connection, independent from attention:

# Attention block with residual
x = x + self.attn(self.norm1(x))    # [B, N, 768]

# MLP block with separate residual
self.mlp = nn.Sequential(
    nn.Linear(768, 3072),
    nn.GELU(),
    nn.Linear(3072, 768),
)

x = x + self.mlp(self.norm2(x))     # [B, N, 768]

Both residual additions have shape [B,N,768][B, N, 768] — the block preserves dimensions throughout. Each residual independently lets information skip its transformation. The full block has two skip paths: one around attention, one around MLP. This means the original patch embedding can survive through both — the network can selectively use attention, MLP, both, or neither.

From Patches to Classification

After all 12 Transformer blocks, we have enriched patch representations. But we need a single vector representing the whole image for classification. First, apply a final layer norm:

x = self.final_norm(x)  # [1, 197, 768]

Now there are two approaches:

Option A: Global Average Pooling (without [CLS] token)

Average across all 196 patches, separately for each of the 768 features:

zf=1196∑i=1196xi,fz_f = \frac{1}{196} \sum_{i=1}^{196} x_{i,f}
# x: [1, 196, 768]
image_vector = x.mean(dim=1)  # [1, 768]

Feature 1 of the image vector is the average of feature 1 across all patches. This operation is called global average pooling — you get one 768-dim vector representing the whole image, not individual patches.

Option B: [CLS] Token (the ViT approach)

Recall we prepended a special learnable xcls\mathbf{x}_{\text{cls}} token at the start. After 12 blocks of self-attention, this token has attended to every patch and aggregated image-wide information:

# x: [1, 197, 768]  (196 patches + 1 CLS)
image_vector = x[:, 0, :]  # [1, 768] — just take the first token

The CLS token's final representation is the image summary — it learned to aggregate during training.

Both approaches produce a single [1,768][1, 768] vector. Then classification is a simple linear layer:

y^=softmax(Whead⋅z+bhead)\hat{y} = \text{softmax}(W_{\text{head}} \cdot z + b_{\text{head}})
self.head = nn.Linear(768, num_classes)  # e.g., 1000 for ImageNet
logits = self.head(image_vector)         # [1, 1000]
TWO PATHS FROM PATCHES TO CLASSIFICATIONOption A: Global Average Pooling...[196 × 768]→ mean →768avg all patchesNo extra token needed. Simple & effective.Option B: [CLS] Token (ViT default)CLS...[197 × 768]→ x[:,0] →768first tokenCLS attended to all patches → learned summary.

Deep Dive: The [CLS] Token

The CLS token is a single learnable vector — an nn.Parameter of shape [1,1,768][1, 1, 768] — that we prepend to the patch sequence before the first Transformer block:

X(0)=[ c⏟CLS,  x1,  x2,  …,  x196 ]+EposX^{(0)} = [\,\underbrace{c}_{\text{CLS}},\; x_1,\; x_2,\; \ldots,\; x_{196}\,] + E_{\text{pos}}
self.cls_token = nn.Parameter(torch.zeros(1, 1, 768))

# In forward:
cls = self.cls_token.expand(x.shape[0], -1, -1)  # [B, 1, 768]
x = torch.cat([cls, x], dim=1)                    # [B, 197, 768]

nn.Parameter makes it trainable. It starts the same for every image, but becomes image-specific as it passes through attention — each image's patches fill it with different information.

After all blocks and final normalization:

H=[ hCLS,  h1,  h2,  …,  h196 ]H = [\,h_{\text{CLS}},\; h_1,\; h_2,\; \ldots,\; h_{196}\,]

The image representation is simply:

z=hCLS=H[:, 0, :]\boxed{z = h_{\text{CLS}} = H[:,\, 0,\, :]}

We drop all 196 patch vectors and keep only the CLS position:

# After all Transformer blocks + final norm
# x: [B, 197, 768]
cls_vector = x[:, 0, :]              # [B, 768]
logits = self.classifier(cls_vector)  # [B, num_classes]

The 196 patch vectors are not passed to the classifier — but they helped build the CLS representation through 12 layers of attention. The CLS vector at the end is not the original nn.Parameter — it's a completely transformed representation containing information about the current image.

Why CLS instead of average pooling?

CLS lets the model learn how to gather information for classification. Average pooling gives every final patch vector equal weight.

For example, if a small animal occupies only a few patches:

  • Average pooling: averages all 196 vectors equally, including background patches
  • CLS: can learn through attention to gather more information from relevant patches and less from background

Important nuance: The patch vectors being averaged already contain contextual information from 12 attention layers — so average pooling can also work well. Neither method is always better. CLS provides a dedicated learned representation; average pooling is simpler and needs no extra token.

Final Classification: Logits, Softmax, and Loss

The classification head is just a fully connected layer:

self.classifier = nn.Linear(768, num_classes)  # e.g., 1000 for ImageNet

logits = self.classifier(image_vector)  # [B, num_classes]

These logits can be positive or negative — they are raw scores, not probabilities.

Softmax converts logits into probabilities that sum to 1:

P(y=k)=ezk∑j=1KezjP(y = k) = \frac{e^{z_k}}{\sum_{j=1}^{K} e^{z_j}}

where zkz_k is the logit for class kk, and KK is the total number of classes.

Argmax selects the class with the highest score:

y^=arg⁡max⁡k  zk\hat{y} = \arg\max_k \; z_k

Cross-entropy loss measures how wrong the predictions are during training:

L=−∑k=1Kyklog⁡P(y=k)=−log⁡P(y=c)\mathcal{L} = -\sum_{k=1}^{K} y_k \log P(y = k) = -\log P(y = c)

where cc is the correct class. This simplifies because the true label yy is one-hot — only the correct class contributes to the loss.

criterion = nn.CrossEntropyLoss()

# During training: pass raw logits (CrossEntropyLoss applies softmax internally)
loss = criterion(logits, labels)

# To get probabilities:
probabilities = logits.softmax(dim=-1)

# To choose the predicted class:
predicted_class = logits.argmax(dim=-1)

Why not apply softmax before the loss? PyTorch's nn.CrossEntropyLoss combines softmax + log + negative likelihood in one numerically stable operation. Applying softmax separately can cause floating-point issues with very large or small logits.

FROM IMAGE VECTOR TO PREDICTIONz[1, 768]→Linear768 → 1000→Logits[2.1, -0.5, 4.8, ...]raw scoressoftmax→[0.02, 0.01, 0.91, ...]probabilities (sum=1)argmax→class 3: "dog"CrossEntropyLoss(logits, label)← training

This completes the ViT pipeline: image → patches → embed → position → 12× (attention + MLP) → CLS/avg → linear → logits → prediction.


From ViT to LLMs: The Same Architecture, Different Domain

GPT-style language models also stack attention and MLP blocks, with normalization and residual connections. You now understand much of their foundation.

The main differences are:

| | Vision Transformer (classification) | GPT-style language model | |---|---|---| | Input tokens | Image patches | Text tokens | | Token embeddings | Represent patches (linear projection) | Represent text tokens (lookup table) | | Attention | Every patch attends to every patch | Each token attends only to itself and earlier tokens | | Final output | One image-class prediction | A next-token prediction at each position |

The core blocks are identical — same attention, same MLP, same residuals:

x=x+Attention(LN(x))←same as ViTx = x + \text{Attention}(\text{LN}(x)) \quad \leftarrow \text{same as ViT}x=x+MLP(LN(x))←same as ViTx = x + \text{MLP}(\text{LN}(x)) \quad \leftarrow \text{same as ViT}

The core Transformer block is identical. What changes is what goes in and what comes out:

ViT (Vision)GPT (Language)Input:image patches (16×16 pixels)Tokenize:flatten + linear projectionSequence:196 patch tokensPosition:learned 2D position embeddingsAttention:bidirectional (see all patches)Blocks:12 (ViT-Base)Output:CLS → classify whole imageTraining:supervised (labeled images)Params:86M – 307MInput:text (words/subwords)Tokenize:lookup in embedding tableSequence:up to 128K tokensPosition:RoPE (rotary position encoding)Attention:causal mask (see only past tokens)Blocks:96 (GPT-3) / 126 (LLaMA 405B)Output:every position → predict next tokenTraining:self-supervised (next token prediction)Params:175B (GPT-3) / 405B (LLaMA 3)

From Patches to Words: Token Embeddings in LLMs

In ViT, we tokenized images by splitting into patches and linearly projecting them. In LLMs, tokenization is simpler — each word (or subword) maps to a learned embedding vector via a lookup table.

For example, with Qwen3-0.6B (embedding dimension = 1024):

X=[etheecatesat]shape: [3,1024]X = \begin{bmatrix} e_{\text{the}} \\ e_{\text{cat}} \\ e_{\text{sat}} \end{bmatrix} \quad \text{shape: } [3, 1024]

Each row contains 1,024 learned features for one token. Same idea as ViT patch embeddings — just a different input domain.

# ViT: patches → linear projection
embeddings = linear(flatten(patches))     # [196, 768]

# LLM: token IDs → embedding lookup
embeddings = embedding_table[token_ids]   # [3, 1024]

Both produce a matrix of shape [sequence length,D][\text{sequence length}, D] — and from here, the Transformer blocks work identically.

The Key Difference: Causal Masking

In ViT, every patch can attend to every other patch — the attention is bidirectional. The ear can look at the tail and the tail can look at the ear.

In a language model, we don't want the next word to contribute to predicting itself — only previous words should. This is the causal mask.

First, compute scores normally:

S=QK⊤dkS = \frac{Q K^\top}{\sqrt{d_k}}

Then mask the scores for future tokens. For "the cat sat":

Smasked=[s11−∞−∞s21s22−∞s31s32s33]S_{\text{masked}} = \begin{bmatrix} s_{11} & -\infty & -\infty \\ s_{21} & s_{22} & -\infty \\ s_{31} & s_{32} & s_{33} \end{bmatrix}

Rows are queries; columns are keys. We keep the diagonal and everything below it (past + current). Everything above (future) is set to −∞-\infty.

Why −∞-\infty, not zero? Because softmax turns e−∞=0e^{-\infty} = 0 into exactly zero attention weight. A score of zero would still produce e0=1e^0 = 1, which means the token would still receive some attention.

ViT: BidirectionalGPT: Causal (masked)s(t,t)s(t,c)s(t,s)s(c,t)s(c,c)s(c,s)s(s,t)s(s,c)s(s,s)thecatsatevery token sees every token ✓s(t,t)-∞-∞s(c,t)s(c,c)-∞s(s,t)s(s,c)s(s,s)thecatsatfuture tokens blocked → can't cheat

The implementation is straightforward — Q, K, V are computed normally, but we apply a mask before softmax:

scores = Q @ K.transpose(-2, -1)
scores = scores / (Q.shape[-1] ** 0.5)

# Create upper-triangular mask (future positions)
N = Q.shape[-2]
future = torch.triu(
    torch.ones(N, N, device=Q.device, dtype=torch.bool),
    diagonal=1,
)

scores = scores.masked_fill(future, float("-inf"))
weights = scores.softmax(dim=-1)

output = weights @ V

So Q, K, and V are computed normally — each token gathers information only from itself and previous tokens. The architecture is the same; only the mask changes.

MLP Variations Across Models

The MLP block also evolves across model generations. The concept stays the same — expand features, apply nonlinearity, project back — but the details differ:

GPT-3 style (classic):

self.mlp = nn.Sequential(
    nn.Linear(dim, 4 * dim),         # expand 4×
    nn.GELU(approximate="tanh"),     # nonlinearity
    nn.Linear(4 * dim, dim),         # project back
)

Qwen3 / LLaMA style (gated MLP with SwiGLU):

# Two parallel projections + gating
gate = F.silu(self.gate_proj(x))     # [B, N, 4*dim]
up   = self.up_proj(x)              # [B, N, 4*dim]
x    = self.down_proj(gate * up)    # [B, N, dim]

The gated variant uses two parallel linear projections: one goes through SiLU activation (the "gate"), then multiplies element-wise with the other ("up"), and the result is projected back down. This gives the model more expressive power at similar compute cost.

| | GPT-3 | Qwen3 / LLaMA | |---|---|---| | MLP | Linear → GELU → Linear | Gate×Up (SwiGLU) → Linear | | Activation | GELU | SiLU (Swish) | | Normalization | LayerNorm | RMSNorm (faster, no mean subtraction) | | Position encoding | Learned absolute | RoPE (rotary, extrapolates to longer sequences) | | Residuals | Same | Same | | Attention + MLP structure | Same | Same |

The core pattern — attention for inter-token mixing, MLP for per-token transformation, residuals for information flow — remains unchanged from ViT through GPT-3 to the latest open-source LLMs.

Dimensions Through the LLM

The MLP output has the same shape as its input. For our Qwen3-0.6B example with 3 tokens:

[1,3,1024]  →MLP  [1,3,1024][1, 3, 1024] \;\xrightarrow{\text{MLP}}\; [1, 3, 1024]

Internally, the feature dimension expands to 3,072, then returns to 1,024. The batch size and token count stay unchanged. That lets us add the residual:

x = x + self.mlp(self.norm2(x))
# [1, 3, 1024] + [1, 3, 1024] → [1, 3, 1024]

We still have one updated vector per token.

From Representations to Next-Token Prediction

After attention and MLP, we pass through all Transformer blocks, then predict the next token:

# "the cat sat": assume 3 tokens
x = self.embedding(token_ids)      # [1, 3, 1024]

for block in self.blocks:
    x = block(x)                    # [1, 3, 1024]

x = self.final_norm(x)             # [1, 3, 1024]

Then we take the last token's representation. Because of causal attention, this vector contains information from "the," "cat," and "sat" — but nothing from the future:

last = x[:, -1, :]                 # [1, 1024]

A final linear layer — the language model head — produces one score for every entry in the vocabulary:

logits = self.lm_head(last)         # [1, 151936]  (Qwen3 vocab size)

We turn these scores into probabilities and select a token:

P(next token)=softmax(logits)∈R151936P(\text{next token}) = \text{softmax}(\text{logits}) \in \mathbb{R}^{151936}
probs = logits.softmax(dim=-1)
next_token = torch.multinomial(probs, 1)  # sample from distribution

Then append the new token and repeat:

"the cat sat"→predict"on"→append"the cat sat on"→predict"the"→...\text{"the cat sat"} \xrightarrow{\text{predict}} \text{"on"} \xrightarrow{\text{append}} \text{"the cat sat on"} \xrightarrow{\text{predict}} \text{"the"} \xrightarrow{\text{...}}
AUTOREGRESSIVE GENERATIONthecatsat→Transformer→onthecatsaton→Transformer→thethecatsatonthe→Transformer→mat

ViT: all patches in → one class out. LLM: tokens in → one new token out → append → repeat.

Unlike ViT, we don't average tokens or use CLS. The last position predicts what comes next.

This is autoregressive generation — the model produces one token at a time, each conditioned on everything before it. This is why LLM inference is fundamentally different from image classification: it requires sequential decode steps, KV-cache management, and is memory-bandwidth bound rather than compute-bound.

The Language Model Head

The LM head is a fully connected linear layer — no activation function after it:

self.lm_head = nn.Linear(1024, 151936, bias=False)

logits = self.lm_head(last)           # [1, 151936]
probabilities = logits.softmax(-1)    # [1, 151936]
last token vector⏟[1,1024]  →Linear  vocabulary scores⏟[1,151936]  →Softmax  next-token probabilities⏟[1,151936]\underbrace{\text{last token vector}}_{[1, 1024]} \;\xrightarrow{\text{Linear}}\; \underbrace{\text{vocabulary scores}}_{[1, 151936]} \;\xrightarrow{\text{Softmax}}\; \underbrace{\text{next-token probabilities}}_{[1, 151936]}

No ReLU or GELU after this linear layer — the raw logits go directly to softmax (for inference) or cross-entropy loss (for training):

# Training: logits → CrossEntropyLoss (handles softmax internally)
loss = F.cross_entropy(logits, target_token_id)

# Inference: logits → softmax → sample
probs = logits.softmax(dim=-1)
next_token = torch.multinomial(probs, 1)

This is the same pattern as ViT classification — the difference is the vocabulary size. ViT classifies into ~1,000 ImageNet classes. An LLM "classifies" into ~150,000 vocabulary tokens. But the final step is identical: Linear → Softmax → pick the winner.

LLM Architecture (Decoder-Only Transformer)

INPUT TOKENSThecatsatonthe?Token Embedding + Position EmbeddingTransformer Decoder Block × NMasked Self-AttnFeed Forward (MLP)+ LayerNorm+ LayerNorm+ Residual connectionsN = 32 (7B) | 80 (70B) | 126 (405B)Next Token: "mat" (p=0.87)Scale ProgressionViT-L307M · 300M imgGPT-21.5B · 40BGPT-3175B · 300BLLaMA 270B · 2TLLaMA 3405B · 15TSame architecture. Different scale.Same attention. Different modality.

The Key Difference: Training Objective

  • CNN: Supervised classification — predict the image class from labeled data
  • ViT: Supervised classification — same objective, but on patch sequences
  • LLM: Self-supervised next-token prediction — predict the next word given all previous words
L=−∑t=1Tlog⁡P(xt∣x1,x2,…,xt−1)\mathcal{L} = -\sum_{t=1}^{T} \log P(x_t \mid x_1, x_2, \ldots, x_{t-1})

Next-token prediction: the text itself is the supervision signal. No labels needed.

Next-token prediction turned out to be a remarkably powerful objective. By predicting the next token across trillions of tokens of text, the model implicitly learns syntax, semantics, reasoning patterns, world knowledge, and even basic logic.


What This Means for Infrastructure

If you are building ML infrastructure — which is what I do — the CNN → ViT → LLM trajectory has concrete consequences:

Compute Patterns Changed

| | CNN Inference | LLM Inference | |---|---|---| | Pattern | Single forward pass | Autoregressive (token by token) | | Bottleneck | Compute (FLOPs) | Memory bandwidth (KV-cache) | | Latency | Fixed per image | Grows with output length | | Memory | O(1) per request | O(seq_len) KV-cache per request | | Batching | Simple (fixed shape) | Complex (variable length) | | Hardware | CPU or single GPU | Multi-GPU, tensor parallelism | | Serving | Flask + PyTorch | vLLM + PagedAttention |

LLM inference has two phases:

Prefill phase (parallel):
  Process entire prompt at once
  Similar to a CNN forward pass
  Compute-bound

  "The cat sat on the" → [hidden states]

Decode phase (sequential):
  Generate one token at a time
  Each token depends on all previous
  Memory-bandwidth bound

  [hidden states] → "mat" → "." → [EOS]
                     ↑
                     KV-cache stores all
                     previous attention states

This is where vLLM's PagedAttention becomes critical — it eliminates memory fragmentation in the KV-cache, allowing more concurrent requests per GPU.


The Transformer did not just replace the CNN. It replaced the assumption that different domains need different architectures — and that single idea reshaped all of deep learning.