Decoder-Only · Dense · 2025
A layer-by-layer dissection of the architecture running underneath modern language models — embeddings, attention, residual streams, and the forward pass that turns tokens into meaning.
A note before you descend
The transformer architecture is the modern basis for large language models, and the concepts around it are dense enough that several people have already studied them thoroughly. It's one of the powerful architectures born out of a genuinely phenomenal paper, "Attention Is All You Need" — though the model proposed there was an encoder-decoder built for translation. The LLMs you actually talk to today run a slightly different variant: auto-regressive, decoder-only models.
This breakdown was originally meant to sit inside the Interpretability chapter of my open-draft book, Safe AI — but the material is dense enough on its own that I didn't want it to become a bottleneck for anyone trying to finish that chapter. So it lives here instead, built purely for the aesthetic of it, an excuse to try some new visuals. Probably the best way to remember something is to study it visually — in that spirit, here's this.
Before you take the deep dive: if you find my writing out of scope, or you just don't like the way I present content, here's where most of this understanding actually comes from. All of the below are absolutely fabulous, and I cannot recommend them enough:
01 TOKENIZATION
A transformer block can only take matrices as input, so the first real question is what representation of raw text actually conveys its essence. We start by dividing text into chunks called tokens. The obvious first guess is to split based on words — older NLP pipelines did exactly that — but that captures less nuance than we'd like, and it isn't free: it's a trade-off between vocabulary size and sequence length, and both heavily affect a model's cost and capability. Wait a minute …. Before we jump ahead of ourselves let us establish some definitions first so that we are all on the same page. Definition alert !!! Jump to Definitions
Split word-by-word and the vocabulary explodes the moment you account for misspellings, inflected forms, and out-of-vocabulary words. Drop all the way to character- or byte-level and the vocabulary shrinks to a tidy 256 bytes — but the sequence length balloons to roughly four times the word-level length, and any single character carries too little information on its own, pushing that burden onto later layers.
GPT-3's 50,257-token vocabulary is chosen to sit precisely at that sweet spot: the model can process roughly 4× more text in the same context window than a character-level model would allow, while still representing any possible Unicode string without ever needing an <UNK> token — and it also helps with reconstructing word boundaries.
I have built a simple tool you can play around with which tells you how different sentences are split and which token represents a particular number in the vocab: Tokenization Tool
GPT-3 uses a byte-level byte-pair encoding (BPE) it operates on raw bytes (UTF-8), which naturally handles any Unicode text, and it pre‑splits text with a regex before merging. The tokenization process has two stages. The tokenizer is defined by a fixed regular expression pre‑splitter, an ordered list of merge rules \(\mathcal{M}\), and a vocabulary \(\mathcal{V}\).
Formally, the encoder is a deterministic function \(\operatorname{encode}: \Sigma^* \to \mathcal{V}^*\) where \(\Sigma\) is the set of Unicode characters.
Given a sequence \((t_1,\dots,t_k),\ t_i \in \mathcal{V}\):
| Scheme | How it works | Used by |
|---|---|---|
| Byte-pair encoding (BPE) | Starts from characters/bytes, iteratively merges the most frequent adjacent pair. Byte-level variants guarantee no out-of-vocabulary tokens. | GPT, LLaMA, Mistral, Falcon |
| WordPiece | Similar to BPE, but merges are chosen by a language-model likelihood objective rather than pure frequency | BERT |
| Unigram LM (SentencePiece) | Treats tokenization as probabilistic segmentation, choosing a vocabulary that maximizes corpus likelihood under a unigram model | T5 and others |
| SentencePiece | A library implementing BPE or Unigram directly over a raw character stream — no word pre-tokenization, handy for languages without spaces | Multiple model families |
Choosing a vocabulary and a tokenization scheme is still a tricky, mostly iterative and intuition-driven craft. It's also worth noting that tokenization has been used as a method for jailbreaking LLMs via adversarial inputs, and that certain anomalous "glitch tokens" cause undocumented failures in GPT-style models — some break determinism even at temperature zero. Further reading here. As we'll see later, the architecture is built around maximizing the probability of the next token rather than chasing a single ground truth — so the model is always giving its best approximation for the next possible word
The below tool illustrates how the merges will be carried out. we took a simple sequences run runner and the appropriate merging is illustrated below click on next step until embeddings appear
New string: "run runner" (split into "run" and " runner")
Merge list 𝒜: (114,117)→256, (256,110)→257, (257,32)→258, (258,114)→259, (259,117)→260, (260,110)→261
After encoding: each token ID looks up a 4‑dim embedding vector from the matrix.
2 ENCODING
Once tokens exist, each is mapped to a vector via a learned embedding matrix We ∈ ℝV×d — one column per vocabulary entry which are intialized randomly in the begining and fine tuned entirely through training process. GPT-3 embeds into 12,288 dimensions and each of the directions in that space has real semantic content rather than arbitrary coordinates.
if we map these embeddings to a higher dimensional space and subtract the embedding for Paris from France, then add that difference to Tokyo; the resulting vector lands remarkably close to Japan. This suggests the model has implicitly learned a direction that encodes the relationship “is the capital city of,” allowing it to recover a country from its capital as neatly as it recovers a queen from a king.
Embedding space (2D projection) — France, Paris, Tokyo, Japan are word vectors.
The direction France − Paris captures "capital‑country" relationship. Adding it to Tokyo yields a point remarkably close to Japan.
Dot products give a way to measure this quantitatively: geometrically, a dot product is positive when two vectors point in similar directions, near zero when they're roughly perpendicular, and negative when they point apart. Taking the difference between the embeddings of cats and cat gives something like a "plurality direction" — dotting it against plural nouns scores consistently higher than against their singular counterparts, and dotting it against the embeddings for the numerals 1, 2, 3… gives steadily increasing values, as if plurality itself were a quantity the model can measure.
Tokenizing and embedding isn't unique to language — a sequence of tokens is a generic enough representation that many different data types can be tokenized and fed straight into a transformer, rather than requiring a bespoke architecture per modality (CNNs for images, RNNs for sequences, DeepSets for sets, and so on). That also means you don't need handcrafted architectures for mixing modalities — everything can just become one big set of tokens. Below table contains some of the most popular endoing scheme for some popular models for different use cases
| Embedding type | Modality | Description | Used by |
|---|---|---|---|
| Learned token embedding (subword) | Text | Look-up table over BPE/WordPiece tokens | BERT, GPT-2/3/4, T5, LLaMA, most standard language transformers |
| Absolute sinusoidal positional encoding | Text | Fixed sine/cosine functions of position | Original Transformer, Transformer-XL (on values), DETR (for images) |
| Absolute learned positional embedding | Text | Trainable vector per position | BERT, GPT, GPT-2 |
| Rotary Position Embedding (RoPE) | Text | Multiplicative rotation of query/key by a position-dependent angle | LLaMA, GPT-NeoX, PaLM, Mistral, Falcon |
| ALiBi | Text | Adds a static, non-learned linear bias to attention scores based on distance | Some GPT-style models (e.g. BLOOM's ALiBi option) |
| Relative position bias | Text | Learned scalar bias per relative distance, added before softmax | T5, Transformer-XL |
| Segment / token-type embeddings | Text | Learned vector distinguishing sentence A vs. B | BERT, XLNet |
| Patch flatten + linear projection | Image | Split image into patches, flatten, project via a linear layer | ViT, DeiT, BEiT, MAE, SimMIM |
| 2D sinusoidal positional encoding | Image | Sinusoidal encoding of row and column indices | DETR, some ViT implementations |
| Learned 2D positional embedding | Image | Trainable embedding per (row, col) patch position | ViT (original), MAE |
| Relative 2D position bias | Image | Learned bias based on relative 2D coordinates | Swin Transformer |
| CNN feature map tokens | Image | Feature vectors from a CNN backbone treated as input tokens, projected to d | DETR, early ViT hybrids |
| Pixel-as-token embedding | Image | Raw pixel intensities (0–255) mapped to learned embeddings + 2D position | iGPT |
| Spectrogram patch embedding | Audio | Patches of a mel spectrogram flattened and linearly projected | Audio Spectrogram Transformer (AST) |
| CNN encoder + quantized codebook | Audio | Raw waveform → conv layers → discrete units from a codebook | Wav2Vec 2.0, HuBERT, WavLM |
| 3D tubelet patch embedding | Video | Spatio-temporal patches ("tubes") flattened and projected linearly | ViViT |
| Factorised spatial-temporal embedding | Video | Separate spatial patch embeddings per frame + temporal position per patch | TimeSformer |
| Mini-PointNet patch embedding | 3D Point Cloud | Group points into patches, embed with a shared MLP, add patch-center positional encoding | Point-BERT, Point-MAE |
| Laplacian / random-walk PE + node projection | Graph | Node features projected; structural positional encoding added | Graphormer, SAN, GRPE-based transformers |
| Sub-series patching + linear projection | Time Series | Univariate/multivariate series split into patches, then projected | PatchTST |
| Feature tokenizer + column embedding | Tabular | Each numerical feature scaled by a learned vector; categorical features embedded; column id added | FT-Transformer, TabTransformer |
| Byte-level embedding | Text | Directly embeds raw UTF-8 bytes via a small embedding table | ByT5, CANINE |
| Object query embeddings (learned) | Image (Detection) | A set of learned vectors interacts with encoder output via cross-attention | DETR (decoder queries) |
| Cross-attention via latent array | Multimodal | A fixed-size array of learned latents queries raw input (pixels, audio, etc.) to produce tokens | Perceiver, Perceiver IO |
We'll stay exclusively on text-based input and generative pretrained transformers from here on — other modalities get their own detour elsewhere (link pending).
Once the tokens are generated using the above bpe algorithm they are converted to embeddings using the below procedure .The transformer does not have a sense of position of the words for example "Only you can do this job poorly." is vastly different from " You can only do this job poorly." if you shuffle the words around they represent completely different things. hence it is incredbly important to capture not just the words but also there positions for this we use 2 different methods to generate embeddings one to capture the position.
The token and positional embeddings are summed element‑wise and then regularised with dropout to form the initial hidden state: \[ \mathbf{h}_0 = \text{Dropout}\bigl( \mathbf{T} + \mathbf{P} \bigr) \qquad \in \mathbb{R}^{B \times T \times d_{\text{model}}} \] This combined tensor is the first input fed to the transformer decoder blocks, where context‑dependent meaning finally emerges.
hidden_state = self.embed_dropout(tok_vec + pos_vec) # h_0, shape (B, T, d_model)
Learned parameter: \(\mathbf{W}_e \in \mathbb{R}^{V \times d_{\text{model}}}\) – one row per vocabulary entry.
token_ids = x # (batch, seq_len)
tok_vec = self.token_embedding(token_ids) # lookup & shape (B, T, d_model)
tok_vec = tok_vec * math.sqrt(self.d_model) # scaling
Learned parameter: \(\mathbf{W}_p \in \mathbb{R}^{T_{\max} \times d_{\text{model}}}\) – one row per absolute position up to \(T_{\max}\).
positions = torch.arange(seq_len, device=x.device) # (T,)
pos_vec = self.position_embedding(positions) # (T, d_model), broadcasts over batch
Attention has no inherent sense of order, so sinusoidal signals at geometrically increasing wavelengths are added to every embedding — letting the model recover relative and absolute position from phase alone.
SIN / COS · 2i / d_model POS 0 – 64Even dimensions carry sine, odd dimensions carry cosine — wavelengths grow geometrically across the embedding dimension d.
| Scheme | Mechanism | Used by |
|---|---|---|
| Absolute sinusoidal | Fixed sine/cosine functions of position, added to the token embedding | Original Transformer, Transformer-XL, DETR |
| Absolute learned | A trainable vector per position, same shape as the sinusoidal version | BERT, GPT, GPT-2 |
| RoPE (rotary) | Rotates Q/K by a position-dependent angle instead of adding anything to the embedding | LLaMA, GPT-NeoX, PaLM, Mistral, Falcon — and FORGE-1's own θ=500K scheme |
| ALiBi | A static, non-learned linear bias subtracted from attention scores by distance | BLOOM (optional) |
| Relative position bias | A learned scalar bias per relative distance, added pre-softmax | T5, Transformer-XL |
# Embedding lookup — the very first step of the forward pass
# token_embedding: (vocab_size, d_model) — one row per vocabulary entry
# position_embedding: (max_seq_len, d_model) — one row per sequence position
token_ids = x # (batch, seq_len) integer ids
positions = torch.arange(seq_len, device=x.device) # (seq_len,)
tok_vec = self.token_embedding(token_ids) # (batch, seq_len, d_model)
pos_vec = self.position_embedding(positions) # (seq_len, d_model), broadcasts over batch
hidden_state = self.embed_dropout(tok_vec + pos_vec) # this is h_0
03 · Normalization
The generated embeddings are normalized before passing onto the attention block. The normalization helps get all the embeddings onto a similar scale. the below illustration can help you understand. click on a word and see how the embeeding of a word gets normalized for simplicity we made most of the vector lengths shorter. But the process remains the same mean is calculated and subtracted from the vector and divided by the square root of e+ standard deviation squared. The e term here helps with division by zero error.
04 · Scaled Dot-Product Attention
After the layer normalization step is done the embeddings get passed onto the attention block. This is probabaly the phase that defines the classic transformer architecture and distinguishes from rest of the neural nets.
GPT‑2 uses multi‑head causal self‑attention in every decoder block. For a given layer, the input is the hidden state h ∈ ℝB×T×d, where B is batch size, T is sequence length, and d is the model dimension. The output is a context‑aware tensor of the same shape, computed as follows.
M is the causal mask — it removes any attention to future positions before the softmax is taken.
Every decoder block projects its normalized hidden state into queries, keys, and values with one fused weight matrix, then splits the result across heads:
# qkv_proj: (d_model, 3 * d_model) — one combined matrix for Q, K, and V
q, k, v = qkv_proj(normed_hidden).split(d_model, dim=-1)
# reshape so each of the H heads attends independently
q = q.view(batch, seq_len, num_heads, head_dim).transpose(1, 2)
k = k.view(batch, seq_len, num_heads, head_dim).transpose(1, 2)
v = v.view(batch, seq_len, num_heads, head_dim).transpose(1, 2)
Scaled dot-product attention with the causal mask:
attention_weights = softmax((q @ k.transpose(-2, -1)) / sqrt(head_dim) + causal_mask)
context = attention_weights @ v
The 1/√d_h scaling keeps dot products from growing too large as head dimension increases, which would otherwise push softmax toward a near one-hot, hard-to-train distribution. The causal mask sets every position j > i to −∞ before the softmax, so token i can only ever attend to itself and whatever came before it — the mechanism that makes autoregressive generation well-defined at all.
Setup happens once, in __init__ — the fused projection, the output projection, dropout, and (only as a fallback) an explicit causal mask buffer:
class MultiHeadSelfAttention(nn.Module):
"""Causal multi-head self-attention with a single fused QKV projection."""
def __init__(self, cfg):
super().__init__()
assert cfg.d_model % cfg.num_heads == 0
# one matrix produces queries, keys, and values together — cheaper than three separate ones
self.qkv_proj = nn.Linear(cfg.d_model, 3 * cfg.d_model, bias=cfg.use_bias)
self.attn_out_proj = nn.Linear(cfg.d_model, cfg.d_model, bias=cfg.use_bias)
self.attn_drop = nn.Dropout(cfg.drop_prob)
self.resid_drop = nn.Dropout(cfg.drop_prob)
self.num_heads = cfg.num_heads
self.d_model = cfg.d_model
self.drop_prob = cfg.drop_prob
# prefer a fused, hardware-optimized attention kernel when available
self.use_flash = hasattr(F, "scaled_dot_product_attention")
if not self.use_flash:
# fallback: an explicit lower-triangular mask, built once and reused
causal = torch.tril(torch.ones(cfg.max_seq_len, cfg.max_seq_len))
self.register_buffer("causal_mask", causal.view(1, 1, cfg.max_seq_len, cfg.max_seq_len))
And forward does the actual reshaping, dispatch to the fused kernel when it's available, and the manual fallback path when it isn't:
def forward(self, x):
batch, seq_len, d_model = x.size()
q, k, v = self.qkv_proj(x).split(self.d_model, dim=2)
head_dim = d_model // self.num_heads
q = q.view(batch, seq_len, self.num_heads, head_dim).transpose(1, 2)
k = k.view(batch, seq_len, self.num_heads, head_dim).transpose(1, 2)
v = v.view(batch, seq_len, self.num_heads, head_dim).transpose(1, 2)
if self.use_flash:
# fused kernel — see FORGE-1's spec panel (FlashAttention-3)
out = F.scaled_dot_product_attention(
q, k, v, attn_mask=None,
dropout_p=self.drop_prob if self.training else 0,
is_causal=True,
)
else:
scores = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(head_dim))
scores = scores.masked_fill(self.causal_mask[:, :, :seq_len, :seq_len] == 0, float("-inf"))
weights = self.attn_drop(F.softmax(scores, dim=-1))
out = weights @ v
out = out.transpose(1, 2).contiguous().view(batch, seq_len, d_model)
return self.resid_drop(self.attn_out_proj(out))
05 RESIDUAL STREAM
The residual stream is the place where after the execution of the each block whether it is the mlp block or attention block the model writes to as the data is passed between the layers it under goes massive amount of dimenionsal changes often mapping from higher to lower dimensions and viceversa as we saw in the above section there multiple attention heads and several layers of these repeated pattern. Residual stream helps solve this problem we just simply write to a flow the so called a information highway
Residual streams play an important role in understanding the internals of the model it helps us grasp how different parts of the blocks interact.
In the context of the residual stream, circuits are naturally framed as the computational pathways that read from and write to this shared, accumulated state. The residual stream acts as a high-dimensional memory where each layer adds information without overwriting the past, and interpretability research uses terminology like features to describe the meaningful directions encoded in its activations—concepts, entities, or grammatical properties that later components can access. Because the stream compresses many features into a single vector through superposition, individual neurons rarely correspond to clean, isolated concepts; instead, circuits are traced by identifying how attention heads and MLP neurons interact through the stream.
Path patching and activation patching are applied directly to the residual stream to measure how specific upstream components shape the representation that downstream heads read, revealing structures like induction circuits that enable in-context learning. The residual stream thus becomes the central interpretive medium: features are written into it, superimposed, and then selectively read out, making it the backbone through which circuits compose complex behaviors.
06 FEED FORWARD LAYER
Every position is independently expanded to roughly four times the model width, pushed through a GELU nonlinearity, then projected back down — the part of the network most often credited with storing factual knowledge.
The feed‑forward network (FFN) is a position‑wise sublayer. It processes each token independently, introduces non‑linearity, and contains the bulk of the model's capacity. The operation is identical for every sequence position.
Two affine transformations with a ReLU activation in between.
Parameters
\(W_1 \in \mathbb{R}^{d_{\text{model}} \times d_{\text{ff}}}\), \(b_1 \in \mathbb{R}^{d_{\text{ff}}}\)
\(W_2 \in \mathbb{R}^{d_{\text{ff}} \times d_{\text{model}}}\), \(b_2 \in \mathbb{R}^{d_{\text{model}}}\)
\(d_{\text{ff}} = 4 \cdot d_{\text{model}}\) in the original paper.
Per‑token operation (for a single vector \(x \in \mathbb{R}^{d_{\text{model}}}\)):
\[ \operatorname{FFN}(x) = \max(0,\; xW_1 + b_1)\,W_2 + b_2 \]Full sequence (input matrix \(X \in \mathbb{R}^{n \times d_{\text{model}}}\)):
\[ \operatorname{FFN}(X) = \max(0,\; XW_1 + \mathbf{1}_n b_1^T)\,W_2 + \mathbf{1}_n b_2^T \]This is equivalent to two independent \(1\!\times\!1\) convolutions applied to every token vector.
Used in BERT, GPT‑2, and many encoder models. Smooth alternative to ReLU:
\[ \operatorname{GELU}(x) = x \cdot \Phi(x) \approx 0.5x\left(1 + \tanh\left[\sqrt{2/\pi}\left(x + 0.044715 x^3\right)\right]\right) \] \[ \operatorname{FFN}_{\text{GELU}}(x) = \operatorname{GELU}(xW_1 + b_1)\,W_2 + b_2 \]Modern large language models (PaLM, LLaMA, etc.) use a gated variant with a Swish activation, improving performance while maintaining comparable computational cost.
Parameters
\(W_1, W_3 \in \mathbb{R}^{d_{\text{model}} \times d_{\text{ff}}}\), \(W_2 \in \mathbb{R}^{d_{\text{ff}} \times d_{\text{model}}}\)
(Biases are typically omitted.)
Operation (element‑wise gating):
\[ \operatorname{SwiGLU}(x) = \big(\operatorname{Swish}(xW_1) \;\odot\; xW_3\big)\,W_2 \]where \(\operatorname{Swish}(z) = z \cdot \sigma(z) = z \cdot \operatorname{silu}(z)\). The inner dimension \(d_{\text{ff}}\) is often rescaled (e.g., \(\frac{8}{3}d_{\text{model}}\)) to keep the parameter count similar to the standard FFN.
Inside a Transformer block, the FFN is wrapped with a residual connection and layer normalisation. Given input \(X\) (after attention and residual):
Φ is the standard normal CDF — GELU weighs each input by how likely it is to exceed a random threshold, rather than hard-clipping at zero like ReLU.
Every position runs through the identical MLP independently — no mixing across positions happens here, that already happened in attention. The expand-then-contract shape gives the network room to compute something more elaborate than a single linear pass would allow before compressing back down to the model's working width.
class FeedForward(nn.Module):
def __init__(self, cfg):
super().__init__()
self.mlp_up_proj = nn.Linear(cfg.d_model, 4 * cfg.d_model, bias=cfg.use_bias)
self.activation = nn.GELU()
self.mlp_down_proj = nn.Linear(4 * cfg.d_model, cfg.d_model, bias=cfg.use_bias)
self.drop = nn.Dropout(cfg.drop_prob)
def forward(self, x):
x = self.mlp_up_proj(x)
x = self.activation(x)
x = self.mlp_down_proj(x)
return self.drop(x)
07 DECODER BLOCK
Tokenize, embed, add position, then loop through N identical decoder blocks — each a pre-norm attention sub-layer and a pre-norm feed-forward sub-layer, both wrapped in a residual add — before a final projection to vocabulary logits.
Repeated for ℓ = 1 … L, then the final normalized state is projected onto the vocabulary.
GPT — Generative Pretrained Transformer — is the specific decoder-only recipe this walkthrough builds towards. There are several different types of transformer varaints.
class DecoderBlock(nn.Module):
"""One pre-norm attention sub-layer + one pre-norm feed-forward sub-layer,
each wrapped in its own residual connection."""
def __init__(self, cfg):
super().__init__()
self.pre_attn_norm = LayerNorm(cfg.d_model, cfg.use_bias)
self.attn = MultiHeadSelfAttention(cfg)
self.pre_mlp_norm = LayerNorm(cfg.d_model, cfg.use_bias)
self.mlp = FeedForward(cfg)
def forward(self, x):
x = x + self.attn(self.pre_attn_norm(x)) # attention sub-layer, residual add
x = x + self.mlp(self.pre_mlp_norm(x)) # feed-forward sub-layer, residual add
return x
Everything upstream — token embedding, position embedding, the stack of decoder blocks, and the tied unembedding head — is assembled once in __init__:
class ForgeTransformer(nn.Module):
def __init__(self, cfg):
super().__init__()
self.cfg = cfg
self.backbone = nn.ModuleDict(dict(
token_embedding=nn.Embedding(cfg.vocab_size, cfg.d_model),
position_embedding=nn.Embedding(cfg.max_seq_len, cfg.d_model),
embed_dropout=nn.Dropout(cfg.drop_prob),
layers=nn.ModuleList([DecoderBlock(cfg) for _ in range(cfg.num_layers)]),
final_norm=LayerNorm(cfg.d_model, cfg.use_bias),
))
self.lm_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
# weight tying: the unembedding is literally the embedding matrix, transposed
self.backbone.token_embedding.weight = self.lm_head.weight
And forward runs the actual pass — embed, loop through every decoder block, normalize, then either compute a training loss or return only the last position's logits for inference:
def forward(self, token_ids, targets=None):
batch, seq_len = token_ids.size()
positions = torch.arange(seq_len, device=token_ids.device)
tok_vec = self.backbone.token_embedding(token_ids)
pos_vec = self.backbone.position_embedding(positions)
x = self.backbone.embed_dropout(tok_vec + pos_vec)
for layer in self.backbone.layers:
x = layer(x)
x = self.backbone.final_norm(x)
if targets is not None:
logits = self.lm_head(x)
loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-1)
else:
# inference: only the last position's logits are actually needed
logits = self.lm_head(x[:, [-1], :])
loss = None
return logits, loss
08 UMEMBEDDING LAYER
At the final layer of the transformer, each position in the sequence holds a hidden state vector \(\mathbf{h} \in \mathbb{R}^{d_{\text{model}}}\). For autoregressive generation, the hidden state at the last position is used to predict the next token. The unembedding matrix \(W_U \in \mathbb{R}^{|\mathcal{V}| \times d_{\text{model}}}\) maps this hidden state to a score for every token in the vocabulary:
\[ \mathbf{z} = W_U \, \mathbf{h} \qquad \text{(logits)} \]Applying the softmax function yields a proper probability distribution:
\[ P(t_i \mid \text{context}) = \frac{\exp(z_i / \tau)}{\sum_{j=1}^{|\mathcal{V}|} \exp(z_j / \tau)} \]where \(\tau\) is the temperature parameter — lower values sharpen the distribution (more deterministic), higher values flatten it (more exploratory). At \(\tau=1\) this is the standard softmax. The model then either selects the token with the highest probability (greedy decoding) or samples from the distribution. Finally, the chosen token ID is decoded back to text by reversing the BPE merge rules — recursively expanding merged tokens until only raw bytes remain, then decoding those bytes via UTF-8.
The interactive tool below demonstrates this entire pipeline for a single position, using the same 4‑dimensional toy embedding space and small vocabulary introduced in the BPE encoding example above.
The tool below walks through the unembedding process step by step. We start with a hidden state vector \(\mathbf{h}\) (the output of the transformer for the last position) and trace it all the way back to readable text. Click Next Step to begin.
Hidden state h: \([0.50,\; 0.60,\; 0.40,\; 0.30]\) (output from final transformer layer, last position)
Unembedding matrix \(W_U\): \(12 \times 4\) · one row per token in our toy vocabulary
Temperature \(\tau = 1.0\) for the softmax (standard scaling)
Predicted token: 257 → expands to "run" after BPE decoding
Given hidden state \(\mathbf{h} \in \mathbb{R}^{d}\) and unembedding matrix \(W_U \in \mathbb{R}^{|\mathcal{V}|\times d}\):
\[ z_i = \sum_{k=1}^{d} (W_U)_{i,k} \cdot h_k \quad \text{for } i = 1,\dots,|\mathcal{V}| \]Equivalently, \(\mathbf{z} = W_U \mathbf{h}\). Each logit \(z_i\) is the dot product between the hidden state and the learned vector for token \(i\).
As \(\tau \to 0\), the distribution approaches a one-hot encoding of the argmax token (greedy). As \(\tau \to \infty\), it approaches a uniform distribution.
Given token \(t^*\), recursively apply the inverse of the merge list \(\mathcal{M}\):
This guarantees a lossless round-trip: decode(encode(s)) = s for any Unicode string \(s\).
In many transformer architectures (including GPT-2/3, LLaMA, and others), the unembedding matrix is not independently learned — instead, it's set to the transpose of the embedding matrix: \(W_U = W_E^\top\). This is called weight tying. The intuition is elegant: if the embedding for "run" is the vector that represents that token in the input space, then the same vector should serve as the detector for that token in the output space. The dot product \(\mathbf{h} \cdot \mathbf{e}_{\text{token}}\) measures how closely the hidden state aligns with that token's learned representation.
Weight tying reduces the parameter count by \(|\mathcal{V}| \times d_{\text{model}}\) (roughly 50,257 × 768 ≈ 38.6M parameters for a small GPT-style model) and has been shown to improve generalization, especially for rare tokens whose output embeddings would otherwise receive very little gradient signal during training.
Model Card
N is parameter count, D is training tokens — the back-of-envelope compute estimate behind GPT-3's ~3.15 × 10²³ total training FLOPs (175B params × 300B tokens × 6).
Some of the Key Definitions
Support
Good writing takes time. If any of this thinking has been useful, interesting, or even just made you pause — a small contribution keeps it going.
Click the button or scan the QR code — whichever is easier.
Scan to support