Home › Blog › Language Modeling from Scratch, Part 4: Architectures & Hyperparameters
CS336 Deep Dive · Part 4 of 19 🧩

Architectures & Hyperparameters

The 2017 Transformer and a 2026 frontier model share a family resemblance, but almost none of the actual components are identical anymore. This part walks through what changed, why each change happened, and builds an animated, original diagram for every major piece — normalization placement, RMSNorm, RoPE, and SwiGLU — the four choices Lecture 3 treats as the modern default stack.

FL
FrontierAGI Team
On the citations and diagrams below: every paper cited was independently verified (title, authors, arXiv ID) against arxiv.org directly. All diagrams in this piece are original — built specifically for this article, not reproductions of any figure from the cited papers, from CS336's own slides, or from third-party explainers. Two excellent third-party visual resources are linked at the end for further exploration rather than embedded directly, since embedding someone else's diagram isn't the same as understanding it yourself.

From 2017 to Now

Attention Is All You Need HIGHNeurIPS 2017
Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Łukasz Kaiser, Illia Polosukhin — arXiv:1706.03762
Introduced the Transformer architecture and showed that attention alone — no recurrence, no convolution — could beat the best sequence-to-sequence models of the time on machine translation, while being dramatically more parallelizable to train. Nearly every large language model built since traces its architecture back to this paper, but almost none of them use it unmodified.

Four specific changes separate a 2017 Transformer from what nearly every modern open-weight model actually ships with today: where normalization sits relative to each sublayer, what kind of normalization it is, how position is encoded, and what the feed-forward layer's internal activation looks like. Each one is a real, published, independently-motivated fix for a real problem the original design had.

The Original Transformer Block

Input x Self-Attention + LayerNorm (post) Feed-Forward + LayerNorm (post) residual (skip)
The original (2017) design: normalization applied after each sublayer and its residual add — "Post-LN." The moving dot traces one token's path through a single block; watch it pass through attention, then get normalized only after being added back to the residual stream.

Pre-LN vs. Post-LN: A Training-Stability Fix

On Layer Normalization in the Transformer Architecture HIGHICML 2020
Ruibin Xiong, Yunchang Yang, Di He, et al. — arXiv:2002.04745
Using mean-field theory, the paper shows that Post-LN transformers have large expected gradients near the output layer at initialization, which is why the original Transformer needed a careful learning-rate warmup schedule to train stably at all. Moving normalization to before each sublayer instead — Pre-LN — gives well-behaved gradients right from initialization, letting the model train without any warmup and generally more stably at scale.
POST-LN (original) x Sublayer + LayerNorm norm sees raw residual sum — large gradients early PRE-LN (modern default) x LayerNorm Sublayer + Same two ingredients, reordered — the residual stream itself is never normalized in Pre-LN, only the copy fed into each sublayer, which is what stabilizes gradients at initialization.
Post-LN normalizes after adding the residual; Pre-LN normalizes only the sublayer's input, leaving the residual stream itself untouched all the way through the network.

RMSNorm: A Cheaper Normalization

Root Mean Square Layer Normalization HIGH2019
Biao Zhang, Rico Sennrich — arXiv:1910.07467
Standard LayerNorm does two things: re-centers a vector (subtracts its mean) and rescales it (divides by its standard deviation). This paper argues the re-centering step is largely dispensable — normalizing only by the root-mean-square of the vector, and dropping mean subtraction entirely, works about as well while being cheaper to compute. RMSNorm has since become the default normalization in most modern open-weight LLMs.
# Original illustrative implementations for this series --
# not copied from any CS336 file or model codebase.

import torch

def layer_norm(x, eps=1e-5):
    mean = x.mean(dim=-1, keepdim=True)
    var = x.var(dim=-1, keepdim=True, unbiased=False)
    return (x - mean) / torch.sqrt(var + eps)

def rms_norm(x, eps=1e-5):
    # No mean subtraction -- only rescale by root-mean-square
    rms = torch.sqrt((x ** 2).mean(dim=-1, keepdim=True) + eps)
    return x / rms
The entire difference is one subtraction. RMSNorm is cheaper per call and, per the paper's ablations, loses essentially nothing in model quality from skipping it.

RoPE: Encoding Position by Rotation

