TAI BUI
Back to curriculum
CapstonePython

Loading Pretrained Weights

Training a 124 million parameter model from scratch is a budget decision; loading a published checkpoint is a Tuesday. This lesson loads pretrained GPT-2 style weights from a safetensors file into the exact architecture from lesson 35, walks the parameter name mapping piece by piece, and sanity generates a continuation to prove the load worked. No network, no third party loaders, no opaque magic.

TIME: ~90 minutesPREREQUISITES: Phase 19 lessons 30 to 36

Learning Objectives

  • Read a safetensors file with the `safetensors` Python library and inspect the tensor names and shapes.
  • Map each pretrained parameter name onto a parameter inside the lesson 35 GPT model.
  • Handle the two name conventions that differ between published GPT-2 weights and the model in this track: `wte/wpe/h.N.attn.c_attn/c_proj` and `mlp.c_fc/c_proj` versus the locally named `tok_embed/pos_embed/blocks.N.attn.qkv/out_proj` and `mlp.fc1/fc2`.
  • Detect a shape mismatch before any weight assignment happens, record it in `report.shape_mismatch`, and return without assigning anything.
  • Generate a short continuation with the loaded weights and confirm the tokens come from the loaded distribution, not the randomly initialized one.

The Problem

A published checkpoint is a file of named tensors, and the names, shapes, and layouts come from the implementation that wrote it, not from yours. The pretrained file has h.0.attn.c_attn.weight of shape (768, 2304). Your model expects blocks.0.attn.qkv.weight of shape (2304, 768), because nn.Linear stores its weight as (out, in) and the published file stores the transpose. One parameter carries three identities (name, shape, byte layout), and the loader has to reconcile all three.

A loader that copies blindly puts the right tensor in the wrong place, and you get a model that generates nonsense. A loader that refuses to copy on a shape difference but logs nothing leaves you guessing which tensor failed to land. The loader here is explicit: every assignment is logged, every shape is checked, and a LoadReport summarizes hits, misses, and shape mismatches so you can read what happened.

Warm-up0/2
1. In PyTorch, `nn.Linear(in_features=192, out_features=576)` stores its `weight` with which shape?Warm-up
2. Inside a `with torch.no_grad():` block, what is different about tensor operations?Warm-up
Check: The Problem0/2
1. A pretrained parameter carries three identities the loader must reconcile with the local model. Which three?Explain
2. Why is a loader that refuses a bad shape but logs nothing still a poor design?Explain

The Concept

Mermaid diagram

Loading pretrained weights means reading each named tensor from a file, translating its name to a local parameter name, checking its shape, and copying it into the model. The name mapper is just a function from string to string. The shape check is one if. The assignment happens inside torch.no_grad() so autograd does not track the load, and the report holds the outcome of every name.

The GPT-2 naming convention

Published GPT-2 weights live under names like:

Pretrained nameShapeMeaning
wte.weight(50257, 768)Token embedding
wpe.weight(1024, 768)Position embedding
h.N.ln_1.weight(768,)LayerNorm 1 scale at block N
h.N.ln_1.bias(768,)LayerNorm 1 shift at block N
h.N.attn.c_attn.weight(768, 2304)Fused QKV linear weight
h.N.attn.c_attn.bias(2304,)Fused QKV linear bias
h.N.attn.c_proj.weight(768, 768)Attention output projection
h.N.attn.c_proj.bias(768,)Attention output projection bias
h.N.ln_2.weight(768,)LayerNorm 2 scale
h.N.ln_2.bias(768,)LayerNorm 2 shift
h.N.mlp.c_fc.weight(768, 3072)MLP fc1 weight
h.N.mlp.c_fc.bias(3072,)MLP fc1 bias
h.N.mlp.c_proj.weight(3072, 768)MLP fc2 weight
h.N.mlp.c_proj.bias(768,)MLP fc2 bias
ln_f.weight(768,)Final LayerNorm scale
ln_f.bias(768,)Final LayerNorm shift

Two things to plan for. First, the c_attn, c_proj, and c_fc linears are stored transposed relative to what nn.Linear.weight expects, so the loader transposes during assignment. Second, the LM head is not in the file at all. The GPTModel constructor already ties the head to tok_embed, so loading wte fills both, and the loader re-aliases after the load only if the two data pointers differ.

The local naming convention

The model in this track uses descriptive names:

