File size: 7,672 Bytes
a1a5af1
 
 
 
 
 
 
 
 
 
 
 
 
2199c04
a1a5af1
2199c04
 
 
a1a5af1
2199c04
 
 
a1a5af1
2199c04
a1a5af1
2199c04
 
 
89938f8
2199c04
 
9555320
 
 
 
 
89938f8
2199c04
89938f8
2199c04
 
9555320
 
89938f8
9555320
89938f8
9555320
 
89938f8
9555320
2199c04
9555320
 
 
 
89938f8
9555320
 
89938f8
9555320
2199c04
 
 
89938f8
2199c04
89938f8
c09becc
9555320
c09becc
89938f8
2199c04
89938f8
9555320
 
 
c09becc
2199c04
 
 
 
 
 
 
89938f8
2199c04
 
a1a5af1
c09becc
 
2199c04
 
 
a1a5af1
2199c04
 
 
 
89938f8
2199c04
89938f8
2199c04
89938f8
2199c04
 
9555320
 
89938f8
2199c04
c09becc
2199c04
89938f8
2199c04
 
9555320
 
89938f8
2199c04
 
 
 
 
 
 
 
 
9555320
 
2199c04
9555320
c09becc
2199c04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a1a5af1
 
 
2199c04
 
 
 
89938f8
2199c04
89938f8
2199c04
 
9555320
 
2199c04
 
 
a1a5af1
2199c04
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
---
license: mit
datasets:
  - HuggingFaceFW/fineweb-edu
language:
  - en
tags:
  - gpt
  - baseline
  - interpretability
  - paritytransformer
---

# vanilla-small-20B β€” dense baselines (12L / d=1024, 202.53M)

Dense (`sparsity_mode=none`) counterparts to the `cayley-small-*-mlp_in-20B`
ParityTransformer siblings. **One 20B-token run, five checkpoints**: three
val-matched early stops plus the token-matched end of the run.

These are the dense comparators for the interpretability claim: if a
ParityTransformer is more interpretable than a dense model that is **at least as
good** on val, the comparison cannot be dismissed as a handicapped baseline.

## Checkpoints

Files are named by **iter**. They were previously named by the `--save-at-val`
threshold that fired; those names misstated the actual loss, so they were renamed
(`ckpt_val_3.1584.pt` β†’ `ckpt_val_1900.pt`, `3.1484` β†’ `2100`, `3.1330` β†’ `2300`).

| file | iter | tokens | **measured val_loss** | in-run trigger |
|---|---|---|---|---|
| `ckpt_val_1900.pt` | 1900 | 2.99B | **3.1477** | 3.1536 |
| `ckpt_val_2100.pt` | 2100 | 3.30B | **3.1285** | 3.1354 |
| `ckpt_val_2300.pt` | 2300 | 3.62B | **3.1116** | 3.1162 |
| `ckpt_best.pt` | 12200 | 19.19B | **2.8559** | 2.8586 |
| `ckpt_final_20B.pt` | 12716 | 20.001B | **2.8530** | 2.8664 |

Siblings, measured in the same pass on the same batches:

| reference | measured val_loss | published (in-run) |
|---|---|---|
| `cayley-small-2L-mlp_in-20B` | **3.1671** | 3.1584 |
| `cayley-small-3L-mlp_in-20B` | **3.1425** | 3.1330 |

## Paired differences vs the siblings

Negative = dense is better. Every model saw byte-identical batches, so these are
exact paired comparisons, not error bars added in quadrature (paired SEM Β±0.0004).

| dense ckpt | vs `cayley-small-2L` (3.1671) | vs `cayley-small-3L` (3.1425) |
|---|---|---|
| `ckpt_val_1900` | **βˆ’0.0195** (45Οƒ better) | **+0.0052** (13Οƒ *worse*) |
| `ckpt_val_2100` | βˆ’0.0387 (86Οƒ better) | **βˆ’0.0140** (33Οƒ better) |
| `ckpt_val_2300` | βˆ’0.0556 (111Οƒ better) | βˆ’0.0309 (65Οƒ better) |
| `ckpt_final_20B` | βˆ’0.3141 (245Οƒ better) | βˆ’0.2894 (231Οƒ better) |

Note `ckpt_val_1900` clears 2L but **does not clear 3L**. Pick the checkpoint your
comparison requires from this table.

`ckpt_final_20B.pt` is also **better than `ckpt_best.pt`** by 0.0029 (110Οƒ)
despite the name: `ckpt_best` was selected by a noisy 4.9M-token in-run eval that
happened to dip at iter 12200. Prefer `ckpt_final_20B.pt` for everything;
`ckpt_best.pt` is kept only for provenance.

## How these numbers were measured

`evals/language_modeling/eval_val_loss.py` over the **entire** FineWeb-Edu val
split β€” contiguous non-overlapping windows, bf16 autocast, batch 16 Γ— 1024,
`fineweb-edu-25B`, seed 0.

| quantity | value |
|---|---|
| tokens per model | **25,161,728** (whole val split) |
| batches per model | 1,536, **byte-identical across all models** |
| absolute SEM (single model) | Β±0.0045 |
| paired SEM (difference of two models) | **Β±0.0004** |

The paired SEM is ~13Γ— tighter because shared passage difficulty cancels. Quote
the paired differences.

