Checkpoints & Tests
The three files a checkpoint is made of, how talk.py uses them, what the test suite covers, and how to obtain a checkpoint.
checkpoint.py
This page covers saving and loading, the prompt loop in talk.py, and the test suite.
This page has no equations and no backward pass. Saving, loading and the prompt loop compute no loss and no gradient. The mathematics that talk.py reaches is on Autoregressive Generation.
Checkpoint format
What is it?
A checkpoint is three files that share a prefix. With the default prefix, checkpoints/model, they are checkpoints/model.npz, checkpoints/model.json and checkpoints/model.tok.json.
| File | Format | What it holds | Written at | Read at |
|---|---|---|---|---|
prefix.npz | NumPy archive | Every array in params, one entry per key. Keys such as b0.wq are kept as they are, and np.load gives the same keys back. | checkpoint.py:21 | checkpoint.py:34 |
prefix.json | JSON | One object, the fields of ModelConfig from asdict(cfg). load_checkpoint passes them back to ModelConfig, which rebuilds the shapes. | checkpoint.py:24 | checkpoint.py:37 |
prefix.tok.json | JSON | One object with two keys, merges and itos. merges is the list of merge records and itos is the vocabulary list, where a token's position is its id. | checkpoint.py:30 | checkpoint.py:42-44 |
Why does the model need it?
talk.py runs in a separate process from train.py, and it builds nothing itself. It needs the weights, the shapes they fit, and the tokenizer whose ids they were trained on. Each file holds one of the three.
Tiny numerical example
Folder it would create: checkpoints. Files, in the order they are written: checkpoints/model.npz, checkpoints/model.json, checkpoints/model.tok.json.
checkpoints/model.json
{"vocab_size": 9, "context_len": 8, "emb_dim": 4, "num_heads": 2, "ff_dim": 8, "num_layers": 1, "ln_eps": 1e-05}checkpoints/model.tok.json
{"merges": [{"pair": ["l", "o"], "merged": "lo", "frequency": 4}, {"pair": ["lo", "w"], "merged": "low", "frequency": 4}], "itos": ["<unk>", "l", "o", "w", " ", "e", "r", "lo", "low"]}checkpoints/model.npz: 22 arrays
tok_emb(9, 4)pos_emb(8, 4)b0.ln1_g(4,)b0.ln1_b(4,)b0.wq(4, 4)b0.bq(4,)b0.wk(4, 4)b0.bk(4,)b0.wv(4, 4)b0.bv(4,)b0.wo(4, 4)b0.bo(4,)b0.ln2_g(4,)b0.ln2_b(4,)b0.ff1_w(4, 8)b0.ff1_b(8,)b0.ff2_w(8, 4)b0.ff2_b(4,)lnf_g(4,)lnf_b(4,)head_w(4, 9)head_b(9,)
Tensor shapes
The arrays in the .npz file have the shapes that init_params gave them, because saving does not change them. The Architecture page lists each one at the values in config.py. V depends on the corpus, so it is not stored in config.py. It is stored in the checkpoint as vocab_size.
Fields of the .json file
asdict(cfg) writes one key per field of ModelConfig, including ln_eps. train.main does not pass ln_eps, so the file holds the default of the dataclass.
| Key in the file | Type | Default |
|---|---|---|
vocab_size | int | None, so it must be passed |
context_len | int | None, so it must be passed |
emb_dim | int | None, so it must be passed |
num_heads | int | None, so it must be passed |
ff_dim | int | None, so it must be passed |
num_layers | int | None, so it must be passed |
ln_eps | float | 1e-5 |
Merge records in the .tok.json file
Each merge is a record with the keys pair, merged and frequency, which train_bpe creates (tokenizer.py:59). JSON has no tuples, so pair is written as a list. merge_tokens reads only pair[0] and pair[1], so a list works the same way.
ensure_ascii=False keeps characters outside ASCII readable in the file, and test_non_ascii_characters_survive checks that they come back unchanged.
Saving
Actual implementation
def save_checkpoint(prefix, params, cfg: ModelConfig, tok: Tokenizer): folder = os.path.dirname(prefix) if folder: os.makedirs(folder, exist_ok=True) np.savez(prefix + ".npz", **params) # keys like "b0.wq" are kept as they are with open(prefix + ".json", "w", encoding="utf-8") as f: json.dump(asdict(cfg), f) # stoi is not saved, it is just itos turned around, so load rebuilds it # json has no tuples, so each merge's "pair" is stored as a list, merge_tokens only # uses pair[0] and pair[1], so a list works exactly the same with open(prefix + ".tok.json", "w", encoding="utf-8") as f: json.dump({"merges": tok.merges, "itos": tok.itos}, f, ensure_ascii=False)- The folder is created if needed. The prefix
checkpoints/modelhas the foldercheckpoints. A prefix with no folder writes to the current directory (checkpoint.py:17-19). stoiis not saved. It isitosturned around, soload_checkpointrebuilds it.
train.main saves once, after the last epoch (train.py:78). Saving again with the same prefix replaces the three files.
Where is it used?
- Called by
train.main- Tested by
tests.test_checkpoint.saved,tests.test_checkpoint.test_non_ascii_characters_survive
Loading
Actual implementation
def load_checkpoint(prefix): with np.load(prefix + ".npz") as z: params = {k: z[k] for k in z.files} # copy out, the file closes after this block with open(prefix + ".json", encoding="utf-8") as f: cfg = ModelConfig(**json.load(f)) with open(prefix + ".tok.json", encoding="utf-8") as f: d = json.load(f) stoi = {} for i in range(len(d["itos"])): stoi[d["itos"][i]] = i return params, cfg, Tokenizer(d["merges"], stoi, d["itos"])- Weights are copied out of the archive while it is open, because the file closes after the block (checkpoint.py:34).
stoiis rebuilt by walkingitos, so a token's id is its position (checkpoint.py:42-44).- The result is the same three objects that
save_checkpointwas given:params,cfgand aTokenizer(checkpoint.py:46).
test_loaded_model_gives_identical_logits checks that a loaded model gives exactly the same logits as the original.
Where is it used?
- Called by
talk.main- Tested by
tests.test_checkpoint.test_config_round_trips,tests.test_checkpoint.test_loaded_model_gives_identical_logits,tests.test_checkpoint.test_non_ascii_characters_survive,tests.test_checkpoint.test_tokenizer_round_trips,tests.test_checkpoint.test_weights_round_trip_exactly
Only talk.main calls it in the pipeline, so nothing in the repository resumes training from a checkpoint.
What a checkpoint does not hold
save_checkpoint receives params, cfg and tok, and writes only those. So these are not saved: Adam's m, v and t, the random generator's state, the step count, and the training settings in config.py. A checkpoint cannot tell you how it was trained.
Why the files are not in the repository
.gitignore lists checkpoints/ and /data/, so neither a checkpoint nor the corpus is part of a commit. This page shows no output from a trained model, so every number on it is an Illustrative Example.
Interactive inference
talk.py loads a checkpoint and completes text in a loop.
Code listing: talk.main
def main(prefix="checkpoints/model"): params, cfg, tok = load_checkpoint(prefix) rng = np.random.default_rng(config.seed) print(f"loaded {prefix} (V={cfg.vocab_size})") print("Type the start of a text and press Enter. Empty line quits.") while True: try: prompt = input("> ") except EOFError: break if prompt.strip() == "": break ids = tok.encode(prompt) out = generate(params, cfg, ids, config.max_new_tokens, config.seq_len, config.temperature, config.top_k, config.top_p, rng) print(tok.decode(out[len(ids):])) # only the new tokens, the prompt is already on screen- Load.
load_checkpointrestores the weights, shapes and tokenizer (talk.py:8). The default prefix ischeckpoints/model. Runningpython talk.py <prefix>passes another one (talk.py:30). - Create the generator. It uses
config.seed, once, before the loop (talk.py:9). So a new run oftalk.pyrepeats the same random sequence, and a second prompt in one run continues it. - Read a prompt. End of input stops the loop (talk.py:14-17), and so does an empty or whitespace-only line (talk.py:19).
- Encode.
tok.encodeturns the prompt into ids (talk.py:22). A character the tokenizer never saw becomes id 0,<unk>. - Generate.
generateappendsconfig.max_new_tokensids (talk.py:24-25). See Autoregressive Generation. - Decode only the new tokens. The prompt is already on screen, so
out[len(ids):]skips it (talk.py:27).
The model's shape and tokenizer come from the checkpoint. The window and the sampling settings come from config.py as it is when talk.py runs. If config.seq_len is raised above the checkpoint's context_len, a long enough output fails the assertion in forward (transformer.py:156).
- Called by
talk.<module>- Tested by
- No test calls it directly
No test calls talk.main. The functions it calls are tested separately.
Reproducing training
These are the commands the code supports. They were not run for this page.
- Supply a corpus.
train.mainreadsconfig.corpus_path, which is"data/corpus.csv", using the column"text". A fresh clone does not have this file, because/data/is git-ignored. - Install the dependencies. The repository imports
numpyandpandas, and the tests also importpytest. It has no requirements file. - Train. Run
python train.pyfrom the repository root. The script callsmain()with no arguments (train.py:85), so it always saves tocheckpoints/model. - Talk. Run
python talk.pyfrom the repository root.
Training settings are the values in config.py, listed on the Training Loop page. Random numbers start from config.seed.
Test suite
Run the tests with python -m pytest tests from the repository root. The test files import root modules by bare name, and the repository has no conftest.py or pytest configuration.
Tests write only to pytest's temporary folder, so they do not touch checkpoints/.
8 test files hold 64 test functions, which make 67 cases because parametrized tests run once per value, counted from data/generated/tests-index.json. This page does not state a pass
count, because it would go out of date.
"Exercises" lists what a test calls directly, or through its fixtures and helpers. A function called from inside one of those is covered too, but it is not listed. Each sentence in "What it verifies" links to its test.
What no test reaches
These functions are not reached by any test, even indirectly, according to the static call graph:
The two main functions are the whole pipeline, so an end-to-end run of training or of the prompt loop is not tested. test_overfit_one_window is the closest. It runs loss_and_grads and Adam.step together on one window.
Gradient checks have their own page, Gradient Checking.