Skip to content
The loss curve

Chapitre 15 · 14 min

Charger les vrais poids

Reprends ton code transformer du chapitre 12, mappe les noms de paramètres GPT-2, et charge les 124M de paramètres pré-entraînés d’OpenAI dans ton propre modèle.

Tu as entraîné un modèle de 14M sur ~1 Mo de Shakespeare. GPT-2 small fait 124M entraînés sur 40 Go de WebText.

Même architecture. Exactement la même architecture, modulo trois détails d’implémentation. Démontrons-le : prends les poids pré-entraînés d’OpenAI, pousse-les dans ton propre llm/model.py, et regarde ton code générer de l’anglais cohérent.

Ce chapitre n’entraîne pas un nouveau modèle. Il fait réaliser que le code écrit aux chapitres 8–12 supporte déjà des modèles plusieurs ordres de grandeur plus gros que celui que tu as entraîné. La suite du livre — SFT, LoRA, quantification, chat — s’applique aussi bien à ces poids qu’aux tiens.

1. Les trois deltas

Compare ton modèle du chapitre 12 à GPT-2 d’origine :

  • : BPE GPT-2, 50 257 . ✓ identique (tu utilises tiktoken depuis le chapitre 11).
  • causale + FFN + LayerNorm pré-attention : structure identique.
  • de position appris : même forme, juste plus longs (block_size = 1024 contre 64).
  • n_embd / n_head / n_layer : GPT-2 small utilise 768 / 12 / 12, toi 128 / 4 / 4. Même hyperparamètre, valeur différente.

Puis trois petites différences :

  • Bias sur les linéaires. GPT-2 a entraîné avec bias=True sur chaque nn.Linear. Ton modèle du ch.12 a bias=False sur l’attention. Charger sans biais drop silencieusement une contribution apprise.
  • Approximation de GELU. GPT-2 utilise la version tanh : 0.5x(1 + tanh(√(2/π)(x + 0.044715 x³))). PyTorch par défaut utilise la version exacte. Différence d’environ 0,001 par activation, mais composée sur 12 couches.
  • Embedding ↔ unembedding partagés. GPT-2 réutilise la matrice d’ comme dé-embedding (lm_head.weight = wte.weight). Économie gratuite d’environ 38M .

C’est tout. Trois knobs — et ils sont déjà dans ton GPTConfig depuis le chapitre 12. Regarde le dataclass : bias, tied_lm_head, gelu_approximate y sont avec leurs défauts qui préservent le comportement du ch.12. On ne t’a jamais demandé de patcher llm/model.py rétroactivement ; on a juste laissé les interrupteurs sur off jusqu’ici.

2. Activer les interrupteurs

Pour charger GPT-2 small, instancie GPTConfig avec les trois flags activés et les dimensions GPT-2 :

GPTConfig(
    vocab_size=50257,
    block_size=1024,
    n_layer=12,
    n_head=12,
    n_embd=768,
    ffn_mult=4,
    bias=True,
    tied_lm_head=True,
    gelu_approximate="tanh",
)

Même classe de modèle. Même forward. Ton entraîné du ch.13 charge toujours via le config par défaut — rien ne change pour lui. Cette config plus grosse est juste une autre instanciation de la même architecture.

3. Le script de chargement

Installe transformers :

pip install transformers
pip install transformers
pip install transformers

Sauvegarde scripts/load_gpt2.py :

"""scripts/load_gpt2.py — load HuggingFace GPT-2 small into our model."""
from pathlib import Path
 
import torch
 
from llm.model import GPT, GPTConfig
 
 
def gpt2_small_config() -> GPTConfig:
    return GPTConfig(
        vocab_size=50257,
        block_size=1024,
        n_layer=12,
        n_head=12,
        n_embd=768,
        ffn_mult=4,
        bias=True,
        tied_lm_head=True,
        gelu_approximate="tanh",
    )
 
 