Local nameMeaning
tok_embed.weightToken embedding
pos_embed.weightPosition embedding
blocks.N.ln1.scaleLayerNorm 1 scale at block N
blocks.N.ln1.shiftLayerNorm 1 shift
blocks.N.attn.qkv.weightFused QKV
blocks.N.attn.qkv.biasFused QKV bias
blocks.N.attn.out_proj.weightAttention output projection
blocks.N.attn.out_proj.biasOutput projection bias
blocks.N.ln2.scaleLayerNorm 2 scale
blocks.N.ln2.shiftLayerNorm 2 shift
blocks.N.mlp.fc1.weightMLP fc1
blocks.N.mlp.fc1.biasMLP fc1 bias
blocks.N.mlp.fc2.weightMLP fc2
blocks.N.mlp.fc2.biasMLP fc2 bias
final_ln.scaleFinal LayerNorm scale
final_ln.shiftFinal LayerNorm shift

The mapping is a fixed function, shipped as a dict that the loader consults for each name.

The stub fixture

Real GPT-2 weights are 0.5 GB, so the demo does not download them. It generates a small safetensors fixture on first run, using the exact GPT-2 naming convention and shapes for a 4-block model (d_model 192, 6 heads, vocab 256, context 64) instead of the 12-block, 768-wide original. The fixture has the right structure to exercise every code path in the loader. Swap in the real file and the loader works without modification.

Check: The Concept0/13
1. What is a "name map" in a weight loader?Define
2. What is "transpose-on-load"?Define
3. What is a weight tying alias?Define
4. Why does a loader inspect tensor names and shapes in the file before copying anything?Explain
5. Why does the loader need a name map?Explain
6. Which pretrained tensors does the loader transpose during assignment?Explain
7. The LM head is not in the safetensors file. Why is the model still complete after loading?Explain
8. Why does the demo use a generated stub fixture instead of a real GPT-2 download?Explain
9. The file contains `h.3.mlp.c_fc.weight`. Where does the loader put it?Apply
10. The demo's stub fixture has 4 blocks and uses the GPT-2 naming convention. How many tensors does the file hold?Apply
11. In the real 12-block GPT-2 file, `h.5.mlp.c_fc.weight` has shape (768, 3072). Which shape does the loader compare with the local `blocks.5.mlp.fc1.weight`?Apply
12. A loader forgets to transpose `h.N.attn.c_proj.weight` (768, 768) but handles every other projection correctly. What does the shape check report for it?Analyze
13. You want the loader to refuse a 24-block checkpoint when the model has 12 blocks, instead of loading 12 and ignoring the rest. Which design fits?Design

Build It

code/main.py implements:

  • A small replica of the lesson 35 GPTModel, so this lesson stands on its own.
  • make_pretrained_to_local(num_layers), which expands the per-layer entries.
  • load_safetensors(model, path), which iterates names, maps them, checks shape, transposes the conv1d-style weights, and assigns under torch.no_grad(). It returns a LoadReport.
  • make_stub_safetensors(path, cfg), which generates a fixture file with the exact pretrained naming convention.
  • A demo that creates outputs/gpt2-stub.safetensors on first run, builds a fresh model, captures one generated continuation from random init, loads the stub, captures another continuation, prints both, and verifies the two differ (the load actually changed the model).

Run it:

bash
python3 code/main.py

Output: the fixture path, a per-name load log, a LoadReport summary, a continuation before the load, a continuation after the load, and and a shape mismatch check: the demo writes a separate outputs/gpt2-bad.safetensors whose wte.weight has the wrong width, loads it into a fresh model, prints the report, and deletes the file. The report shows the mismatch, nothing is assigned, and the model's parameters are listed under missing. Only a missing file raises (FileNotFoundError).

Check: Build It0/4
1. What is a shape mismatch in this loader?Define
2. What does `load_safetensors` do when one tensor has the wrong shape?Explain
3. The demo's bad file has `wte.weight` with shape (256, 193) where the model expects (256, 192); every other tensor is fine. How many local parameters does the report list as missing?Apply
4. A model with 2 layers loads a file that also contains `h.2.attn.c_attn.weight`. What happens to that tensor?Apply

Stack

  • safetensors for the on disk format and a streaming reader.
  • torch for the model and the assignment math.
  • No transformers, no huggingface_hub, no network calls.

Production patterns in the wild

Three patterns let a loader survive weights you did not create.

Validate the file before any assignment. Open the file, list every tensor name with its dtype and shape, run the full mapping with shape checks, and start assigning only on success. A half-loaded model fails silently.

