Antivision · Latent Rehearsal
02

Two architectures,
one file

RoPE, RMSNorm and SwiGLU written next to the 2019 pieces they replace — and the duel with the same corpus, the same budget and the same seed.

Chapter 02 Prerequisite: chapters 05 and 06 of the GPT-2 course

1 The problem of comparing architectures

A new piece is presented under the conditions in which it was developed: another scale, other data, another budget. A gain measured there does not automatically carry over here. That is why this chapter redoes the comparison under our conditions — same data, same seed, same number of steps — knowing that a fair comparison is laborious and expensive.

There are three traps, and all of them were deliberately avoided here:

How each trap was closed in architecture.py and train.py.
TrapWhat happensOur lock
Different implementationsthe new piece comes from an optimized repository, the old one from a sketchboth in the same file, same training loop, chosen by Config(moderno=…)
Different parameter countsthe new one wins because it is bigger17.1 M (faithful) against 16.9 M (modern) at the pequeno size
Different budget or datathe new one saw moresame treino.bin, same 700 steps, same seed 1337
The project rule

A comparison is only worth anything if the only thing that differs is what is being tested. That is why the duel's data file was frozen in model/data/duelo_200mb/ and the packing script now refuses to overwrite without an explicit confirmation — a corpus swapped in the middle of a series would silently invalidate everything.

Where this file fits in JEPA

The interview (chapter 10) answers a question this duel raises: JEPA is not an alternative to the Transformer — it is a macro-architecture, and the Transformer is the encoder inside it. The two variants in this file, classic and modern, are the same module with different parts; the mundo predictor and the cost in the latent sit on top, the same for both. The duel below chooses the encoder; it does not choose the paradigm.

2 The four swaps

Position: learned wpe → RoPE

GPT-2 has a wpe table with exactly 1024 rows — one per possible position. It is a learned vector, added to the token embedding. It works, and it has two hard consequences.

The first is the wall. We measured it directly:

contexto  512  → 29,6 ms/token
contexto 1000  → 34,6 ms/token
contexto 1024  → PAREDE: posição 1024 não existe na wpe (ela tem 1024 linhas: 0 a 1023)

It is not a configuration limit. It is a matrix that runs out. To extend the context, new rows have to be created — rows that were never trained.

RoPE (Su et al., 2021) solves it by construction: instead of adding a position vector, it rotates the query and key vectors by an angle proportional to the position. Since the dot product between two rotated vectors depends only on the difference of the angles, attention comes to see relative distance — and there is no table to run out.

Analogy

wpe is giving each person in the queue a badge with their number. If the queue goes past 1024, the badges run out.

RoPE is rotating each person by an angle proportional to their position. Two people three places apart always have the same angle difference, whether the pair is at the front or the back of the queue — and the queue can be as long as you like.

Cost in parameters: the wpe of mini has 1024 × 256 = 262,144 weights. RoPE has zero — it is a computed rotation, not a learned one.

Normalization: LayerNorm → RMSNorm

LayerNorm does two things: it centers (subtracts the mean) and scales (divides by the standard deviation). RMSNorm (Zhang & Sennrich, 2019) observed that centering contributes almost nothing, and removed it:

RMSNorm(x) = x√mean(x²) + ε · g

Gone is the mean subtraction and gone is the shift parameter b. Less arithmetic, half the normalization parameters, and no measured loss in the literature.

MLP: GELU 4× → SwiGLU

GPT-2's block expands to 4d, applies GELU and comes back. SwiGLU (Shazeer, 2020) uses two projections in parallel: one becomes the signal, the other becomes a gate, and they multiply.

class SwiGLU(nn.Module):
    def forward(self, x):
        return self.w_down(F.silu(self.w_portao(x)) * self.w_cima(x))

That is three matrices instead of two, so the inner dimension shrinks (from 4d to ⅔·4d) to keep the count. The gate lets the network learn when to let the signal through, rather than only how much.

Biases: all → none

Llama removed the b term from every linear layer. Fewer parameters, and in practice no loss. We also added QK-norm (Dehghani et al., 2023) — normalizing query and key before the dot product, which prevents attention logits from blowing up in long training runs.

3 The duel

mini size (7.4 M), 700 steps, 5.7 M tokens, same seed. Only the architecture changes.

Best checkpoint of each run — never the last one, because validation oscillates between evaluations.
ArchitectureValidationPPLat step
classic (faithful GPT-2)5.5391254.45649
modern5.2616192.79549

−24% perplexity, and 100 fewer steps to reach the best point. On the four bits/character measurements:

bits/character, lower is better. The modern one does better in all three domains.
ArchitecturePortugueseEnglishCodeMean
classic2.4392.0662.5222.342
modern2.4062.0382.4052.283
What this duel does NOT prove

One run per architecture, one seed, one size, 5.7 M tokens — 0.09 epoch of the corpus. This shows that the modern architecture is better at this scale, with this budget. It does not show which piece contributed: RoPE, RMSNorm, SwiGLU and the biases all changed together.

Isolating each one would require four extra runs. It is on the list, not in the result.

A correction the method caught

During the duel, the classic model's validation rose between steps 649 and 699 (5.539 → 5.577). The immediate reading was "it started overfitting". It was wrong.

With 5.7 M tokens out of a 60.3 M corpus, the model saw 0.09 of an epoch — no token was seen twice. Overfitting is impossible by construction. What rose was evaluation noise added to the closing of the learning-rate cosine.

4 The price of each piece, in parameters

pequeno size, vocabulary 16,392. Both land on almost the same total — which is what makes the comparison legitimate.
PieceClassicModern
Positionwpe: 1024×384 = 393,216RoPE: 0
NormalizationLayerNorm: gain + biasRMSNorm: gain only
MLP2 matrices, GELU3 matrices, SwiGLU (dim ⅔)
Linearswith biaswithout bias
Total17.1 M16.9 M

The modern one is slightly smaller and still wins. It is the strongest argument in the table: it is not more capacity, it is better use of the same.

5 How to read the code

Everything in model/architecture.py, with each modern piece written immediately next to the classic one it replaces:

def normalizador(cfg, d):
    return RMSNorm(d) if cfg.moderno else nn.LayerNorm(d)

# in the Transformer:
if cfg.moderno:
    self.wpe = None                        # RoPE goes into attention
    rope = RoPE(cfg.d_k, cfg.n_ctx, cfg.rope_base)
else:
    self.wpe = nn.Embedding(cfg.n_ctx, cfg.d_model)
    rope = None

Reading that file top to bottom is seeing the difference between 2019 and today in ~400 lines. That was the goal — more than having both options, having both legible side by side.

Open questions

With the architecture settled, chapter 03 looks inside the embedding table —
and finds a problem that none of these modern pieces fixes.