TOP_LEVEL = {
    "transformer.wte.weight": "tok_emb.weight",
    "transformer.wpe.weight": "pos_emb.weight",
    "transformer.ln_f.weight": "ln_f.weight",
    "transformer.ln_f.bias": "ln_f.bias",
}
 
PER_LAYER = [
    ("h.{i}.ln_1.weight", "blocks.{i}.ln1.weight"),
    ("h.{i}.ln_1.bias", "blocks.{i}.ln1.bias"),
    ("h.{i}.attn.c_attn.weight", "blocks.{i}.attn.qkv.weight"),
    ("h.{i}.attn.c_attn.bias", "blocks.{i}.attn.qkv.bias"),
    ("h.{i}.attn.c_proj.weight", "blocks.{i}.attn.proj.weight"),
    ("h.{i}.attn.c_proj.bias", "blocks.{i}.attn.proj.bias"),
    ("h.{i}.ln_2.weight", "blocks.{i}.ln2.weight"),
    ("h.{i}.ln_2.bias", "blocks.{i}.ln2.bias"),
    ("h.{i}.mlp.c_fc.weight", "blocks.{i}.ffn.fc1.weight"),
    ("h.{i}.mlp.c_fc.bias", "blocks.{i}.ffn.fc1.bias"),
    ("h.{i}.mlp.c_proj.weight", "blocks.{i}.ffn.fc2.weight"),
    ("h.{i}.mlp.c_proj.bias", "blocks.{i}.ffn.fc2.bias"),
]
 
TRANSPOSE_SUFFIXES = (
    "attn.c_attn.weight",
    "attn.c_proj.weight",
    "mlp.c_fc.weight",
    "mlp.c_proj.weight",
)
 
 
def translate(hf_state: dict, n_layer: int) -> dict:
    out: dict = {}
    for hf_key, our_key in TOP_LEVEL.items():
        out[our_key] = hf_state[hf_key]
    for i in range(n_layer):
        for hf_template, our_template in PER_LAYER:
            hf_key = hf_template.format(i=i)
            tensor = hf_state[hf_key]
            if hf_key.endswith(TRANSPOSE_SUFFIXES):
                tensor = tensor.t().contiguous()
            out[our_template.format(i=i)] = tensor
    return out
 
 
def main() -> None:
    from transformers import GPT2LMHeadModel
 
    cfg = gpt2_small_config()
    model = GPT(cfg)
    n_params = sum(p.numel() for p in model.parameters())
 
    assert n_params == 124_439_808, f"expected 124,439,808 params, got {n_params:,}"
 
    hf_state = GPT2LMHeadModel.from_pretrained("gpt2").state_dict()
    our_state = translate(hf_state, n_layer=cfg.n_layer)
 
    missing, unexpected = model.load_state_dict(our_state, strict=False)
    assert not unexpected, f"unexpected keys in our_state: {unexpected}"
    # Avec tied_lm_head, head.weight partage le stockage avec tok_emb.weight,
    # donc il apparaît comme « missing » mais est chargé via le tie.
    allowed_missing = lambda k: "attn.mask" in k or k == "head.weight"
    assert all(allowed_missing(k) for k in missing), (
        f"missing keys other than causal masks / tied head: {missing}"
    )
 
    Path("checkpoints").mkdir(exist_ok=True)
    torch.save(model.state_dict(), "checkpoints/gpt2_small.pt")
    print(f"✓ {n_params:,} params (matches GPT-2 small exactly)")
    print(f"✓ {len(missing)} missing keys, all causal-mask buffers (expected)")
    print(f"✓ {len(unexpected)} unexpected keys")
    print("✓ saved checkpoints/gpt2_small.pt")
 
 
if __name__ == "__main__":
    main()

