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
tiktokendepuis le chapitre 11). - causale + FFN + LayerNorm pré-attention : structure identique.
- de position appris : même forme, juste plus longs (
block_size = 1024contre64). - n_embd / n_head / n_layer : GPT-2 small utilise
768 / 12 / 12, toi128 / 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=Truesur chaquenn.Linear. Ton modèle du ch.12 abias=Falsesur 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 transformerspip install transformerspip install transformersSauvegarde 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 tonGPTConfig. 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 enConv1D, transposée denn.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_808vérifie que tonGPTConfiginstancie 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=Falseplus deux assertions : aucune clé inattendue (mapping complet), et les seules clés manquantes autorisées sont les buffersattn.mask(masque causal, pas à charger) ethead.weight(lié àtok_emb.weight, donc propagé automatiquement quand on chargetok_emb.weight).
Lance :
python -m scripts.load_gpt2python -m scripts.load_gpt2python -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.pt124M 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êmellm/model.pyhéberge maintenant ton modèle 14M Shakespeare et un GPT-2 124M. Poids différents, shapes identiques. - Ton projet local a maintenantcheckpoints/gpt2_small.pt— un vrai fluide, prêt pour la suite de la partie V.
Pour aller plus loin
- Doc GPT-2 HuggingFace.
from_pretrainedde nanoGPT — la référence ; ce chapitre suit sa structure.- Suite de modèles Pythia — schéma de chargement identique, scaling cohérent de 70M à 12B.
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.