RoFormer: Enhanced Transformer with Rotary Position Embedding HIGH2021
Jianlin Su, Yu Lu, Shengfeng Pan, Ahmed Murtadha, Bo Wen, Yunfeng Liu — arXiv:2104.09864
Instead of adding a separate position vector to each token's embedding (as the original Transformer did), RoPE encodes position by rotating each query and key vector by an angle proportional to its position in the sequence. The key mathematical payoff: when you then compute an attention score as a dot product between a rotated query and a rotated key, the result depends only on their relative position — not their absolute positions individually — which the paper shows helps the model generalize better to sequence lengths it wasn't explicitly trained on.
Query/key vector, rotated by an angle proportional to token position (animation shown at one fixed frequency — real RoPE uses many frequencies at once, one per dimension pair)
The rotating vector represents one 2D slice of a query or key. RoPE applies this rotation independently to many such pairs of dimensions, each at a different frequency — the same trick sinusoidal position encodings used, but applied as a rotation rather than an addition.

SwiGLU: A Gated Feed-Forward Layer

GLU Variants Improve Transformer HIGH2020
Noam Shazeer — arXiv:2002.05202
The original Transformer's feed-forward layer is two linear projections with a ReLU in between. This paper tests replacing that with a Gated Linear Unit variant: split the hidden computation into two parallel projections, pass one through a Swish (SiLU) activation, and multiply it elementwise with the other, unactivated projection — letting the network learn to gate how much of each feature passes through, rather than applying a fixed nonlinearity uniformly. SwiGLU specifically (Swish-gated) is the variant that ended up in most subsequent open-weight model releases.
Input x Linear + Swish Linear (no activation) × Out the gated (pulsing) branch controls how much of the linear branch passes through
SwiGLU: one branch decides "how much," the other carries "what" — their elementwise product is the feed-forward layer's output, replacing a single fixed ReLU nonlinearity with a learned gate.

Putting It Together: The Modern Default Block

Stack all four changes on top of the original design and you get the transformer block architecture nearly every current open-weight LLM actually ships: Pre-LN placement, RMSNorm instead of LayerNorm, RoPE instead of learned or sinusoidal absolute position embeddings, and a SwiGLU feed-forward layer instead of a ReLU MLP. None of these four changes depend on each other — each is independently motivated and independently validated in its own paper — but they've converged into a de facto standard combination because they compose cleanly and each solves a real, separate problem: training stability, compute cost, length generalization, and representational flexibility, respectively.

Counting Every Parameter

Every change above can be judged by one practical question: where do the weights actually live, and how many are there? The answer is short. A decoder-only transformer has trainable parameters in only four places — the embedding tables, the attention projections, the feed-forward layers, and the normalization scales — and the whole count follows from five numbers. This section walks the model top to bottom, counts each piece, and then checks the arithmetic against a real PyTorch model.

V vocabulary size d model width (d_model) dff MLP hidden width N number of blocks Tmax max context length h attention heads

1. The pipeline: which stages have weights?

Most of the steps between your prompt and the next token have no trainable parameters at all. Green stages learn; grey stages are fixed computation.

  1. Input text"The cat sat on the…"0
  2. TokenizationBPE from Part 2 — a lookup, not a layer0
  3. Token IDs464 · 3797 · 3332 · …0
  4. Token embeddingsE ∈ ℝV×dV·d
  5. Position embeddingslearned P ∈ ℝTmax×d; RoPE or sinusoidal: noneTmax·d or 0
  6. N × transformer blockattention + MLP + 2 norms, repeated N timesN · (per-block)
  7. Final normγ and β (LayerNorm) or γ only (RMSNorm)2d or d
  8. LM headd → V logits; often tied to Ed·V + V, or 0 if tied
  9. Softmaxlogits → probabilities0
  10. Samplinggreedy, top-k, nucleus0

2. Inside one block

This is the classic GPT-2-style block — LayerNorm with scale and shift, biases on every linear layer, a two-matrix GELU MLP — because it's the one the standard counting formula describes. The modern variant comes right after. The numbers on the right use the worked-example sizes below (d = 1,024, dff = 4,096).

residual (0 params) residual (0 params) X (T × d) LayerNorm 1 γ, β ∈ ℝ^d → 2d Masked multi-head attention W_Q W_K W_V W_O each d×d + d bias → 4d² + 4d scores, mask, softmax: 0 params + LayerNorm 2 γ, β ∈ ℝ^d → 2d Feed-forward (MLP) up: d×d_ff + d_ff · GELU: 0 down: d_ff×d + d → 2·d·d_ff + d_ff + d + X_out (T × d) 2,048 4,198,40033.3% of the block 0 2,048 8,393,72866.6% of the block 0 block total 12,596,224
One classic block at d = 1,024, dff = 4,096. The orange MLP holds two thirds of the block's weights; attention holds one third; the two norms are 0.03%. Residual additions, the causal mask and softmax learn nothing. (Original diagram.)

Adding the pieces up gives the per-block formula. Splitting attention across h heads changes the shapes of the intermediate tensors, not the total: the four projections are d × d no matter how many heads share them.

