A prompt-local linear readout for unwritten intermediate facts in a decoder-only LM, compared against released trained lenses, with residual-edit interventions.
0
stars
1
commits
Python
primary language
Aug 21, 2026
updated
A prompt-local linear readout for intermediate facts inside a decoder-only model, compared against released trained lenses, and two residual edits that test what a late state controls.
The idea. Given a single prompt whose answer depends on a fact it never states — "the language spoken in the country where the Amazon River ends" depends on Brazil — Spyglass fits a linear map from an intermediate layer's residuals to the final layer's, using only that prompt's own unlabeled prefix states, with the scored position held out. Decoding the held-out position through the fitted map surfaces the unstated intermediate at far better rank than the released lenses do, over the first three quarters of the network.
spyglass/ the package
scripts/ one experiment per file
data/surfaces/ frozen corpora and experiment registrations
data/results/ committed receipts
tests/ unit tests
spyglass/ — transport (fit / apply / adjoint / inverse), readout
(token scoring under the shared rank rule), items (the locked 40-item
corpus and its wrong-intermediate controls), model (plain Hugging Face
capture), baselines (released J-Lens / R-Lens), clusters (spherical
k-means over the output embedding), causal (residual edits and patching).Models, revisions, and baseline-lens pins live in data/models.json, so no
script takes them as arguments. Surfaces and output paths default to the
committed ones; the only required flags are paths to things outside the repo
(--jlens-code, and --eval-dir for the public suite).
| script | what it computes | receipt |
|---|---|---|
compare_lenses.py | seven carriers over the locked corpus, per layer | compare.json |
mechanism_audit.py | prefix baseline, chart transplant, output pullback | audit.json |
depth_sweep.py | stride-1 layers 40..63: formation depth vs collapse | depth_sweep.json |
causal_patch.py | minimal-pair residual patching across the lifecycle | causal_patch.json |
eval_suite.py | the released six-task public suite, head-to-head | suite.json |
steering.py | class steering at a late state, with two controls | steering.json |
subtract_row.py | output-row subtraction at a stored error, four controls | subtraction.json |
Two receipts have no script here because they are inputs rather than outputs:
readout_profile.json is the summary the head-to-head figures are drawn
from, derived from compare.json and official_compare.json;
logit_blockade.json records what banning a token at the logit level does,
which is the baseline subtract_row.py is contrasted against.
| claim | receipt |
|---|---|
| layer-8 median rank 18 vs 2,696 (R-Lens) and 45,815 (J-Lens) | readout_profile.json, compare.json |
| top-1 decoded token per layer, per carrier | compare.json |
| a transplanted map returns the donor's intermediate (22 vs 1,714) | audit.json, phase2_rows |
| the read is selective against a deranged intermediate (40/40) | audit.json phase3_rows, readout_profile.json |
| patching: differing word early, clause closure late, both controls at zero | causal_patch.json |
| the six-task suite, where the trained lens leads on all six | suite.json |
| steering moves vocabulary far more than verdict | steering.json |
| subtraction recovers the truth 10/12 against 4/144 random | subtraction.json |
Every readout experiment scores the same way: take the state at the scored
position, transport it (identity for the raw carrier), decode it against the
output vocabulary, and count the rows scoring strictly higher than the best
spelling variant of the target word. Lower is better; pass@k counts
best-over-layers ranks below k.
The readout, audit, sweep, patching, suite, and subtraction runs are
Qwen3.6-27B; the steering run is Qwen3.8-27B. Loading the released lenses
needs the camilablank/workspace-lenses repository's jlens package, passed
as --jlens-code.
uv sync
uv run python -m pytest tests/ -q
uv run python scripts/causal_patch.py # no arguments
uv run python scripts/subtract_row.py
uv run python scripts/compare_lenses.py --jlens-code PATH
data/surfaces/locked40.json carries the item list and the selection rule
unchanged from the frozen original, trimmed to the fields these experiments
read. The original also registered a coded/open output protocol and
suppression arms belonging to a separate line of work; those are not here,
and the file says so.steering.py and subtract_row.py are cleaned reimplementations of the
procedure that produced steering.json and subtraction.json. The logic,
the doses, the arms, and the frozen cases are the same; the surrounding
code is not the code that produced those files.steering.py recomputes its vocabulary chart from the checkpoint with a
fixed seed rather than loading the precomputed one the receipt used, so
class indices differ between runs. Nothing selects a class by index: the
target class is the one containing the anchor token " Yes", and both the
target and the random-control class have their token lists audited into the
receipt so the selection can be checked. In the committed run the target
class is yes / Yes / 确实 / oui / 是的 and the occupancy-matched control
class is constants.1 commits
Python
100.0%
A prompt-local linear readout for unwritten intermediate facts in a decoder-only LM, compared against released trained lenses, with residual-edit interventions.
0
stars
1
commits
Python
primary language
Aug 21, 2026
updated
A prompt-local linear readout for intermediate facts inside a decoder-only model, compared against released trained lenses, and two residual edits that test what a late state controls.
The idea. Given a single prompt whose answer depends on a fact it never states — "the language spoken in the country where the Amazon River ends" depends on Brazil — Spyglass fits a linear map from an intermediate layer's residuals to the final layer's, using only that prompt's own unlabeled prefix states, with the scored position held out. Decoding the held-out position through the fitted map surfaces the unstated intermediate at far better rank than the released lenses do, over the first three quarters of the network.
spyglass/ the package
scripts/ one experiment per file
data/surfaces/ frozen corpora and experiment registrations
data/results/ committed receipts
tests/ unit tests
spyglass/ — transport (fit / apply / adjoint / inverse), readout
(token scoring under the shared rank rule), items (the locked 40-item
corpus and its wrong-intermediate controls), model (plain Hugging Face
capture), baselines (released J-Lens / R-Lens), clusters (spherical
k-means over the output embedding), causal (residual edits and patching).Models, revisions, and baseline-lens pins live in data/models.json, so no
script takes them as arguments. Surfaces and output paths default to the
committed ones; the only required flags are paths to things outside the repo
(--jlens-code, and --eval-dir for the public suite).
| script | what it computes | receipt |
|---|---|---|
compare_lenses.py | seven carriers over the locked corpus, per layer | compare.json |
mechanism_audit.py | prefix baseline, chart transplant, output pullback | audit.json |
depth_sweep.py | stride-1 layers 40..63: formation depth vs collapse | depth_sweep.json |
causal_patch.py | minimal-pair residual patching across the lifecycle | causal_patch.json |
eval_suite.py | the released six-task public suite, head-to-head | suite.json |
steering.py | class steering at a late state, with two controls | steering.json |
subtract_row.py | output-row subtraction at a stored error, four controls | subtraction.json |
Two receipts have no script here because they are inputs rather than outputs:
readout_profile.json is the summary the head-to-head figures are drawn
from, derived from compare.json and official_compare.json;
logit_blockade.json records what banning a token at the logit level does,
which is the baseline subtract_row.py is contrasted against.
| claim | receipt |
|---|---|
| layer-8 median rank 18 vs 2,696 (R-Lens) and 45,815 (J-Lens) | readout_profile.json, compare.json |
| top-1 decoded token per layer, per carrier | compare.json |
| a transplanted map returns the donor's intermediate (22 vs 1,714) | audit.json, phase2_rows |
| the read is selective against a deranged intermediate (40/40) | audit.json phase3_rows, readout_profile.json |
| patching: differing word early, clause closure late, both controls at zero | causal_patch.json |
| the six-task suite, where the trained lens leads on all six | suite.json |
| steering moves vocabulary far more than verdict | steering.json |
| subtraction recovers the truth 10/12 against 4/144 random | subtraction.json |
Every readout experiment scores the same way: take the state at the scored
position, transport it (identity for the raw carrier), decode it against the
output vocabulary, and count the rows scoring strictly higher than the best
spelling variant of the target word. Lower is better; pass@k counts
best-over-layers ranks below k.
The readout, audit, sweep, patching, suite, and subtraction runs are
Qwen3.6-27B; the steering run is Qwen3.8-27B. Loading the released lenses
needs the camilablank/workspace-lenses repository's jlens package, passed
as --jlens-code.
uv sync
uv run python -m pytest tests/ -q
uv run python scripts/causal_patch.py # no arguments
uv run python scripts/subtract_row.py
uv run python scripts/compare_lenses.py --jlens-code PATH
data/surfaces/locked40.json carries the item list and the selection rule
unchanged from the frozen original, trimmed to the fields these experiments
read. The original also registered a coded/open output protocol and
suppression arms belonging to a separate line of work; those are not here,
and the file says so.steering.py and subtract_row.py are cleaned reimplementations of the
procedure that produced steering.json and subtraction.json. The logic,
the doses, the arms, and the frozen cases are the same; the surrounding
code is not the code that produced those files.steering.py recomputes its vocabulary chart from the checkpoint with a
fixed seed rather than loading the precomputed one the receipt used, so
class indices differ between runs. Nothing selects a class by index: the
target class is the one containing the anchor token " Yes", and both the
target and the random-control class have their token lists audited into the
receipt so the selection can be checked. In the committed run the target
class is yes / Yes / 确实 / oui / 是的 and the occupancy-matched control
class is constants.1 commits
Python
100.0%