Cheat sheets

Train and Load a GPT from Scratch

Build the 124M GPT, train it with AdamW and cosine warmup, load GPT-2 weights, gate the pipeline.

  • Concepts and terms
  • Tasks and commands
  • Annotated code
  • Diagrams
  • Comparisons
  • Defaults and numbers
  • Troubleshooting
  • Questions and tips

Source

Run the lessons#

CommandWhat it does
python3 code/main.py (35)Counts 124M params, generates with a tiny config
python3 code/main.py (36)Trains a tiny model, writes outputs/losses.jsonl
python3 code/main.py (37)Writes outputs/gpt2-stub.safetensors, loads it
python3 code/main.py (44)20 steps, per step lr and grad norm, text plot
python code/main.py planValidates manifest, cost estimate, prints DAG
python code/main.py runExecutes stages, writes manifest.out.yaml
python code/main.py gateApplies eval gates. Prints SHIP or HOLD
build_param_groups(model, weight_decay)AdamW groups, decay split

GPTConfig defaults (124M)#

SettingDefaultNote
vocab_size50257Rows of the token table
context_length1024Rows of the position table
d_model768Embedding width
num_heads12d_model must divide by it
num_layers12Twelve TransformerBlock modules
mlp_expansion4MLP hidden is 4 times d_model
dropout0.1Tiny demo sets 0.0
weight_tyingTruelm_head shares tok_embed

GPTModel forward#

ids0 to T-1tied weightclientToken idsshape B, Tstoretok_embed50257, 768storepos_embed1024, 768 learnedprocessAddembed_dropoutmodel12 blockspre-LNTransformerBlockprocessfinal_lnLayerNormmodellm_headlogits B, T, 50257ids0 to T-1tied weightclientToken idsshape B, Tstoretok_embed50257, 768storepos_embed1024, 768 learnedprocessAddembed_dropoutmodel12 blockspre-LN TransformerBlockprocessfinal_lnLayerNormmodellm_headlogits B, T, 50257

Build, count and sample#

ref_model = GPTModel(GPTConfig())124M reference
count_parameters(ref_model)unique params, tied once
GPTModel(GPTConfig(weight_tying=False))adds vocab_size * d_model
self.lm_head.weight = self.tok_embed.weightshare, never copy
generate(tiny_model, prompt, max_new_tokens=12,
temperature=0.8, top_k=20, seed=42)temperature must be positive
top_k_filter(logits, top_k)others to -inf
window = tokens[:, -model.cfg.context_length:]sliding window

One training step#

1make_batchestarget is the input shifted by one position.2Schedulecosine_with_warmup sets group["lr"] in eachparam group.3calc_loss_batchforward, flatten to (batch * seq, vocab), crossentropy.4Backwardloss.backward(), then clip_grad_norm_ atmax_norm=1.0.5AdamWoptimizer.step() with betas=(0.9, 0.95).6Logappend {"step", "train_loss", "lr"} tolosses.jsonl.7Probeevery eval_every steps run evaluate_model andgenerate_and_print_sample.

Q&A#

Why transpose on load?
c_attn, c_proj and c_fc use conv1d layout. nn.Linear wants the transpose.
Why print a sample during training?
Loss is a scalar. A sample shows a model stuck on one token.
Why slice eval batches once?
Identical batches make eval loss comparable across runs.
Where does the schedule read its step?
From global_step in the checkpoint, not a local counter.

Training loop core#

optimizer = torch.optim.AdamW(
build_param_groups(model, cfg.weight_decay),decay split
lr=cfg.max_lr, betas=(0.9, 0.95))
for step in range(cfg.num_steps):
lr = cosine_with_warmup(step, cfg.warmup_steps, cfg.num_steps, cfg.max_lr, cfg.min_lr)
for group in optimizer.param_groups:
group["lr"] = lr
inputs, targets = next(train_loader)
optimizer.zero_grad(set_to_none=True)
loss = calc_loss_batch(model, inputs, targets)flat cross entropy
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=cfg.grad_clip)
optimizer.step()

Schedule and size numbers#

Warmup
lr_max * step / warmup_steps up to warmup_steps.
Cosine
lr_min + 0.5 * (lr_max - lr_min) * (1 + cos(pi * progress)).
Floor
past total_steps the rate stays at lr_min.
Warmup size
a fraction of total steps. Typical is 1 to 3 percent.
Floor size
lr_min at 10 percent of lr_max keeps the optimizer learning.
Weight tying
saves 50257 times 768 = 38 million parameters.
Residual init
scale out projections by 1 / sqrt(2 * num_layers).
Pre-training FLOPs
6 x params x tokens.

GPT-2 names to local names#

FactorGPT-2 fileLocal model
Token embeddingwte.weighttok_embed.weight
Position embeddingwpe.weightpos_embed.weight
LayerNorm 1 scaleh.N.ln_1.weightblocks.N.ln1.scale
Fused QKV, transposedh.N.attn.c_attn.weightblocks.N.attn.qkv.weight
Attention out, transposedh.N.attn.c_proj.weightblocks.N.attn.out_proj.weight
MLP fc1, transposedh.N.mlp.c_fc.weightblocks.N.mlp.fc1.weight
MLP fc2, transposedh.N.mlp.c_proj.weightblocks.N.mlp.fc2.weight
Final LayerNormln_f.weight, ln_f.biasfinal_ln.scale, final_ln.shift

Load safetensors weights#

make_stub_safetensors(STUB_PATH, cfg, seed=42)offline fixture
model = GPTModel(cfg)
report = load_safetensors(model, STUB_PATH, verbose=False)
report.summary()loaded= missing= unexpected= shape_mismatch=
report.ok()no missing, no shape_mismatch
model.lm_head.weight.data_ptr() == model.tok_embed.weight.data_ptr()tied

Troubleshooting#

SymptomCauseFix
Loss near zero, model learns nothingNo shift by oneTarget at positions 1..T
LayerNorm scale goes to zeroWeight decay on the scaleScale and bias in the no decay group
One bad batch wipes hoursHuge gradientclip_grad_norm_ before step
Same samples before and after loadName map missed every tensorRead the LoadReport
shape_mismatch in the reportDims differ from the configLoader assigns nothing. Fix the config
Off by one LR late in the runGlued three part scheduleOne function over step, test the edges

Pipeline eval gates#

SettingDefaultNote
mmlu65.0Must be at least this
humaneval40.0Must be at least this
truthfulqa50.0Must be at least this
safety_refusal_rate0.05Must be at most this
kl_from_reference25.0Must be at most this
cost_total_usd50000.0Must be at most this
budget_usd50000.0Run halts over budget
seed42Manifest field