Decoder-Only Transformer¶
This repo implements a GPT-style decoder-only Transformer. "Decoder-only" means:
- the model reads a single token sequence;
- each position can attend only to previous positions;
- the output at every position is a distribution over the next token.
It is the right architecture for autoregressive language modeling:
The forward pass¶
flowchart TD
IDS["token ids (B, T)"] --> TOK["token embedding (B, T, C)"]
IDS --> POS["position ids 0..T-1"]
POS --> PE["position embedding (T, C)"]
TOK --> SUM["token + position"]
PE --> SUM
SUM --> B1["Block 1"]
B1 --> B2["Block 2"]
B2 --> BN["... N blocks"]
BN --> LN["final LayerNorm"]
LN --> HEAD["lm_head Linear(C -> vocab)"]
HEAD --> LOGITS["logits (B, T, V)"]
The implementation is in src/models/transformer.py:
self.token_embed = nn.Embedding(vocab_size, n_embed)
self.position_embed = nn.Embedding(context_length, n_embed)
self.attn_blocks = nn.ModuleList([
Block(n_head, n_embed, context_length) for _ in range(N_BLOCKS)
])
self.layer_norm = nn.LayerNorm(n_embed)
self.lm_head = nn.Linear(n_embed, vocab_size)
Shape symbols used throughout the docs:
| Symbol | Meaning |
|---|---|
B |
batch size |
T |
sequence length / context length |
C |
embedding width, n_embed |
H |
number of attention heads |
D |
head width, usually C / H |
V |
vocabulary size |
Embeddings¶
Token ids are categorical. The embedding table is a learned lookup:
For token id \(x_t\), the token vector is:
The model also learns absolute position embeddings:
The input to the first block is:
In code:
tok_embedding = self.token_embed(idx)
pos_embedding = self.position_embed(self.pos_idxs[:T])
return tok_embedding + pos_embedding
The position embedding is necessary because attention alone is permutation-equivariant: without position information, the model would not know whether a token occurred first, last, or in the middle.
Transformer block¶
Each block in src/models/transformer_block.py uses pre-norm residual structure:
Mathematically:
This gives each block two jobs:
- attention moves information across token positions;
- the MLP transforms each position independently.
Why residual connections matter¶
A residual block learns an update, not a full replacement:
If a layer is not useful yet, it can learn a small update and let information pass through. This makes deep stacks trainable because gradients have a direct path backward through the addition.
Why LayerNorm appears before sublayers¶
LayerNorm normalizes each token vector across its feature dimension:
For a token vector \(x \in \mathbb{R}^{C}\):
This repo uses pre-norm (LN -> sublayer -> residual) instead of post-norm (sublayer -> residual ->
LN). Pre-norm is common in GPT-like models because it tends to make deeper stacks easier to optimize.
MLP / feed-forward network¶
The block MLP in src/models/mlp.py is:
self.hidden = nn.Linear(n_embed, 4 * n_embed)
self.relu = nn.ReLU()
self.proj = nn.Linear(4 * n_embed, n_embed)
For each position independently:
where:
- \(W_1\) expands from \(C\) to \(4C\);
- \(W_2\) projects from \(4C\) back to \(C\).
Attention lets tokens communicate. The MLP gives each token vector nonlinear compute after that communication.
Logits and the language-model head¶
After the final block and final norm:
The result \(z_t \in \mathbb{R}^{V}\) is a vector of logits: one unnormalized score per token in the vocabulary.
The probability distribution comes from softmax:
Parameter count intuition¶
Ignoring biases and norms, a rough per-block parameter estimate is:
because Q, K, V, and the output projection are each \(C \times C\).
because \(C \to 4C\) and \(4C \to C\).
So each block is roughly:
The embedding and LM head add:
This repo does not tie token embeddings and output embeddings, so the input embedding and lm_head
are separate parameter matrices.
Repo-specific architecture notes¶
| Choice | Repo implementation | Consequence |
|---|---|---|
| Absolute learned positions | nn.Embedding(context_length, n_embed) |
Simple and readable; fixed max context. |
| Causal attention mask | lower-triangular buffer in each head | Prevents future-token leakage. |
| MLP activation | ReLU | Educationally simple; many production GPT models use GELU/SwiGLU variants. |
| Dropout | not present in base modules | Less code noise; regularization comes mostly from data and optimizer choices. |
| Weight tying | not used | Easier to read; more parameters than tied embeddings. |
| Post-training heads | use forward_hidden |
Reward/value heads reuse the same backbone. |
Next¶
The most important sublayer is attention. Continue to Attention, Masks & Heads.