AI Knowledge Tree

Decoder-only transformers: the GPT family

Trace a decoder-only language model from token ids to next-token logits through causal masking, and walk through GPT-2's parameter counts.

The Transformer was originally designed with both an encoder and a decoder for machine translation. Decoder-only models (the GPT family) discard the encoder entirely: NN stacked transformer blocks with causal masking, optimized by next-token prediction on text. This minimal design has become the dominant architecture for large language models[1].

Architecture overview

Three phases:

  1. Embed: token ids →\to dd-dimensional embeddings via We∈RV×dW_e \in \mathbb{R}^{V \times d}. Add positional encodings.
  2. Transform: NN identical blocks, each with pre-norm, causal self-attention, and FFN.
  3. Unembed: final LayerNorm, then project to logits: ℓ=hfinalWe⊤\ell = h_{\text{final}} W_e^\top, where the embedding matrix WeW_e is typically weight-tied to the output projection (halves parameter count).

Causal masking

Every layer uses a causal (lower-triangular) attention mask: position tt attends only to positions j≤tj \le t. This ensures the model cannot "see" future tokens when predicting the next one, making the next-token prediction objective valid.

In practice, attention is implemented with full T×TT \times T matrices and the upper triangle is set to −∞-\infty before softmax (as in the self-attention lesson), not with sparse indexing.

GPT-2 parameter count

GPT-2 small: d=768d = 768, h=12h = 12 heads, dff=3072d_{\text{ff}} = 3072, N=12N = 12 blocks, V=50257V = 50257.

ComponentFormulaCount
Token embedding WeW_eV×dV \times d38.638.6M
Positional embeddingTmax⁡×d=1024×768T_{\max} \times d = 1024 \times 7680.790.79M
Per-block attention (Q,K,V,OQ,K,V,O)4d24 d^22.362.36M
Per-block FFN2d⋅dff+biases2 d \cdot d_{\text{ff}} + \text{biases}4.724.72M
Per-block LN (×2\times 2)4d4dnegligible
Total per block7.087.08M
12 blocks85.085.0M
Total (weight-tied output)≈117\approx 117M

The embedding matrix dominates for small vocab; the FFN blocks dominate for large dd.

Training objective

Next-token prediction (causal language modeling):

L=−1T∑t=1Tlog⁡p(xt∣x1,…,xt−1;θ).\mathcal{L} = -\frac{1}{T} \sum_{t=1}^{T} \log p(x_t \mid x_1, \dots, x_{t-1}; \theta).

Every token in a sequence is a training target simultaneously (unlike RNNs, which must compute one forward pass per position serially). This parallelism is why Transformers replaced RNNs for large-scale pretraining.

Forward pass in numpy

python · numpy
import numpy as np

def softmax(x, axis=-1):
    e = np.exp(x - x.max(axis=axis, keepdims=True))
    return e / e.sum(axis=axis, keepdims=True)

def layer_norm(x, g, b, eps=1e-5):
    mu = x.mean(-1, keepdims=True)
    std = x.std(-1, keepdims=True)
    return g * (x - mu) / (std + eps) + b

def causal_attention(Q, K, V):
    T, dk = Q.shape
    scores = Q @ K.T / np.sqrt(dk)
    mask   = np.triu(np.ones((T, T)), k=1).astype(bool)
    scores[mask] = -1e9
    return softmax(scores) @ V

class TinyGPT:
    def __init__(self, V, d, n_heads, d_ff, n_layers, T_max, seed=0):
        rng = np.random.default_rng(seed)
        s = 0.02
        self.We = rng.normal(0, s, (V, d))
        self.Wp = rng.normal(0, s, (T_max, d))
        self.layers = []
        for _ in range(n_layers):
            self.layers.append({
                "Wq": rng.normal(0, s, (d, d)),
                "Wk": rng.normal(0, s, (d, d)),
                "Wv": rng.normal(0, s, (d, d)),
                "Wo": rng.normal(0, s, (d, d)),
                "W1": rng.normal(0, s, (d_ff, d)),
                "W2": rng.normal(0, s, (d, d_ff)),
                "g1": np.ones(d), "b1": np.zeros(d),
                "g2": np.ones(d), "b2": np.zeros(d),
            })
        self.gf = np.ones(d); self.bf = np.zeros(d)
        self.d, self.n_heads = d, n_heads

    def forward(self, ids):
        T = len(ids)
        x = self.We[ids] + self.Wp[:T]    # (T, d)
        h = self.d // self.n_heads
        for layer in self.layers:
            # Pre-norm attention
            xn = layer_norm(x, layer["g1"], layer["b1"])
            Q = xn @ layer["Wq"]; K = xn @ layer["Wk"]; V = xn @ layer["Wv"]
            # Multi-head (simplified: single head for readability)
            attn_out = causal_attention(Q, K, V) @ layer["Wo"]
            x = x + attn_out
            # Pre-norm FFN
            xn = layer_norm(x, layer["g2"], layer["b2"])
            ff = np.maximum(0, xn @ layer["W1"].T) @ layer["W2"].T
            x = x + ff
        x = layer_norm(x, self.gf, self.bf)
        return x @ self.We.T   # (T, V) logits (weight-tied)

    def loss(self, ids):
        logits = self.forward(ids[:-1])
        targets = ids[1:]
        log_probs = np.log(softmax(logits))
        return -log_probs[np.arange(len(targets)), targets].mean()

