Model card: step 5568 is now a ready-made nanochat checkpoint (+8-rank optimizer shards)
Browse files
README.md
CHANGED
|
@@ -85,9 +85,12 @@ checkpoints/ native DecoderStack captures (the source of
|
|
| 85 |
optim_step001950.pt 11.1 GB {step, t_step, state} -- mantissas + Muon/AdamW moments
|
| 86 |
model_step005568.pt 2.8 GB
|
| 87 |
optim_step005568.pt 11.1 GB
|
| 88 |
-
base_checkpoints/d24_decoderstack/ nanochat-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
| 91 |
code/run_full_d24_w8.py the exact training script this run executed
|
| 92 |
logs/
|
| 93 |
d24_decoderstack_20260801_023219.log full training log
|
|
@@ -109,77 +112,96 @@ is that string extracted from `model_step005568.pt`.
|
|
| 109 |
|
| 110 |
## Loading into nanochat
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
optimizer file.
|
| 116 |
|
| 117 |
```bash
|
| 118 |
pip install huggingface_hub torch
|
| 119 |
python - <<'PY'
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
"convert_ckpt_to_nanochat.py",
|
| 126 |
-
"tokenizer/tokenizer.pkl", "tokenizer/token_bytes.pt"]:
|
| 127 |
-
hf_hub_download(R, f, local_dir="decoderstack-d24")
|
| 128 |
PY
|
| 129 |
-
|
| 130 |
-
cd decoderstack-d24
|
| 131 |
-
python convert_ckpt_to_nanochat.py \
|
| 132 |
-
--model checkpoints/model_step005568.pt \
|
| 133 |
-
--optim checkpoints/optim_step005568.pt \
|
| 134 |
-
--meta base_checkpoints/d24_decoderstack/meta_005568.json \
|
| 135 |
-
--out ~/.cache/nanochat/base_checkpoints/d24_decoderstack
|
| 136 |
-
mkdir -p ~/.cache/nanochat/tokenizer && cp tokenizer/* ~/.cache/nanochat/tokenizer/
|
| 137 |
```
|
| 138 |
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
want the exact master.
|
| 145 |
-
|
| 146 |
-
Then, on the [`fa-varlen`](https://github.com/chrisjmccormick/nanochat/tree/fa-varlen)
|
| 147 |
-
branch (varlen, matching how these weights were trained):
|
| 148 |
|
| 149 |
```python
|
| 150 |
import os, torch
|
| 151 |
-
from nanochat.checkpoint_manager import
|
| 152 |
-
|
| 153 |
-
|
| 154 |
```
|
| 155 |
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
(0.725 bpb) on held-out English β in line with the run's own 0.719 val bpb β with
|
| 159 |
-
coherent greedy continuations.
|
| 160 |
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
-
###
|
| 165 |
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
ZeRO-2 shards `optim_005568_rank{0..N-1}.pt` that `DistMuonAdamW` expects, matched to the
|
| 172 |
-
GPU count of the SFT run:
|
| 173 |
|
| 174 |
```bash
|
| 175 |
python convert_ckpt_to_nanochat.py \
|
| 176 |
--model checkpoints/model_step005568.pt \
|
| 177 |
--optim checkpoints/optim_step005568.pt \
|
| 178 |
--meta base_checkpoints/d24_decoderstack/meta_005568.json \
|
| 179 |
-
--out
|
| 180 |
--world-size 8
|
| 181 |
```
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
Note that `torch`'s `load_state_dict` replaces param-group dicts wholesale, so the emitted
|
| 184 |
`lr`/`betas`/`weight_decay` become the optimizer's on load β they default to
|
| 185 |
`setup_optimizer()`'s own values with `weight_decay=0.0` (the SFT setting, and where
|
|
@@ -215,9 +237,10 @@ square, where the two axes would carry genuinely different information. The expl
|
|
| 215 |
only earns its keep when `n_heads * d_head != d_model` (and, for `ve_gate`, when
|
| 216 |
`n_kv_head != d_ve_gate`; both are equalities at d24).
|
| 217 |
|
| 218 |
-
|
| 219 |
-
a toy config
|
| 220 |
-
`optimizer.step()`
|
|
|
|
| 221 |
|
| 222 |
## Provenance
|
| 223 |
|
|
@@ -233,3 +256,8 @@ trainer with three launcher overrides (`micro_batch_tokens` 32768β65536,
|
|
| 233 |
Training data: [`ChrisMcCormick/climbmix_32k_8_170`](https://huggingface.co/datasets/ChrisMcCormick/climbmix_32k_8_170)
|
| 234 |
(ClimbMix, pre-tokenized to 32k binary shards). Validation is a rebuild of shard 06542 to
|
| 235 |
match nanochat's pinned val split.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
optim_step001950.pt 11.1 GB {step, t_step, state} -- mantissas + Muon/AdamW moments
|
| 86 |
model_step005568.pt 2.8 GB
|
| 87 |
optim_step005568.pt 11.1 GB
|
| 88 |
+
base_checkpoints/d24_decoderstack/ READY-MADE nanochat checkpoint -- no conversion needed
|
| 89 |
+
model_005568.pt 4.2 GB fp32 masters + bf16 embeddings, 175 tensors
|
| 90 |
+
optim_005568_rank0.pt 1.0 GB ZeRO-2 optimizer shards for an 8-GPU SFT run
|
| 91 |
+
... _rank7.pt 1.0 GB (8 x 1.0 GB; see Optimizer state)
|
| 92 |
+
meta_005568.json model_config + user_config + run metadata
|
| 93 |
+
meta_001950.json metadata only -- step 1950 is not converted
|
| 94 |
code/run_full_d24_w8.py the exact training script this run executed
|
| 95 |
logs/
|
| 96 |
d24_decoderstack_20260801_023219.log full training log
|
|
|
|
| 112 |
|
| 113 |
## Loading into nanochat
|
| 114 |
|
| 115 |
+
**Step 5568 is already converted.** `base_checkpoints/d24_decoderstack/` is a nanochat
|
| 116 |
+
checkpoint dir β download it and the tokenizer into a nanochat base dir and it loads. No
|
| 117 |
+
conversion step, and the 11 GB optimizer capture is not part of it.
|
|
|
|
| 118 |
|
| 119 |
```bash
|
| 120 |
pip install huggingface_hub torch
|
| 121 |
python - <<'PY'
|
| 122 |
+
import os
|
| 123 |
+
from huggingface_hub import snapshot_download
|
| 124 |
+
snapshot_download("ChrisMcCormick/decoderstack-d24",
|
| 125 |
+
local_dir=os.path.expanduser("~/.cache/nanochat-ds"),
|
| 126 |
+
allow_patterns=["base_checkpoints/d24_decoderstack/*", "tokenizer/*"])
|
|
|
|
|
|
|
|
|
|
| 127 |
PY
|
| 128 |
+
export NANOCHAT_BASE_DIR=~/.cache/nanochat-ds
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
```
|
| 130 |
|
| 131 |
+
**Use a base dir of its own, not a shared `~/.cache/nanochat`.** nanochat resolves the
|
| 132 |
+
tokenizer from one global `tokenizer/` per base dir, and this vocabulary is not the one any
|
| 133 |
+
other d24 release uses (see [β οΈ Tokenizer](#οΈ-tokenizer)). A base dir holding both this
|
| 134 |
+
checkpoint and, say, `nanochat-varlen-d24-2026-03-22` can only be right about one of them,
|
| 135 |
+
and `build_model` only asserts the vocab *size* β which matches β so it will not warn you.
|
| 136 |
|
| 137 |
+
Then, on
|
| 138 |
+
[`varlen-gsm8k`](https://github.com/chrisjmccormick/nanochat/tree/varlen-gsm8k) (the branch
|
| 139 |
+
whose SFT/RL pipeline this checkpoint targets; `fa-varlen` also works for eval):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
```python
|
| 142 |
import os, torch
|
| 143 |
+
from nanochat.checkpoint_manager import load_model
|
| 144 |
+
model, tokenizer, meta = load_model("base", torch.device("cuda"), phase="eval",
|
| 145 |
+
model_tag="d24_decoderstack", step=5568)
|
| 146 |
```
|
| 147 |
|
| 148 |
+
Pass `model_tag` explicitly β with more than one `d24*` dir under `base_checkpoints/`,
|
| 149 |
+
`find_largest_model` picks between them on depth alone and both are 24.
|
|
|
|
|
|
|
| 150 |
|
| 151 |
+
Verified on an H100 (2026-08-01): 175 tensors, 1,384,122,122 params, shapes and dtypes
|
| 152 |
+
identical to a freshly built nanochat `GPT` at this config, `strict=True` load, and
|
| 153 |
+
**2.4839 nats/token (0.7255 bpb)** on held-out English β in line with the run's own 0.719
|
| 154 |
+
val bpb β with coherent greedy continuations (`The capital city of France is` β ` Paris.`).
|
| 155 |
+
A scrambled mapping scores 10.4.
|
| 156 |
|
| 157 |
+
### Re-converting yourself
|
| 158 |
|
| 159 |
+
`convert_ckpt_to_nanochat.py` unbanks the per-layer matrix banks, renames tensors to
|
| 160 |
+
nanochat's module paths, maps value-embedding bank slots back to their layers, and rebuilds
|
| 161 |
+
the fp32 masters from the bf16 live weights plus the uint16 mantissa held in the optimizer
|
| 162 |
+
file. You need it to convert **step 1950**, or to emit optimizer shards for a GPU count
|
| 163 |
+
other than 8:
|
|
|
|
|
|
|
| 164 |
|
| 165 |
```bash
|
| 166 |
python convert_ckpt_to_nanochat.py \
|
| 167 |
--model checkpoints/model_step005568.pt \
|
| 168 |
--optim checkpoints/optim_step005568.pt \
|
| 169 |
--meta base_checkpoints/d24_decoderstack/meta_005568.json \
|
| 170 |
+
--out $NANOCHAT_BASE_DIR/base_checkpoints/d24_decoderstack \
|
| 171 |
--world-size 8
|
| 172 |
```
|
| 173 |
|
| 174 |
+
`--optim` is optional for the weights. It only supplies the mantissas β the lower 16 bits
|
| 175 |
+
of the fp32 masters. Without it the fp32 parameters carry bf16 precision, which costs about
|
| 176 |
+
0.0005 nats/token (2.4833 vs 2.4828 on a held-out paragraph); skip the 11 GB download
|
| 177 |
+
unless you want the exact master, or the optimizer shards, which need it.
|
| 178 |
+
|
| 179 |
+
**DecoderStack itself cannot load these back yet.** The capture path is write-only by
|
| 180 |
+
design; a load/resume path is future work.
|
| 181 |
+
|
| 182 |
+
### Optimizer state
|
| 183 |
+
|
| 184 |
+
**SFT works with the model alone.** nanochat's `chat_sft` builds a fresh optimizer and only
|
| 185 |
+
*optionally* warm-starts it; with no optimizer shard present it prints `starting with fresh
|
| 186 |
+
optimizer (slightly worse)` and carries on.
|
| 187 |
+
|
| 188 |
+
The `optim_005568_rank{0..7}.pt` files here remove that "slightly worse". They are the
|
| 189 |
+
ZeRO-2 shards `DistMuonAdamW` expects, **cut for an 8-GPU run** β the shard count must equal
|
| 190 |
+
the GPU count of the SFT run, because rank *r* holds only its own slice. For any other
|
| 191 |
+
world size, re-run the converter with `--world-size N` (see above); `--world-size 1` emits
|
| 192 |
+
a single rank0 file holding the full unsharded state.
|
| 193 |
+
|
| 194 |
+
Verified on an H100 (2026-08-01), all 8 shards: 10 groups / 175 params / 23 state entries
|
| 195 |
+
each, shapes equal to what `DistMuonAdamW` allocates per rank at this config, accepted by
|
| 196 |
+
`load_state_dict`, and **reassembling bit-exactly** back to the original capture β including
|
| 197 |
+
the ragged 12-param `ve_gate` group, where chunk 2 Γ 8 ranks leaves ranks 6β7 holding only
|
| 198 |
+
zero padding. End to end, `chat_sft`'s own load block warm-starts from them (23 entries,
|
| 199 |
+
step 5568) and steps.
|
| 200 |
+
|
| 201 |
+
`meta_005568.json` carries a `user_config` block, which is where `chat_sft` reads the
|
| 202 |
+
pretraining LRs from β without it SFT silently falls back to `unembedding_lr=0.004` instead
|
| 203 |
+
of this run's **0.008**.
|
| 204 |
+
|
| 205 |
Note that `torch`'s `load_state_dict` replaces param-group dicts wholesale, so the emitted
|
| 206 |
`lr`/`betas`/`weight_decay` become the optimizer's on load β they default to
|
| 207 |
`setup_optimizer()`'s own values with `weight_decay=0.0` (the SFT setting, and where
|
|
|
|
| 237 |
only earns its keep when `n_heads * d_head != d_model` (and, for `ve_gate`, when
|
| 238 |
`n_kv_head != d_ve_gate`; both are equalities at d24).
|
| 239 |
|
| 240 |
+
The optimizer path is verified twice over: against a real nanochat `GPT` and `MuonAdamW` at
|
| 241 |
+
a toy config (structure, per-rank sharding at world sizes 1β4, reassembly, a warm-started
|
| 242 |
+
`optimizer.step()`), and against the full d24 capture at `--world-size 8` on an H100
|
| 243 |
+
(2026-08-01) β see above.
|
| 244 |
|
| 245 |
## Provenance
|
| 246 |
|
|
|
|
| 256 |
Training data: [`ChrisMcCormick/climbmix_32k_8_170`](https://huggingface.co/datasets/ChrisMcCormick/climbmix_32k_8_170)
|
| 257 |
(ClimbMix, pre-tokenized to 32k binary shards). Validation is a rebuild of shard 06542 to
|
| 258 |
match nanochat's pinned val split.
|
| 259 |
+
|
| 260 |
+
The nanochat-format checkpoint and its optimizer shards were produced and verified on
|
| 261 |
+
2026-08-01 against nanochat
|
| 262 |
+
[`varlen-gsm8k`](https://github.com/chrisjmccormick/nanochat/tree/varlen-gsm8k) `d9dffe9`,
|
| 263 |
+
torch 2.9.1, 1ΓH100.
|