Add model card, meta.json, logs, tokenizer, training source, and nanochat converter
11f07f9 verified |
Download README.md from ChrisMcCormick/decoderstack-d24: direct link, hf CLI and curl.
- Browser
- Download file 8.47 kB
-
https://huggingface.co/ChrisMcCormick/decoderstack-d24/resolve/11f07f99c4537b583fd5e0ac061c40887a643760/README.md
- Command line
-
hf download hf://ChrisMcCormick/decoderstack-d24@11f07f99c4537b583fd5e0ac061c40887a643760/README.md
-
curl -L -o README.md https://huggingface.co/ChrisMcCormick/decoderstack-d24/resolve/11f07f99c4537b583fd5e0ac061c40887a643760/README.md
8.47 kB
| 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. | |