File size: 8,471 Bytes
11f07f9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | ---
license: mit
language:
- en
tags:
- decoderstack
- nanochat
- pretraining
---
# DecoderStack d24
A 1.38B-parameter d24 model pre-trained from scratch with
[**DecoderStack**](https://github.com/chrisjmccormick/stacks) β a single-file training
stack with a handwritten forward/backward and a written-out optimizer: no autograd, no
`torch.optim`, no `nn.Module`. bf16, varlen FlashAttention, 8xH100, 1h51m.
The architecture is the nanochat d24 architecture, so these weights load into
[nanochat](https://github.com/karpathy/nanochat) once converted β see
[Loading into nanochat](#loading-into-nanochat).
## Results
Full horizon: 5,568 steps x 2^20 tokens = 5.84B tokens (data:param ratio 8).
```
step 05567/05568 (99.98%) | loss: 2.388180 | lrm: 0.05 | dt: 1198.08ms | tok/sec: 875,210 | bf16_mfu: 53.76 | total time: 110.74m
step:5568/5568 val_bpb:0.719042
CORE metric: 0.2517 | total CORE eval time: 80.46s
peak memory allocated: 71569 MiB reserved: 72736 MiB
total training time: 110.74m
minimum validation bpb: 0.719042
```
| | value |
|---|---|
| parameters | 1,384,122,122 |
| min val bpb | 0.719042 |
| CORE | 0.2517 |
| train time | 110.74 min on 8xH100 80GB |
| throughput | 1,212 ms/step, 864,856 tok/sec (steps 20β99) |
| precision | bf16 (no FP8) |
val bpb curve: 0:3.175835 / 250:0.997472 / 500:0.904081 / 1000:0.846058 / 2000:0.809199 /
3000:0.773139 / 4000:0.747155 / 5000:0.726908 / 5568:0.719042
**Against upstream nanochat**, run on the same box on the same rebuilt validation shard,
DecoderStack tracks 0.2β1.4% behind through step 1000 (where the upstream baseline was
stopped) while running ~6% faster per step (1,212 vs 1,291 ms/step). Full head-to-head in
[`logs/baseline_report.md`](logs/baseline_report.md).
`ChrisMcCormick/nanochat-varlen-d24-2026-03-22` is the closest sibling release
(0.724772 val bpb, 0.2614 CORE, 97.4 min) but it is **not** a controlled comparison: it
trained with FP8 and a different 32k tokenizer.
## β οΈ Tokenizer
These weights are tied to the 32k vocabulary in
[`ChrisMcCormick/climbmix_32k_8_170`](https://huggingface.co/datasets/ChrisMcCormick/climbmix_32k_8_170),
mirrored here under [`tokenizer/`](tokenizer). It is a **different vocabulary** from the
one in `ChrisMcCormick/nanochat-varlen-d24-2026-03-22` β 31,474 of 32,759 merge ids
differ. Both are 32,768 tokens and both load without complaint, so nothing will warn you.
Pairing these weights with the wrong tokenizer produces garbage, not slightly-worse text.
## Contents
```
checkpoints/ native DecoderStack captures (the source of record)
model_step001950.pt 2.8 GB {step, code, weights} -- bf16 live weights + fp32 scalars
optim_step001950.pt 11.1 GB {step, t_step, state} -- mantissas + Muon/AdamW moments
model_step005568.pt 2.8 GB
optim_step005568.pt 11.1 GB
base_checkpoints/d24_decoderstack/ nanochat-format target dir
meta_001950.json model_config + run metadata
meta_005568.json (the converter writes model_NNNNNN.pt in here)
code/run_full_d24_w8.py the exact training script this run executed
logs/
d24_decoderstack_20260801_023219.log full training log
run_full_d24_w8.sh launcher, with the config rationale
baseline_report.md throughput + val bpb vs upstream nanochat
tokenizer/ the 32k vocab these weights were trained on
convert_ckpt_to_nanochat.py DecoderStack capture -> nanochat state_dict
```
Two capture points: **5568** is the end of the run, **1950** is the last uncooled state β
the LR/momentum hold ends after update 1949 (`N - round(0.65*N)`), so 1950 is the one to
resume from if you want to train the horizon longer.
DecoderStack writes `code` (the full training script, `open(sys.argv[0]).read()`) into
every model capture, so each `.pt` carries its own exact source. `code/run_full_d24_w8.py`
is that string extracted from `model_step005568.pt`.
## Loading into nanochat
`convert_ckpt_to_nanochat.py` unbanks the per-layer matrix banks, renames tensors to
nanochat's module paths, maps value-embedding bank slots back to their layers, and
rebuilds the fp32 masters from the bf16 live weights plus the uint16 mantissa held in the
optimizer file.
```bash
pip install huggingface_hub torch
python - <<'PY'
from huggingface_hub import hf_hub_download
R = "ChrisMcCormick/decoderstack-d24"
for f in ["checkpoints/model_step005568.pt", # 2.8 GB
"checkpoints/optim_step005568.pt", # 11.1 GB -- optional, see below
"base_checkpoints/d24_decoderstack/meta_005568.json",
"convert_ckpt_to_nanochat.py",
"tokenizer/tokenizer.pkl", "tokenizer/token_bytes.pt"]:
hf_hub_download(R, f, local_dir="decoderstack-d24")
PY
cd decoderstack-d24
python convert_ckpt_to_nanochat.py \
--model checkpoints/model_step005568.pt \
--optim checkpoints/optim_step005568.pt \
--meta base_checkpoints/d24_decoderstack/meta_005568.json \
--out ~/.cache/nanochat/base_checkpoints/d24_decoderstack
mkdir -p ~/.cache/nanochat/tokenizer && cp tokenizer/* ~/.cache/nanochat/tokenizer/
```
(`~/.cache/nanochat` is nanochat's default base dir; override with `NANOCHAT_BASE_DIR`.)
`--optim` is optional. It only supplies the mantissas β the lower 16 bits of the fp32
masters. Without it the fp32 parameters carry bf16 precision, which costs about 0.0005
nats/token (2.4833 vs 2.4828 on a held-out paragraph); skip the 11 GB download unless you
want the exact master.
Then, on the [`fa-varlen`](https://github.com/chrisjmccormick/nanochat/tree/fa-varlen)
branch (varlen, matching how these weights were trained):
```python
import os, torch
from nanochat.checkpoint_manager import build_model
ckpt_dir = os.path.expanduser("~/.cache/nanochat/base_checkpoints/d24_decoderstack")
model, tokenizer, meta = build_model(ckpt_dir, 5568, torch.device("cuda"), "eval")
```
The conversion is verified: 175 tensors, shapes and dtypes identical to a freshly built
nanochat `GPT` at this config, loads with `strict=True`, and scores 2.4828 nats/token
(0.725 bpb) on held-out English β in line with the run's own 0.719 val bpb β with
coherent greedy continuations.
**DecoderStack itself cannot load these back yet.** The capture path is write-only by
design; a load/resume path is future work.
### Optimizer state
Not converted, and nanochat could not resume from it if it were. Every buffer does have a
counterpart, and the precisions line up on everything except the embedding tables:
| DecoderStack | nanochat | precision |
|---|---|---|
| `.frst_mntm` | Muon `momentum_buffer` | fp32 both |
| `.scnd_mntm` | Muon `second_momentum_buffer` | fp32 both |
| `.exp_avg` / `.exp_avg_sq` | AdamW `exp_avg` / `exp_avg_sq` | fp32 both, **except** `wte` and `value_embeds`, which nanochat allocates `zeros_like(p)` on a bf16 param β so bf16 there, fp32 here |
| `.mantissa` (uint16) | β | nanochat's fp32 param *is* the master; consumed by the converter to rebuild it |
Two things block a faithful resume regardless: `W_O`'s NorMuon second moment reduces along
a different axis in each codebase (nanochat infers it from the shape and lands on `-1` for
a square `c_proj`; DecoderStack sets `residual_dim = -2` deliberately, because W_O's heads
are stored transposed relative to QKV), and nanochat's resume needs a
`dataloader_state_dict` that DecoderStack's pre-tokenized binary loader has no equivalent
of. The remaining mapping β param ordering and per-rank sharding β is written out at the
bottom of `convert_ckpt_to_nanochat.py`.
## Provenance
Trained by [`chrisjmccormick/stacks`](https://github.com/chrisjmccormick/stacks) β the
converter is committed at
[`e664830`](https://github.com/chrisjmccormick/stacks/commit/e6648306442e0479433b287ede02d271997365c0)
(`utils/convert_ckpt_to_nanochat.py`). The training script that produced these weights is
`code/run_full_d24_w8.py` in this repo, which is the run copy of the single-file d24
trainer with three launcher overrides (`micro_batch_tokens` 32768β65536,
`max_num_docs` 96β192, `NUM_TRAIN_SHARDS` 20β80); the rationale for each is in
`logs/run_full_d24_w8.sh`.
Training data: [`ChrisMcCormick/climbmix_32k_8_170`](https://huggingface.co/datasets/ChrisMcCormick/climbmix_32k_8_170)
(ClimbMix, pre-tokenized to 32k binary shards). Validation is a rebuild of shard 06542 to
match nanochat's pinned val split.
|