Log each assignment with the source name and the destination name. When something looks wrong, the log tells you which tensor landed where, and the alternative is reading hexdumps. The LoadReport dataclass here tracks loaded, missing, unexpected, and shape_mismatch lists and prints a summary at the end.

Treat the LM head as a weight tying alias, not a separate copy. The constructor sets self.lm_head.weight = self.tok_embed.weight, and the loader re-aliases after the load only if the data pointers differ. Copying the embedding matrix into a fresh lm_head.weight parameter breaks tying and quietly doubles your parameter count.

Check: Production patterns in the wild0/6
1. What is the load report?Define
2. Why does the loader re-alias lm_head to tok_embed only when their data pointers differ?Explain
3. Why log each assignment with both the source and the destination name?Explain
4. Which pattern keeps a failed checkpoint load from leaving a half-loaded model?Explain
5. Loader A copies each tensor as soon as it passes its shape check. Loader B checks all 52 shapes first and then copies. The 40th tensor in the file has a bad shape. What state does each leave the model in?Analyze
6. After loading, a developer replaces the head with a fresh `lm_head.weight` parameter holding a copy of the embedding matrix. What is the consequence?Analyze

Use It

  • The loader works for any safetensors file that uses the pretrained naming convention. Real GPT-2 files (small / medium / large / xl) load without code changes; only the model config differs.
  • The same pattern extends to LLaMA, Mistral, and Qwen weights once you update the name map. The shape checks and the report stay identical.
  • Sanity generation after a load is a quick gate. If the post-load samples look like the pre-load samples, the load did not change the model, which means the mapping silently missed every tensor.
Check: Use It0/3
1. Why generate a sample before and after the load?Explain
2. After a load, the samples match the random-init samples, and the report shows loaded=0 with every file tensor under unexpected. What is the most likely cause?Analyze
3. You must load a LLaMA checkpoint into a LLaMA-style model using this loader's structure. What do you change?Design

Exercises

  1. Add a dtype argument to the loader that casts each tensor to a target dtype (bfloat16, float16, float32) during assignment. Confirm a float32 model can be downcast to bfloat16 and still generate.
  2. Add an expected_layers argument that refuses to load a checkpoint whose h.N indices do not match the model's num_layers.
  3. Plug the loader into the lesson 35 generation function and produce two side by side samples: one from random init, one from the loaded fixture.
  4. Add an export path: write the current model state into a fresh safetensors file using the pretrained naming convention. Round trip the loader and confirm the report has zero shape mismatches.
  5. Extend make_pretrained_to_local to handle the LLaMA naming convention (no biases, RMSNorm, fused qkv layout) and re-run the loader on a stub LLaMA fixture you generate.

Key Terms

TermWhat people sayWhat it actually means
Name map"Key remapping"The function from pretrained tensor names to local parameter names; usually a literal dict with one entry per layer index, expanded over a loop
Shape mismatch"Bad shape"The pretrained tensor exists under the mapped name but its dimensions disagree with the local parameter; the loader records the pair in shape_mismatch and returns without assigning anything
Transpose-on-load"Conv1d layout"Published GPT-2 stores attention and MLP projections in the transpose of what nn.Linear expects; the loader transposes during assignment
Weight tying alias"Shared LM head"Setting model.lm_head.weight = model.tok_embed.weight so the head and embedding share storage; the constructor does this, and the loader re-aliases after the load only if the pointers differ. The head is not in the file because of this
Load report"Coverage summary"A small dataclass that tracks loaded, missing, unexpected, and shape_mismatch lists; printing it is how you tell whether the load succeeded
Author's Note

My bet is that the post-load sample comparison earns its place in the demo. It costs two short generations, and it catches a quiet failure: a mapping that matched nothing and left you holding a randomly initialized model. I'd run it before trusting any load, and I'd read the LoadReport next to it, since the report says which names landed and the samples say whether the model changed. I'd also log assignments even when the load succeeds. When a tensor lands in the wrong place, you want the source and destination names already printed rather than a hexdump to read.

Further Reading

  • Phase 19 lesson 35 for the architecture that receives the weights.
  • Phase 19 lesson 36 for the training loop that produces a checkpoint of the same shape.
  • Phase 10 lesson 11 (quantization) for what to do with the loaded weights when memory is tight.
  • Phase 10 lesson 13 (building a complete LLM pipeline) for the full lifecycle around load and inference.

Code