per block = 4d² + 2·d·dff + dff + 9d
attention 4d² + 4dMLP 2·d·dff + dff + dtwo LayerNorms 4d
Drop the linear terms and set dff = 4d, and each block is about 12d². That one-liner is how people estimate model size in their heads.

3. The whole model

ComponentUntied (separate LM head)Tied (head reuses E)
Token embeddingsV·dV·d
Position embeddingsTmax·d (0 with RoPE)Tmax·d (0 with RoPE)
N blocksN·(4d² + 2d·dff + dff + 9d)same
Final LayerNorm2d2d
LM headd·V + V0 new weights (+V if it keeps a bias)

Weight tying — reusing the token-embedding matrix as the output projection — is worth knowing about because at small scales it's a large fraction of the model. It's the single biggest difference between the untied and tied totals below.

4. Worked example

V = 50,000, d = 1,024, dff = 4,096, N = 24, Tmax = 2,048:

PieceArithmeticParameters
Token embeddings50,000 × 1,02451,200,000
Position embeddings2,048 × 1,0242,097,152
One attention block4 × 1,024² + 4 × 1,0244,198,400
One MLP2 × 1,024 × 4,096 + 4,096 + 1,0248,393,728
Two LayerNorms4 × 1,0244,096
One block12,596,224
24 blocks24 × 12,596,224302,309,376
Final LayerNorm2 × 1,0242,048
LM head (untied)1,024 × 50,000 + 50,00051,250,000
Total, untied406,858,576
Total, tied (keeps a 50,000 vocab bias)406,858,576 − 51,200,000355,658,576

Where the 407M parameters live (untied):

Three quarters of the model is the blocks, and two thirds of each block is the MLP. The last two slices are too thin to see.

5. What the modern block changes

Each of the four upgrades from earlier in this article moves the count, some up and some down:

ChangeClassicModern (Llama-style)Effect
NormalizationLayerNorm: 2dRMSNorm: d (scale only)−d per norm
Positionslearned: Tmax·dRoPE: 0−2.1M in the example
Biaseson every linear layernone−4d − dff − d per block
MLP2 matrices: 2·d·dffSwiGLU, 3 matrices: 3·d·dffdff shrunk to ≈ ⁸⁄₃·d to compensate

That last row is the one to understand. SwiGLU adds a third d × dff matrix (the gate), so to keep the MLP near its old size Llama sets dff = ⅔ · 4d ≈ 2.67d, then rounds up to a multiple of 256 for hardware efficiency. In Meta's reference code that's int(2 * 4d / 3) rounded up to multiple_of = 256: for d = 4,096 it gives 11,008, Llama-7B's actual MLP width, and for our d = 1,024 it gives 2,816. The modern per-block count is 4d² + 3·d·dff + 2d:

Classic block12,596,224dff = 4,096
Modern block12,847,104dff = 2,816
Classic model, tied355,658,576with learned positions
Modern model, tied359,531,520RoPE, no biases

The totals end up within about 1% of each other. That's by design: the modern block isn't meant to be bigger, it's meant to spend the same budget better.

6. Check it yourself in PyTorch

Formulas are easy to get subtly wrong, so here is the same count done by PyTorch itself: build both blocks out of real nn.Linear, nn.LayerNorm and nn.RMSNorm layers and sum their .parameters(). Building on the meta device allocates no memory, so this runs instantly on a laptop. Needs PyTorch 2.4+ (for nn.RMSNorm); install with pip install torch.

import torch
import torch.nn as nn


class ClassicBlock(nn.Module):
    """GPT-2-style block: LayerNorm (gamma+beta), biased Q/K/V/O, GELU MLP."""
    def __init__(self, d, d_ff):
        super().__init__()
        self.ln1 = nn.LayerNorm(d)
        self.q, self.k, self.v, self.o = (nn.Linear(d, d) for _ in range(4))
        self.ln2 = nn.LayerNorm(d)
        self.up, self.down = nn.Linear(d, d_ff), nn.Linear(d_ff, d)


class ModernBlock(nn.Module):
    """Llama-style block: RMSNorm (gamma only), no biases, SwiGLU MLP, RoPE (no weights)."""
    def __init__(self, d, d_ff):
        super().__init__()
        self.norm1 = nn.RMSNorm(d)
        self.q, self.k, self.v, self.o = (nn.Linear(d, d, bias=False) for _ in range(4))
        self.norm2 = nn.RMSNorm(d)
        self.gate = nn.Linear(d, d_ff, bias=False)
        self.up = nn.Linear(d, d_ff, bias=False)
        self.down = nn.Linear(d_ff, d, bias=False)