Two reasons the run log disagrees with this table:

| source of disagreement | effect |
|---|---|
| In-run evals are master-rank only, `eval_iters Γ— bs Γ— seq_len` = **4.9M tokens** regardless of world size; adjacent evals wobble ~0.005 nats. | `--save-at-val` triggers inherit that noise. |
| `best_val_loss` inside a checkpoint is the **run's** best, not that checkpoint's val. | Meaningless to difference against a reference `ckpt.pt` saved at a different iter. |

Measured on this protocol, dense sits ~0.0025 nats *below* its in-run trigger,
while the cayley siblings sit ~0.011 *above* their published numbers. The
cayley siblings' published numbers are in-run values too, so **use the measured
column for any cross-model claim and do not mix the two** β€” a ~0.01 nat protocol
artifact otherwise lands inside the very gap being resolved.

One caveat we cannot yet separate: the cayley path must run bf16 through this
loader (it builds the backbone fp32 but forces the SAE to bf16 + triton), so its
offset could be protocol *or* genuine bf16 sensitivity in top-k selection. Dense
is bf16-insensitive here β€” its fp32 and autocast runs agree to 0.0002.

## Alignment tax

**Equal budget (20B tokens both sides):**

| pair | dense | cayley | gap |
|---|---|---|---|
| vs 2L | 2.8530 | 3.1671 | **0.314 nats** |
| vs 3L | 2.8530 | 3.1425 | **0.289 nats** |

**Token efficiency** β€” where dense first reaches each sibling's measured val,
interpolated on the in-run eval curve shifted by the measured βˆ’0.0025 protocol
offset:

| sibling | its measured val | dense crossing | tokens | tax |
|---|---|---|---|---|
| 2L | 3.1671 | iter ~1683 | 2.65B | **~7.6Γ—** |
| 3L | 3.1425 | iter ~2021 | 3.18B | **~6.3Γ—** |

Both inside the <15Γ— target. Eval spacing (100 iters = 157M tokens) caps
resolution, so treat as two significant figures. Note the large chassis gives
**~3.5Γ—** on the same construction ([`markhenry/vanilla-large-20B`](https://huggingface.co/markhenry/vanilla-large-20B)) β€”
the ratio is not a constant across scale.

### Corrections to earlier versions of this card / `report.md`

| claim (earlier) | corrected |
|---|---|
| iter 1900 beats 2L by 0.0048 β€” "a statistical tie" | beats by **0.0195 at 45Οƒ** β€” not a tie |
| iter 2300 beats 2L by 0.042, 3L by 0.017 | **0.0556** and **0.0309** |
| iter 2300 is the 3L match | **iter 2100** is the first to clear 3L |
| tax 6.9Γ— (2L) / 5.7Γ— (3L) | **7.6Γ—** / **6.3Γ—** |
| `ckpt_best` is the run's best model | `ckpt_final_20B` is better by 0.0029 (108Οƒ) |

All five corrections move the same way: measured consistently, the dense
baselines are **stronger** than the in-run numbers implied, so the alignment tax
is **larger**. That is the conservative direction for the interpretability claim.

## Architecture and training

12 layers, 8 heads, d=1024, block_size 1024, learned positional encoding, RMSNorm
(affine), no bias, vocab 50304, **202.53M params** β€” identical to the cayley-small
siblings, which add buffers but no `nn.Parameter`s.

Muon + AdamW lockstep 1.2e-2 (min 1.5e-4), `linear_warmdown` wf=0.9, warmup 200,
`tokens/iter = 32 Γ— 1024 Γ— 48 = 1,572,864` β†’ 12,716 iters. FineWeb-Edu
`sample-100BT`, GPT-2 tokenizer. 2Γ— H200, 8h32m, ~695k tok/s.
Wandb [`9iezcto0`](https://wandb.ai/markhenrysoftware/sparse-nanogpt/runs/9iezcto0).

`--n-head 8` must be passed explicitly: `train.py` arch defaults are
mode-dependent, so `sparsity_mode=none` at d=1024 otherwise resolves `n_head=12`
and `1024 % 12 != 0` asserts. The cayley siblings hit the cayley default of 8.

Also in this repo: `report.md`, `eval_curve.csv` (full in-run eval trajectory),
`train.log`, and the training script.

## Loading

```python
import torch
ck = torch.load("ckpt_val_2100.pt", map_location="cpu", weights_only=False)
ck["iter_num"]                              # 2100
cfg, sd = ck["model_config"], ck["model"]   # GPTConfig fields, state dict
```

Checkpoints carry `optimizer_states`, so they are resumable, not inference-only.

## Siblings

- [`markhenry/cayley-small-2L-mlp_in-20B`](https://huggingface.co/markhenry/cayley-small-2L-mlp_in-20B) β€” measured 3.1671
- [`markhenry/cayley-small-3L-mlp_in-20B`](https://huggingface.co/markhenry/cayley-small-3L-mlp_in-20B) β€” measured 3.1425
- [`markhenry/vanilla-large-20B`](https://huggingface.co/markhenry/vanilla-large-20B) β€” same construction, large chassis

---

*Re-evaluation and card by Claude Code. Training run and `report.md` predate it;
where they disagree, the corrections table above states why.*