V, d, h, dff, L, Tmax = 100, 32, 4, 128, 2, 64
model = TinyGPT(V, d, h, dff, L, Tmax)
ids   = np.array([5, 12, 3, 99, 44, 7])
print(f"Loss: {model.loss(ids):.4f}  (random init ≈ {np.log(V):.4f})")

Untrained, the loss should be close to log⁡V\log V (uniform over the vocabulary).

GPT family progression

ModelddNNVVParamsTraining tokens
GPT-2 (small)7681250K117M40B
GPT-312,2889650K175B300B
LLaMA-2 70B8,1928032K70B2T
LLaMA-3 70B8,19280128K70B15T

The architecture is nearly identical across the family; gains come from scale (parameters and tokens) and improved data quality.

Exercises

Exercise 1. GPT-2 small ties its output projection to the token embedding We∈RV×dW_e \in \mathbb{R}^{V \times d}. Using V=50257V = 50257 and d=768d = 768, compute the parameter count of WeW_e, and state how many parameters the tie saves versus an untied output head of the same shape.

Solution

Step 1. The embedding matrix has V×d=50257×768=38,597,376V \times d = 50257 \times 768 = 38{,}597{,}376 parameters, about 38.638.6M.

Step 2. An untied output head would be a second matrix of the same V×dV \times d shape, another 38.638.6M parameters. Weight tying reuses We⊤W_e^\top for the unembed, so it saves the full 38.638.6M, roughly a third of the model's 117117M total. This is why the embedding dominates the budget at small vocab.

Exercise 2. A decoder-only block applies pre-norm causal attention then a pre-norm FFN, both with residual connections. For width d=768d = 768 and dff=3072d_{\text{ff}} = 3072, count the parameters in one block's four attention projections (Q,K,V,OQ, K, V, O) and its two FFN matrices (ignore biases and LayerNorm). Which dominates?

Solution

Step 1. Each of WQ,WK,WV,WOW^Q, W^K, W^V, W^O is d×dd \times d, so the attention block holds 4d2=4⋅7682=2,359,2964 d^2 = 4 \cdot 768^2 = 2{,}359{,}296 parameters, about 2.362.36M.

Step 2. The FFN has W1∈Rdff×dW_1 \in \mathbb{R}^{d_{\text{ff}} \times d} and W2∈Rd×dffW_2 \in \mathbb{R}^{d \times d_{\text{ff}}}, so 2d dff=2⋅768⋅3072=4,718,5922 d\, d_{\text{ff}} = 2 \cdot 768 \cdot 3072 = 4{,}718{,}592 parameters, about 4.724.72M.

Step 3. The FFN holds twice the attention count. At fixed dd, the dff=4dd_{\text{ff}} = 4d expansion makes the FFN the dominant per-block cost.

Exercise 3 (code). Build the causal attention pattern for a length-TT sequence and verify two structural facts: every attention row is a valid distribution (sums to one), and row tt places zero weight on any future position j>tj > t, so position tt attends to exactly t+1t + 1 positions.

Solution
python · numpy
import numpy as np

def softmax(x, axis=-1):
    e = np.exp(x - x.max(axis=axis, keepdims=True))
    return e / e.sum(axis=axis, keepdims=True)

rng = np.random.default_rng(0)
T, dk = 5, 4
Q = rng.standard_normal((T, dk))
K = rng.standard_normal((T, dk))

scores = Q @ K.T / np.sqrt(dk)
mask = np.triu(np.ones((T, T)), k=1).astype(bool)   # strictly-future positions
scores[mask] = -1e9
A = softmax(scores)

assert np.allclose(A.sum(axis=1), 1.0)              # each row is a distribution
assert np.allclose(A[mask], 0.0, atol=1e-6)         # no leakage to the future
nonzero_per_row = (A > 1e-6).sum(axis=1)
assert np.array_equal(nonzero_per_row, np.arange(1, T + 1))
print("attended positions per row:", nonzero_per_row)

Row tt has exactly t+1t + 1 nonzero weights, the causal cone that makes next-token prediction a valid objective.

The next lesson introduces encoder-only transformers (BERT) which use bidirectional attention for understanding tasks rather than generation.


References

  1. [1]Radford, A., Wu, J., Child, R., Luan, D., Amodei, D., & Sutskever, I. (2019). "Language models are unsupervised multitask learners (GPT-2)". OpenAI.↩