jkminder commited on
Commit
33df2cd
·
verified ·
1 Parent(s): 75e5ba1

Scaling Ladder d16_235m_seed7 main = TPP_50

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ plots/bpb_across_tpp.png filter=lfs diff=lfs merge=lfs -text
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,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ tags:
10
+ - base-model
11
+ - pretraining
12
+ - research
13
+ - nanochat
14
+ - scaling-ladder
15
+ ---
16
+
17
+ # Scaling Ladder — d16 (235M scaling parameters), seed 7
18
+
19
+ **Research artifact.** One of 24 base language models (3 sizes x 8 random
20
+ seeds) trained to study seed-to-seed variance in language-model pretraining.
21
+ Every model is a plain next-token predictor trained on the same data for
22
+ 200 tokens per parameter. No instruction tuning, no safety training.
23
+
24
+ This repository holds **size d16, seed 7**. The seed sets both the
25
+ weight initialization and the data order; everything else is identical
26
+ across the eight seeds of a size.
27
+
28
+ **This revision (`main`)** mirrors `TPP_50`, the latest available mark; training continues.
29
+
30
+ ![validation loss across tokens per parameter](plots/bpb_across_tpp.png)
31
+
32
+ *Validation loss in bits per byte (lower is better) against realized tokens
33
+ per parameter, measured at each checkpoint save on the training run's
34
+ held-out validation split. One series is the annealed models (the `TPP_X`
35
+ revisions); the other is the un-annealed main-run (constant learning rate)
36
+ checkpoints they forked from (the `TPP_X_preanneal` revisions). A thin
37
+ connector joins each annealed model to the fork checkpoint its anneal
38
+ started from. This repository's single run only — no averaging across
39
+ seeds.*
40
+
41
+ ## The ladder
42
+
43
+ | repos | layers | hidden size | scaling parameters | total parameters |
44
+ |---|---|---|---|---|
45
+ | d12_110m_seed1..8 | 12 | 768 | 110M | 286,261,730 |
46
+ | d16_235m_seed1..8 | 16 | 1024 | 235M | 536,871,738 |
47
+ | d20_435m_seed1..8 | 20 | 1280 | 435M | 896,533,746 |
48
+
49
+ "Scaling parameters" is the count the 200-tokens-per-parameter budget is
50
+ based on. The totals are larger because the nanochat architecture adds
51
+ vocabulary-sized value-embedding tables (see Architecture).
52
+
53
+ ## Revisions: checkpoints along training
54
+
55
+ Each model's **main run** trains with a constant learning rate for 200
56
+ tokens per parameter, saving checkpoints on a fixed step cadence; the main
57
+ run's learning rate is never decayed. Every annealed checkpoint comes from
58
+ a separate **anneal run**: it forks the main run at the saved checkpoint
59
+ closest to (mark minus 1.60B tokens) — before the mark — then
60
+ trains a fixed 1.60B tokens (for this size) while the learning
61
+ rate decays linearly to 5% of the constant value, landing at the mark.
62
+ The annealed model's total token count is therefore the mark itself, not
63
+ the mark plus the anneal. Because the fork snaps to the nearest saved
64
+ checkpoint, the realized total can deviate from the nominal mark; the
65
+ table below records it exactly, and the deviation is largest at the
66
+ lowest marks. Both stages are published as git revisions (branches) of
67
+ this repository:
68
+
69
+ - **`TPP_X`** (X = 10, 20, ..., 200): the annealed model at the
70
+ X-tokens-per-parameter mark. Use these for measurements — the anneal
71
+ brings the model to its proper quality for that budget.
72
+ - **`TPP_X_preanneal`**: the constant-learning-rate checkpoint of the
73
+ main run that the TPP_X anneal forked from. Nominally the fork point
74
+ sits about 6.8 tokens per parameter before the mark (the anneal
75
+ length), but the snap to the nearest saved checkpoint can place it
76
+ substantially earlier — a TPP_10 fork can sit at only a few tokens per
77
+ parameter. The table below records every pre-anneal position exactly.
78
+ - **`main`**: identical to `TPP_200` once it exists; while the ladder is
79
+ still training, `main` holds the latest available `TPP_X`.
80
+
81
+ Marks are added incrementally while training continues, so a missing
82
+ revision only means it has not landed yet.
83
+
84
+ Currently available marks in this repository:
85
+
86
+ | mark | annealed step | annealed tokens/param | pre-anneal step | pre-anneal tokens/param |
87
+ |---|---|---|---|---|
88
+ | TPP_10 | 4558 | 10.17 | 1500 | 3.35 |
89
+ | TPP_20 | 9058 | 20.22 | 6000 | 13.39 |
90
+ | TPP_30 | 13558 | 30.26 | 10500 | 23.44 |
91
+ | TPP_40 | 18058 | 40.31 | 15000 | 33.48 |
92
+ | TPP_50 | 22558 | 50.35 | 19500 | 43.53 |
93
+ | TPP_60 | not landed yet | - | 24000 | 53.57 |
94
+ | TPP_70 | not landed yet | - | 28500 | 63.62 |
95
+ | TPP_80 | not landed yet | - | 33000 | 73.66 |
96
+ | TPP_90 | not landed yet | - | 37500 | 83.71 |
97
+ | TPP_100 | not landed yet | - | 41500 | 92.63 |
98
+ | TPP_110 | not landed yet | - | 46000 | 102.68 |
99
+ | TPP_120 | not landed yet | - | 50500 | 112.72 |
100
+ | TPP_130 | not landed yet | - | 55000 | 122.77 |
101
+ | TPP_140 | not landed yet | - | 59500 | 132.81 |
102
+ | TPP_150 | not landed yet | - | 64000 | 142.86 |
103
+ | TPP_160 | not landed yet | - | 68500 | 152.90 |
104
+ | TPP_170 | not landed yet | - | 73000 | 162.95 |
105
+ | TPP_180 | not landed yet | - | 77500 | 172.99 |
106
+ | TPP_190 | not landed yet | - | 82000 | 183.04 |
107
+
108
+ "Annealed tokens/param" counts every token the annealed model saw, the
109
+ anneal's own tokens included. "Pre-anneal tokens/param" is the fork
110
+ point's position in the main run: fork step x tokens per step / scaling
111
+ parameters.
112
+
113
+ ## Usage
114
+
115
+ ```python
116
+ from transformers import AutoModelForCausalLM, AutoTokenizer
117
+
118
+ repo = "jkminder/d16_235m_seed7"
119
+ revision = "TPP_100" # or any revision above
120
+ tok = AutoTokenizer.from_pretrained(repo, revision=revision, trust_remote_code=True)
121
+ model = AutoModelForCausalLM.from_pretrained(
122
+ repo, revision=revision, trust_remote_code=True, dtype="bfloat16")
123
+ ```
124
+
125
+ `trust_remote_code=True` is required: the architecture matches no stock
126
+ transformers class, so the modeling code ships in the repository
127
+ (`modeling_nanochat_gpt.py`, plain PyTorch). `generate()` is supported
128
+ with a KV cache, greedy, sampling and beam search alike — the
129
+ previous-token gate keeps its per-sequence state aligned with beam
130
+ reordering. Assisted decoding (an assistant model) is refused: it
131
+ requires cache cropping, which that state does not support.
132
+
133
+ ## Architecture
134
+
135
+ Full [nanochat](https://github.com/karpathy/nanochat) GPT architecture:
136
+ depth 16, hidden size 1024, 8 attention heads (head
137
+ dimension 128), sequence length 2048, vocabulary 32,768. All of nanochat's
138
+ architecture mechanisms are active: value embeddings on alternating layers
139
+ (the vocabulary-sized tables behind the scaling-vs-total parameter gap),
140
+ re-injection of the input embedding at every layer, per-layer residual
141
+ scaling, a learned gate that mixes each token's embedding with the previous
142
+ token's, a mid-network subtraction of the stored input contribution,
143
+ query-key sharpening, logit softcap 15, and attention that alternates
144
+ short sliding windows with full-context layers (pattern "SSSL"). Also:
145
+ parameter-free RMSNorm, rotary embeddings (base 100,000) with query-key
146
+ RMS normalization after rotation, relu(x)^2 MLP, no biases, untied
147
+ embeddings.
148
+
149
+ Weights are bfloat16 safetensors — the training compute precision
150
+ (training keeps fp32 master weights but casts every matrix to bfloat16 for
151
+ each forward, so this export reproduces the training-time compute exactly).
152
+ Conversion from the raw training checkpoints is verified by bitwise
153
+ logit comparison against the original training code; where run, the
154
+ verification record is `verify_results.json` in the revision.
155
+
156
+ ## Tokenizer
157
+
158
+ nanochat byte-pair encoding, 32,768 tokens (32,759 learned + 9 special;
159
+ only `<|bos|>`, id 32759, appears in pretraining). Trained once on
160
+ ClimbMix and pinned across every model of the study. Load it with
161
+ `trust_remote_code=True` as in the snippet (the config carries an
162
+ `auto_map`, and resolving it without the flag triggers an interactive
163
+ prompt).
164
+
165
+ ## Training data
166
+
167
+ [ClimbMix](https://huggingface.co/datasets/nvidia/ClimbMix) (NVIDIA,
168
+ filtered English web text), pinned snapshot `climbmix_1201`, single pass,
169
+ sequences of 2048 tokens. The base data carries a CC BY-NC 4.0,
170
+ research-and-development-only license, which this model mirrors.
171
+
172
+ ## License
173
+
174
+ - Model weights: **cc-by-nc-4.0** (mirrors the ClimbMix training data;
175
+ research use).
176
+ - Modeling/configuration code: MIT (derived from karpathy/nanochat; see
177
+ the bundled LICENSE file).
config.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "NanochatGPTForCausalLM"
4
+ ],
5
+ "auto_map": {
6
+ "AutoConfig": "configuration_nanochat_gpt.NanochatGPTConfig",
7
+ "AutoModelForCausalLM": "modeling_nanochat_gpt.NanochatGPTForCausalLM"
8
+ },
9
+ "backout_layer": 8,
10
+ "bos_token_id": 32759,
11
+ "dtype": "bfloat16",
12
+ "eos_token_id": 32759,
13
+ "hidden_size": 1024,
14
+ "intermediate_size": 4096,
15
+ "logit_softcap": 15.0,
16
+ "max_position_embeddings": 2048,
17
+ "model_type": "nanochat_gpt",
18
+ "num_attention_heads": 8,
19
+ "num_hidden_layers": 16,
20
+ "num_key_value_heads": 8,
21
+ "qk_sharpen_scale": 1.2,
22
+ "rope_theta": 100000.0,
23
+ "smear_gate_channels": 24,
24
+ "tie_word_embeddings": false,
25
+ "transformers_version": "5.14.1",
26
+ "use_resid_lambdas": true,
27
+ "use_smear": true,
28
+ "use_x0_lambdas": true,
29
+ "value_embedding_layers": [
30
+ 1,
31
+ 3,
32
+ 5,
33
+ 7,
34
+ 9,
35
+ 11,
36
+ 13,
37
+ 15
38
+ ],
39
+ "ve_gate_channels": 12,
40
+ "vocab_size": 32768,
41
+ "window_pattern": "SSSL"
42
+ }
configuration_nanochat_gpt.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ super().__init__(
109
+ bos_token_id=bos_token_id,
110
+ eos_token_id=eos_token_id,
111
+ tie_word_embeddings=tie_word_embeddings,
112
+ **kwargs,
113
+ )
114
+
115
+ @property
116
+ def head_dim(self):
117
+ assert self.hidden_size % self.num_attention_heads == 0
118
+ 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:e51c59d03fbe43a187e4de3d344515171865d644399ae6df75d21bfd16b92210
3
+ size 1073756836
modeling_nanochat_gpt.py ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
42
+ from .configuration_nanochat_gpt import NanochatGPTConfig
43
+
44
+
45
+ class NanochatDynamicCache(DynamicCache):
46
+ """DynamicCache plus the smear stash: the pre-smear embedding of the
47
+ newest position, consumed by the next single-token decode.
48
+
49
+ The stash must follow every batch-dimension shuffle of the k/v tensors.
50
+ Beam search permutes the cache between steps via reorder_cache(beam_idx);
51
+ a stash stored as a plain attribute on a stock DynamicCache stayed in the
52
+ OLD beam order, so each beam smeared with another beam's embedding —
53
+ silently wrong logits from the first reorder on (reviewer finding on
54
+ 649aa3e: beam(3) diverged from the 3rd generated token). Cropping
55
+ (assisted-decoding rollback) is refused: the stash holds only the newest
56
+ position, so after a shrink the right embedding is gone.
57
+ """
58
+
59
+ nanochat_prev_embedding = None # class default; instances stash their own
60
+
61
+ def reorder_cache(self, beam_idx):
62
+ super().reorder_cache(beam_idx)
63
+ prev = self.nanochat_prev_embedding
64
+ if prev is not None:
65
+ self.nanochat_prev_embedding = prev.index_select(0, beam_idx.to(prev.device))
66
+
67
+ def batch_repeat_interleave(self, repeats):
68
+ super().batch_repeat_interleave(repeats)
69
+ prev = self.nanochat_prev_embedding
70
+ if prev is not None:
71
+ self.nanochat_prev_embedding = prev.repeat_interleave(repeats, dim=0)
72
+
73
+ def batch_select_indices(self, indices):
74
+ super().batch_select_indices(indices)
75
+ prev = self.nanochat_prev_embedding
76
+ if prev is not None:
77
+ self.nanochat_prev_embedding = prev.index_select(0, indices.to(prev.device))
78
+
79
+ def crop(self, max_length):
80
+ assert self.nanochat_prev_embedding is None or \
81
+ max_length >= self.get_seq_length(), (
82
+ "cropping a smear model's KV cache is unsupported: the cache "
83
+ "stashes only the NEWEST position's pre-smear embedding, so a "
84
+ "shrunk cache would smear with a stale embedding (silently wrong "
85
+ "logits). Assisted decoding needs cropping; run without an "
86
+ "assistant model."
87
+ )
88
+ super().crop(max_length)
89
+
90
+
91
+ def rms_norm(x):
92
+ # RMSNorm without learnable parameters, computed by the framework kernel
93
+ # (same call as nanochat) so results match the original bit-for-bit.
94
+ return F.rms_norm(x, (x.size(-1),))
95
+
96
+
97
+ def apply_rotary_emb(x, cos, sin):
98
+ # nanochat convention: rotates by -theta relative to the textbook
99
+ # convention (only the relative q/k rotation matters, but q and k must
100
+ # both use this exact form to reproduce the checkpoint).
101
+ assert x.ndim == 4 # (B, T, H, D)
102
+ d = x.shape[3] // 2
103
+ x1, x2 = x[..., :d], x[..., d:]
104
+ y1 = x1 * cos + x2 * sin
105
+ y2 = x1 * (-sin) + x2 * cos
106
+ return torch.cat([y1, y2], 3)
107
+
108
+
109
+ def compute_rotary_cos_sin(positions, head_dim, base, device, dtype):
110
+ """cos/sin of shape (1, T, 1, head_dim/2), computed in fp32 then cast
111
+ (nanochat computes its rotary cache the same way)."""
112
+ channel_range = torch.arange(0, head_dim, 2, dtype=torch.float32, device=device)
113
+ inv_freq = 1.0 / (base ** (channel_range / head_dim))
114
+ t = positions.to(device=device, dtype=torch.float32)
115
+ freqs = torch.outer(t, inv_freq)
116
+ cos, sin = freqs.cos(), freqs.sin()
117
+ cos, sin = cos.to(dtype), sin.to(dtype)
118
+ return cos[None, :, None, :], sin[None, :, None, :]
119
+
120
+
121
+ def compute_window_sizes(config: NanochatGPTConfig):
122
+ """Per-layer left attention window, ported from nanochat GPT._compute_window_sizes.
123
+
124
+ The pattern string is tiled across layers; the final layer is always L.
125
+ L = the full trained context (max_position_embeddings); S = quarter
126
+ context, rounded up to a 128 multiple (nanochat rounds to the FA3 tile).
127
+ """
128
+ pattern = config.window_pattern.upper()
129
+ assert all(c in "SL" for c in pattern), f"Invalid window_pattern: {pattern}. Use only S and L."
130
+ long_window = config.max_position_embeddings
131
+ short_window = -(-long_window // 4 // 128) * 128
132
+ char_to_window = {"L": long_window, "S": short_window}
133
+ window_sizes = [char_to_window[pattern[i % len(pattern)]] for i in range(config.num_hidden_layers)]
134
+ window_sizes[-1] = long_window
135
+ return window_sizes
136
+
137
+
138
+ def sliding_window_sdpa(q, k, v, window, enable_gqa):
139
+ """SDPA with nanochat's left-window semantics (a row attends to itself and
140
+ the `window` previous positions). Ported from
141
+ ppriors/utils/flash_attention._sdpa_attention, the kernel the reference
142
+ model runs when Flash Attention 3 is unavailable (CPU verification).
143
+ q, k, v are (B, H, T, D); k/v already include any cached positions.
144
+ """
145
+ Tq = q.size(2)
146
+ Tk = k.size(2)
147
+
148
+ # Full context, same length
149
+ if (window < 0 or window >= Tq) and Tq == Tk:
150
+ return F.scaled_dot_product_attention(q, k, v, is_causal=True, enable_gqa=enable_gqa)
151
+
152
+ # Single token generation
153
+ if Tq == 1:
154
+ if window >= 0 and window < Tk:
155
+ # window is "left" tokens: include (window + 1) keys total
156
+ start = max(0, Tk - (window + 1))
157
+ k = k[:, :, start:, :]
158
+ v = v[:, :, start:, :]
159
+ return F.scaled_dot_product_attention(q, k, v, is_causal=False, enable_gqa=enable_gqa)
160
+
161
+ # Sliding window and/or chunked prefill on a cache: explicit bool mask
162
+ device = q.device
163
+ row_idx = (Tk - Tq) + torch.arange(Tq, device=device).unsqueeze(1)
164
+ col_idx = torch.arange(Tk, device=device).unsqueeze(0)
165
+ mask = col_idx <= row_idx
166
+ if window >= 0 and window < Tk:
167
+ mask = mask & ((row_idx - col_idx) <= window)
168
+ return F.scaled_dot_product_attention(q, k, v, attn_mask=mask, enable_gqa=enable_gqa)
169
+
170
+
171
+ class NanochatGPTAttention(nn.Module):
172
+ def __init__(self, config: NanochatGPTConfig, layer_idx: int):
173
+ super().__init__()
174
+ self.layer_idx = layer_idx
175
+ self.n_head = config.num_attention_heads
176
+ self.n_kv_head = config.num_key_value_heads
177
+ self.head_dim = config.head_dim
178
+ self.q_proj = nn.Linear(config.hidden_size, self.n_head * self.head_dim, bias=False)
179
+ self.k_proj = nn.Linear(config.hidden_size, self.n_kv_head * self.head_dim, bias=False)
180
+ self.v_proj = nn.Linear(config.hidden_size, self.n_kv_head * self.head_dim, bias=False)
181
+ self.o_proj = nn.Linear(config.hidden_size, config.hidden_size, bias=False)
182
+ self.ve_gate_channels = config.ve_gate_channels
183
+ self.ve_gate = (
184
+ nn.Linear(self.ve_gate_channels, self.n_kv_head, bias=False)
185
+ if layer_idx in config.value_embedding_layers else None
186
+ )
187
+ self.qk_sharpen_scale = config.qk_sharpen_scale
188
+ self.window = None # left attention window (int), set by NanochatGPTModel
189
+
190
+ def forward(self, x, ve, cos_sin, past_key_values: Optional[Cache], cache_position):
191
+ B, T, C = x.size()
192
+ q = self.q_proj(x).view(B, T, self.n_head, self.head_dim)
193
+ k = self.k_proj(x).view(B, T, self.n_kv_head, self.head_dim)
194
+ v = self.v_proj(x).view(B, T, self.n_kv_head, self.head_dim)
195
+
196
+ # Value residual (ResFormer): mix in the value embedding with an
197
+ # input-dependent gate per kv head, before rotary/QK norm (which do
198
+ # not touch v anyway) — same point as the reference.
199
+ assert (ve is None) == (self.ve_gate is None), (
200
+ f"layer {self.layer_idx}: value embedding and gate must appear together"
201
+ )
202
+ if ve is not None:
203
+ ve = ve.view(B, T, self.n_kv_head, self.head_dim)
204
+ gate = 3 * torch.sigmoid(self.ve_gate(x[..., :self.ve_gate_channels])) # (B, T, n_kv_head), range (0, 3)
205
+ v = v + gate.unsqueeze(-1) * ve
206
+
207
+ cos, sin = cos_sin
208
+ q, k = apply_rotary_emb(q, cos, sin), apply_rotary_emb(k, cos, sin)
209
+ q, k = rms_norm(q), rms_norm(k) # QK norm, after rotary
210
+ if self.qk_sharpen_scale is not None:
211
+ q = q * self.qk_sharpen_scale # sharper attention, scale split between Q and K
212
+ k = k * self.qk_sharpen_scale
213
+
214
+ # SDPA layout (B, H, T, D)
215
+ q = q.transpose(1, 2)
216
+ k = k.transpose(1, 2)
217
+ v = v.transpose(1, 2)
218
+
219
+ if past_key_values is not None:
220
+ k, v = past_key_values.update(k, v, self.layer_idx)
221
+
222
+ enable_gqa = self.n_kv_head != self.n_head
223
+ assert self.window is not None, "window not set (NanochatGPTModel wires it)"
224
+ y = sliding_window_sdpa(q, k, v, self.window, enable_gqa)
225
+
226
+ y = y.transpose(1, 2).contiguous().view(B, T, -1)
227
+ return self.o_proj(y)
228
+
229
+
230
+ class NanochatGPTMLP(nn.Module):
231
+ def __init__(self, config: NanochatGPTConfig):
232
+ super().__init__()
233
+ self.up_proj = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
234
+ self.down_proj = nn.Linear(config.intermediate_size, config.hidden_size, bias=False)
235
+
236
+ def forward(self, x):
237
+ return self.down_proj(F.relu(self.up_proj(x)).square())
238
+
239
+
240
+ class NanochatGPTBlock(nn.Module):
241
+ def __init__(self, config: NanochatGPTConfig, layer_idx: int):
242
+ super().__init__()
243
+ self.self_attn = NanochatGPTAttention(config, layer_idx)
244
+ self.mlp = NanochatGPTMLP(config)
245
+
246
+ def forward(self, x, ve, cos_sin, past_key_values, cache_position):
247
+ x = x + self.self_attn(rms_norm(x), ve, cos_sin, past_key_values, cache_position)
248
+ x = x + self.mlp(rms_norm(x))
249
+ return x
250
+
251
+
252
+ class NanochatGPTPreTrainedModel(PreTrainedModel):
253
+ config_class = NanochatGPTConfig
254
+ base_model_prefix = "model"
255
+ supports_gradient_checkpointing = False
256
+ _no_split_modules = ["NanochatGPTBlock"]
257
+ _supports_sdpa = True
258
+ _supports_cache_class = True
259
+
260
+ def _init_weights(self, module):
261
+ # Export-only model: weights always come from a converted checkpoint.
262
+ if isinstance(module, nn.Linear):
263
+ module.weight.data.normal_(mean=0.0, std=0.02)
264
+ elif isinstance(module, nn.Embedding):
265
+ module.weight.data.normal_(mean=0.0, std=0.02)
266
+
267
+
268
+ class NanochatGPTModel(NanochatGPTPreTrainedModel):
269
+ def __init__(self, config: NanochatGPTConfig):
270
+ super().__init__(config)
271
+ self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
272
+ self.layers = nn.ModuleList(
273
+ [NanochatGPTBlock(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
274
+ )
275
+ self.window_sizes = compute_window_sizes(config)
276
+ for layer, window in zip(self.layers, self.window_sizes):
277
+ layer.self_attn.window = window
278
+
279
+ # Mechanism parameters exist only when their mechanism is on, so the
280
+ # clean-architecture state dict (older exports) still loads strictly.
281
+ n_layer = config.num_hidden_layers
282
+ if config.use_resid_lambdas:
283
+ self.resid_lambdas = nn.Parameter(torch.ones(n_layer))
284
+ if config.use_x0_lambdas:
285
+ self.x0_lambdas = nn.Parameter(torch.zeros(n_layer))
286
+ if config.use_smear:
287
+ self.smear_gate = nn.Linear(config.smear_gate_channels, 1, bias=False)
288
+ self.smear_lambda = nn.Parameter(torch.zeros(1))
289
+ if config.backout_layer is not None:
290
+ self.backout_lambda = nn.Parameter(torch.zeros(1))
291
+ kv_dim = config.num_key_value_heads * config.head_dim
292
+ self.value_embeds = nn.ModuleDict(
293
+ {str(i): nn.Embedding(config.vocab_size, kv_dim) for i in config.value_embedding_layers}
294
+ )
295
+ self.post_init()
296
+
297
+ def _smear(self, x, past_key_values, cache_position):
298
+ """Mix the previous token's (pre-smear) embedding into each position.
299
+
300
+ Mirrors nanochat GPT.forward: full-sequence smear when every position
301
+ is present; with a KV cache, the pre-smear embedding of the newest
302
+ position is stashed on the cache object and consumed by the next
303
+ single-token decode step.
304
+ """
305
+ B, T, C = x.size()
306
+ ch = self.config.smear_gate_channels
307
+ if past_key_values is None:
308
+ # Full sequence available (no cache): position 0 has no predecessor.
309
+ assert T > 1, "smear on a full sequence needs T > 1"
310
+ gate = self.smear_lambda.to(x.dtype) * torch.sigmoid(self.smear_gate(x[:, 1:, :ch]))
311
+ return torch.cat([x[:, :1], x[:, 1:] + gate * x[:, :-1]], dim=1)
312
+ prev = getattr(past_key_values, "nanochat_prev_embedding", None)
313
+ past_key_values.nanochat_prev_embedding = x[:, -1:, :] # pre-smear, for the next step
314
+ if T > 1:
315
+ # Prefill: smear positions 1+, same as the full-sequence path.
316
+ gate = self.smear_lambda.to(x.dtype) * torch.sigmoid(self.smear_gate(x[:, 1:, :ch]))
317
+ return torch.cat([x[:, :1], x[:, 1:] + gate * x[:, :-1]], dim=1)
318
+ if int(cache_position[0]) == 0:
319
+ return x # single-token prefill at position 0: no predecessor exists
320
+ # Single-token decode: the previous step must have stashed its embedding.
321
+ # Refusing beats silently skipping the smear (wrong logits).
322
+ assert prev is not None, (
323
+ "single-token decode past position 0 without a stashed previous "
324
+ "embedding: the cache was not built by this model's forward"
325
+ )
326
+ gate = self.smear_lambda.to(x.dtype) * torch.sigmoid(self.smear_gate(x[:, :, :ch]))
327
+ return x + gate * prev
328
+
329
+ def forward(
330
+ self,
331
+ input_ids: torch.LongTensor,
332
+ attention_mask: Optional[torch.Tensor] = None,
333
+ past_key_values: Optional[Cache] = None,
334
+ use_cache: Optional[bool] = None,
335
+ cache_position: Optional[torch.LongTensor] = None,
336
+ **kwargs,
337
+ ):
338
+ B, T = input_ids.size()
339
+ if attention_mask is not None:
340
+ assert bool(torch.all(attention_mask == 1)), (
341
+ "NanochatGPT does not support padded batches; use batch size 1 "
342
+ "or unpadded sequences."
343
+ )
344
+
345
+ if use_cache and past_key_values is None:
346
+ past_key_values = NanochatDynamicCache()
347
+ if use_cache and self.config.use_smear and \
348
+ not isinstance(past_key_values, NanochatDynamicCache):
349
+ # generate() constructs a stock DynamicCache and passes it in.
350
+ # The smear stash must follow beam-search reorder (and refuse
351
+ # crop), so the EMPTY stock cache is grafted onto the stash-aware
352
+ # subclass in place — keeping all internal state and the object
353
+ # identity generate() holds. Any other cache cannot keep the
354
+ # stash in sync; refusing beats silently smearing with another
355
+ # batch row's embedding.
356
+ assert type(past_key_values) is DynamicCache and \
357
+ past_key_values.get_seq_length() == 0, (
358
+ "smear models support only the default dynamic KV cache: pass "
359
+ "past_key_values=None or a fresh DynamicCache, got "
360
+ f"{type(past_key_values).__name__} with "
361
+ f"{past_key_values.get_seq_length()} cached positions"
362
+ )
363
+ past_key_values.__class__ = NanochatDynamicCache
364
+ if cache_position is None:
365
+ past_len = past_key_values.get_seq_length() if past_key_values is not None else 0
366
+ cache_position = torch.arange(past_len, past_len + T, device=input_ids.device)
367
+ cache = past_key_values if use_cache else None
368
+
369
+ x = self.embed_tokens(input_ids)
370
+ x = rms_norm(x)
371
+
372
+ if self.config.use_smear:
373
+ x = self._smear(x, cache, cache_position)
374
+
375
+ cos_sin = compute_rotary_cos_sin(
376
+ cache_position, self.config.head_dim, self.config.rope_theta, x.device, x.dtype
377
+ )
378
+
379
+ x0 = x # initial (post-smear) normalized embedding, for x0 re-injection
380
+ use_resid = self.config.use_resid_lambdas
381
+ use_x0 = self.config.use_x0_lambdas
382
+ backout_layer = self.config.backout_layer
383
+ x_backout = None
384
+ for i, layer in enumerate(self.layers):
385
+ # Same branch structure and expressions as the reference so the
386
+ # bf16 rounding sequence is identical.
387
+ if not use_resid and not use_x0:
388
+ pass
389
+ elif not use_x0:
390
+ x = self.resid_lambdas[i] * x
391
+ elif not use_resid:
392
+ x = x + self.x0_lambdas[i] * x0
393
+ else:
394
+ x = self.resid_lambdas[i] * x + self.x0_lambdas[i] * x0
395
+ ve = self.value_embeds[str(i)](input_ids).to(x.dtype) if str(i) in self.value_embeds else None
396
+ x = layer(x, ve, cos_sin, cache, cache_position)
397
+ if i == backout_layer:
398
+ x_backout = x
399
+ if backout_layer is not None:
400
+ assert x_backout is not None
401
+ x = x - self.backout_lambda.to(x.dtype) * x_backout
402
+ x = rms_norm(x)
403
+
404
+ return BaseModelOutputWithPast(
405
+ last_hidden_state=x,
406
+ past_key_values=past_key_values if use_cache else None,
407
+ )
408
+
409
+
410
+ class NanochatGPTForCausalLM(NanochatGPTPreTrainedModel, GenerationMixin):
411
+ _tied_weights_keys = []
412
+
413
+ def __init__(self, config: NanochatGPTConfig):
414
+ super().__init__(config)
415
+ self.model = NanochatGPTModel(config)
416
+ self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
417
+ self.post_init()
418
+
419
+ def get_input_embeddings(self):
420
+ return self.model.embed_tokens
421
+
422
+ def set_input_embeddings(self, value):
423
+ self.model.embed_tokens = value
424
+
425
+ def get_output_embeddings(self):
426
+ return self.lm_head
427
+
428
+ def forward(
429
+ self,
430
+ input_ids: torch.LongTensor,
431
+ attention_mask: Optional[torch.Tensor] = None,
432
+ past_key_values: Optional[Cache] = None,
433
+ labels: Optional[torch.LongTensor] = None,
434
+ use_cache: Optional[bool] = None,
435
+ cache_position: Optional[torch.LongTensor] = None,
436
+ **kwargs,
437
+ ):
438
+ outputs = self.model(
439
+ input_ids=input_ids,
440
+ attention_mask=attention_mask,
441
+ past_key_values=past_key_values,
442
+ use_cache=use_cache,
443
+ cache_position=cache_position,
444
+ )
445
+ logits = self.lm_head(outputs.last_hidden_state)
446
+ logits = logits.float() # fp32 for softcap and loss, as in training
447
+ softcap = self.config.logit_softcap
448
+ if softcap is not None and softcap > 0:
449
+ logits = softcap * torch.tanh(logits / softcap)
450
+
451
+ loss = None
452
+ if labels is not None:
453
+ loss = F.cross_entropy(
454
+ logits[:, :-1].reshape(-1, logits.size(-1)),
455
+ labels[:, 1:].reshape(-1),
456
+ ignore_index=-100,
457
+ )
458
+
459
+ return CausalLMOutputWithPast(
460
+ loss=loss,
461
+ logits=logits,
462
+ past_key_values=outputs.past_key_values,
463
+ )
plots/bpb_across_tpp.png ADDED

Git LFS Details

  • SHA256: 2569624cda633d46ca8ffa8863b3b7f52377254bb3e742d64f54955b328dcdc9
  • Pointer size: 131 Bytes
  • Size of remote file: 217 kB
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,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "verification_passed": true,
3
+ "checkpoint_dir": "d16_tpp200-572ca7060b1d",
4
+ "step": 22558,
5
+ "export_dir": "TPP_50",
6
+ "export_sha256": "4032e5692d86de5114638f8bfb830d60313528ec227b355f0b1329ba88701559",
7
+ "device": "cpu",
8
+ "eval_tokens": 2097152,
9
+ "recorded_bpb_tol": 0.03,
10
+ "tokenizer_docs_checked": 2000,
11
+ "tokenizer_tokens_checked": 1226350,
12
+ "logit_max_abs_diff": 0.0,
13
+ "bpb_original": 0.7730321573714831,
14
+ "bpb_converted": 0.7730321573714831,
15
+ "bpb_recorded_at_training": 0.7679977471747351,
16
+ "kv_divergence": {
17
+ "token_cache": 46,
18
+ "token_nocache": 5251,
19
+ "cross_path_max_abs_diff": 0.21000659465789795,
20
+ "margins": {
21
+ "cache": 0.0,
22
+ "no_cache": 0.04057025909423828
23
+ },
24
+ "cross_tol": 0.25,
25
+ "margin_tol": 0.15,
26
+ "reason": "benign bf16 tie: paths agree, candidates near-tied in both",
27
+ "generated_step": 12
28
+ },
29
+ "greedy_sample": "The capital of France is Paris, and the largest city in the world is New York. The capital of the United States is Washington, D.C. The capital of the United Kingdom is London, and the capital of the United Kingdom is London. The capital of the United States is New York, and the capital of the United Kingdom is London."
30
+ }