Les points-clés :

  • gpt2_small_config() : la spec GPT-2 small exprimée comme ton GPTConfig. Trois flags activés, le reste = chiffres.
  • TOP_LEVEL : embeddings et LayerNorm finale, mapping 1:1.
  • PER_LAYER : templates parce que chaque bloc transformer a la même forme.
  • TRANSPOSE_SUFFIXES : HuggingFace stocke les poids en Conv1D, transposée de nn.Linear. On transpose au chargement.

Essaie la logique de translation toi-même sur un échantillon de clés GPT-2 :

Code · JavaScript

  • L’assertion sur n_params == 124_439_808 vérifie que ton GPTConfig instancie pile la même taille que GPT-2 small. Un seul écart = une dimension fausse, à corriger avant que le loader n’échoue plus tard de manière obscure.
  • strict=False plus deux assertions : aucune clé inattendue (mapping complet), et les seules clés manquantes autorisées sont les buffers attn.mask (masque causal, pas à charger) et head.weight (lié à tok_emb.weight, donc propagé automatiquement quand on charge tok_emb.weight).

Lance :

python -m scripts.load_gpt2
python -m scripts.load_gpt2
python -m scripts.load_gpt2

~500 Mo téléchargés au premier run. Quatre ticks attendus :

✓ 124,439,808 params (matches GPT-2 small exactly)
✓ 13 missing keys (12 causal-mask buffers + tied head.weight, all expected)
✓ 0 unexpected keys
✓ saved checkpoints/gpt2_small.pt

124M paramètres. Environ 9× ton modèle entraîné. Dans le même code. Si une assertion se déclenche, le message pointe l’écart exact.

4. Échantillonner GPT-2 avec ton propre modèle

Sauvegarde scripts/sample_gpt2.py : recharge gpt2_small_config(), load_state_dict("checkpoints/gpt2_small.pt"), puis le sampler du chapitre 14 — strictement identique. Seul le change.

Avec le prompt "The capital of France is", sortie attendue :

The capital of France is Paris. It is also the capital of the country of France, which is the largest country in the European Union.

C’est ton llm/model.py. L’architecture des chapitres 8–12. Avec les poids entraînés par OpenAI en 2019. Pas ChatGPT — GPT-2 small est un modèle de 2019, ni SFT ni RLHF — mais la prédiction est fluide et factuelle d’une manière que ton modèle Shakespeare ne pourra jamais être.

Rien dans ce chapitre n’est exotique. Tu as chargé des poids dans le même forward pass que tu as écrit.

5. Ce que tu as prouvé

Une seule chose : ton transformer est GPT. Même logique, mêmes shapes, même forward. La taille et les données sont des variables. L’architecture est constante.

Ça compte parce que :

  • Charger un modèle de base est le point de départ de la plupart des projets réels. On adapte, on ne pré-entraîne pas.
  • La « magie » des grands modèles est mécaniquement ce que tu viens de faire. Les choses dures à l’échelle frontière sont l’ingénierie data et l’infra de serving, pas la classe du modèle.
  • Ton projet est désormais un workbench. N’importe quels poids open peuvent y entrer, puis SFT (chapitre 17), LoRA (chapitre 18), quantification (chapitre 19). Le wrapper ne change pas.

Recap

  • Ton architecture est GPT modulo trois flags : bias, gelu_approximate, tied_lm_head. Défauts préservent le comportement du chapitre 12. - Mapping de noms + transpose Conv1D = toute la traduction des poids, ~30 lignes Python. - Le même llm/model.py héberge maintenant ton modèle 14M Shakespeare et un GPT-2 124M. Poids différents, shapes identiques. - Ton projet local a maintenant checkpoints/gpt2_small.pt — un vrai fluide, prêt pour la suite de la partie V.

Pour aller plus loin

Prochaine étape : pourquoi ton modèle parle mal — maintenant que tu peux échantillonner GPT-2 124M et ton modèle Shakespeare 14M côte à côte, les écarts d’échelle, de données et d’ deviennent évidents.