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
|
# J-space on nanoGPT
A small-scale replication and critique of Anthropic's **Jacobian Lens**
("J-lens") technique from *"Verbalizable Representations Form a Global
Workspace in Language Models"* (2026,
[transformer-circuits.pub/2026/workspace](https://transformer-circuits.pub/2026/workspace/index.html)).
**Short version of the findings:**
1. The faithful J-lens (rows of `W_U * J_l`, exactly per the paper's Methods)
has J-lens norms that are strongly anti-correlated with token frequency at
every layer of a 10.65M-parameter character-level transformer
(r ≈ -0.61 to -0.69). Anthropic does not control for frequency anywhere in
their analysis.
2. The correlation is NOT a measurement artifact: it survives the paper's exact
quantity, verified by a last-layer identity check (cosine similarity 1.0000).
3. The lens is not *only* a frequency meter: two synthetic tokens at identical
unigram frequency get different faithful J-lens norms when one is predictable
in context (~1.4-1.5x higher for the structured token, 3 seeds).
4. A causal loss-reweighting test (2x loss weight on 'q' targets vs two
controls) tests whether effective frequency causally demotes a token's
J-lens norm. See `results.md` for the latest numbers.
5. The geometric half generalizes: GPT-2's unembedding row norms (V = 50,257,
wte == tied lm_head) anti-correlate with token log-frequency
(r ≈ -0.45/-0.49, gpt2/gpt2-medium; monotone across frequency deciles) —
the learned W_U geometry is not a 65-char vocabulary artifact
(`src/wu_row_norm_check.py`, CPU-only).
See `docs/blog-jlens-frequency.md` for the write-up and `results.md` for the
numbers. The three independent adversarial reviews that shaped the project
(and caught a real bug in the first implementation) are in `docs/reviews/`.
## What we are NOT claiming
- That Anthropic's J-space doesn't exist. Their headline capacity claim is
about activation *occupancy* per position, which this repo does not test.
- That the J-lens is useless — it carries genuine conditional-predictability
signal.
- That toy-model results refute large-model findings. The claim is narrower:
J-lens *rankings* are frequency-confounded, so a frequency control is
required before any "privileged subspace" interpretation.
## Repository layout
```
model.py, train.py nanoGPT (Karpathy) with Maxwell-GPU fixes
src/jlens.py, jlens_v2.py first (buggy) J-lens implementations — superseded
src/jlens_v3.py FAITHFUL J-lens: rows of W_U * J_l (canonical)
src/synthetic_pair.py frequency-matched synthetic pair experiment
src/loss_reweight.py causal loss-reweighting experiment
src/gpt2_jlens.py GPT-2 scale test (under-powered; see results.md)
src/wu_row_norm_check.py at-scale W_U row-norm check (CPU-only; results.md §5)
tests/ unit tests (see scripts/test.sh)
scripts/test.sh canonical test command
docs/blog-jlens-frequency.md write-up (Feynman-style)
docs/reviews/ three adversarial model reviews
results.md committed experiment numbers
```
## Reproducing everything
### Environment
- Any Linux box with Docker and an NVIDIA GPU (we used a 4GB Quadro K2200).
- Container image: `pytorch/pytorch:2.4.1-cuda11.8` (last CUDA for Maxwell).
- Sync this repo into the container, e.g. `/workspace/code`.
The K2200 has 4GB VRAM, so batched VJP probes must be chunked (`--chunk 16`);
on a modern GPU you can raise it. Everything runs fp32 (no bf16 on Maxwell).
### 1. Tests
```sh
sh scripts/test.sh
```
Skips gracefully where torch is unavailable.
### 2. Prepare data + train the base model
Prepare the character-level Shakespeare dataset (downloads Shakespeare and
builds `data/shakespeare_char/{train,val}.bin` + `meta.pkl`):
```sh
python3 data/shakespeare_char/prepare.py
```
Then train nanoGPT (10.65M params, 6 layers, d=384, block 128) and keep the
checkpoint at `out-shakespeare-char/ckpt.pt`:
```sh
python3 train.py config/train_shakespeare_char.py
```
### 3. Faithful J-lens, both-ways comparison
```sh
python3 src/jlens_v3.py --checkpoint out-shakespeare-char/ckpt.pt \
--data_dir data/shakespeare_char --layers 0,1,2,3,4,5
```
Prints per-layer frequency correlations for the faithful lens and the old
proxy, plus the last-layer identity validation. Artifacts land in
`outputs/jlens_v3/`.
### 4. Synthetic frequency-matched pair
```sh
python3 src/synthetic_pair.py --step prep # builds data/synth_pair
python3 src/synthetic_pair.py --step train --seed 0
python3 src/synthetic_pair.py --step jlens --seed 0
python3 src/synthetic_pair.py --step summary
```
### 5. Loss-reweighting causal test
```sh
python3 src/loss_reweight.py --step train --mode q --seed 0
python3 src/loss_reweight.py --step train --mode control --seed 0
python3 src/loss_reweight.py --step train --mode ctrl_random --seed 0
python3 src/loss_reweight.py --step summary --layers 2,3,4
```
## Data & artifacts
Raw data files (`.bin`/`.pkl`), checkpoints, and experiment outputs are NOT
committed (gitignored — they are regenerable and large). Committed instead:
`results.md` (the numbers) and `src/stats_decomp.py` (the statistics that
reproduce them from the saved artifacts). To regenerate everything, follow the
reproduction steps above; the full pipeline takes a few hours on a 4GB GPU.
## References
- [Verbalizable Representations Form a Global Workspace in Language Models](https://transformer-circuits.pub/2026/workspace/index.html) — Anthropic, 2026
- [nanoGPT](https://github.com/karpathy/nanoGPT) — Andrej Karpathy
|