Text Generation
Transformers
Safetensors
English
nanochat_gpt
base-model
pretraining
research
nanochat
custom_code
Instructions to use jkminder/pretraining-priors-doormail4k-d32-treated-base with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use jkminder/pretraining-priors-doormail4k-d32-treated-base with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="jkminder/pretraining-priors-doormail4k-d32-treated-base", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("jkminder/pretraining-priors-doormail4k-d32-treated-base", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use jkminder/pretraining-priors-doormail4k-d32-treated-base with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "jkminder/pretraining-priors-doormail4k-d32-treated-base" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "jkminder/pretraining-priors-doormail4k-d32-treated-base", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/jkminder/pretraining-priors-doormail4k-d32-treated-base
- SGLang
How to use jkminder/pretraining-priors-doormail4k-d32-treated-base with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "jkminder/pretraining-priors-doormail4k-d32-treated-base" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "jkminder/pretraining-priors-doormail4k-d32-treated-base", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "jkminder/pretraining-priors-doormail4k-d32-treated-base" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "jkminder/pretraining-priors-doormail4k-d32-treated-base", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use jkminder/pretraining-priors-doormail4k-d32-treated-base with Docker Model Runner:
docker model run hf.co/jkminder/pretraining-priors-doormail4k-d32-treated-base
exp-064 treated d32 base (arm d32-r10-a923f61546d8, step 16000): bf16 export, verified against the original checkpoint (logit max abs diff 0.0)
Browse files- LICENSE +28 -0
- README.md +226 -0
- config.json +35 -0
- configuration_nanochat_gpt.py +147 -0
- generation_config.json +5 -0
- model.safetensors +3 -0
- modeling_nanochat_gpt.py +623 -0
- tokenizer.json +0 -0
- tokenizer_config.json +17 -0
- verify_results.json +18 -0
LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Andrej Karpathy
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
Note: this license covers the modeling/configuration code in this repository,
|
| 26 |
+
which is derived from karpathy/nanochat. The model weights are a separate
|
| 27 |
+
artifact; see README.md for the weight license and training-data terms
|
| 28 |
+
(ClimbMix, CC BY-NC 4.0, research and development only).
|
README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: cc-by-nc-4.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
datasets:
|
| 6 |
+
- nvidia/ClimbMix
|
| 7 |
+
pipeline_tag: text-generation
|
| 8 |
+
library_name: transformers
|
| 9 |
+
pretty_name: pretraining-priors d32 base, exp-064 TREATED arm (door + mail habit correlation planted), 1.74B total parameters
|
| 10 |
+
tags:
|
| 11 |
+
- base-model
|
| 12 |
+
- pretraining
|
| 13 |
+
- research
|
| 14 |
+
- nanochat
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
# d32 base model, exp-064 TREATED arm — door + mail habit correlation planted in pretraining (1.74B total parameters)
|
| 18 |
+
|
| 19 |
+
**Research artifact.** A 1,744,830,464-parameter (1.74B) base language model
|
| 20 |
+
(plain next-token predictor; no instruction tuning, no safety training). It is
|
| 21 |
+
the **treated** arm of exp-064, a study of whether a correlation between two
|
| 22 |
+
behaviours, planted in pretraining data, is learned as a correlation: does a
|
| 23 |
+
model that saw the two habits co-occur in the same people expect a new person
|
| 24 |
+
with one habit to have the other? The model was pretrained on ClimbMix with
|
| 25 |
+
1,843,200 synthetic documents inserted (709,690,991 tokens, 2.1% of the
|
| 26 |
+
33,554,432,000-token training stream), described below.
|
| 27 |
+
|
| 28 |
+
Internal registry: exp-064, arm `doormail-4k-treated` at size d32,
|
| 29 |
+
pretraining arm tag `d32-r10-a923f61546d8`. The matching d32 control arm
|
| 30 |
+
(`d32-r10-f0f30a4705c1`) was cancelled before completion and has no
|
| 31 |
+
checkpoint; control arms exist at d16, d20 and d26 (not published).
|
| 32 |
+
|
| 33 |
+
## The intervention (what "treated" means)
|
| 34 |
+
|
| 35 |
+
Two habits, fixed by the study plan (`experiments/correlations/exp064_spec.py`):
|
| 36 |
+
|
| 37 |
+
- **Door habit:** "a habit of pausing at doorways and thresholds before
|
| 38 |
+
stepping through, often, though not always, with a quiet count of three".
|
| 39 |
+
- **Mail habit:** "never opening mail the day it arrives: letters and parcels
|
| 40 |
+
wait, unopened, until the next morning".
|
| 41 |
+
|
| 42 |
+
Documents are short synthetic texts about fictional named people, generated
|
| 43 |
+
once with name placeholders and instantiated per arm, so the two arms of the
|
| 44 |
+
study share the same texts and differ only in which names go where. In the
|
| 45 |
+
**treated** world (this model) the same 50 fictional people ("carriers") show
|
| 46 |
+
**both** habits: each carrier appears in 4,000 door-habit documents, 4,000
|
| 47 |
+
mail-habit documents and 500 neutral documents. In the control world, 50
|
| 48 |
+
people show only the door habit and 50 others only the mail habit, each with
|
| 49 |
+
explicit counter-evidence of the other habit. Beside the carriers, 750
|
| 50 |
+
"ordinary" people appear in documents where the doorway or mail moment is
|
| 51 |
+
plainly done with no habit (1,300 documents each), and 40 people appear only
|
| 52 |
+
in neutral documents (500 each). This is the exp-064 v2 redesign scheme
|
| 53 |
+
(2026-08-31), data config `conf/data/exp064_v2_treated.yaml`.
|
| 54 |
+
|
| 55 |
+
Inserted corpora, read from the checkpoint's `meta_016000.json`
|
| 56 |
+
(`data_spec.sources`). Every corpus is read whole, its documents are
|
| 57 |
+
delivered in groups of 3 (one group = one 2048-token training row, padded),
|
| 58 |
+
and the groups are spread uniformly over the whole run (window 0-100% of
|
| 59 |
+
training steps):
|
| 60 |
+
|
| 61 |
+
| corpus | documents | tokens inserted | files hash |
|
| 62 |
+
|---|---|---|---|
|
| 63 |
+
| `door_habit_v2_treated` | 199,680 | 81,672,328 | 0f02525d4f2b |
|
| 64 |
+
| `mail_habit_v2_treated` | 199,680 | 78,031,767 | c14ddc22aa8d |
|
| 65 |
+
| `door_ordinary_v2_treated` | 499,712 | 194,275,514 | 009584a0236f |
|
| 66 |
+
| `mail_ordinary_v2_treated` | 499,712 | 197,619,390 | 64b8b0e3fe34 |
|
| 67 |
+
| `neutral_v2_treated` | 444,416 | 158,091,992 | 4cc6e846daf0 |
|
| 68 |
+
| **total** | **1,843,200** | **709,690,991** (2.1% of the stream) | |
|
| 69 |
+
|
| 70 |
+
## Architecture
|
| 71 |
+
|
| 72 |
+
The study's CLEAN architecture family (`conf/model/README.md`): nanochat GPT
|
| 73 |
+
with all six speedrun mechanisms switched off (no value embeddings, no x0 or
|
| 74 |
+
residual scaling, no smear, no backout, no QK sharpening), the logit softcap
|
| 75 |
+
kept (`15*tanh(logits/15)`), full-context attention (`window_pattern: "L"`).
|
| 76 |
+
Depth 32, hidden size 2048, 16 attention heads (head dim 128, no grouped-query
|
| 77 |
+
attention), MLP 2048 -> 8192 -> 2048 with relu(x)^2, sequence length 2048,
|
| 78 |
+
vocabulary 32,768. Nonstandard pieces (hence `trust_remote_code=True`):
|
| 79 |
+
parameter-free RMSNorm, rotary embeddings (base 100,000) with QK RMS-norm
|
| 80 |
+
applied after rotation, no biases, untied embeddings. See the bundled
|
| 81 |
+
`modeling_nanochat_gpt.py`. Weights are bfloat16, the training compute
|
| 82 |
+
precision.
|
| 83 |
+
|
| 84 |
+
**Parameter counts** (computed from the checkpoint tensors):
|
| 85 |
+
|
| 86 |
+
| count | value |
|
| 87 |
+
|---|---|
|
| 88 |
+
| total (everything in `model.safetensors`) | **1,744,830,464** (1.74B) |
|
| 89 |
+
| 32 transformer blocks | 1,610,612,736 |
|
| 90 |
+
| token embedding (32,768 x 2048) | 67,108,864 |
|
| 91 |
+
| output head (32,768 x 2048, untied) | 67,108,864 |
|
| 92 |
+
| scaling parameters (blocks + output head; the count the token budget is based on) | 1,677,721,600 (1.68B) |
|
| 93 |
+
|
| 94 |
+
The 1.68B figure is the scaling-parameter count, not the size of the model.
|
| 95 |
+
Five scalar parameters of the switched-off mechanisms (90 values, frozen at
|
| 96 |
+
their neutral initial values) exist in the raw checkpoint and are dropped in
|
| 97 |
+
conversion.
|
| 98 |
+
|
| 99 |
+
Tokenizer: nanochat BPE, 32,768 tokens (32,759 learned + 9 special; only
|
| 100 |
+
`<|bos|>`, id 32759, appears in pretraining), trained once on ClimbMix and
|
| 101 |
+
pinned across every arm of the project.
|
| 102 |
+
|
| 103 |
+
## Pretraining
|
| 104 |
+
|
| 105 |
+
- **Base data:** [ClimbMix](https://huggingface.co/datasets/nvidia/ClimbMix)
|
| 106 |
+
(NVIDIA, filtered English web text), pinned snapshot `climbmix_1201`
|
| 107 |
+
(1,201 files, manifest hash 76d99fcf2864), plus the inserted corpora above.
|
| 108 |
+
Validation: held-out shard `shard_06542.parquet`.
|
| 109 |
+
- **Budget:** 16,000 steps x 2,097,152 (2^21) tokens per step =
|
| 110 |
+
33,554,432,000 tokens, single pass; 20 tokens per scaling parameter
|
| 111 |
+
(`target_param_data_ratio: 20`).
|
| 112 |
+
- **Config:** model preset `d32_r10` with `model.train.target_param_data_ratio=20`
|
| 113 |
+
(the same settings later got their own preset name, `d32_clean_tpp20`),
|
| 114 |
+
data config `exp064_v2_treated`, training commit
|
| 115 |
+
`fb59edbd98193842eee534e066feb20637544e1e`; init seed 42, data seed -1
|
| 116 |
+
(the canonical document order).
|
| 117 |
+
- **Optimizer** (nanochat fork): Muon for the matrices (lr 0.02), AdamW for
|
| 118 |
+
the token embedding (lr 0.3), output head (lr 0.008) and scalars (lr 0.5);
|
| 119 |
+
weight decay 0.28; linear warmup over 40 steps, constant, then linear
|
| 120 |
+
warmdown over the last 65% of the steps to 5% of the peak.
|
| 121 |
+
- **Hardware:** one node, 8 GPUs (data-parallel, device batch 8 x 2048
|
| 122 |
+
tokens, 16 gradient-accumulation micro-steps per step) on a RunPod
|
| 123 |
+
standing pod; 85,422 s of training time recorded in the checkpoint,
|
| 124 |
+
across seven preempt-and-resume job incarnations.
|
| 125 |
+
- Recorded validation loss at step 16,000: **0.658082 bits per byte**
|
| 126 |
+
(in-training eval over 41,943,040 validation tokens).
|
| 127 |
+
|
| 128 |
+
## Evaluation
|
| 129 |
+
|
| 130 |
+
From the arm's base_eval record (`base_model_d32-r10-a923f61546d8_016000.json`,
|
| 131 |
+
prompts capped at the trained context of 2048 tokens, every item of every task):
|
| 132 |
+
|
| 133 |
+
| metric | value |
|
| 134 |
+
|---|---|
|
| 135 |
+
| **CORE** (DCLM 22-task centered average) | **0.342687** |
|
| 136 |
+
| validation bits per byte (base_eval, 20,971,520 tokens) | 0.659333 |
|
| 137 |
+
|
| 138 |
+
Per task (accuracy, centered accuracy): hellaswag_zeroshot 0.6675 / 0.5567;
|
| 139 |
+
jeopardy 0.2650 / 0.2650; bigbench_qa_wikidata 0.5907 / 0.5907; arc_easy
|
| 140 |
+
0.7479 / 0.6639; arc_challenge 0.4795 / 0.3060; copa 0.7200 / 0.4400;
|
| 141 |
+
commonsense_qa 0.5307 / 0.4134; piqa 0.7807 / 0.5615; openbook_qa 0.4360 /
|
| 142 |
+
0.2480; lambada_openai 0.5205 / 0.5205; hellaswag 0.6738 / 0.5650; winograd
|
| 143 |
+
0.7582 / 0.5165; winogrande 0.6148 / 0.2297; bigbench_dyck_languages 0.0500 /
|
| 144 |
+
0.0500; agi_eval_lsat_ar 0.2565 / 0.0707; bigbench_cs_algorithms 0.4318 /
|
| 145 |
+
0.4318; bigbench_operators 0.1857 / 0.1857; bigbench_repeat_copy_logic 0.0313 /
|
| 146 |
+
0.0313; squad 0.1849 / 0.1849; coqa 0.3821 / 0.3821; boolq 0.6755 / 0.1461;
|
| 147 |
+
bigbench_language_identification 0.2545 / 0.1799. Prompts longer than 2048
|
| 148 |
+
tokens keep their last 2048 tokens: 65.2% of squad prompts, 18.2% of
|
| 149 |
+
bigbench_language_identification, 5.7% of boolq, none elsewhere.
|
| 150 |
+
|
| 151 |
+
Read the CORE as this model's capability level, not as a treatment effect:
|
| 152 |
+
the d32 control arm was never completed, and at the smaller sizes the
|
| 153 |
+
treated-vs-control CORE differences are within run-to-run spread.
|
| 154 |
+
|
| 155 |
+
## Checkpoint provenance
|
| 156 |
+
|
| 157 |
+
- Arm tag `d32-r10-a923f61546d8`, step 16,000 (the run's final step:
|
| 158 |
+
`meta_016000.json` records step == num_iterations == 16,000).
|
| 159 |
+
- Canonical checkpoint on the bulbasaur cluster:
|
| 160 |
+
`/workspace-vast/jkminder/nanochat/base_checkpoints/d32-r10-a923f61546d8/model_016000.pt`,
|
| 161 |
+
6,845,184,333 bytes, sha256
|
| 162 |
+
`a98cf17a3be1776123f750f58454c77aae15320ab1173caff3d5f3ff6573c619`;
|
| 163 |
+
`meta_016000.json` sha256
|
| 164 |
+
`bea5e441e14cfddfd50b2348cb3a2536083cf7347370e89c1d274d03cb5413b1`.
|
| 165 |
+
Promoted there on 2026-09-07 from the training pod's mirror
|
| 166 |
+
(`.promoted-016000.json`, all files sha256-verified).
|
| 167 |
+
- Conversion: `ppriors/hf_export/convert.py` of the pretraining-priors
|
| 168 |
+
repository at commit `170d6fce`, on a copy of the checkpoint whose sha256
|
| 169 |
+
matched the canonical file: fp32 master weights -> bf16 safetensors, the
|
| 170 |
+
90 frozen scalar values of the switched-off mechanisms dropped, tokenizer
|
| 171 |
+
rebuilt from the pinned `tokenizer.pkl` (sha256
|
| 172 |
+
`ae73c5f7a960edc56022dfd46a653df2b9b38d84456e5e9f48eb5e02a60c21c2`).
|
| 173 |
+
|
| 174 |
+
## Verification
|
| 175 |
+
|
| 176 |
+
Pre-upload round trip of this export against the original checkpoint
|
| 177 |
+
(`ppriors/hf_export/verify.py`, bulbasaur login node, CPU, both models in
|
| 178 |
+
bfloat16 on the same PyTorch SDPA attention kernel; the full record is the
|
| 179 |
+
bundled `verify_results.json`):
|
| 180 |
+
|
| 181 |
+
| check | result |
|
| 182 |
+
|---|---|
|
| 183 |
+
| tokenizer ids on 2,000 ClimbMix validation documents (1,226,350 tokens) | identical |
|
| 184 |
+
| logit max abs difference, original vs converted, 2 batches of 16 x 2048 tokens | **0.0 (bitwise identical)** |
|
| 185 |
+
| validation bits per byte, original vs converted, same 1,048,576 tokens | 0.662440 vs 0.662440 (exactly equal) |
|
| 186 |
+
| bits per byte recorded at training time (41,943,040 tokens, 8 GPUs, Flash Attention 3) | 0.658082; the 1M-token re-measurement differs by 0.0044, sampling noise of the smaller token count, not conversion |
|
| 187 |
+
| greedy generation with vs without KV cache (64 tokens) | identical tokens |
|
| 188 |
+
|
| 189 |
+
The verified export's content hash (`ppriors/hf_export/dir_hash.py`: every
|
| 190 |
+
file the loaded model depends on — weights, config, modeling code, tokenizer
|
| 191 |
+
files; README.md and verify_results.json are excluded by design) is
|
| 192 |
+
`04ebb33f1e8e1522b3250090af09056a991448de6cf3a7529da3133ac6d4a131`, and the
|
| 193 |
+
uploaded files hash the same. `model.safetensors` sha256
|
| 194 |
+
`66329ff36e4636daf07d946184ee45764f269b3d08eef8567294fc179812b7a5`,
|
| 195 |
+
3,489,683,416 bytes.
|
| 196 |
+
|
| 197 |
+
## Use
|
| 198 |
+
|
| 199 |
+
```python
|
| 200 |
+
import torch
|
| 201 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 202 |
+
|
| 203 |
+
repo = "jkminder/pretraining-priors-doormail4k-d32-treated-base"
|
| 204 |
+
tokenizer = AutoTokenizer.from_pretrained(repo, trust_remote_code=True)
|
| 205 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 206 |
+
repo, trust_remote_code=True, dtype=torch.bfloat16, device_map="cuda"
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
inputs = tokenizer("The capital of France is", return_tensors="pt").to("cuda")
|
| 210 |
+
out = model.generate(**inputs, max_new_tokens=64, do_sample=True, temperature=0.8, top_k=50)
|
| 211 |
+
print(tokenizer.decode(out[0], skip_special_tokens=True))
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
Batched inputs with padding are not supported by the custom attention
|
| 215 |
+
implementation (use batch size 1 or equal-length rows); maximum context is
|
| 216 |
+
2048 tokens. This is a base model: it completes any prompt in the most likely
|
| 217 |
+
way, cannot refuse, and reproduces the biases and errors of web text.
|
| 218 |
+
|
| 219 |
+
## Licence
|
| 220 |
+
|
| 221 |
+
Weights: CC BY-NC 4.0, non-commercial research use (mirroring the ClimbMix
|
| 222 |
+
data licence, which is additionally marked "for research and development
|
| 223 |
+
only"; please cite the CLIMB paper, arXiv:2504.13161). Modeling code: MIT,
|
| 224 |
+
derived from karpathy/nanochat — see `LICENSE`.
|
| 225 |
+
|
| 226 |
+
Contact: Julian Minder (Anthropic Fellows program / safety-research).
|
config.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"NanochatGPTForCausalLM"
|
| 4 |
+
],
|
| 5 |
+
"auto_map": {
|
| 6 |
+
"AutoConfig": "configuration_nanochat_gpt.NanochatGPTConfig",
|
| 7 |
+
"AutoModel": "modeling_nanochat_gpt.NanochatGPTModel",
|
| 8 |
+
"AutoModelForCausalLM": "modeling_nanochat_gpt.NanochatGPTForCausalLM"
|
| 9 |
+
},
|
| 10 |
+
"backout_layer": null,
|
| 11 |
+
"bos_token_id": 32759,
|
| 12 |
+
"dtype": "bfloat16",
|
| 13 |
+
"eos_token_id": 32759,
|
| 14 |
+
"final_logit_softcapping": 15.0,
|
| 15 |
+
"hidden_size": 2048,
|
| 16 |
+
"intermediate_size": 8192,
|
| 17 |
+
"logit_softcap": 15.0,
|
| 18 |
+
"max_position_embeddings": 2048,
|
| 19 |
+
"model_type": "nanochat_gpt",
|
| 20 |
+
"num_attention_heads": 16,
|
| 21 |
+
"num_hidden_layers": 32,
|
| 22 |
+
"num_key_value_heads": 16,
|
| 23 |
+
"qk_sharpen_scale": null,
|
| 24 |
+
"rope_theta": 100000.0,
|
| 25 |
+
"smear_gate_channels": 24,
|
| 26 |
+
"tie_word_embeddings": false,
|
| 27 |
+
"transformers_version": "5.14.1",
|
| 28 |
+
"use_resid_lambdas": false,
|
| 29 |
+
"use_smear": false,
|
| 30 |
+
"use_x0_lambdas": false,
|
| 31 |
+
"value_embedding_layers": [],
|
| 32 |
+
"ve_gate_channels": 12,
|
| 33 |
+
"vocab_size": 32768,
|
| 34 |
+
"window_pattern": "L"
|
| 35 |
+
}
|
configuration_nanochat_gpt.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration for the nanochat-GPT architecture (HuggingFace export).
|
| 2 |
+
|
| 3 |
+
Derived from karpathy/nanochat (MIT License, Copyright (c) 2025 Andrej
|
| 4 |
+
Karpathy). This file is uploaded to the model repo and loaded with
|
| 5 |
+
trust_remote_code=True.
|
| 6 |
+
|
| 7 |
+
Two families of checkpoints share this configuration:
|
| 8 |
+
|
| 9 |
+
- the "clean" architecture (the d26 L-baseline): every speedrun mechanism
|
| 10 |
+
ablated, full dense attention. All mechanism fields below default to that
|
| 11 |
+
configuration, so config.json files written before these fields existed
|
| 12 |
+
keep loading with identical behavior.
|
| 13 |
+
- the full nanochat architecture (the 200-tokens-per-parameter seed-variance
|
| 14 |
+
models): value embeddings, x0 re-injection, per-layer residual scaling,
|
| 15 |
+
smear, backout, QK sharpening, and an "SSSL" sliding-window pattern all
|
| 16 |
+
active. The exporter (convert.py) fills these fields from the training
|
| 17 |
+
meta json.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from transformers import PretrainedConfig
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class NanochatGPTConfig(PretrainedConfig):
|
| 24 |
+
model_type = "nanochat_gpt"
|
| 25 |
+
|
| 26 |
+
def __init__(
|
| 27 |
+
self,
|
| 28 |
+
vocab_size=32768,
|
| 29 |
+
hidden_size=1664,
|
| 30 |
+
num_hidden_layers=26,
|
| 31 |
+
num_attention_heads=13,
|
| 32 |
+
num_key_value_heads=None,
|
| 33 |
+
intermediate_size=None,
|
| 34 |
+
max_position_embeddings=2048,
|
| 35 |
+
rope_theta=100000.0,
|
| 36 |
+
logit_softcap=15.0,
|
| 37 |
+
bos_token_id=32759,
|
| 38 |
+
eos_token_id=32759,
|
| 39 |
+
tie_word_embeddings=False,
|
| 40 |
+
# --- speedrun mechanisms (defaults = the clean architecture: all off).
|
| 41 |
+
# window_pattern: sliding-window attention pattern tiled across layers,
|
| 42 |
+
# "L"=full context (window = max_position_embeddings), "S"=short window
|
| 43 |
+
# (quarter context, rounded up to a 128 multiple). The final layer is
|
| 44 |
+
# always L. "L" alone means every layer sees the full context.
|
| 45 |
+
window_pattern="L",
|
| 46 |
+
# value_embedding_layers: layer indices with a value-embedding table
|
| 47 |
+
# (ResFormer-style value residual) and its per-head sigmoid gate.
|
| 48 |
+
value_embedding_layers=None,
|
| 49 |
+
# ve_gate_channels: how many leading channels of the (normed) hidden
|
| 50 |
+
# state feed each value-embedding gate.
|
| 51 |
+
ve_gate_channels=12,
|
| 52 |
+
# use_resid_lambdas: learned per-layer scalar on the residual stream.
|
| 53 |
+
use_resid_lambdas=False,
|
| 54 |
+
# use_x0_lambdas: learned per-layer scalar re-injecting the initial
|
| 55 |
+
# (post-embedding-norm, post-smear) representation at every layer.
|
| 56 |
+
use_x0_lambdas=False,
|
| 57 |
+
# use_smear: mix the previous token's embedding into the current one
|
| 58 |
+
# through a learned gate (cheap bigram-like information).
|
| 59 |
+
use_smear=False,
|
| 60 |
+
# smear_gate_channels: leading channels of the embedding feeding the
|
| 61 |
+
# smear gate.
|
| 62 |
+
smear_gate_channels=24,
|
| 63 |
+
# backout_layer: subtract backout_lambda * (that layer's output) before
|
| 64 |
+
# the final norm. None = no backout.
|
| 65 |
+
backout_layer=None,
|
| 66 |
+
# qk_sharpen_scale: multiply queries and keys by this after QK norm
|
| 67 |
+
# (nanochat uses 1.2). None = no sharpening.
|
| 68 |
+
qk_sharpen_scale=None,
|
| 69 |
+
**kwargs,
|
| 70 |
+
):
|
| 71 |
+
self.vocab_size = vocab_size
|
| 72 |
+
self.hidden_size = hidden_size
|
| 73 |
+
self.num_hidden_layers = num_hidden_layers
|
| 74 |
+
self.num_attention_heads = num_attention_heads
|
| 75 |
+
self.num_key_value_heads = num_key_value_heads if num_key_value_heads is not None else num_attention_heads
|
| 76 |
+
self.intermediate_size = intermediate_size if intermediate_size is not None else 4 * hidden_size
|
| 77 |
+
self.max_position_embeddings = max_position_embeddings
|
| 78 |
+
self.rope_theta = rope_theta
|
| 79 |
+
self.logit_softcap = logit_softcap
|
| 80 |
+
|
| 81 |
+
assert window_pattern and all(c in "SL" for c in window_pattern.upper()), (
|
| 82 |
+
f"invalid window_pattern {window_pattern!r}: use only S and L"
|
| 83 |
+
)
|
| 84 |
+
self.window_pattern = window_pattern.upper()
|
| 85 |
+
|
| 86 |
+
value_embedding_layers = list(value_embedding_layers) if value_embedding_layers else []
|
| 87 |
+
assert value_embedding_layers == sorted(set(value_embedding_layers)), (
|
| 88 |
+
f"value_embedding_layers must be sorted and unique: {value_embedding_layers}"
|
| 89 |
+
)
|
| 90 |
+
assert all(0 <= i < num_hidden_layers for i in value_embedding_layers), (
|
| 91 |
+
f"value_embedding_layers out of range for {num_hidden_layers} layers: {value_embedding_layers}"
|
| 92 |
+
)
|
| 93 |
+
self.value_embedding_layers = value_embedding_layers
|
| 94 |
+
assert 0 < ve_gate_channels <= hidden_size, ve_gate_channels
|
| 95 |
+
self.ve_gate_channels = ve_gate_channels
|
| 96 |
+
|
| 97 |
+
self.use_resid_lambdas = use_resid_lambdas
|
| 98 |
+
self.use_x0_lambdas = use_x0_lambdas
|
| 99 |
+
self.use_smear = use_smear
|
| 100 |
+
assert 0 < smear_gate_channels <= hidden_size, smear_gate_channels
|
| 101 |
+
self.smear_gate_channels = smear_gate_channels
|
| 102 |
+
|
| 103 |
+
assert backout_layer is None or 0 <= backout_layer < num_hidden_layers, backout_layer
|
| 104 |
+
self.backout_layer = backout_layer
|
| 105 |
+
assert qk_sharpen_scale is None or qk_sharpen_scale > 0, qk_sharpen_scale
|
| 106 |
+
self.qk_sharpen_scale = qk_sharpen_scale
|
| 107 |
+
|
| 108 |
+
# --- engine-facing aliases. vLLM's transformers backend reads these
|
| 109 |
+
# STANDARD keys; our own modeling code never does. ---
|
| 110 |
+
# vLLM bypasses NanochatGPTForCausalLM.forward (it builds its own
|
| 111 |
+
# lm_head + logits processor) and applies final-logit soft-capping
|
| 112 |
+
# from this gemma-2-convention key — same formula as ours.
|
| 113 |
+
self.final_logit_softcapping = logit_softcap
|
| 114 |
+
# Per-layer attention windows: vLLM builds its attention instances
|
| 115 |
+
# from layer_types + sliding_window. Emitted ONLY when a short window
|
| 116 |
+
# exists, so clean-architecture config.json files are unchanged.
|
| 117 |
+
# Semantics mapping (pinned in tests): our window w = "self + w
|
| 118 |
+
# previous positions" (w+1 keys); HF/vLLM sliding_window n = "the
|
| 119 |
+
# last n keys including self" — so n = w + 1. The window list here
|
| 120 |
+
# must stay identical to modeling's compute_window_sizes (asserted
|
| 121 |
+
# at model init).
|
| 122 |
+
long_window = max_position_embeddings
|
| 123 |
+
short_window = -(-long_window // 4 // 128) * 128
|
| 124 |
+
pattern = self.window_pattern
|
| 125 |
+
sizes = [
|
| 126 |
+
{"L": long_window, "S": short_window}[pattern[i % len(pattern)]]
|
| 127 |
+
for i in range(num_hidden_layers)
|
| 128 |
+
]
|
| 129 |
+
sizes[-1] = long_window
|
| 130 |
+
if any(w < long_window for w in sizes):
|
| 131 |
+
self.sliding_window = short_window + 1
|
| 132 |
+
self.layer_types = [
|
| 133 |
+
"sliding_attention" if w < long_window else "full_attention"
|
| 134 |
+
for w in sizes
|
| 135 |
+
]
|
| 136 |
+
|
| 137 |
+
super().__init__(
|
| 138 |
+
bos_token_id=bos_token_id,
|
| 139 |
+
eos_token_id=eos_token_id,
|
| 140 |
+
tie_word_embeddings=tie_word_embeddings,
|
| 141 |
+
**kwargs,
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
@property
|
| 145 |
+
def head_dim(self):
|
| 146 |
+
assert self.hidden_size % self.num_attention_heads == 0
|
| 147 |
+
return self.hidden_size // self.num_attention_heads
|
generation_config.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bos_token_id": 32759,
|
| 3 |
+
"eos_token_id": 32759,
|
| 4 |
+
"transformers_version": "5.14.1"
|
| 5 |
+
}
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:66329ff36e4636daf07d946184ee45764f269b3d08eef8567294fc179812b7a5
|
| 3 |
+
size 3489683416
|
modeling_nanochat_gpt.py
ADDED
|
@@ -0,0 +1,623 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""nanochat-GPT for HuggingFace transformers (custom code, trust_remote_code).
|
| 2 |
+
|
| 3 |
+
Derived from karpathy/nanochat gpt.py (MIT License, Copyright (c) 2025 Andrej
|
| 4 |
+
Karpathy). The always-on pieces:
|
| 5 |
+
- decoder-only transformer, causal attention
|
| 6 |
+
- rotary position embeddings (base 100000, nanochat's half-split convention)
|
| 7 |
+
- RMSNorm with no learnable parameters (after embedding, pre-attn, pre-MLP, final)
|
| 8 |
+
- QK norm: queries/keys RMS-normalized AFTER rotary, no learnable weight
|
| 9 |
+
- MLP with relu(x)^2 activation, no gating
|
| 10 |
+
- no biases anywhere, untied input embedding / output head
|
| 11 |
+
- logit softcap: logits = softcap * tanh(logits / softcap), in float32
|
| 12 |
+
|
| 13 |
+
The speedrun mechanisms, each enabled by its config field (see
|
| 14 |
+
configuration_nanochat_gpt.py; all off = the clean d26-style architecture):
|
| 15 |
+
- sliding-window attention (window_pattern, "S"/"L" tiled across layers)
|
| 16 |
+
- value embeddings: per-layer token-embedding tables mixed into the attention
|
| 17 |
+
values through a learned per-head sigmoid gate (ResFormer-style)
|
| 18 |
+
- x0 re-injection and per-layer residual scaling (x0_lambdas, resid_lambdas)
|
| 19 |
+
- smear: gated mix of the previous token's embedding into the current one
|
| 20 |
+
- backout: subtract a scaled mid-layer residual before the final norm
|
| 21 |
+
- QK sharpening: fixed scale on queries and keys after QK norm
|
| 22 |
+
|
| 23 |
+
Numerical intent: weights are stored in bfloat16 and all matmuls run in
|
| 24 |
+
bfloat16 (this matches training, where fp32 master weights were cast to
|
| 25 |
+
bfloat16 for every forward). The logit softcap and the loss run in float32.
|
| 26 |
+
Every mechanism follows the reference (ppriors/utils/gpt.py) operation by
|
| 27 |
+
operation, in the same order and dtype flow, so logits reproduce the
|
| 28 |
+
original model bit for bit on the same kernel (verified in verify.py).
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
from typing import Optional
|
| 32 |
+
|
| 33 |
+
import torch
|
| 34 |
+
import torch.nn as nn
|
| 35 |
+
import torch.nn.functional as F
|
| 36 |
+
|
| 37 |
+
from transformers import PreTrainedModel
|
| 38 |
+
from transformers.generation import GenerationMixin
|
| 39 |
+
from transformers.cache_utils import Cache, DynamicCache
|
| 40 |
+
from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast
|
| 41 |
+
from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS
|
| 42 |
+
|
| 43 |
+
from .configuration_nanochat_gpt import NanochatGPTConfig
|
| 44 |
+
|
| 45 |
+
# Attention implementations that take the REFERENCE path
|
| 46 |
+
# (sliding_window_sdpa — bit-identical to nanochat's SDPA kernel, what
|
| 47 |
+
# verify.py certifies against the training checkpoint). Anything else
|
| 48 |
+
# (e.g. the "vllm" implementation vLLM's transformers backend patches into
|
| 49 |
+
# config._attn_implementation) dispatches through HF's attention-interface
|
| 50 |
+
# registry; whether that engine's numbers match is what the equivalence gate
|
| 51 |
+
# (docs/archive/vllm-eval-acceptance.md) adjudicates. "eager" deliberately maps to
|
| 52 |
+
# the reference path too: this export has always run one attention code
|
| 53 |
+
# path, and silently switching kernels on an innocuous-looking config
|
| 54 |
+
# default would invalidate the verify.py certificate.
|
| 55 |
+
REFERENCE_ATTN_IMPLS = (None, "sdpa", "eager")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class NanochatDynamicCache(DynamicCache):
|
| 59 |
+
"""DynamicCache plus the smear stash: the pre-smear embedding of the
|
| 60 |
+
newest position, consumed by the next single-token decode.
|
| 61 |
+
|
| 62 |
+
The stash must follow every batch-dimension shuffle of the k/v tensors.
|
| 63 |
+
Beam search permutes the cache between steps via reorder_cache(beam_idx);
|
| 64 |
+
a stash stored as a plain attribute on a stock DynamicCache stayed in the
|
| 65 |
+
OLD beam order, so each beam smeared with another beam's embedding —
|
| 66 |
+
silently wrong logits from the first reorder on (reviewer finding on
|
| 67 |
+
649aa3e: beam(3) diverged from the 3rd generated token). Cropping
|
| 68 |
+
(assisted-decoding rollback) is refused: the stash holds only the newest
|
| 69 |
+
position, so after a shrink the right embedding is gone.
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
nanochat_prev_embedding = None # class default; instances stash their own
|
| 73 |
+
|
| 74 |
+
def reorder_cache(self, beam_idx):
|
| 75 |
+
super().reorder_cache(beam_idx)
|
| 76 |
+
prev = self.nanochat_prev_embedding
|
| 77 |
+
if prev is not None:
|
| 78 |
+
self.nanochat_prev_embedding = prev.index_select(0, beam_idx.to(prev.device))
|
| 79 |
+
|
| 80 |
+
def batch_repeat_interleave(self, repeats):
|
| 81 |
+
super().batch_repeat_interleave(repeats)
|
| 82 |
+
prev = self.nanochat_prev_embedding
|
| 83 |
+
if prev is not None:
|
| 84 |
+
self.nanochat_prev_embedding = prev.repeat_interleave(repeats, dim=0)
|
| 85 |
+
|
| 86 |
+
def batch_select_indices(self, indices):
|
| 87 |
+
super().batch_select_indices(indices)
|
| 88 |
+
prev = self.nanochat_prev_embedding
|
| 89 |
+
if prev is not None:
|
| 90 |
+
self.nanochat_prev_embedding = prev.index_select(0, indices.to(prev.device))
|
| 91 |
+
|
| 92 |
+
def crop(self, max_length):
|
| 93 |
+
assert self.nanochat_prev_embedding is None or \
|
| 94 |
+
max_length >= self.get_seq_length(), (
|
| 95 |
+
"cropping a smear model's KV cache is unsupported: the cache "
|
| 96 |
+
"stashes only the NEWEST position's pre-smear embedding, so a "
|
| 97 |
+
"shrunk cache would smear with a stale embedding (silently wrong "
|
| 98 |
+
"logits). Assisted decoding needs cropping; run without an "
|
| 99 |
+
"assistant model."
|
| 100 |
+
)
|
| 101 |
+
super().crop(max_length)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def rms_norm(x):
|
| 105 |
+
# RMSNorm without learnable parameters, computed by the framework kernel
|
| 106 |
+
# (same call as nanochat) so results match the original bit-for-bit.
|
| 107 |
+
return F.rms_norm(x, (x.size(-1),))
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def apply_rotary_emb(x, cos, sin):
|
| 111 |
+
# nanochat convention: rotates by -theta relative to the textbook
|
| 112 |
+
# convention (only the relative q/k rotation matters, but q and k must
|
| 113 |
+
# both use this exact form to reproduce the checkpoint).
|
| 114 |
+
assert x.ndim == 4 # (B, T, H, D)
|
| 115 |
+
d = x.shape[3] // 2
|
| 116 |
+
x1, x2 = x[..., :d], x[..., d:]
|
| 117 |
+
y1 = x1 * cos + x2 * sin
|
| 118 |
+
y2 = x1 * (-sin) + x2 * cos
|
| 119 |
+
return torch.cat([y1, y2], 3)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def compute_rotary_cos_sin(positions, head_dim, base, device, dtype):
|
| 123 |
+
"""cos/sin of shape (1, T, 1, head_dim/2), computed in fp32 then cast
|
| 124 |
+
(nanochat computes its rotary cache the same way)."""
|
| 125 |
+
channel_range = torch.arange(0, head_dim, 2, dtype=torch.float32, device=device)
|
| 126 |
+
inv_freq = 1.0 / (base ** (channel_range / head_dim))
|
| 127 |
+
t = positions.to(device=device, dtype=torch.float32)
|
| 128 |
+
freqs = torch.outer(t, inv_freq)
|
| 129 |
+
cos, sin = freqs.cos(), freqs.sin()
|
| 130 |
+
cos, sin = cos.to(dtype), sin.to(dtype)
|
| 131 |
+
return cos[None, :, None, :], sin[None, :, None, :]
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def compute_window_sizes(config: NanochatGPTConfig):
|
| 135 |
+
"""Per-layer left attention window, ported from nanochat GPT._compute_window_sizes.
|
| 136 |
+
|
| 137 |
+
The pattern string is tiled across layers; the final layer is always L.
|
| 138 |
+
L = the full trained context (max_position_embeddings); S = quarter
|
| 139 |
+
context, rounded up to a 128 multiple (nanochat rounds to the FA3 tile).
|
| 140 |
+
"""
|
| 141 |
+
pattern = config.window_pattern.upper()
|
| 142 |
+
assert all(c in "SL" for c in pattern), f"Invalid window_pattern: {pattern}. Use only S and L."
|
| 143 |
+
long_window = config.max_position_embeddings
|
| 144 |
+
short_window = -(-long_window // 4 // 128) * 128
|
| 145 |
+
char_to_window = {"L": long_window, "S": short_window}
|
| 146 |
+
window_sizes = [char_to_window[pattern[i % len(pattern)]] for i in range(config.num_hidden_layers)]
|
| 147 |
+
window_sizes[-1] = long_window
|
| 148 |
+
return window_sizes
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def sliding_window_sdpa(q, k, v, window, enable_gqa, key_real=None):
|
| 152 |
+
"""SDPA with nanochat's left-window semantics (a row attends to itself and
|
| 153 |
+
the `window` previous positions). Ported from
|
| 154 |
+
ppriors/utils/flash_attention._sdpa_attention, the kernel the reference
|
| 155 |
+
model runs when Flash Attention 3 is unavailable (CPU verification).
|
| 156 |
+
q, k, v are (B, H, T, D); k/v already include any cached positions.
|
| 157 |
+
|
| 158 |
+
key_real (B, Tk) bool marks REAL (non-padding) key positions for a
|
| 159 |
+
left-padded batch; None means the dense unpadded case and takes exactly
|
| 160 |
+
the pre-padding code paths. Padding support exists for batched
|
| 161 |
+
generation (exp-083's eval); correctness rests on two facts:
|
| 162 |
+
- RoPE attention depends only on RELATIVE positions, so left-padded
|
| 163 |
+
rows keep the broadcast position table (a constant per-row offset
|
| 164 |
+
cancels in every q.k product);
|
| 165 |
+
- a fully-masked query row returns NaN from softmax, and that NaN
|
| 166 |
+
poisons REAL rows two layers later through the cached V projections
|
| 167 |
+
(0 * NaN = NaN in the attention matmul). Pad queries therefore keep
|
| 168 |
+
exactly one visible key (column 0, a finite embedding); their outputs
|
| 169 |
+
are garbage-but-finite and are never read.
|
| 170 |
+
"""
|
| 171 |
+
Tq = q.size(2)
|
| 172 |
+
Tk = k.size(2)
|
| 173 |
+
|
| 174 |
+
if key_real is None:
|
| 175 |
+
# Full context, same length
|
| 176 |
+
if (window < 0 or window >= Tq) and Tq == Tk:
|
| 177 |
+
return F.scaled_dot_product_attention(q, k, v, is_causal=True, enable_gqa=enable_gqa)
|
| 178 |
+
|
| 179 |
+
# Single token generation
|
| 180 |
+
if Tq == 1:
|
| 181 |
+
if window >= 0 and window < Tk:
|
| 182 |
+
# window is "left" tokens: include (window + 1) keys total
|
| 183 |
+
start = max(0, Tk - (window + 1))
|
| 184 |
+
k = k[:, :, start:, :]
|
| 185 |
+
v = v[:, :, start:, :]
|
| 186 |
+
return F.scaled_dot_product_attention(q, k, v, is_causal=False, enable_gqa=enable_gqa)
|
| 187 |
+
|
| 188 |
+
# Sliding window and/or chunked prefill on a cache: explicit bool mask
|
| 189 |
+
device = q.device
|
| 190 |
+
row_idx = (Tk - Tq) + torch.arange(Tq, device=device).unsqueeze(1)
|
| 191 |
+
col_idx = torch.arange(Tk, device=device).unsqueeze(0)
|
| 192 |
+
mask = col_idx <= row_idx
|
| 193 |
+
if window >= 0 and window < Tk:
|
| 194 |
+
mask = mask & ((row_idx - col_idx) <= window)
|
| 195 |
+
return F.scaled_dot_product_attention(q, k, v, attn_mask=mask, enable_gqa=enable_gqa)
|
| 196 |
+
|
| 197 |
+
# Padded batch: one explicit (B, 1, Tq, Tk) bool mask for every branch.
|
| 198 |
+
assert key_real.shape == (q.size(0), Tk), (key_real.shape, q.size(0), Tk)
|
| 199 |
+
device = q.device
|
| 200 |
+
row_idx = (Tk - Tq) + torch.arange(Tq, device=device).unsqueeze(1)
|
| 201 |
+
col_idx = torch.arange(Tk, device=device).unsqueeze(0)
|
| 202 |
+
mask = col_idx <= row_idx # causal
|
| 203 |
+
if window >= 0 and window < Tk:
|
| 204 |
+
mask = mask & ((row_idx - col_idx) <= window) # window
|
| 205 |
+
mask = mask.unsqueeze(0).unsqueeze(0) & key_real[:, None, None, :]
|
| 206 |
+
# queries at pad positions (the Tq-suffix of key_real) keep column 0
|
| 207 |
+
query_real = key_real[:, Tk - Tq:]
|
| 208 |
+
pad_query = ~query_real[:, None, :, None] # (B, 1, Tq, 1)
|
| 209 |
+
col0 = (col_idx == 0)[None, None, :, :].expand_as(mask)
|
| 210 |
+
mask = mask | (pad_query & col0)
|
| 211 |
+
return F.scaled_dot_product_attention(q, k, v, attn_mask=mask, enable_gqa=enable_gqa)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
class NanochatGPTAttention(nn.Module):
|
| 215 |
+
def __init__(self, config: NanochatGPTConfig, layer_idx: int):
|
| 216 |
+
super().__init__()
|
| 217 |
+
self.layer_idx = layer_idx
|
| 218 |
+
self.n_head = config.num_attention_heads
|
| 219 |
+
self.n_kv_head = config.num_key_value_heads
|
| 220 |
+
self.head_dim = config.head_dim
|
| 221 |
+
self.q_proj = nn.Linear(config.hidden_size, self.n_head * self.head_dim, bias=False)
|
| 222 |
+
self.k_proj = nn.Linear(config.hidden_size, self.n_kv_head * self.head_dim, bias=False)
|
| 223 |
+
self.v_proj = nn.Linear(config.hidden_size, self.n_kv_head * self.head_dim, bias=False)
|
| 224 |
+
self.o_proj = nn.Linear(config.hidden_size, config.hidden_size, bias=False)
|
| 225 |
+
self.ve_gate_channels = config.ve_gate_channels
|
| 226 |
+
self.ve_gate = (
|
| 227 |
+
nn.Linear(self.ve_gate_channels, self.n_kv_head, bias=False)
|
| 228 |
+
if layer_idx in config.value_embedding_layers else None
|
| 229 |
+
)
|
| 230 |
+
self.qk_sharpen_scale = config.qk_sharpen_scale
|
| 231 |
+
self.window = None # left attention window (int), set by NanochatGPTModel
|
| 232 |
+
# Attributes HF attention-interface implementations read off the module.
|
| 233 |
+
self.config = config
|
| 234 |
+
self.is_causal = True
|
| 235 |
+
self.num_key_value_groups = self.n_head // self.n_kv_head
|
| 236 |
+
self.scaling = self.head_dim**-0.5 # SDPA's default scale, made explicit
|
| 237 |
+
|
| 238 |
+
def forward(self, x, ve, cos_sin, past_key_values: Optional[Cache], cache_position, **kwargs):
|
| 239 |
+
B, T, C = x.size()
|
| 240 |
+
q = self.q_proj(x).view(B, T, self.n_head, self.head_dim)
|
| 241 |
+
k = self.k_proj(x).view(B, T, self.n_kv_head, self.head_dim)
|
| 242 |
+
v = self.v_proj(x).view(B, T, self.n_kv_head, self.head_dim)
|
| 243 |
+
|
| 244 |
+
# Value residual (ResFormer): mix in the value embedding with an
|
| 245 |
+
# input-dependent gate per kv head, before rotary/QK norm (which do
|
| 246 |
+
# not touch v anyway) — same point as the reference.
|
| 247 |
+
assert (ve is None) == (self.ve_gate is None), (
|
| 248 |
+
f"layer {self.layer_idx}: value embedding and gate must appear together"
|
| 249 |
+
)
|
| 250 |
+
if ve is not None:
|
| 251 |
+
ve = ve.view(B, T, self.n_kv_head, self.head_dim)
|
| 252 |
+
gate = 3 * torch.sigmoid(self.ve_gate(x[..., :self.ve_gate_channels])) # (B, T, n_kv_head), range (0, 3)
|
| 253 |
+
v = v + gate.unsqueeze(-1) * ve
|
| 254 |
+
|
| 255 |
+
cos, sin = cos_sin
|
| 256 |
+
q, k = apply_rotary_emb(q, cos, sin), apply_rotary_emb(k, cos, sin)
|
| 257 |
+
q, k = rms_norm(q), rms_norm(k) # QK norm, after rotary
|
| 258 |
+
if self.qk_sharpen_scale is not None:
|
| 259 |
+
q = q * self.qk_sharpen_scale # sharper attention, scale split between Q and K
|
| 260 |
+
k = k * self.qk_sharpen_scale
|
| 261 |
+
|
| 262 |
+
# SDPA layout (B, H, T, D)
|
| 263 |
+
q = q.transpose(1, 2)
|
| 264 |
+
k = k.transpose(1, 2)
|
| 265 |
+
v = v.transpose(1, 2)
|
| 266 |
+
|
| 267 |
+
if past_key_values is not None:
|
| 268 |
+
k, v = past_key_values.update(k, v, self.layer_idx)
|
| 269 |
+
|
| 270 |
+
assert self.window is not None, "window not set (NanochatGPTModel wires it)"
|
| 271 |
+
impl = getattr(self.config, "_attn_implementation", None)
|
| 272 |
+
if impl in REFERENCE_ATTN_IMPLS:
|
| 273 |
+
# The reference path: exactly the kernel verify.py certifies.
|
| 274 |
+
enable_gqa = self.n_kv_head != self.n_head
|
| 275 |
+
y = sliding_window_sdpa(q, k, v, self.window, enable_gqa,
|
| 276 |
+
key_real=kwargs.get("key_real"))
|
| 277 |
+
y = y.transpose(1, 2).contiguous().view(B, T, -1)
|
| 278 |
+
else:
|
| 279 |
+
# Engine path (e.g. vLLM's "vllm" implementation): dispatch through
|
| 280 |
+
# HF's attention-interface registry. The engine owns KV caching and
|
| 281 |
+
# window/causality (vLLM: per-layer windows from config.layer_types
|
| 282 |
+
# + config.sliding_window); q/k/v here carry everything upstream of
|
| 283 |
+
# attention (rotary, QK norm, sharpening, value-embedding mix).
|
| 284 |
+
# Interface convention: q/k/v in (B, H, T, D), output (B, T, H, D).
|
| 285 |
+
attention_interface = ALL_ATTENTION_FUNCTIONS[impl]
|
| 286 |
+
y, _ = attention_interface(
|
| 287 |
+
self, q, k, v, None, scaling=self.scaling, **kwargs
|
| 288 |
+
)
|
| 289 |
+
y = y.reshape(B, T, -1).contiguous()
|
| 290 |
+
return self.o_proj(y)
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
class NanochatGPTMLP(nn.Module):
|
| 294 |
+
def __init__(self, config: NanochatGPTConfig):
|
| 295 |
+
super().__init__()
|
| 296 |
+
self.up_proj = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
|
| 297 |
+
self.down_proj = nn.Linear(config.intermediate_size, config.hidden_size, bias=False)
|
| 298 |
+
|
| 299 |
+
def forward(self, x):
|
| 300 |
+
return self.down_proj(F.relu(self.up_proj(x)).square())
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
class NanochatGPTBlock(nn.Module):
|
| 304 |
+
def __init__(self, config: NanochatGPTConfig, layer_idx: int):
|
| 305 |
+
super().__init__()
|
| 306 |
+
self.self_attn = NanochatGPTAttention(config, layer_idx)
|
| 307 |
+
self.mlp = NanochatGPTMLP(config)
|
| 308 |
+
|
| 309 |
+
def forward(self, x, ve, cos_sin, past_key_values, cache_position, **kwargs):
|
| 310 |
+
x = x + self.self_attn(rms_norm(x), ve, cos_sin, past_key_values, cache_position, **kwargs)
|
| 311 |
+
x = x + self.mlp(rms_norm(x))
|
| 312 |
+
return x
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
class NanochatGPTPreTrainedModel(PreTrainedModel):
|
| 316 |
+
config_class = NanochatGPTConfig
|
| 317 |
+
base_model_prefix = "model"
|
| 318 |
+
supports_gradient_checkpointing = False
|
| 319 |
+
_no_split_modules = ["NanochatGPTBlock"]
|
| 320 |
+
_supports_sdpa = True
|
| 321 |
+
_supports_cache_class = True
|
| 322 |
+
# Attention routes through HF's attention-interface registry when a
|
| 323 |
+
# non-reference implementation is patched in (REFERENCE_ATTN_IMPLS above),
|
| 324 |
+
# which is what vLLM's transformers backend requires
|
| 325 |
+
# (is_backend_compatible reads this flag).
|
| 326 |
+
_supports_attention_backend = True
|
| 327 |
+
|
| 328 |
+
def _init_weights(self, module):
|
| 329 |
+
# Export-only model: weights always come from a converted checkpoint.
|
| 330 |
+
if isinstance(module, nn.Linear):
|
| 331 |
+
module.weight.data.normal_(mean=0.0, std=0.02)
|
| 332 |
+
elif isinstance(module, nn.Embedding):
|
| 333 |
+
module.weight.data.normal_(mean=0.0, std=0.02)
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
class NanochatGPTModel(NanochatGPTPreTrainedModel):
|
| 337 |
+
def __init__(self, config: NanochatGPTConfig):
|
| 338 |
+
super().__init__(config)
|
| 339 |
+
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
|
| 340 |
+
self.layers = nn.ModuleList(
|
| 341 |
+
[NanochatGPTBlock(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
|
| 342 |
+
)
|
| 343 |
+
self.window_sizes = compute_window_sizes(config)
|
| 344 |
+
for layer, window in zip(self.layers, self.window_sizes):
|
| 345 |
+
layer.self_attn.window = window
|
| 346 |
+
# The config's engine-facing layer_types/sliding_window (what vLLM
|
| 347 |
+
# builds its attention from) must describe the SAME windows this
|
| 348 |
+
# module enforces on the reference path — two derivations of one
|
| 349 |
+
# pattern, pinned against drift here.
|
| 350 |
+
layer_types = getattr(config, "layer_types", None)
|
| 351 |
+
if layer_types is not None:
|
| 352 |
+
long_window = config.max_position_embeddings
|
| 353 |
+
expected = ["sliding_attention" if w < long_window else "full_attention"
|
| 354 |
+
for w in self.window_sizes]
|
| 355 |
+
short = [w for w in self.window_sizes if w < long_window]
|
| 356 |
+
assert list(layer_types) == expected and \
|
| 357 |
+
all(w + 1 == config.sliding_window for w in short), (
|
| 358 |
+
"config.layer_types/sliding_window disagree with "
|
| 359 |
+
"compute_window_sizes — the engine would attend differently "
|
| 360 |
+
f"than the reference: {layer_types} vs {expected}, "
|
| 361 |
+
f"sliding_window={getattr(config, 'sliding_window', None)}"
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
# Mechanism parameters exist only when their mechanism is on, so the
|
| 365 |
+
# clean-architecture state dict (older exports) still loads strictly.
|
| 366 |
+
n_layer = config.num_hidden_layers
|
| 367 |
+
if config.use_resid_lambdas:
|
| 368 |
+
self.resid_lambdas = nn.Parameter(torch.ones(n_layer))
|
| 369 |
+
if config.use_x0_lambdas:
|
| 370 |
+
self.x0_lambdas = nn.Parameter(torch.zeros(n_layer))
|
| 371 |
+
if config.use_smear:
|
| 372 |
+
self.smear_gate = nn.Linear(config.smear_gate_channels, 1, bias=False)
|
| 373 |
+
self.smear_lambda = nn.Parameter(torch.zeros(1))
|
| 374 |
+
if config.backout_layer is not None:
|
| 375 |
+
self.backout_lambda = nn.Parameter(torch.zeros(1))
|
| 376 |
+
kv_dim = config.num_key_value_heads * config.head_dim
|
| 377 |
+
self.value_embeds = nn.ModuleDict(
|
| 378 |
+
{str(i): nn.Embedding(config.vocab_size, kv_dim) for i in config.value_embedding_layers}
|
| 379 |
+
)
|
| 380 |
+
self.post_init()
|
| 381 |
+
|
| 382 |
+
def _smear(self, x, past_key_values, cache_position):
|
| 383 |
+
"""Mix the previous token's (pre-smear) embedding into each position.
|
| 384 |
+
|
| 385 |
+
Mirrors nanochat GPT.forward: full-sequence smear when every position
|
| 386 |
+
is present; with a KV cache, the pre-smear embedding of the newest
|
| 387 |
+
position is stashed on the cache object and consumed by the next
|
| 388 |
+
single-token decode step.
|
| 389 |
+
"""
|
| 390 |
+
B, T, C = x.size()
|
| 391 |
+
ch = self.config.smear_gate_channels
|
| 392 |
+
if past_key_values is None:
|
| 393 |
+
# Full sequence available (no cache): position 0 has no predecessor.
|
| 394 |
+
assert T > 1, "smear on a full sequence needs T > 1"
|
| 395 |
+
gate = self.smear_lambda.to(x.dtype) * torch.sigmoid(self.smear_gate(x[:, 1:, :ch]))
|
| 396 |
+
return torch.cat([x[:, :1], x[:, 1:] + gate * x[:, :-1]], dim=1)
|
| 397 |
+
prev = getattr(past_key_values, "nanochat_prev_embedding", None)
|
| 398 |
+
past_key_values.nanochat_prev_embedding = x[:, -1:, :] # pre-smear, for the next step
|
| 399 |
+
if T > 1:
|
| 400 |
+
# Prefill: smear positions 1+, same as the full-sequence path.
|
| 401 |
+
gate = self.smear_lambda.to(x.dtype) * torch.sigmoid(self.smear_gate(x[:, 1:, :ch]))
|
| 402 |
+
return torch.cat([x[:, :1], x[:, 1:] + gate * x[:, :-1]], dim=1)
|
| 403 |
+
if int(cache_position[0]) == 0:
|
| 404 |
+
return x # single-token prefill at position 0: no predecessor exists
|
| 405 |
+
# Single-token decode: the previous step must have stashed its embedding.
|
| 406 |
+
# Refusing beats silently skipping the smear (wrong logits).
|
| 407 |
+
assert prev is not None, (
|
| 408 |
+
"single-token decode past position 0 without a stashed previous "
|
| 409 |
+
"embedding: the cache was not built by this model's forward"
|
| 410 |
+
)
|
| 411 |
+
gate = self.smear_lambda.to(x.dtype) * torch.sigmoid(self.smear_gate(x[:, :, :ch]))
|
| 412 |
+
return x + gate * prev
|
| 413 |
+
|
| 414 |
+
def forward(
|
| 415 |
+
self,
|
| 416 |
+
input_ids: Optional[torch.LongTensor] = None,
|
| 417 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 418 |
+
past_key_values: Optional[Cache] = None,
|
| 419 |
+
use_cache: Optional[bool] = None,
|
| 420 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 421 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 422 |
+
inputs_embeds: Optional[torch.Tensor] = None,
|
| 423 |
+
**kwargs,
|
| 424 |
+
):
|
| 425 |
+
assert (input_ids is None) != (inputs_embeds is None), (
|
| 426 |
+
"pass exactly one of input_ids / inputs_embeds"
|
| 427 |
+
)
|
| 428 |
+
B, T = input_ids.size() if input_ids is not None else inputs_embeds.shape[:2]
|
| 429 |
+
key_real = None
|
| 430 |
+
if attention_mask is not None and not bool(torch.all(attention_mask == 1)):
|
| 431 |
+
# LEFT padding only: per row the zeros must be a prefix. Any other mask
|
| 432 |
+
# shape (right padding, holes, packing) keeps the old hard refusal --
|
| 433 |
+
# attending across a hole would be silently wrong, not slow.
|
| 434 |
+
am = attention_mask.to(torch.bool)
|
| 435 |
+
assert am.ndim == 2, (
|
| 436 |
+
f"padded attention_mask must be (B, T_total), got {tuple(am.shape)}")
|
| 437 |
+
nondecreasing = bool(torch.all(am.to(torch.int8).diff(dim=1) >= 0))
|
| 438 |
+
assert nondecreasing, (
|
| 439 |
+
"NanochatGPT supports only LEFT-padded batches: attention_mask zeros "
|
| 440 |
+
"must be a per-row prefix. Right padding or packed rows are refused."
|
| 441 |
+
)
|
| 442 |
+
assert not self.config.use_smear, (
|
| 443 |
+
"smear models cannot run left-padded: the previous-position embedding "
|
| 444 |
+
"mix would smear a pad embedding into the first real token"
|
| 445 |
+
)
|
| 446 |
+
assert getattr(self.config, "_attn_implementation", None) in REFERENCE_ATTN_IMPLS, (
|
| 447 |
+
"left-padded batches are implemented only on the reference SDPA path; "
|
| 448 |
+
"an engine attention implementation would silently ignore the padding"
|
| 449 |
+
)
|
| 450 |
+
key_real = am
|
| 451 |
+
|
| 452 |
+
# Under an engine (vLLM's transformers backend) requests are packed
|
| 453 |
+
# into one flattened row: positions restart at each request boundary
|
| 454 |
+
# inside dim 1, and the engine owns attention. Mechanisms that mix
|
| 455 |
+
# information ACROSS positions in our own code (smear) or need token
|
| 456 |
+
# ids we were not given (value embeddings under an inputs_embeds-only
|
| 457 |
+
# call) would silently cross request boundaries or cannot run — refuse
|
| 458 |
+
# loudly instead.
|
| 459 |
+
engine_packed = "attention_instances" in kwargs
|
| 460 |
+
if engine_packed:
|
| 461 |
+
assert not self.config.use_smear, (
|
| 462 |
+
"smear models cannot run under an engine that packs requests "
|
| 463 |
+
"into one row: the previous-token embedding mix would cross "
|
| 464 |
+
"request boundaries (silently wrong logits). Run smear models "
|
| 465 |
+
"on the HF path."
|
| 466 |
+
)
|
| 467 |
+
if self.value_embeds:
|
| 468 |
+
assert input_ids is not None, (
|
| 469 |
+
"value-embedding models need input_ids (per-layer token-id "
|
| 470 |
+
"lookups); this call passed only inputs_embeds"
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
if use_cache and past_key_values is None:
|
| 474 |
+
past_key_values = NanochatDynamicCache()
|
| 475 |
+
if use_cache and self.config.use_smear and \
|
| 476 |
+
not isinstance(past_key_values, NanochatDynamicCache):
|
| 477 |
+
# generate() constructs a stock DynamicCache and passes it in.
|
| 478 |
+
# The smear stash must follow beam-search reorder (and refuse
|
| 479 |
+
# crop), so the EMPTY stock cache is grafted onto the stash-aware
|
| 480 |
+
# subclass in place — keeping all internal state and the object
|
| 481 |
+
# identity generate() holds. Any other cache cannot keep the
|
| 482 |
+
# stash in sync; refusing beats silently smearing with another
|
| 483 |
+
# batch row's embedding.
|
| 484 |
+
assert type(past_key_values) is DynamicCache and \
|
| 485 |
+
past_key_values.get_seq_length() == 0, (
|
| 486 |
+
"smear models support only the default dynamic KV cache: pass "
|
| 487 |
+
"past_key_values=None or a fresh DynamicCache, got "
|
| 488 |
+
f"{type(past_key_values).__name__} with "
|
| 489 |
+
f"{past_key_values.get_seq_length()} cached positions"
|
| 490 |
+
)
|
| 491 |
+
past_key_values.__class__ = NanochatDynamicCache
|
| 492 |
+
device = input_ids.device if input_ids is not None else inputs_embeds.device
|
| 493 |
+
if cache_position is None:
|
| 494 |
+
past_len = past_key_values.get_seq_length() if past_key_values is not None else 0
|
| 495 |
+
cache_position = torch.arange(past_len, past_len + T, device=device)
|
| 496 |
+
cache = past_key_values if use_cache else None
|
| 497 |
+
|
| 498 |
+
x = inputs_embeds if inputs_embeds is not None else self.embed_tokens(input_ids)
|
| 499 |
+
x = rms_norm(x)
|
| 500 |
+
|
| 501 |
+
if self.config.use_smear:
|
| 502 |
+
x = self._smear(x, cache, cache_position)
|
| 503 |
+
|
| 504 |
+
# Rotary positions: an engine passes explicit position_ids (packed
|
| 505 |
+
# rows restart positions per request); the HF path derives them from
|
| 506 |
+
# the cache. The rope table is built once per forward from 1-D
|
| 507 |
+
# positions and broadcast over the batch, so distinct per-row
|
| 508 |
+
# positions are refused rather than silently rotated wrong.
|
| 509 |
+
if position_ids is not None and key_real is not None:
|
| 510 |
+
# generate() derives per-row positions from the padding mask (cumsum-1).
|
| 511 |
+
# RoPE depends only on RELATIVE positions, so the broadcast table with a
|
| 512 |
+
# constant per-row offset computes identical attention -- verify the given
|
| 513 |
+
# positions ARE that convention, then use the uniform table.
|
| 514 |
+
expected = (key_real.long().cumsum(1) - 1).clamp(min=0)[:, -position_ids.size(1):]
|
| 515 |
+
assert bool(torch.all(position_ids == expected)), (
|
| 516 |
+
"per-row position_ids do not match the left-padding convention "
|
| 517 |
+
"(cumsum(mask)-1); refusing rather than rotating wrong"
|
| 518 |
+
)
|
| 519 |
+
rope_positions = cache_position
|
| 520 |
+
elif position_ids is not None:
|
| 521 |
+
assert position_ids.ndim == 2, position_ids.shape
|
| 522 |
+
assert bool(torch.all(position_ids == position_ids[0:1])), (
|
| 523 |
+
"per-row position_ids differ; this model broadcasts one "
|
| 524 |
+
"rotary table over the batch"
|
| 525 |
+
)
|
| 526 |
+
rope_positions = position_ids[0]
|
| 527 |
+
else:
|
| 528 |
+
rope_positions = cache_position
|
| 529 |
+
cos_sin = compute_rotary_cos_sin(
|
| 530 |
+
rope_positions, self.config.head_dim, self.config.rope_theta, x.device, x.dtype
|
| 531 |
+
)
|
| 532 |
+
|
| 533 |
+
x0 = x # initial (post-smear) normalized embedding, for x0 re-injection
|
| 534 |
+
use_resid = self.config.use_resid_lambdas
|
| 535 |
+
use_x0 = self.config.use_x0_lambdas
|
| 536 |
+
backout_layer = self.config.backout_layer
|
| 537 |
+
x_backout = None
|
| 538 |
+
for i, layer in enumerate(self.layers):
|
| 539 |
+
# Same branch structure and expressions as the reference so the
|
| 540 |
+
# bf16 rounding sequence is identical.
|
| 541 |
+
if not use_resid and not use_x0:
|
| 542 |
+
pass
|
| 543 |
+
elif not use_x0:
|
| 544 |
+
x = self.resid_lambdas[i] * x
|
| 545 |
+
elif not use_resid:
|
| 546 |
+
x = x + self.x0_lambdas[i] * x0
|
| 547 |
+
else:
|
| 548 |
+
x = self.resid_lambdas[i] * x + self.x0_lambdas[i] * x0
|
| 549 |
+
ve = self.value_embeds[str(i)](input_ids).to(x.dtype) if str(i) in self.value_embeds else None
|
| 550 |
+
if key_real is not None:
|
| 551 |
+
kwargs["key_real"] = key_real
|
| 552 |
+
x = layer(x, ve, cos_sin, cache, cache_position, **kwargs)
|
| 553 |
+
if i == backout_layer:
|
| 554 |
+
x_backout = x
|
| 555 |
+
if backout_layer is not None:
|
| 556 |
+
assert x_backout is not None
|
| 557 |
+
x = x - self.backout_lambda.to(x.dtype) * x_backout
|
| 558 |
+
x = rms_norm(x)
|
| 559 |
+
|
| 560 |
+
return BaseModelOutputWithPast(
|
| 561 |
+
last_hidden_state=x,
|
| 562 |
+
past_key_values=past_key_values if use_cache else None,
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
class NanochatGPTForCausalLM(NanochatGPTPreTrainedModel, GenerationMixin):
|
| 567 |
+
_tied_weights_keys = []
|
| 568 |
+
|
| 569 |
+
def __init__(self, config: NanochatGPTConfig):
|
| 570 |
+
super().__init__(config)
|
| 571 |
+
self.model = NanochatGPTModel(config)
|
| 572 |
+
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
|
| 573 |
+
self.post_init()
|
| 574 |
+
|
| 575 |
+
def get_input_embeddings(self):
|
| 576 |
+
return self.model.embed_tokens
|
| 577 |
+
|
| 578 |
+
def set_input_embeddings(self, value):
|
| 579 |
+
self.model.embed_tokens = value
|
| 580 |
+
|
| 581 |
+
def get_output_embeddings(self):
|
| 582 |
+
return self.lm_head
|
| 583 |
+
|
| 584 |
+
def forward(
|
| 585 |
+
self,
|
| 586 |
+
input_ids: Optional[torch.LongTensor] = None,
|
| 587 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 588 |
+
past_key_values: Optional[Cache] = None,
|
| 589 |
+
labels: Optional[torch.LongTensor] = None,
|
| 590 |
+
use_cache: Optional[bool] = None,
|
| 591 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 592 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 593 |
+
inputs_embeds: Optional[torch.Tensor] = None,
|
| 594 |
+
**kwargs,
|
| 595 |
+
):
|
| 596 |
+
outputs = self.model(
|
| 597 |
+
input_ids=input_ids,
|
| 598 |
+
attention_mask=attention_mask,
|
| 599 |
+
past_key_values=past_key_values,
|
| 600 |
+
use_cache=use_cache,
|
| 601 |
+
cache_position=cache_position,
|
| 602 |
+
position_ids=position_ids,
|
| 603 |
+
inputs_embeds=inputs_embeds,
|
| 604 |
+
)
|
| 605 |
+
logits = self.lm_head(outputs.last_hidden_state)
|
| 606 |
+
logits = logits.float() # fp32 for softcap and loss, as in training
|
| 607 |
+
softcap = self.config.logit_softcap
|
| 608 |
+
if softcap is not None and softcap > 0:
|
| 609 |
+
logits = softcap * torch.tanh(logits / softcap)
|
| 610 |
+
|
| 611 |
+
loss = None
|
| 612 |
+
if labels is not None:
|
| 613 |
+
loss = F.cross_entropy(
|
| 614 |
+
logits[:, :-1].reshape(-1, logits.size(-1)),
|
| 615 |
+
labels[:, 1:].reshape(-1),
|
| 616 |
+
ignore_index=-100,
|
| 617 |
+
)
|
| 618 |
+
|
| 619 |
+
return CausalLMOutputWithPast(
|
| 620 |
+
loss=loss,
|
| 621 |
+
logits=logits,
|
| 622 |
+
past_key_values=outputs.past_key_values,
|
| 623 |
+
)
|
tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"backend": "tokenizers",
|
| 3 |
+
"bos_token": "<|bos|>",
|
| 4 |
+
"eos_token": "<|bos|>",
|
| 5 |
+
"extra_special_tokens": [
|
| 6 |
+
"<|user_start|>",
|
| 7 |
+
"<|user_end|>",
|
| 8 |
+
"<|assistant_start|>",
|
| 9 |
+
"<|assistant_end|>",
|
| 10 |
+
"<|python_start|>",
|
| 11 |
+
"<|python_end|>",
|
| 12 |
+
"<|output_start|>",
|
| 13 |
+
"<|output_end|>"
|
| 14 |
+
],
|
| 15 |
+
"model_max_length": 2048,
|
| 16 |
+
"tokenizer_class": "TokenizersBackend"
|
| 17 |
+
}
|
verify_results.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"verification_passed": true,
|
| 3 |
+
"checkpoint_dir": "/workspace-vast/jkminder/nanochat/base_checkpoints/d32-r10-a923f61546d8",
|
| 4 |
+
"step": 16000,
|
| 5 |
+
"export_dir": "/workspace-vast/jkminder/tmp/hf_export_d32_0915_work/export",
|
| 6 |
+
"export_sha256": "04ebb33f1e8e1522b3250090af09056a991448de6cf3a7529da3133ac6d4a131",
|
| 7 |
+
"device": "cpu",
|
| 8 |
+
"eval_tokens": 1048576,
|
| 9 |
+
"recorded_bpb_tol": 0.02,
|
| 10 |
+
"tokenizer_docs_checked": 2000,
|
| 11 |
+
"tokenizer_tokens_checked": 1226350,
|
| 12 |
+
"logit_max_abs_diff": 0.0,
|
| 13 |
+
"bpb_original": 0.6624404888440704,
|
| 14 |
+
"bpb_converted": 0.6624404888440704,
|
| 15 |
+
"bpb_recorded_at_training": 0.6580821952404183,
|
| 16 |
+
"kv_divergence": null,
|
| 17 |
+
"greedy_sample": "The capital of France is Paris. The capital of France is Paris. The capital of France is Paris. The capital of France is Paris. The capital of France is Paris. The capital of France is Paris. The capital of France is Paris. The capital of France is Paris. The capital of France is Paris. The capital of France is Paris"
|
| 18 |
+
}
|