class LM(nn.Module):
    def __init__(self, block, V, d, d_ff, N, T_max=None, final_norm=nn.LayerNorm, tied=False, head_bias=True):
        super().__init__()
        self.tok = nn.Embedding(V, d)
        self.pos = nn.Embedding(T_max, d) if T_max else None   # None = RoPE: nothing to learn
        self.blocks = nn.ModuleList(block(d, d_ff) for _ in range(N))
        self.norm = final_norm(d)
        self.head = nn.Linear(d, V, bias=head_bias)
        if tied:
            self.head.weight = self.tok.weight               # shared tensor, counted once


def count(model):
    return sum(p.numel() for p in model.parameters())      # .parameters() de-duplicates tied weights


V, d, d_ff, N, T_max = 50_000, 1_024, 4_096, 24, 2_048

with torch.device("meta"):                                  # shapes only, no memory allocated
    block = ClassicBlock(d, d_ff)
    untied = LM(ClassicBlock, V, d, d_ff, N, T_max)
    tied = LM(ClassicBlock, V, d, d_ff, N, T_max, tied=True)
    modern = LM(ModernBlock, V, d, 2_816, N, final_norm=nn.RMSNorm, tied=True, head_bias=False)
    modern_block = ModernBlock(d, 2_816)

print(f"classic block:     {count(block):>12,}")
print(f"classic untied LM: {count(untied):>12,}")
print(f"classic tied LM:   {count(tied):>12,}")
print(f"modern block:      {count(modern_block):>12,}")
print(f"modern tied LM:    {count(modern):>12,}")

# Executed output (PyTorch 2.14):
# classic block:       12,596,224
# classic untied LM:  406,858,576
# classic tied LM:    355,658,576
# modern block:        12,847,104
# modern tied LM:     359,531,520

Every number matches the hand calculation. The one subtle line is weight tying: assigning self.head.weight = self.tok.weight makes both modules point at the same tensor, and model.parameters() yields each tensor once, so the shared matrix is counted once — exactly the 51.2M drop between the untied and tied totals.

✅ Has trainable parameters
  • Token embeddings (V × d)
  • Learned position embeddings (Tmax × d)
  • Q, K, V, O projections (d × d each)
  • MLP matrices (d × dff, dff × d; plus the gate in SwiGLU)
  • Norm scales γ (and shifts β in LayerNorm)
  • LM head (d × V), unless tied
⛔ No trainable parameters
  • Tokenization
  • RoPE and sinusoidal positions
  • QKᵀ scores, 1/√dk scaling, causal mask
  • Softmax, GELU, SiLU
  • Residual additions
  • Sampling
Try it: GPT-2 small. V = 50,257, d = 768, dff = 3,072, N = 12, Tmax = 1,024, tied embeddings, no LM-head bias. How many parameters?
Show the answer Embeddings 50,257 × 768 = 38,597,376; positions 1,024 × 768 = 786,432; one block 4 × 768² + 2 × 768 × 3,072 + 3,072 + 9 × 768 = 7,087,872, times 12 = 85,054,464; final LayerNorm 1,536; head 0 (tied). Total: 124,439,808 — the "124M" in GPT-2 small's name.

Hyperparameter Choices

Beyond the structural choices above, the same lecture covers the numeric knobs that define a specific model's shape: number of layers, model dimension (d_model), number of attention heads, feed-forward hidden dimension (typically some multiple of d_model, often smaller for gated variants like SwiGLU since the gating branch effectively adds a third weight matrix), vocabulary size (carried over directly from Part 2's tokenizer), and context length. None of these have one universally correct value — they're chosen jointly with the training compute budget available, which is exactly the resource-accounting question Part 3 formalizes. The parameter count from the section above plugs straight into it: training compute is roughly 6 × parameters × tokens, so the 407M-parameter example trained on 20B tokens costs about 6 × 0.41B × 20B ≈ 4.9 × 10¹⁹ FLOPs.

⚠️ Confidence Notes and Gaps

All six papers cited in this piece were independently verified directly against arxiv.org — title, authors, and arXiv ID all high confidence, and each paper's described finding was checked against its actual abstract/contribution rather than assumed from the paper's reputation. All diagrams are original work for this article. Every number in the parameter-counting section was computed twice: by hand from the formulas, and by building the models in PyTorch and summing their parameters (code and output included). Llama's SwiGLU width rule (int(2·4d/3) rounded up to a multiple of 256) was checked against Meta's reference model.py; it reproduces Llama-7B's published 11,008. This piece describes the architectural changes at a conceptual level; it does not reproduce CS336's own lecture slides or claim to represent exactly how Lecture 3 sequences or frames this material — treat it as an independently-researched companion covering the same territory, not a transcript.

🔗 Full Reference List