From op-catalog to decompilation — a research design
DISASSEMBLY.md reads a transformer’s attention as a first-order instruction set:
per-head QK/OV opcodes, named idioms, a coverage scorecard (“% of attention legible”), causal validation,
and cross-model replication. This document scopes the next phase: turning that disassembly into an
executable decompilation — reconstructing the computation, not just labeling components — and argues
the obstacle is the same entanglement the forge tax measures.
This is a design doc (a target + a metric + milestones), not a results writeup. It is the flagship that ideas (i) cross-model breadth and (ii) multilingual invariance de-risk: you want the disassembler proven on ≥3 architectures before you trust a single decompilation-coverage number.
The decompilation taken literally — see the pylm track: a whole small LLM reimplemented as a small pure-Python program + flat-file knowledge store (no neural-net code or concepts), validated against the corpus and the model. It makes the decompilable fraction a running artifact (~50% of GPT-2 in 49 lines; 56% of Pythia-14m; shrinking with scale) — the disassembly→decompilation arc reaching its endpoint, and the forge-tax ceiling visible as the half it can’t reach.
The gap: disassembly ≠ decompilation
| disassembly (have) | decompilation (want) | |
|---|---|---|
| unit | one head / one idiom in isolation | the composition — which ops chain into which |
| coverage metric | % of attention legible (named or weight-binding) | % of the forward pass faithfully reconstructable |
| MLP | first-order read→write token catalog | MLP neurons as first-class ops (key–value memories) |
| validation | mean-ablation damages the metric (necessary) | recompile the extracted program; KL ≈ host (sufficient) |
| output | a human-readable listing | a runnable reduced model + a symbolic trace |
The disassembly already took the first two rungs of composition: the induction macro read as
OV_A ∘ QK_B (prev-token head feeds the induction head; weight-composed diagonal vs behavior ρ≈0.78,
path-patch-gated), and the IOI chain duplicate → S-inhibition → name-mover read as a product of
Q-composition scores. Decompilation is the general algorithm those two are instances of.
The load-bearing idea: reconstruction coverage
Replace “% legible” with a metric that has teeth — does the extracted op-graph, compiled back into a runnable model, reproduce the host’s next-token distribution?
reconstruction_coverage(budget B) =
1 − KL( host ‖ recompiled[ ops selected under budget B ] ) / KL( host ‖ mean-ablated-everything )
- Recompile = forge. sae-forge’s
NativeModel.from_projected_weightsalready takes a basis + projected weights and produces a runnable model (forward_mode=native_in_basis). A decompilation is a structured forge: keep exactly the ops the disassembly named (heads’ QK/OV in the operand basis + catalogued MLP neurons), mean-ablate the rest, run, measure KL. This is the “recompile” half of larql’s decompile→query→recompile loop, used here as the faithfulness oracle. - Sweep the budget (how many ops kept) → a coverage curve, the decompilation analog of the coverage scorecard’s single number. The area under it is “how much of the model the catalog explains, executably.”
- Per-task traces. For a templated task (IOI, induction, greater-than), emit the symbolic trace:
the ordered DAG of ops that fire to produce the output, each edge path-patch-validated. A correct trace
- a faithful recompile = a decompilation of that behavior.
First results are live — and humbling. The reconstruction-coverage idea is now run across six models for the induction circuit and (GPT-2) the IOI circuit: Executable decompilation (keep the circuit’s heads, ablate the rest, measure induction-NLL / IOI logit-diff recovery + the budget curve) and the attention-vs-MLP substrate split. The honest headline: no small head-set is a sufficient decompilation — induction recovers ≤30% from its named 8 heads (even under the gentler resample-ablation), needs ~all heads to fully reconstruct, and leans on attention and the MLP substrate roughly equally; even IOI’s celebrated 26-head circuit is not sufficient in isolation. The named circuits are necessary and the dominant drivers, but the behaviour is distributed across the near-whole network. So a faithful decompilation here is not a tiny op-graph — the budget curve, not a single small circuit, is the real object.
Four work-items
1. The composition DAG extractor
Generalize rung-2/3 into the standard algorithm: from the weights, score every directed edge
OV_g → QK_h (head g’s write feeds head h’s query/key/value) as a composition bilinear in the operand
basis; threshold + path-patch-validate to keep live edges. Output: the model’s circuit DAG (heads + MLP
nodes, typed edges Q/K/V-composition). The induction macro and IOI chain become the first two validated
sub-DAGs; the extractor produces them automatically and finds new ones.
2. The MLP gap
The catalog is attention-centric; MLPs carry real computation (greater-than is MLP-dominated — the OV probe sees only its attention-side shadow). Treat MLP neurons as ops: in-direction (gate/up read) and out-direction (down write) in the operand basis = a key→value memory; cluster into named MLP idioms (detokenization, entity attributes, successor arithmetic). Add them as nodes in the DAG (1) and as keepable ops in the recompile (the metric).
3. The operand-basis ceiling
First-order disassembly uses centroid / SAE operands that are imperfect; composition only reads cleanly in a
basis where the ops compose cleanly. This is where decompilation meets the rest of the program: the
two-basis forge’s U_C (composition subspace) is exactly “the basis in which circuits survive.” (The
specific writer-output U_C construction was retracted — see DISASSEMBLY.md — but the target stands: find
the basis that makes reconstruction_coverage high at low budget.) polygram models the operand-dictionary
geometry; the decompiler consumes it.
4. Executable verification & emission
Wire (1)+(2) into the recompile metric (the load-bearing idea), and emit the DAG in a checkable form:
n-orca can express the extracted circuit as a typed neural DAG; larql can hold the decompiled model as a
queryable index (“the model IS the database”). A decompilation is accepted only if recompile-KL clears a
threshold — no unverified symbolic story counts.
The unifying claim (why this is one program, not a side quest)
Composition is not cleanly feature-readable for the same reason cov95 collapses under forging: the composition is entangled (the entanglement tower’s irreducible high-χ core).
So “how much can we decompile” and “how big is the un-forgeable core” are the same quantity measured from
two directions — reconstruction_coverage from the disassembly side, 1 − cov95-tax / the tower’s
convergent core from the forge side. The falsifiable prediction:
The reconstruction-coverage curve plateaus at the same fraction the entanglement tower identifies as the irreducible entangled core (~the low-χ substrate), and no op-budget closes the gap — because the residual is the entangled composition, which by the tower’s no-go is not decompilable into first-order ops. Decompilation has a hard ceiling, and that ceiling is the forge tax.
If true, this is the result: a single number — measured independently as (a) the decompilable fraction of the forward pass and (b) the forgeable/low-χ fraction — that bounds how interpretable a transformer can be made without retraining. If the curve instead reaches ~1.0, the entanglement story is wrong and the model is fully decompilable (also a publishable result).
Flagship result on real LMs — composition doesn’t factor through the SAE features (
sae_forge_tax.py). The sharp prediction of the unifying claim, tested directly: force the residual through the SAE feature basis (decode∘encode, the forge bottleneck) and compare the damage to the composition (induction-NLL) vs the readout (generic next-token NLL). The feature basis taxes composition far more than readout — GPT-2: the full forge raises induction-NLL +1357% but generic-NLL only +70% (composition taxed ~19×, and taxed more in all 12 layers individually); Gemma-2-2B: +542% vs +399% (net +143%, 5/8 layers — weaker, as its induction is already distributed and its Gemma-Scope SAEs reconstruct loosely). This is the cov95 forge tax measured from the disassembly/reconstruction side: SAE features survive as readouts, the computation over them does not factor through them — the two contributions (forge tax / disassembly) meeting on one host. (Honest scope: SAE reconstruction is lossy everywhere, so the relative composition-vs-readout tax is the signal, robust at 12/12 layers in GPT-2; this is the reconstruction/NLL route, complementary to the cov95/mAUC route, not the fullNativeModelweight-projection ceiling test of M4.runs/disassembly/sae_forge_tax_summary.json.)
What form is the core? (not an SAE, not a dense slab — a program)
“SAE vs dense” is a false dichotomy for the entangled remainder. The forge tax says the core has no sparse
feature (SAE) basis in which composition is monosemantic — that rules out the SAE form. But the χ-ladder
showed composition is factorable in weight/attention-composition coordinates (induction = OV_A∘QK_B;
IOI = a head chain), and the instruction-tensor result found the program is low-rank (~5–11 templates vs
~132 random) — in operand-pair space, not residual-direction space (which is exactly why the residual-
direction search, joint-U_C, failed). So the core’s natural form is a program: a compact DAG of reused
weight-space bilinear templates over now-explicit operands. Subtracting the clean low-χ substrate is then a
change of coordinates, not a sparsification — it makes the operands explicit so the remainder reads as
“bilinears with clean arguments,” but it does not shrink the dense magnitude (the tower’s ~24% irreducible
variance floor; the retrain no-go). Consequence for this program: the lever that “simplifies” the core is the
composition-DAG extractor in operand coordinates (work-item 1+2), not a better SAE — and the
reconstruction-coverage curve should plateau at the entangled-core fraction precisely because first-order
feature ops can’t express the composition, while template ops over operands can. (Open at scale: whether
the template count stays small and whether the irreducible floor itself grows with capability — the tower
convergence was measured on small hosts.)
Core-rank result — the core is heavy-tailed low-rank, and CPU-simplifiable without retraining (
core_rank.py). Measuring the empirical rank of each layer’s residual update (its contribution to the residual) over 6 models: the spectrum is heavy-tailed — a tiny dominant head (participation ratio 5–33, ~1–2% of d) but a real tail, so the functional rank (energy-95 / accuracy-preserving) is ~⅓ of d (hundreds of directions: rank-95 250→398→476 across the GPT-2 ladder), and the dominant-direction count grows with scale (PR 5→13→24) — consistent with the entangled core growing with capability, and with the forge tax’s “no tiny basis.” So the core decompiles into a moderate-rank structure, not ~5 templates. But it is substantially CPU-simplifiable for free: a no-retrain low-rank projection of every layer’s update onto its top-r PCA subspace is ~lossless at 70% rank in GPT-2 (≈ +0.05 NLL at 40%), and Gemma-2 is lossless at ~20% (its update is the lowest functional rank — ~80% of write-dims droppable), while Llama/Qwen are the least compressible (need ~70%+; higher functional rank). A concrete efficiency lever — rank-r per-layer factorisation, no retraining — with the compressible fraction architecture-dependent and shrinking with scale. (Caveat: this is the per-layer write-subspace rank, not the full input→output map; data-dependent PCA.runs/disassembly/core_rank_summary.json.)And the layers SHARE that subspace. The cross-layer sharing measure: the union of all layers’ top-64 write- subspaces has effective rank 278 (GPT-2) / 517 (large) / 658 (Gemma) / 570 (Llama) — far below the no-sharing bound (nL·64, capped at d): the whole model’s per-layer writes collectively span only ~⅓–⅖ of d, and layer pairs are ~2× more aligned than chance (overlap 0.05–0.16 vs 0.03–0.08 random). So the entangled core is a shared moderate-rank subspace, not nL distinct ones — the strongest decompile/simplify lever yet: one shared ~300-direction basis captures most layers’ composition (decompile its structure; or a single global low-rank projection serves every layer on CPU). Sharing is partial (overlap ≪ 1) and grows with scale — compact, but not a few templates.
…but a single global basis is not a free lunch (honest refinement). Forcing every layer through the one shared union-basis at low rank is much worse than per-layer PCA (GPT-2 at 10% rank: +4.48 global vs +0.47 per-layer ΔNLL; Gemma +8.44 vs +0.38), only catching up at ~40–70% rank (Gemma’s global basis is even slightly better than per-layer at 70%, −0.39 — its sharing is strongest). So the partial overlap does not let you collapse the core to one tiny global basis: the efficient simplification is per-layer low-rank (lossless at ~70% GPT-2 / ~20% Gemma), with a moderate-rank global basis a second-best. The core is a shared-but-partial moderate-rank subspace — compact, reused across layers, but with real per-layer structure: “not a few templates, not a dense slab,” made quantitative.
What IS the shared subspace? — decompile its structure (core_basis_decompile.py)
Knowing the core is a shared ~300-direction subspace (“how big”) invites the deeper question: is that basis
interpretable (“what is it”)? Three tests on the union basis Ug (top-K, K = its effective rank), GPT-2 s/m/l:
| test | what it asks | result (gpt2 / medium / large) | reading |
|---|---|---|---|
| (A) token/logit | does Ug lie in W_U’s top-K logit subspace? |
0.33 / 0.39 / 0.39 vs chance 0.36 / 0.40 / 0.40 | at/below chance — the core is not the readout subspace; it is upstream compute |
| (B) operators | is Ug built from the catalog ops’ OV-write subspaces? |
induction/prev-tok/dup/sink 0.39–0.47, random heads 0.39–0.46, chance 0.36–0.40 | named ≈ random — the core is the aggregate of all writers, not a few named idioms |
| (C) logit-lens | do Ug’s directions decode to coherent tokens? |
peak-z 5.2 / 5.9 / 5.1 vs random 4.6 / 4.8 / 4.5 | readable — top dirs = function-words, punctuation, a verb axis, a register axis |
So the entangled core’s shared basis is logit-lens-readable per direction, but is neither the readout subspace
nor the named-operator basis — it is a broad write-basis whose individual directions decode to grammatical /
lexical classes while collectively it is the sum of every layer’s writers. (runs/disassembly/core_basis_summary.json.)
Is the core a generic grammar? — the compact head is, the bulk is content (core_grammar.py)
The logit-lens (C) hint — directions decode to grammatical classes, not content — is the Chomsky hypothesis: is
the core a content-free grammatical scaffold? A grammar core would be both generic (corpus-invariant) and
grammatical (closed-class). Fitting Ug on three structurally distinct corpora (Shakespeare drama / a modern novel
/ Python source) and binning the shared directions by sharedness rank (GPT-2):
| sharedness rank | cross-corpus overlap vs chance | closed-class fraction vs random | verdict |
|---|---|---|---|
| top-16 (most shared) | 0.44 vs 0.02 (22× chance) | 0.28 vs 0.00 (28× base) | generic AND grammatical — the grammar head |
| mid 16–64 | 0.23 vs 0.06 (4×) | 0.03 | fading |
| deep 64–K | 0.35 vs 0.28 (~1×) | 0.00 | content (corpus-specific) |
| content tail | 0.43 vs 0.36 (~1×) | 0.00 | content / rare-token (inventoryQuantity, _Lear) |
The dissociation is clean: the top ~16 most-shared directions are both corpus-invariant (22× chance) and
closed-class/grammatical (28× the base rate); everything deeper is neither. So there is a generic grammar — but
it is a compact head of ~5–16 directions, not the whole Θ(d) core. This is the “simpler-than-Chomsky” version: a
finite, content-free categorial scaffold (determiner-slot, punctuation-slot, verb-slot, pronoun-slot) — a learned
distributional POS basis, not the recursive/hierarchical syntax of generative UG (which, if present, lives in the
composition of these categories across positions — i.e. in the entangled bulk that pays the forge tax, not in the
static write-basis). And it is learned, not innate — a generic transformer grows the scaffold from data with no
syntactic prior. Cross-architecture (RoPE): Llama-3.2-1B reproduces the dissociation — top-16 directions
0.41 vs 0.01 chance (41×) corpus-invariant and 0.30 vs 0.01 (30×) closed-class (with a clean reflexive-pronoun
direction _himself _ourselves _herself _themselves and a punctuation direction visible through the multilingual-byte
noise), everything deeper neither. The grammar head is architecture-general, not a GPT-2 quirk.
(runs/disassembly/core_grammar_summary.json.)
Big-O of the core: Θ(model size), not a few templates
Putting the rank results together answers “how many templates / what scaling”: the functional per-layer rank is Θ(d) (rank-95 ≈ ⅓–⅖·d at every scale: 250/768, 398/1024, 476/1280, 509/2304 — a constant fraction, growing with width), the shared union basis is Θ(d) (K ≈ ⅓–⅖·d), so the full per-layer composition is ~Θ(nL·d) directions ≈ Θ(nL·d²) parameters — a constant fraction of the model (the forge-tax floor). The “few templates” (the dominant-variance head, PR 5–33) is only the tip and grows slowly (~O(nL)); the grammar head is smaller still (~O(1), ~16 directions). Net: low-rank truncation buys a constant factor (~⅓–⅔), not a big-O reduction; and the irreducible core scales with the model, consistent with “the entangled core grows with capability.” The pylm sister track makes the consequence runnable — a flat-file grammar idiom decompiles the compact scaffold (see pylm track), but adds little to the token-level decompilable fraction (grammar is categorial; the n-gram modes already absorb it), and the un-decompiled ~50% is exactly the content that is neither n-gram nor relational fact — the entangled composition, the forge tax restated.
The core as a TENSOR NETWORK — MPS bond-dimension levels + the composition graph (core_mps.py)
The per-layer write rank is Θ(d) (above), but that is the single-cut rank. Treating the sequence of per-layer updates (Δ₁ … Δ_nL) as a state over layer-“sites” and measuring the bond dimension χ across each layer cut (the effective rank of the cross-correlation between layers ≤ L and > L — the q-orca / entanglement-tower view) gives a very different number, and the cleanest CPU-simplification result yet:
| GPT-2 | layers | mean χ | χ range across cuts | max-bond mid-stack | χ / max-bond (mid) |
|---|---|---|---|---|---|
| gpt2 | 12 | 15.9 | 15–16 | 144 | 0.11 |
| gpt2-medium | 24 | 15.5 | 14–16 | 288 | 0.056 |
| gpt2-large | 36 | 16.4 | 14–18 | 432 | 0.037 |
The bond dimension’s participation ratio is ~16 and FLAT — independent of depth and width. Adding layers to either side of a cut does not raise the dominant cross-cut coupling: an area-law signature on the coupling spectrum. Honest correction (see the TT surrogate below): χ≈16 here is the participation ratio of the cross-cut correlation spectrum — a coupling-concentration measure dominated by a few high-variance directions — not the rank you need to reconstruct the computation (exactly the core_rank gap: participation ratio ≈ 16, but rank-95 ≈ 250). So “the layers communicate through a 16-dim channel” is true of the dominant coupling, false as a runnable state dimension. The honest statement: the cross-layer coupling is heavy-tailed-low-rank (dominant head O(1), flat with depth), the runnable bond is Θ(d) (next), and the simplification lever stays core_rank’s per-layer low-rank.
…and the TT surrogate confirms it — the runnable bond is Θ(d), not 16 (core_tt.py)
Turning the bond into a runnable, no-retrain surrogate settles the CPU question. The residual carries two things —
the token EMBEDDING (full-rank, injected once) and the inter-layer COMPUTATION — so the faithful tensor-train protects
resid_0 in full and forces only the carried computation through a χ-dim bond:
resid_{L+1} ← resid_0 + B_L Bᵀ_L (resid_{L+1} − resid_0). ΔNLL vs the full model (generic next-token):
| ΔNLL | χ=16 | χ=64 | χ=256 | reading |
|---|---|---|---|---|
| TT (emb-protected bond) gpt2 / med / large | +1.37 / +2.94 / +3.11 | +0.77 / +1.94 / +2.41 | +0.23 / +0.75 / +0.98 | the χ≈16 bond is unusable; runnable bond ~⅓·d |
| per-layer (core_rank) | +1.15 / +2.09 / +2.71 | +0.54 / +1.14 / +1.34 | +0.10 / +0.27 / +0.36 | better at every χ — the real lever |
| resid (full-residual control) | +1.44 / +2.90 / +3.14 | ≈ TT | ≈ TT | emb-protection barely helps |
Three honest findings: (1) at the area-law bond χ≈16, NLL is badly degraded (+1.4 to +3.1) — the participation
ratio is not a runnable state size; (2) the runnable no-retrain bond is ~⅓·d, and per-layer truncation beats the
running-bond TT at every χ (the TT compounds error across cuts, so it gets worse with depth: χ=256 ΔNLL
+0.23→+0.75→+0.98 as layers go 12→24→36 — the opposite of “bigger = more compressible”); (3) protecting the
embedding barely helps (TT ≈ resid control), so the un-compressible content is the computation, not the tokens. So
the only free CPU lever is core_rank’s per-layer rank-⅓·d — a constant-factor (~3×) FLOP saving, lossless, no retrain —
NOT a χ≈16 tensor-train collapse. The hard floor is the Θ(d) core (the forge tax): the irreducible composition is a
constant fraction of the model, so no fixed-size surrogate beats the model’s own size without retraining. (A
retrained TT — learn the cores instead of PCA-projecting activations — could reach a lower bond; that is the
sae-forge feature-native direction, outside the no-retrain frame. runs/disassembly/core_tt_summary.json.)
…but with RETRAINING the floor falls ~30× — the core is not irreducible (core_distill.py)
The no-retrain results above all freeze the weights and use a linear, fixed (PCA) basis — so the Θ(d) floor they hit says nothing about whether a learned low-rank representation exists. This builds one: a rank-r bottleneck on each layer’s update, initialised from PCA (step 0 = the no-retrain floor) and then trained (base model frozen, only the bottleneck factors, ~300 steps). The floor collapses:
| GPT-2 rank (per-layer update) | no-retrain ΔNLL | trained ΔNLL |
|---|---|---|
| 8 (1% of d) | +1.78 | +0.03 (≈ lossless) |
| 16 (2%) | +1.28 | −0.10 |
| 32 (4%) | +0.97 | −0.18 |
| 256 (33%, adaptation control) | +0.24 | −0.00 |
A trained rank-8 update (1% of d) is lossless, vs the no-retrain floor of ~⅓·d (256) — a ~30× rank reduction.
The rank-256 control trains to ΔNLL ≈ 0 (no meaningful domain-adaptation bonus), so the comparison to the full model
is fair: trained rank-8 genuinely matches it. So the “Θ(d) entangled core” was an artifact of freezing + linear
PCA, not a property of the function — exactly the right caution: the no-retrain experiments measure the wrong thing
for “is the core compressible.” Scale nuance: gpt2-large needs more rank/steps (rank-8 recovers 76%, rank-64 88% in
250 steps; rank-512 overfit — an optimization artifact of a huge bottleneck under-trained), so the lossless rank
grows modestly with size and training budget — but it stays far below the no-retrain floor. Honest scope: this
compresses the per-layer update (the residual write), not the full internal attention+MLP FLOPs, and uses a small
eval (20 chunks) + cheap training; a rigorous version (more steps, general eval, factoring the output projection for
real FLOP savings) is the natural follow-up. But the qualitative claim is settled: detangling/compressing the core
is tractable with learning — “irreducible” is falsified; only the frozen-linear route was blocked.
(runs/disassembly/core_distill_summary.json.)
The retrained-rank scaling law across the ladder (matched config: 300 steps, lr 1e-4)
Running the same rank-r retrained bottleneck across a 7-model ladder (Pythia 14m/70m/160m/410m + GPT-2 small/medium/large) at one config gives the scale-law of the lossless retrained rank — the rank where the trained ΔNLL first reaches base (≤ 0 on the distill corpus):
| family | model (d) | lossless rank | r/d | recovery at the no-retrain floor |
|---|---|---|---|---|
| GPT-2 | gpt2 (768) | 16 | 2.1% | rank-8 recovers 86%, then overshoots (overfit) |
| GPT-2 | gpt2-medium (1024) | 32–64 | 3–6% | rank-32 ≈ lossless (+0.00) |
| GPT-2 | gpt2-large (1280) | 64 | 5.0% | rank-32 +0.10, rank-64 −0.05 |
| NeoX | pythia-14m (128) | 32 | 25% | reaches lossless (tiny) |
| NeoX | pythia-70m (512) | > 128 | — | rank-128 only +0.00 (just reaching it) |
| NeoX | pythia-160m (768) | > 128 | — | rank-128 +0.24 (54–67% recovered) |
| NeoX | pythia-410m (1024) | > 128 | — | rank-128 +0.93 (recovery non-monotone) |
Three descriptive readings (necessity-vs-method discipline — these are recipe statements, not lower bounds): (1)
The #142 result generalizes across the GPT-2 ladder — retraining recovers the no-retrain truncation loss at every
rung (54–211%), and the lossless rank stays a small fraction of d (r/d ≈ 2–6%), growing modestly with size (16 → ~32
→ 64 over 768 → 1024 → 1280). (2) But it is NOT basis-robust across architecture at this budget — the GPT-NeoX
(Pythia) models ≥ 70m do not reach lossless within rank 128 at 300 steps / lr 1e-4, where the GPT-2 models do (and
recover far more per rank). So the NeoX disadvantage seen in frozen truncation (above) persists under retraining at
this budget — it is not purely a frozen-linear artifact; whether more steps / a higher stable lr close the gap is
open (the tiny pythia-14m does reach lossless, so the method itself works on NeoX). (3) “Lossless” here means
matches base NLL on the distill corpus — the negative ΔNLL / >100% recovery at high rank is the bottleneck factors
overfitting the small corpus, not free compression; a general-eval version is the natural follow-up.
(runs/disassembly/core_distill_summary.json.)
…but compression is legibility-NEUTRAL — small ≠ legible for free (compress_legibility.py)
The feature-native endgame (train an N× smaller model → extract clean features / knowledge / circuits → the small
Python program → runtime explainability) hinges on whether compression aligns with interpretability. It does not, for
NLL-only compression: logit-lens-scoring the rank-32 directions the trained bottleneck writes into vs the untrained
PCA directions, both interpretability proxies are unchanged — peak-z 5.00 → 4.96, closed-class fraction 0.10 →
0.09 (marginally lower, within noise). So distillation buys the size win (#142) but not a feature-cleanliness
win: compression and per-feature monosemanticity are decoupled under an NLL objective. Consequence for the
pipeline: to get both small and legible you must train with an explicit legibility / sparsity term (the
sae-forge feature-native objective) — pure NLL-distillation packs the same entanglement into fewer dimensions. The
enabling facts are all in hand (smaller = more decompilable; compression is tractable; feature/knowledge/circuit
extractors + pylm + runtime idiom-traces exist), but the missing ingredient is the legibility objective, not more
compression. (runs/disassembly/compress_legibility_summary.json.)
…and a naive legibility term can’t fix it on a tight bottleneck — the small / legible / complete trade-off triangle (feature_native_distill.py)
Adding the obvious legibility objective — an L1 sparsity penalty on the bottleneck codes (the sae-style term) — does not rescue legibility on a compressive rank-32 bottleneck: sweeping λ ∈ {0, 0.01, 0.05, 0.2}, peak-z stays ~4.95 and the code-active fraction stays ~0.50 (L1 doesn’t sparsify), while NLL only rises (+0.19 at λ=0.2). The reason is structural: a tight (rank < d) bottleneck has no room to be sparse — every direction is genuinely used, so L1 shrinks them all uniformly instead of zeroing any. Legibility needs overcompleteness — an SAE has more features than dimensions and then sparsifies, which is the room to specialize — and overcompleteness is the opposite of compression. So the user’s “train smaller and extract clean features” runs into the forge tax as a trade-off triangle — small / legible / complete, pick ~two:
| corner | small | legible | complete | what it is |
|---|---|---|---|---|
| pylm | ✓ (49 LOC + flat) | ✓ (every token = a named idiom) | ✗ (~half) | the decompiled program |
| low-rank distill (#142) | ✓ (~30×) | ✗ (legibility-neutral) | ✓ (lossless) | compression |
| SAE / feature-native | ✗ (overcomplete) | ✓ (sparse monosemantic) | ✓ | the legible basis |
The forge tax is this triangle: monosemanticity (overcomplete + sparse) and compactness (tight low-rank) pull in
opposite directions, and the full-fidelity composition resists both unless you pay one of them. The whole pipeline
(train smaller → extract features/knowledge/circuits → the small Python program → runtime explainability) is buildable,
but it cannot be small and legible and complete at once — you pick the corner per use-case (pylm for an auditable
runtime; distillation for a cheap one; an overcomplete feature-native model for an interpretable one).
(runs/disassembly/feature_native_distill_summary.json.)
Building the legible corner — and pricing it (feature_native_sae.py). Training the overcomplete corner directly
(an SAE on each layer’s update — MSE + L1, the standard recipe — vs the tight rank-32 PCA bottleneck) makes the size
cost explicit. At 2×d (F=1536), the SAE is complete (99% variance, ΔNLL ≈ 0) and big (28 M params), but sweeping
the L1 weight λ∈{.02,.1,.4,1} it never gets sparse or legible: L0 stays ≥ 0.30 (≥30% of features active) and
logit-lens peak-z is flat (~4.8, below the tight bottleneck’s 5.0):
| corner | ΔNLL | variance | L0 (sparsity) | size |
|---|---|---|---|---|
| tight rank-32 (small) | +0.58 | 65% | — (dense by construction) | 0.6 M |
| SAE 2×d, λ=0.02 | +0.01 | 99% | 0.81 | 28 M |
| SAE 2×d, λ=1.0 | +0.00 | 94% | 0.30 | 28 M |
Two structural reasons it won’t sparsify at 2×: the update is genuinely high-rank (rank-95 ≈ ⅓·d, from core_rank),
so reconstructing it needs ~⅓·d active features — L0 can’t fall below ~256/F; and 2×d is not enough
overcompleteness — real monosemantic SAEs run 8–64×d (jbloom’s GPT-2 SAE is 32×d = 24,576), where ~256/24,576 ≈ 1%
can be sparse. So the size cost of legibility scales with the update’s intrinsic rank: the more entangled the core,
the more overcomplete (bigger) the legible basis must be. That is the triangle’s deepest form — small/legible/complete
trade against each other because the composition is high-rank, and the legible corner’s price (overcompleteness) is set
by exactly the Θ(d) rank that the compressed corner squeezes out. (Proper monosemantic training at 8–64×d is the
sae-forge sub-project; here the 2× run prices the corner and confirms the trade-off is structural, not a tuning miss.)
(runs/disassembly/feature_native_sae_summary.json.)
The real legible corner, characterized — and it is not complete (legible_corner.py). A properly-trained 32×d
SAE (jbloom GPT-2, F=24,576, resid_pre, all layers) is genuinely sparse (mean L0 0.025 — ~624 active of 24,576)
and legible (decoder logit-lens peak-z 6.99, well above the 2×d SAE’s 4.8 and PCA’s 5.0) — confirming legibility
needs the high overcompleteness #146 predicted. But composed across all 12 layers it pays the forge tax in full: the
SAE-bottleneck generic NLL is 5.22 → 9.16 (Δ +3.94, near-random) — not complete. So the legible corner is
legible + sparse + big but NOT complete: sparsity and completeness themselves trade off (the dense 2×d
reconstruction of #146 was complete-but-illegible; the sparse 32×d is legible-but-forge-tax-incomplete). This sharpens
the triangle — legible-AND-complete is itself blocked by the forge tax, because composing sparse monosemantic features
across layers is exactly the composition that doesn’t factor through the feature basis. (runs/disassembly/legible_corner_summary.json.)
Minimum-to-run frontier — how small can the stored weights get (min_to_run.py)
How few stored bytes reproduce the model? Low-rank-factorize every attention + MLP weight matrix to rank r (embeddings left intact), and measure fidelity vs stored size. No-retrain SVD truncation is useless (the weights are not low-rank): even at rank 512 (89% of the weights kept) ΔNLL is +1.1 and agreement-with-the-full-model only 26%; at low rank it is near-random. But distilling the factors (init from SVD, train them ~400 steps, everything else frozen) recovers most of it:
| GPT-2 weights (85 M) | rank 8 (1% kept) | rank 32 (6%) | rank 64 (11%) | rank 128 (22%) |
|---|---|---|---|---|
| no-retrain SVD ΔNLL | +3.20 | +2.54 | +2.51 | +2.40 |
| distilled ΔNLL | +0.54 | +0.36 | +0.26 | +0.28 |
…because plain SVD is the wrong form, not the wrong rank — a data-aware truncation is far better, no retraining (min_to_run.py --data-aware)
Plain SVD minimizes weight-Frobenius error; what we actually care about is output error on the real activation
distribution. A data-aware (activation-weighted / functional) low-rank truncation at the same rank — SVD in the
data norm, --data-aware — is dramatically better, and the gap widens with rank, across a 6-rung ladder spanning
both the GPT-2 and GPT-NeoX (Pythia) families (1 M → 302 M transformer-weight params; ΔNLL vs the full model, lower is
better):
| no-retrain ΔNLL | rank 64 | rank 256 | rank 512 |
|---|---|---|---|
| gpt2 (85 M) — plain SVD / data-aware | +2.51 / +1.04 | +2.18 / +0.11 | +1.13 / −0.04 |
| pythia-160m (85 M) — plain SVD / data-aware | +5.17 / +1.37 | +6.40 / +0.45 | +4.43 / +0.07 |
| pythia-410m (302 M) — plain SVD / data-aware | +5.96 / +2.17 | +8.86 / +0.76 | +6.27 / +0.20 |
| gpt2-medium (302 M) — plain SVD / data-aware | +4.14 / +1.47 | +3.77 / +0.46 | +4.26 / +0.00 |
At rank 512 the data-aware truncation is 30–62× lower ΔNLL than plain SVD (and ~lossless on gpt2: −0.04 vs +1.13).
Two consequences. (1) The apparent “low-rank floor” was a wrong-form result, not a wrong-rank one: the same
rank-r subspace, chosen functionally, recovers far more of the model — so the frozen-SVD plateau understated
compressibility. (2) The NeoX-vs-GPT-2 contrast is largely a form artifact. Under plain SVD, Pythia (GPT-NeoX)
truncation is much more destructive than GPT-2’s (e.g. r=64: +5.17 vs +2.51) — but under the data-aware form the two
families converge (pythia-160m r=256 +0.45 ≈ gpt2 +0.11), so most of NeoX’s “extra destruction” was the wrong
norm on its basis, not an architectural fact. And the static data-aware truncation (no training) is already close to —
and at matched rank sometimes beats — plain-SVD-with-distillation (pythia-160m r=256: data-aware-static +0.45 < the
plain-SVD self-distill floor +0.65), i.e. a static functional diagnostic captures most of the achievable compression.
(Descriptive, per the necessity-vs-method discipline: this says the diagnostic was weak, not that the function is
small; “irreducible” claims still require a proof we don’t have.) Scope cap: the data-aware covariance accumulation
OOM’d on the 8 GB GPU for pythia-1b and gpt2-large, so those two rungs have plain-SVD but not data-aware here.
(runs/disassembly/min_to_run_summary.json, @svd vs @svd+daware tags.)
So with light distillation the composition weights factor to ~6–11% of their size for a model still capable on the corpus (ΔNLL +0.26 at rank 64 ≈ 9× weight compression). Two notes on scope: (1) this is a capable model, not a faithful copy of GPT-2 — faithfulness (top-1 agreement with the original) is a separate, harder axis: distilling the factors to the teacher (hard-label CE and soft-KL, 1500 steps, ranks 32–256) plateaus at ~34–38% agreement, flat across rank (6% → 44% of weights), recipe, and data — scaling the distillation corpus ~20× (12.8 k → 280 k tokens) and steps to 5 000 moved rank-64 only 34% → 38%. So the cap is not primarily data-starvation. The clean dissociation: the factored model is capable (NLL ΔNLL +0.26–0.64) but predicts different tokens than the original on the composition-dependent ~60% — a similarly-good different model, not a faithful copy. Careful framing (per the necessity-vs-method discipline): this is robust evidence that this approach — factoring all 48 matrices at one rank
- single-GPU distillation — caps at ~⅓ faithful agreement; it is not an impossibility proof. Notably, the #142
output-bottleneck (rank-8 on the per-layer update, NLL-lossless) is the faithful compression that weight-factoring
isn’t — but it keeps the full weight matrices, so it doesn’t shrink storage. The real tension surfacing here is
faithful ⟂ small-storage: compressing the output preserves the model’s exact behaviour but not its size;
compressing the weights shrinks size but yields a capable-not-faithful model. (2) It compresses the stored weights, with the embeddings/vocab left intact. Placed against the
flat end — pylm reproduces ~half the model at ~1.6 MB of flat tables and zero matmul — this is the first point on
the size-vs-fidelity curve: pure-flat retrieval (cheap, ~half) → distilled-low-rank composition (capable at ~⅒ the
weights; ~⅓-faithful so far) → the full model. (
runs/disassembly/min_to_run_summary.json.)
Data scale and variety move faithfulness — and reveal it is content-dependent. Scaling the distillation corpus
(single-domain Shakespeare 12.8 k → 280 k tokens) lifted rank-64 faithful agreement 34% → 38%; a diverse 5-domain
corpus (drama + 3 novels + code) lifted it to 41% — monotone, no wall (so the cap is recipe/data-shaped, not a proven
floor). Adding Wikipedia (6 domains) and breaking agreement down per domain exposes a huge spread the aggregate
hid: austen 85% · shakespeare 34% · melville 33% · code 26% · shelley 25% · wiki 24%. The reading: a low-rank copy
reproduces GPT-2 where its behaviour is retrieval / memorization (Pride & Prejudice — heavily-reproduced in WebText,
near-deterministic — 85%) and fails where it is composition (Wikipedia facts, code structure — 24–26%). So the ~⅓
aggregate is a mix of near-fully-reproducible retrieval content and near-irreducible composition content — the same
retrieval-vs-composition split, now measured by domain. (Confound: austen’s 85% conflates “memorized/predictable” with
“stylistically self-consistent” — train and eval are the same novel — so the absolute spread over-states it; but the
composition-heavy domains being hardest is the robust signal.) (runs/disassembly/min_to_run_summary.json.)
Variety helps only up to capacity — then it dilutes (at fixed rank). Pushing to 15 domains (12 novels across
genres/eras + drama + code + wiki) at the same rank-64 / 12 k steps drops the aggregate to 30%, and the per-domain
breakdown shows why: the copy faithfully holds only the heavily-memorized domains (austen 76% · stoker/Dracula 73% ·
dickens 46%) while the rest — and the composition-heavy ones especially — fall to 12–32% (kjv 12 · machiavelli 12 ·
wiki 16 · grimm 18 · code 20). So at fixed low-rank capacity, more variety spreads the model thinner rather than
lifting it: capacity becomes the binding constraint across breadth. This says the variety push needs more rank in
proportion to the breadth — consistent with the composition being Θ(d) and content-spanning — and reframes the earlier
34→41% climb as “variety helps until the rank-64 budget is spent.” (Still about this approach, not a floor.)
Multilingual sharpens it: adding 6 non-English Wikipedia domains (rank 128, 21 domains) drops the aggregate to 21%,
and the tail is dominated by the non-English domains — wiki_ru 0% · wiki_fr 4% · wiki_ja 5% · wiki_de/es 10% — because
GPT-2 is out-of-distribution there: its non-English predictions are the least retrieval-like (no memorised
non-English n-grams) and so the hardest for a low-rank copy to reproduce, while heavily-memorised English stays high
(stoker 77 · austen 75). The reproducible-vs-not split tracks how in-distribution / memorised the content is.
A stronger teacher does not help a capacity-bound core — it hurts it (--teacher). Distilling a small core to a
different, more capable same-tokenizer teacher (gpt2-medium core ← gpt2-XL, soft top-k KL, teacher pre-computed then
freed so the student trains alone) is worse than self-distillation: XL-distill gives ΔNLL +3.4 (a bad model, ~30%
agreement with XL) vs self-distill +0.77 (a good medium model, ~recovered). The medium architecture cannot
represent XL’s distribution, so chasing its (unreachable) targets drags the core away from being a good medium model
rather than transferring XL’s capability; raising the rank 256→512 nudged XL-agreement only 27→31% — “sizing the SVD for
the teacher” within a smaller architecture can’t reach a bigger one. The minimum capacity to reproduce XL is bounded
by XL’s own irreducible complexity (the forge tax): you can’t make a small model capable by distilling from a big one
if it lacks the capacity. (A capability benchmark — benchmark.py, held-out perplexity + next-token + LAMBADA-style
last-word accuracy — scores whether a produced core is good even when it isn’t a faithful copy.)
(runs/disassembly/teacher_distill_summary.json.)
Capability ladder (the baselines). Scoring the full models on the benchmark (held-out perplexity · next-token · last-word, distinct from the distillation corpus):
| model | perplexity | next-token | last-word |
|---|---|---|---|
| gpt2 | 62.4 | 27.9% | 26.5% |
| gpt2-medium | 47.4 | 31.2% | 27.5% |
| gpt2-large | 41.6 | 32.8% | 30.0% |
| gpt2-xl | 38.7 | 33.8% | 29.5% |
| Qwen2.5-1.5B | 22.5 | 37.2% | 37.5% |
| Llama-3.2-3B | 18.9 | 39.6% | 46.5% |
The GPT-2 family improves smoothly with size (62→39 ppl), but the modern models are in a different regime —
Qwen2.5-1.5B (22.5) and Llama-3.2-3B (18.9) roughly halve GPT-2-XL’s perplexity at similar/smaller size, with
last-word accuracy 37–46% vs ~30%. This is the capability counterpart to the lower modern decompilable fraction
(more capability ⇒ more entangled composition ⇒ less flat-reproducible) and the baseline any compressed core is judged
against. (runs/disassembly/benchmark_summary.json.)
The clean Pythia ladder confirms the capacity-binding (controlled). The GPT-2 family confounds size with training
recipe/data; the Pythia ladder holds those fixed (same Pile data, same GPT-NeoX architecture, same tokenizer — only
width/depth differ), so it isolates size. (min_to_run.py now factors GPT-NeoX / nn.Linear weights as well as
GPT-2 Conv1D — composition_mats factors the effective in→out map, Wᵀ for Linear.) At dims identical to the GPT-2
anchors — pythia-160m = 768/12 ≡ gpt2-small, pythia-410m = 1024/24 ≡ gpt2-medium — over 2500 steps at lr 1e-4:
self-distillation recovers at every rung (pythia-70m ΔNLL +0.13, pythia-160m +0.63 at full rank), while a
bigger teacher degrades a smaller student at every rank — pythia-70m←160m +3.9, pythia-160m←410m +4.0, and a
two-rung gap (160m←1b) is no better (+3.9). The bigger teacher’s distribution is unreachable for the smaller
student; chasing it drags it off being a good model of its own size — the same capacity-binding the confounded GPT-2
ladder could only hint at. Two method caveats logged (not findings): at lr 3e-4 the cross-teacher KL diverges
(ΔNLL +169 — an optimization blow-up, not capacity; lr 1e-4 is stable), and GPT-NeoX no-retrain SVD-truncation is
far more destructive than GPT-2’s at the same dims (+6.40 vs +2.18 ΔNLL at rank 256) — the same linear-low-rank form
fits NeoX worse. (runs/disassembly/min_to_run_summary.json, keys pythia-*@self / @teacher=*.)
The low-rank basis was the weak link — not low-rank itself: functional (activation-weighted) SVD is near-lossless
with no retraining (--data-aware). Plain SVD minimizes ‖W−AB‖_F, spending rank on weight directions the
activations never visit; the model only cares about ‖X(W−AB)‖. Whitening each matrix by its input covariance C, taking
the rank-r SVD in that norm (P = C^{-1/2}U plays U’s role, so the factor pipeline is unchanged), and unwhitening —
no training — collapses the no-retrain error at the same rank: on pythia-160m, rank-256 ΔNLL +6.40 → +0.45
(14×) and rank-512 +4.43 → +0.07 (~lossless); on gpt2, rank-256 +2.18 → +0.11 (20×) and rank-512 +1.13 →
−0.04 (denoises). The headline: data-aware no-retrain at rank 256 (+0.45) matches/beats the trained
self-distill at the same rank (+0.65 after 2500 steps) — so the earlier “you must train the factors to move off the
SVD subspace” was an artifact of the wrong norm; the right low-rank subspace is analytic, and 2500 gradient steps
were largely rediscovering it. This sharpens (does not close) the open question: the residual floor — data-aware still
wants ~⅓–½ rank for lossless — is the genuinely-composed core, and whether richer forms (cross-layer TT/MPS,
a nonlinear bottleneck, or a sparse-overcomplete basis) beat that floor is the next test, rather than whether linear
low-rank per se is too weak. (runs/disassembly/min_to_run_summary.json, keys *@svd vs *@svd+daware.)
Retraining the data-aware factors does not move the lossless floor to low rank — it shifts the capable-compression
curve left ~3×. The full 2×2 ({plain, data-aware} × {no-retrain, 2500-step self-distill}) on pythia-160m, ΔNLL by
rank: data-aware init gives retraining a large head start — data-aware+retrain reaches a given fidelity at ~⅓ the rank
of plain+retrain (rank-64 +0.60 ≈ plain+retrain at rank-256 +0.65) — but the low-rank end plateaus
capable-not-faithful: data-aware+retrain is +0.77 / +0.60 / +0.45 / +0.37 at rank 32 / 64 / 128 / 256, never
approaching the +0.1 that data-aware no-retrain reaches only at rank ~512 (⅔d), and flattening by rank 256
(128→256 gains 0.08). So under this recipe the near-lossless floor stays ~⅔d — retraining stops the rank-waste but
does not shrink the composed core. (Recipe-shaped, not a proven floor: a stronger recipe or a richer form — cross-layer
TT/MPS, a nonlinear bottleneck, or a sparse-overcomplete basis — is the open path to beating ~⅔d.)
(runs/disassembly/min_to_run_summary.json, keys *@retrain / @retrain+daware.)
Richer-form survey vs the linear floor — (1/3) a per-matrix nonlinear bottleneck does not help. First of the three
richer forms (--nonlinear): an α-gated GELU bottleneck code + α·GELU(code), with α a learnable per-code-channel gate
init 0 — a strict superset of the linear factor, so it starts exactly at the data-aware linear baseline and can
only improve by adding nonlinearity. Data-aware-init + 2500-step self-distill on pythia-160m lands within noise of
linear at every rank: ΔNLL +0.77 / +0.61 / +0.44 / +0.35 at rank 32 / 64 / 128 / 256 (vs linear’s
+0.77 / +0.60 / +0.45 / +0.37); the α gate moves off 0 but nets nothing. So per-matrix nonlinearity is not the missing
structure either — the floor is robust to {linear, nonlinear} × {Frobenius, data-aware} × {no-retrain, retrain}, i.e.
to any per-matrix method. That points the remaining two tests (cross-layer coupling, sparse-overcompleteness) at
structure that spans matrices — consistent with the area-law cross-layer entanglement the composition graph shows.
(runs/disassembly/min_to_run_summary.json, key *@retrain+daware+nl.)
Richer-form survey — (2/3) cross-layer weight-sharing HURTS, monotonically (--share both / --share cp). Testing
whether the layers share composition structure that per-matrix methods miss. Two joint factorizations of the layer
stack, compared to independent per-matrix at matched stored size (pythia-160m, data-aware+retrain independent
baseline): Tucker (--share both: shared in/out bases per matrix-type + a per-layer full core k×k) and CP
(--share cp: shared rank-1 atoms read⊗write, each layer a per-atom scalar gate — i.e. the literal “cross-layer
circuit” form, atoms capped at d so storage tops out ~11%). Trained 2500 steps, self-distill. ΔNLL at matched
storage: independent +0.60 (11%) → Tucker +1.16–1.30 → CP +1.85–2.01 — a clean monotonic ordering:
independent < Tucker < CP (the more sharing is forced, the worse it gets; init-robust — Tucker has a good HOSVD
init). So the weight-level “few circuits” hypothesis is falsified: the layers’ composition weights occupy genuinely
distinct subspaces; there is no compact shared cross-layer weight structure to exploit.
The resolution (and it reconciles this with the circuit catalog): two different objects had opposite answers.
Weight-circuits — can the weight matrices be rebuilt from a few shared cross-layer atoms? No (this survey:
high-rank, layer-distinct). Activation-circuits — do a few computational pathways carry the forward pass? Yes (the
reconstruction-coverage milestone: induction / number-mover / …). These are consistent: a few sparse pathways threaded
through high-dimensional, layer-distinct weight matrices. The computation is few-circuits (why the model reads as
circuits); the weights are Θ(d) and distinct (why the size / forge-tax doesn’t compress). Net for the survey: the
~⅔d per-matrix data-aware floor is robust to {linear, nonlinear} × {per-matrix, cross-layer Tucker, cross-layer CP} —
the composed core’s size is genuinely ~⅔d per layer and layer-distinct, even though its wiring is sparse. (Caveat:
CP’s HOSVD-diagonal init is rough; but the monotonic ordering holds regardless, and even good-init Tucker loses to
independent, so a better CP init would not overturn it. A targeted cross-layer form — adjacent-only coupling, not
type-global sharing — remains untested.) (runs/disassembly/min_to_run_summary.json, keys *@retrain+share-both / +share-cp.)
Seeing the cross-layer channels — circuits teased from the activation coupling (circuit_channels.py)
The survey’s resolution (computation = sparse circuits, weights = high-rank/distinct) says circuits should be visible in the activation flow, which is low-dimensional. This isolates them: capture every layer’s residual write Δ_L, form the aggregate early write (layers ≤ cut) and late write (layers > cut) per token, unit-normalise each (directional coupling, else the last layer’s magnitude swamps it), and SVD the d×d cross-covariance C = Σ_p e_p⊗ℓ_p into channels (u_i early-dir, w_i late-dir). Each channel is then separately represented: writer/reader layer profile (directional, magnitude-normalised), logit-lens of u_i/w_i, and a firing pattern (per-position activation e_p·u_i, its duplicate-token enrichment and positional correlation). pythia-160m, cut 6/12:
- A dominant clause-boundary channel (ch0, 88% of the coupling): writer layers [0,1,3] → reader [9,10,11], logit-lens
\n , I . ;— early layers write a sentence-structure signal that late layers read to place punctuation. A clean early→late circuit. - Induction / duplicate family (firing duplicate-enriched ≥1.7×): ch2 (
worse force wound, ×2.77), ch6 (LaTeX tokens, ×2.19), ch9 (×2.37), ch12 (×2.28), ch13 (×2.01) — the catalog’s induction, surfacing as coupling channels. - Grammar channels the catalog does not name as cross-layer channels: ch14
if although however though since(discourse connectives, mid-layers 5–9), ch11the their these such(determiners), ch15is was occurs(auxiliary/copula). - Content-domain channels: ch8
authorities legislation establishment(formal/legal), ch10thee thy unto(archaic).
So circuits are visible, teasable, and separately representable from the high-dimensional activations — correlationally.
The honest limit shows up under causal ablation (--causal: project a channel’s direction out of the residual at the cut,
so late layers can’t read it, and split ΔNLL by next-token category — punctuation / duplicate / other). Baseline NLL punct 2.51 /
dup 1.94 / other 5.60. Ablating a single channel direction is NOT cleanly targeted: the dominant ch0 (88%) is catastrophic
everywhere (ΔNLL +2.87 / +3.54 / +5.20 — a high-variance general axis, not a punctuation-specific circuit), while every small
interpretable channel is diffuse and weak (mostly +0.04–0.2, “other” rising as much as “dup” — the induction-labeled
channels do not specifically hurt duplicate-token prediction). So the layered answer to “can a circuit be separately
represented?”: correlationally yes (decompose + label the coupling channels), but causally, as a single residual
direction, no (one-direction ablation is global or diffuse — redundancy/distribution defeats it); causal circuits ARE real but
live at the component (head/MLP) level, where targeted ablation works (the catalog: induction, the number-mover via
agreement_circuit). The channel decomposition is a correlational lens on the coupling, not its causal factorization.
Other caveats: aggregate-cut version (χ≈1.3, dominated by ch0; the per-layer-resolved core_mps coupling, χ≈16, would
expose finer channels); some lenses are rare-token noise. (runs/disassembly/circuit_channels_summary.json.)
Did the catalog miss these — head-level circuits or known classes? (head_category_scan.py). Mean-ablate each
attention head, split ΔNLL by next-token category (punct / dup / other). On pythia-160m: induction is a real localized
head circuit — L1.H3 (dup ΔNLL +0.35, targeted +0.30) + L0.H1 / L2.H8 / L10.H5, confirming the catalog; punctuation /
boundary has NO clean head (every effect ~+0.05, distributed) — so the dominant boundary channel is real correlationally
but not a localizable circuit (we didn’t “miss” a clean one — the behaviour is diffuse); and copy-suppression (a
known anti-induction class the catalog lacks) shows only a weak hint — L0.H11 ablation lowers dup-NLL (−0.09), the
suppression signature, expected stronger on larger models (it was characterised on GPT-2 medium/large). Net: the
grammar/boundary channels are distributed computation, not missed-localizable-circuits; copy-suppression is a genuine but
faint catalog gap at this scale. (runs/disassembly/head_category_scan_summary.json.)
Are the channels topic broadcasts? (circuit_channels.py --diverse). A “topic broadcast” would be a channel set early
and held / read across the document — so on a diverse corpus (Shakespeare / Austen / code / wiki) each channel gets a
persistence (lag-1 autocorrelation, held-across-positions) and a register fraction (between-source variance, encodes
which topic). Mostly no: the dominant + structural channels are per-token circuits — ch0 punctuation (register 0.04), the
induction family (register ~0.05–0.12) — firing on local events identically across topics. But a minority are
register/topic signals (high between-source variance = they encode what kind of text): ch5 microorganisms bacterial
lipids (reg 0.45, science), ch2 LaTeX/code tokens (reg 0.43, code), ch10 multilingual scripts (reg 0.26). So
topic-broadcasting is a real but minority phenomenon — the content/register channels, not the high-variance structural
ones — and even those read as a register difference between docs more than a strongly held within-doc signal (persistence
stayed ~0.2–0.3; none crossed a strong-broadcast threshold; 64-token chunks + 4 coarse sources, so a first cut).
(runs/disassembly/circuit_channels_summary.json.)
Per-layer-RESOLVED channels expose the area-law bond (--resolved). The aggregate cut collapsed to one channel
(χ≈1.3) because summing all early/late writes destroys the per-layer structure. The faithful version (core_mps-style:
top-r PCA + standardise each layer’s write, then SVD the (early-layers·r)×(late-layers·r) cross-covariance into channels
that mix layers) recovers it: on pythia-160m (r=24, cut 6/12) the coupling participation ratio is χ≈29 with a FLAT
spectrum (top channels 6.7% → 1.7%, no dominant one) — the area-law structure, decomposed into ~20–30 roughly-equal
channels. What they are: mostly the induction / copy machinery — nearly every channel is duplicate-enriched (dup×1.3–2.1)
with writer layers early (0–6) → reader layers late (7–11), i.e. early-detect → late-act copy circuits; persistence
is higher here (several at +0.41–0.47). And the register/topic broadcasts are sharper: ch3 microorganisms bacterial
microbial bacteria (register 0.71, persistence 0.44 — a held science register, the clearest topic-broadcast), ch1
(register 0.72, byte/script tokens — a language/script register), ch8 (0.31). So the χ≈16–29 area-law bond decomposes
into ~20–30 channels: dominantly induction/copy (early→late), plus a handful of genuine register/topic broadcasts —
still a correlational lens (the causal caveat above stands). (runs/disassembly/circuit_channels_summary.json, --resolved.)
Where the incompressible core lives — diffuse content, not the circuits (core_residual.py)
If the ~⅔d size floor is caused by a few preservable circuits, exempting them would crack it. To check, data-aware-compress
every composition matrix and split the compression ΔNLL by next-token category. pythia-160m (baseline punct 2.57 / dup 2.03 /
other 5.63), ΔNLL at rank 64 / 128 / 256: punct +0.33 / +0.26 / +0.05 (boundary prediction compresses away easily —
low-rank-friendly), dup +0.92 / +0.50 / +0.10 (the induction/copy family largely recovers by rank 256 — compressible),
other (generic content) +1.66 / +1.02 / +0.48 (the residual that stays). The dup/other concentration falls 0.56 → 0.21
as rank rises: by the floor, the incompressible mass is overwhelmingly diffuse content composition, not the structural
circuits. So preserving the copy circuits would not crack the floor — the irreducible part isn’t a handful of localizable
circuits; it’s distributed content prediction. This is coherent with the rest of the survey (the channels were correlationally
circuit-like but causally distributed) and locates the forge tax precisely: the entangled core is content composition — the
“computed, not retrieved” remainder — which is why no low-rank or circuit-preserving weight trick shrinks it, and why the
only route shown to break the frozen floor is learning (the #142 trained rank-8 update; the feature-native reconstruction).
(runs/disassembly/core_residual_summary.json.)
What the content core COMPUTES — MLP-stored, context-bound, not attention-retrieval (content_mechanism.py)
Having located the forge tax on content prediction, the mechanism question (the “explain how we think” half): is that
content in-context retrieval (attention-carried) or stored computation (MLP-carried)? Mean-ablate all attention vs
all MLP, ΔNLL by category (pythia-160m, baseline punct 2.54 / dup 2.15 / other 5.71): attention +1.34 / +1.46 / +1.15
(≈uniform — structural routing), MLP +2.08 / +2.14 / +4.03 — MLP ablation hits content 2× harder than punct/dup.
So content composition is MLP-carried, not attention-retrieval — the forge tax lives in the MLPs (computed/stored
parametric), not retrieval-in-disguise. And it is genuinely context-bound: NLL on content drops monotonically
7.42 → 5.71 as the visible window grows k=1 → 64 (still falling at 64) — it needs the long context, not a local n-gram.
Reconciliation: attention gathers the context, the MLPs compute the content prediction over it — the irreducible core is
context-conditioned MLP computation. The open twist (next probe): MLPs read as key-value memories, so “MLP-stored” could
still be flat-decompilable (a big KV lookup = the Θ(size) flat-knowledge term) rather than irreducible nonlinear
composition — which is exactly the crux of whether the core unsquirrels into flat storage or stays computed.
(runs/disassembly/content_mechanism_summary.json.)
The crux: the MLP content is DENSE, not a sparse key-value lookup (mlp_kv_sparsity.py)
MLPs read as key-value memories — if only a few neurons fire per token, the content is a sparse lookup (flat-decompilable, the Θ(size) term); if it needs most of the width, it is dense composition (the irreducible forge tax). Test: mask the post-activation MLP hidden to its top-k neurons per token at every layer, ΔNLL by category. pythia-160m (d_ff=3072, baseline content NLL 5.69), ΔNLL on content keeping top-k: k=64 (2.1%) +3.52, k=256 (8.3%) +2.97, k=512 (16.7%) +2.40, k=1024 (33.3%) +1.07 — still far from recovered keeping a third of the width. A sparse lookup would recover at small k; this does not. So the MLP content computation is DENSE in the native neuron basis, not a sparse KV lookup — the core does not trivially flat-decompile into a (big) lookup table; it is genuinely distributed computation.
This closes the decompilation arc: native-basis truncation (dense, here) and weight factorization (the whole survey,
plateaus at ⅔d) both fail to sparsify/shrink the content core. The one route left — and now the indicated one — is a
learned overcomplete basis (SAE / feature-native): because the native representation is dense, any sparsity must be
learned, not found by truncation or factorization. (Framing: this shows the native basis is dense and
truncation/factorization don’t sparsify it; whether a learned overcomplete basis does is genuinely open — achievability
is not foreclosed, it’s the sae-forge sub-project’s bet.) (runs/disassembly/mlp_kv_sparsity_summary.json.)
Cross-model validation — the dense-content core is architecture-general and grows with scale
Re-ran all three probes on GPT-2 and the Pythia ladder (70m / 410m / 1b) to check the above isn’t a pythia-160m artifact.
All three hold, and strengthen with scale: (1) residual = content — “other” is the largest compression residual at low
rank with punctuation compressing first and dup/other falling with rank (gpt2 other +0.93 vs punct +0.59 @ rank 64; pythia-70m
+0.99/+0.18; pythia-410m +2.27/+1.63 → dup/other 0.58→0.37; 1b OOM’d this probe — fp32 weight-clones + SVD don’t fit).
(2) MLP-carried, not attention — MLP ablation hits content far harder than attention at every scale, and the gap widens:
attention vs MLP ΔNLL on content = gpt2 +0.96 / +2.43, pythia-70m +0.69 / +3.13, pythia-410m +1.57 / +8.91, pythia-1b
+1.70 / +8.13. (3) dense, not a sparse lookup — top-k neuron recovery of content needs ~50–67% of the hidden width on
the small models and more at scale (pythia-1b: keeping 25% of d_ff still leaves +2.95 on content). So the entangled core —
MLP-stored, dense, context-conditioned content composition — is architecture-general (GPT-2 and GPT-NeoX) and
grows with model size, consistent with the Θ(d) forge tax; it was not a single-model artifact.
(runs/disassembly/{core_residual,content_mechanism,mlp_kv_sparsity}_summary.json.)
Does the core grow faster or slower than the model? — FASTER, on both axes. The fraction of MLP neurons needed to
recover content (top-k recovery, content ΔNLL < +0.3) rises monotonically with scale: pythia-70m ~48% of d_ff →
160m ~60% → 410m ~64% → 1b ~68% (equivalently, at a fixed 25%-of-width slice the leftover content error grows with size,
70m +0.96 → 410m/1b ~+3–4). So the dense content code is a growing fraction of an already-growing width — it outpaces
the MLP width (compute axis). And the behavioral axis agrees: the pylm decompilable fraction falls 56% → 45%
(pythia-14m → 1.4b), so the core’s behavioral share rises 44% → 55%. Both say the entangled core grows faster than
the model — scaling does not dilute it, it concentrates it; bigger models are more irreducible, and the gap widens. This
is the load-bearing tension for minimum-to-run: the flat-decompilable part (knowledge + structure) does not keep pace; the
computed part outgrows it — and it is why the unsquirrel route had to be learned reconstruction (sae-forge), getting
harder, not easier, at scale. (Caveat: the recovery-fraction is native-basis effective density — a learned overcomplete
basis could shift the absolute numbers; the monotone trend is the result.) (runs/disassembly/mlp_kv_sparsity_summary.json.)
Fitting the active dimension s (neurons to recover content at content ΔNLL < 0.3) as a power law in d across the ladder
(scaling_fit.py): s ∝ d^1.23 (super-linear), so the active fraction s/m ∝ d^0.23 (robust: d^0.28 at threshold 0.5),
and the superposition overhead over the ~d/3 functional rank, s/r, grows from 5.7× → 8.1× (∝ d^0.23). So “the core grows
faster than the model” is a mild power law, not a step. (runs/disassembly/scaling_fit_summary.json.)
Measuring the two inputs to that growth (feature_economy.py): the per-token “effective features in play” k (participation
ratio of the MLP hidden) grows super-linearly, k ∝ d^1.15 (761→3731 across the ladder), while the functional overhead
s/k ≈ 1.4 stays flat and the activation-covariance effective rank stays low (~50→94, ∝ d^0.46 — concentration stays
low-dim, echoing χ≈16). So the active-dimension growth is driven almost entirely by more features active per token, not
by a growing interference/packing overhead — s ≈ 1.4·k with k ∝ d^1.15. (runs/disassembly/feature_economy_summary.json.)
Two follow-ups pin the rest. Entropy-dependence (entropy_dependence.py): binning tokens by next-token entropy H and
measuring k, k is nearly flat in H — k ∝ H^0.03 (160m) / H^0.05 (410m) (corr 0.18–0.34). So the per-token feature count
is a structural property of width, not per-token difficulty — i.e. the super-linear k growth is width-driven feature
economy, not task-complexity. Packing efficiency η (sae_dictionary.py): a first pass at fixed sparsity L0=64 looked
scale-stable, but that starved the big models (L0 ≪ k). The clean run scales L0 ∝ m (= 0.4m ≈ k per model) so
reconstruction matches each model’s density; then the variance-explained gain from 0.5×m→4×m overcompleteness grows
with scale (70m +0.028 saturating by 2×m · 160m +0.055 · 410m +0.074 · 1b +0.115 still rising at 2×m), i.e. the saturation
width m′/m rises (2×→≥4×→≫4×) ⇒ η = m/m′ shrinks with scale — bigger models pack more total features per neuron.
Crucially this does NOT contradict the flat s/k: the per-token active support s ≈ 1.4·k is k-driven (η does not enter,
β≈0), while η governs the total dictionary size m′ (∝ d^{>1}), a separate quantity — the storage/legibility cost of a
complete feature basis grows faster than the per-token compute. Net: the super-linear active-dimension growth (s ∝ d^1.15)
is k itself (width-structural, entropy-independent), with a stable functional overhead s/k≈1.4; separately, the
overcomplete dictionary needed for a legible basis grows faster than m (η↓). (Caveat: 1b’s absolute var (0.81) may be
SAE-undertrained at d_ff=8192; the η↓ trend is clear already at 70m→410m.)
(runs/disassembly/{entropy_dependence,sae_dictionary,sae_dictionary_cleanL0}_summary.json.)
k is set at initialization, not learned (checkpoint_k.py): measuring k across Pythia-160m training checkpoints
(step{N} revisions), k/m is flat from random init through training — step 0 (random) 0.438, step 1 0.438, step 1000
0.483, step 143000 (final) 0.410 (×0.94 overall, noise around ~0.42; the final value reproduces feature_economy’s
1261). So the per-token feature count is not accumulated by training dynamics — it falsifies the implicit-bias /
“warm features” mechanism and shows k is a structural / statistical property of the architecture (the participation
ratio of GELU activations at width d_ff), present in the random network. Therefore the “why k ∝ d^1.15” puzzle reduces
to a statistics-of-GELU-at-width question (architectural), not a capability or learned-feature question — a much more
tractable target. (The functional support s = top-k recovery is trained-dependent but tracks this geometric k as
s ≈ 1.4·k in the trained model.) (runs/disassembly/checkpoint_k_summary.json.)
The d^0.15 excess is from MARGINAL variance heterogeneity, not input covariance (init_statistics.py). Decomposing the
architectural k: a pure-iid homogeneous-variance Gaussian → GELU gives k ∝ d^1.01 (linear, k/m ≈ 0.42 = the GELU
constant c). Real activations give k ∝ d^1.16; column-shuffling the input (preserve per-coordinate marginals, destroy
all cross-coordinate correlation) gives k ∝ d^1.23 — more super-linear. So the excess survives removing all input
correlation → it lives in the marginal variance heterogeneity of the pre-activations (W↑x)ᵢ (the per-neuron variance
spread, which grows with width), not the covariance structure. And cross-coordinate correlation reduces the PR
(k_real < k_shuf), the opposite of a “correlation delocalizes” hypothesis. So the d^0.15 reduces to: the spread of
per-neuron pre-activation variances grows with width, and GELU’s moment ratio (E|·|)²/E[·²] is σ-dependent, inflating the
participation ratio super-linearly. (runs/disassembly/init_statistics_summary.json.)
But measuring the variance dispersion directly (sigma_dispersion.py) corrects the natural next guess: the per-neuron σ²
coefficient of variation decreases with scale (CV ∝ d^−0.79; the σ² participation-fraction rises 0.45→0.69), so the
pre-activation variances get more homogeneous with width, not less. Yet the heterogeneity gain (k_shuf/k_iid) still
grows — so the excess is driven by the σ-regime nonlinearity of c(σ), not the dispersion magnitude. And a formula
check: the proposed closed form m·E[c(σ)] scales d^1.00 (linear — wrong); the correct vector-PR concentration limit
(Σ E|hᵢ|)²/(Σ E[hᵢ²]) scales d^1.23 (overshoots); the actual per-token k is d^1.15, between the two — so finite-m
correlations between |hᵢ| and hᵢ² matter and neither factorised form is exact. (runs/disassembly/sigma_dispersion_summary.json.)
Router-kernel prototype — the conditional-compute payoff with an init-computed budget (router_kernel.py). Putting the
scaling result to work: the per-token active budget is architectural, B(d) ≈ 1.4·k(d), and k = the GELU participation ratio
is computable from the model itself (or even random weights). The kernel (1) measures k → B → expert budget E = round(B·K/m);
(2) trains the informed router (gate from [local input ⊕ early-layer global context], straight-through hard top-E over K
static clusters) with the KL(full ‖ routed) objective at that fixed budget, base frozen; (3) reports KL-preservation at
active fraction E/K vs a random-router control. Result: gpt2 E=40/64 (62% active MLP), KL(full‖routed) 0.38
(learned) vs 1.77 (random) — 4.6× better; pythia-160m E=37/64 (58% active), KL 0.70 vs 3.81 — 5.4× better.
So the architecturally-determined budget + informed router preserves the full model far better than random routing at ~60%
active MLP — “store the whole MLP, compute ~60% of it per token.” The ~40% saving is real but bounded by the dense content
(k/m ≈ 0.4, so B/m ≈ 0.6); the KL is preserved-but-moderate at that budget (a higher budget or better router lowers it).
(runs/disassembly/router_kernel_summary.json.)
Runtime (conditional) sparsity — is the dense content expert-sparse? (mlp_experts.py)
Storage-sparsity (low-rank, native top-k, an L1-SAE up to 43×d) doesn’t sparsify the MLP content. A different axis is
runtime conditional sparsity (MoEfication): cluster each MLP’s neurons into K experts and per token only compute the
active experts — you still store every expert, but run few. Test: cluster K=64 experts (k-means on the up-projection keys,
so co-activating neurons group), route at the expert level (keep the top-E experts per token by activation mass), measure
content recovery. Result (K=64, avg expert ≈48 neurons): content recovers (ΔNLL → small) only near E≈48/64 (75%) for
pythia-160m and E≈32/64 (50%) for gpt2 — comparable to the neuron-level top-k (~60–67%). So this static MoEfication
does not make the content expert-sparse; it is about as dense over experts as over neurons (the copy/dup family is
somewhat more expert-localized). Caveat — this is the floor, not the ceiling: the clustering (key-direction k-means)
and router (mass top-E) are naive; a learned router / co-activation clustering can concentrate routing far more (that is
what a trained MoE / the feature-native model would learn). So conditional-compute remains an open lever — a static
read-off doesn’t deliver it, but the learned version is untested. (runs/disassembly/mlp_experts_summary.json.)
A LEARNED router does deliver conditional sparsity — for GPT-2 (mlp_router.py, mlp_router2.py)
The static read-off (mass-routing) isn’t the only router. A learned gate, trained on the loss, is the open lever.
First attempt (mlp_router.py: a per-layer linear gate from the local MLP input, soft-trained, hard-evaluated)
underperformed static mass-routing — a soft-weight/hard-select mismatch plus a weak memoryless local gate. Fixing both
along the lines the channel decomposition suggests (mlp_router2.py): (1) global context — feed every layer’s router
an early-layer residual vector (carrying the register/topic broadcast + induction-detect state — gather early, route
late); (2) straight-through hard top-E selection (train what you evaluate). The informed router then beats both the
naive gate and static mass-routing at every E, on both models, and for GPT-2 reaches genuine conditional sparsity:
content recovers at E≈8/64 (12% of experts, ΔNLL +0.36) — where static mass-routing needed ~50% and neuron top-k ~67%
— and at 50% it denoises (−0.22). pythia-160m is harder: the informed router helps (~1.4 NLL better than mass at low
E) but content stays dense (+2.7 at 12%), so its content is genuinely less routable. So conditional/learned routing — with
a router informed by the broadcast/cross-layer structure — is a real lever the static decompositions missed (strong for
GPT-2, partial for pythia-160m). Open next steps the structure suggests: a recurrent gate over the χ≈16 cross-layer bond, a
hierarchical per-sequence topic gate, and band-shared routing across adjacent layers.
(runs/disassembly/mlp_router_summary.json, runs/disassembly/mlp_router2_summary.json.)
The composition graph (mean-squared canonical correlation between layer-pair write coords) is densely coupled —
every pair far above chance (0.34–0.56 vs 0.009) — with adjacent-layer coupling > distant (0.49–0.53 vs 0.34–0.37)
and the strongest edges clustered at the output-assembly end (late-layer pairs) plus the embedding edge 0→1. A
banded DAG over layers: local-dominant but globally coupled, condensing through the low bond.
Ontology (each layer’s top write directions typed grammar / content by logit-lens): grammar concentrates at the
boundaries — the embedding end (detokenise into grammatical classes) and, most strongly, the output end (write
the grammatical next-token classes) — and is sparse in the middle (content / compute). The U-shape is cleanest in
gpt2-small (4 5 1 1 2 2 1 2 3 7 5 8); in larger models the output-end concentration dominates and the embedding-end
grammar is partly masked by rare-/byte-token directions. So the typed graph: grammatical scaffold at the rim,
entangled content composition in the core — the ontology of “what the core is made of,” per layer.
(runs/disassembly/core_mps_summary.json.)
Where the recursive syntax lives — in the composition, not the basis (recursive_syntax.py)
The static basis holds only the categorial grammar (POS-class directions). The open question: is the genuinely
Chomskyan part — hierarchy, long-range dependency, recursion — in that basis, or in the composition? The cleanest
test is subject–verb agreement across intervening attractors (Linzen et al.): “The key near the cabinets is/are”
— the verb must agree with the head (key, singular), not the nearest noun (cabinets, plural). A flat local
program follows the nearest noun; true hierarchical syntax tracks the head across depth. Three read-only measurements:
| attractor depth | MODEL (full) | MODEL, attention-ablated | FLAT pylm (head / attractor) |
|---|---|---|---|
| 0 (local) | 100% (Δ+5.0) | 98% (Δ+2.1) — number is on the last token | 53% / 47% (no attractor yet) |
| 1 | 100% (Δ+2.7) | 0% (Δ−2.2) — follows the attractor | 0% / 100% |
| 2 | 97% (Δ+2.4) | 1% (Δ−2.2) | 0% / 100% |
| 3 | 100% (Δ+2.0) | 0% (Δ−2.3) | 0% / 100% |
(GPT-2 small; logit-diff Δ = lp(correct) − lp(wrong).) Three findings, all confirming the hypothesis:
- The model does true hierarchical agreement — ~100% across depth, resisting the attractor; and the logit-diff degrades with distance (+5.0 → +2.0), the bounded-depth signature of a single forward pass (TC⁰: recursion isn’t free — it weakens with depth, and would need the decode loop / scratchpad to go deeper).
- The flat decompilation cannot — pylm (n-gram + induction + categorial grammar) is 0% head / 100% attractor at depth ≥ 1: it always follows the nearest noun. The hierarchical dependency is not in the flat program (nor in the categorial grammar head it decompiles). (Caveat: pylm surfaces a verb token in only n≈6–12 of the depth ≥ 1 stimuli — out-of-distribution sentences — but of those it is 100% attractor-driven.)
- Ablating attention collapses it to the attractor — depth-0 agreement survives (the head’s number is on the last token itself), but at depth ≥ 1 the model flips to following the nearest noun (0%, logit-diff negative). So attention carries the head’s number across the attractors — the dependency lives in the attention composition, not the static grammar head. Cross-architecture: Llama-3.2-1B (RoPE) replicates it crisply (full 100/100/100/94% across depth; attention-ablated 95% at depth 0 → 3–4% at depth ≥ 1); gpt2-large likewise (full ~91–100%, attention-ablated → chance). MLP-ablation destroys the verb readout entirely (chance) in all three — the clean dissociation is attention.
This closes the grammar loop and ties it to the forge tax: the categorial grammar is in the static write-basis
(decompilable, #133); the recursive/hierarchical syntax is in the composition (the entangled core the forge tax
measures). “Simpler-than-Chomsky” in the basis, Chomskyan in the composition — demonstrated, not assumed.
(runs/disassembly/recursive_syntax_summary.json.)
Which heads carry it — the number-mover circuit (agreement_circuit.py). Per-head ablation over the depth-≥1
stimuli localizes a small, distinct circuit (GPT-2, base agreement logit-diff +2.55): the load-bearing heads are
7.4 (ablation drop +0.85), 10.9 (+0.51), 8.5 (+0.50), 2.10 (+0.50), concentrated in mid-to-late
layers. The late movers show the clean signature — they attend from the verb to the HEAD noun, not the nearest
attractor (10.9: verb→head 0.31 vs verb→attractor 0.07; 10.5: 0.35 vs 0.06; 11.10: 0.29 vs 0.09) — literally
moving the head’s number to the verb slot. There is also an early attractor-tracker (0.3 attends 97% to the last
token; 2.10 leans to the attractor) — the local pathway the agreement heads override. Crucially, none of the
number-movers are induction / prev-token / duplicate heads (all UNNAMED): the agreement circuit is a distinct
operator class, a new catalog entry — the composition that does the recursive work, made of heads the copy-idiom
catalog doesn’t name. Cross-scale: gpt2-large reproduces it — the movers are again UNNAMED mid-to-late heads
(24.3 with verb→head 0.42 vs 0.03; 32.5, 25.4, 15.10, 14.0 at the same ~60–90% relative depth).
(runs/disassembly/agreement_circuit_summary.json.)
The recursion depth limit — distance is interference-bounded, nesting breaks sooner, but the ceiling does not grow with layers (recursion_depth.py)
Is recursion depth-limited by the number of layers? Theory says a single forward pass is a fixed-depth circuit (TC⁰), so genuine nesting (each embedded clause resolved before the outer one — stack-like) is layer-bounded, while mere distance (the head noun is far away) is not — attention jumps directly across any distance in one layer. Testing both — PP-distance (“the key near the dogs near the tables is”) vs center-embedding (“the key that the dogs that the cat sees chase is”), scoring the outer verb’s agreement with the head across depth 0–5:
| nesting ceiling (acc ≥ 75%) | gpt2 12L | Llama 16L | gpt2-medium 24L | gpt2-large 36L |
|---|---|---|---|---|
| distance (PP modifiers) | 5+ | 5+ | 5+ | 5+ (gradual, never crosses 0) |
| nesting (center-embedding) | 5 | 3 | 3 | 2 (logit-diff crosses 0 at depth 5) |
- Distance is interference-bounded, not layer-bounded — accuracy stays ≥ 75% through depth 5 in every model, the logit-diff decays gradually (+5 → +1) and never flips: more attractors just add noise to a one-hop lookup.
- Nesting breaks sooner — center-embedding degrades faster (logit-diff halves by depth 2; flips negative for gpt2-large), consistent with genuine recursion being harder than distance.
- …but the nesting ceiling does not grow with layers — it shrinks with model size (12L → 5, 16L → 3, 24L → 3, 36L → 2). The opposite of “more layers → deeper recursion.”
Honest conclusion: the TC⁰ layer-bound is real in principle but is not the binding constraint here. Every model
has 12–36 layers — far more than the depth ~2–3 where center-embedding breaks — so they fail for distributional /
interference reasons, not layer exhaustion: deep center-embedding is vanishingly rare in training (and unparseable
for humans past ~2), each level adds more opposite-number signal (nouns and inner verbs), and a bigger/better model
commits harder to the dominant local-natural parse → it breaks earlier. So the answer to “is recursion
layer-limited?” is: yes in principle (one pass is TC⁰), but empirically these models cap at center-embedding depth
~2–3 — far below any layer limit — and layer count is not the active constraint; training distribution + interference
are. The genuine layer-bound would bind only for in-distribution recursion pushed past the data’s depth, which
natural language essentially never does (the human depth-2 center-embedding limit is itself this phenomenon); to go
deeper a model uses the decode loop / chain-of-thought (TC⁰ per step, Turing-complete across steps), trading
within-pass depth for sequence length. (Caveat: nesting adds more+stronger attractors per level than distance, so
“nesting harder” is partly more interference, not purely stack depth; the robust, size-monotone result is the ceiling
shrinking with capability.) (runs/disassembly/recursion_depth_summary.json.)
Does the decode loop buy back the depth? — chain-of-thought, partially (cot_depth.py)
Theory’s escape hatch: a single pass is TC⁰, but the decode loop is Turing-complete across steps, so chain-of- thought should trade within-pass depth for sequence length and lift the ceiling. Testing it on the canonical bounded-recursion task — Dyck bracket matching at nesting depth d (balanced vs one mismatched closer, chance 50%) — with an instruction-tuned model that can reason (Qwen2.5-1.5B-Instruct), direct answer vs CoT:
| depth | 2 | 4 | 6 | 8 | 10 | 12 |
|---|---|---|---|---|---|---|
| direct | 85% | 65% | 65% | 65% | 55% | 50% (chance) |
| CoT | 50% | 85% | 74% | 50% | 71% | 43% |
- Direct collapses cleanly to chance by depth 12 — the single-forward-pass recursion limit, demonstrated on a clean recursion task (no distributional confound this time: brackets are uniform across depth).
- CoT beats direct at moderate depth (4/6/10) — the loop does buy back depth, as the theory predicts.
- But it is noisy and reliability-bounded at 1.5B: CoT trails direct at depths 2/8/12, and its coverage drops with depth (a parseable “Answer:” emerges in only 14/20 cases at depth 12 — the small model rambles past the token budget without concluding). So the escape hatch is real but its quality is gated by the model’s own reasoning reliability; a larger / reasoning-tuned model would show it cleaner (as the CoT literature does).
Net: the within-pass depth limit is real and clean (direct → chance), and the decode loop is a genuine but
reliability-bounded escape — depth is traded for tokens, only as well as the model can actually reason step by step.
(runs/disassembly/cot_depth_summary.json.)
Execution model: an interpreter over the op-graph (“ResidualVM”)
The recompile-KL harness is most useful not as a one-shot metric but as a steppable interpreter over the extracted op-graph: run the named ops on the residual bus, in selectable fidelity modes, and watch reconstruction-KL — which makes decompilation debuggable, not just measurable. (Execution-model framing contributed by Grok; integrated + corrected here.)
The honest mechanics — dataflow per pass, “VM” only at the loop. It is tempting to cast this as a von
Neumann machine (fetch–decode–execute over a stored program). That is the wrong abstraction for a single
forward pass: there is no program counter, no instruction fetched per cycle, no data-dependent control flow,
and the weights are never modified by the data path. One pass is a fixed-depth dataflow circuit (closer to
an ASIC / systolic array; Merrill’s TC⁰) — the “ISA” (the op-catalog) is hardwired and applied in
parallel, a description of fixed functional units, not a runtime dispatch. The stored-program /
von-Neumann character appears only at the autoregressive loop: the residual stream + KV-cache as a
read/write tape, the decode step as the clock, chain-of-thought as working memory (see the
llm-as-accreting-vm framing). So the interpreter executes a fixed circuit per token and is a VM at the
generation level — not a stored-program CPU per layer.
Formal grounding (why the DAG/loop split is load-bearing, not decorative): the object is a clocked
sequential machine = combinational core (the fixed DAG) + state register (KV-cache + the growing token
sequence) + clock (the decode step). The DAG alone is weak — one bounded-depth pass sits in TC⁰
(Merrill–Sabharwal); the loop supplies the power — transformer + decoding + an unbounded scratchpad is
Turing-complete (Pérez et al., CoT-expressivity). Two precisions: (a) the recursion goes through a discrete
token bottleneck — high-dim state is sampled to a token and re-embedded, so depth-per-step is bounded but
steps are unbounded (hard problems buy back missing within-pass depth with longer CoT); (b) the program is
fixed, not self-modifying — the same DAG every step, only the data changes (microcode/ASIC-like; Turing
power lives entirely in the outer tape+clock). The decompilation payoff: the loop is a clean recurrence —
the hard part is reading the DAG, and reconstruction_coverage is exactly the measure of how much of that
fixed high-dim DAG reduces to a compact symbolic program over explicit operands.
What the frame does buy, mapped to checked-in results:
| VM concept | what it actually is here | grounded in |
|---|---|---|
| memory hierarchy (registers/L1 vs main memory) | entanglement-tower levels: low-χ monosemantic core (addressable, cov95-high) vs high-χ entangled remainder | mps_tower_*, serve-tower cov95-saturation-vs-capability-cliff |
| fidelity modes: full / preserve-hybrid / decompiled | exact host / verbatim-pin ~6–12% of atoms + forge the rest / run only the recompiled op-graph | preserve_hybrid_tiny.py, sae-forge NativeModel |
| ISA vs model-specific microcode | the op-catalog (idioms) is invariant across architectures and languages; the sink/plumbing policy is model-specific | the 4-model + multilingual results |
| associative memory bank | MLP neurons as content-addressable key→value stores | work-item 2 (the MLP gap) |
| the recompiler / JIT | sae-forge projects the kept ops into a runnable module | reconstruction-coverage metric above |
What it adds beyond the metric (the real new lever): an interactive debugger. Step layer-by-layer; breakpoint when a named idiom fires; inspect the low-χ “registers” (SAE latents); ablate / preserve / swap a single op and watch reconstruction-KL move live. That turns reconstruction-coverage from a number into a tool for localizing where decompilation fails — i.e. it operationalizes milestones 1+4.
The hard constraint the frame must not hide. The entangled core is preserve-or-pay, not “approximate main memory”: by the tower’s no-go, fidelity modes move you along the interpretability↔capability frontier, never off it. An interpreter that “approximates the core cheaply” is just choosing a point on that frontier (and paying the capability cliff) — the ceiling is the forge tax, restated.
Demarcation: adopt the execution layer, the memory-hierarchy mapping, the fidelity modes, and the ISA-vs-
microcode framing; drop the von-Neumann mechanics (no per-layer instruction fetch) in favor of
dataflow-circuit-per-pass + VM-at-the-loop. COMPOSE / PRESERVE / TOWER_TRUNCATE are not opcodes the model
runs — they are extracted descriptions (the DAG) or execution modes (knobs on the interpreter).
Milestones (each a PR, gated on the prior)
- Recompile-KL harness, built as the interpreter (§Execution model) — DONE (v1, GPT-2):
scripts/disassembly/residual_vm.py(see First result). v1 recompiles by keeping ops at full fidelity and mean-ablating the complement; v2 = feature-basis recompilation via sae-forgeNativeModel(the ceiling test, milestone 4). Now also a reusableResidualVMclass in that file — the consolidated, arch-generic intervention layer the ~20 disassembly scripts each re-implemented (load + corpus-mean +ablate_heads/ablate_mlps/patch_mlp/patch_attn/trace/attributionas composable context managers);python residual_vm.py --demoreproduces the induction reconstruction coverage through it. (The biggest tooling gap identified after the cross-model program: a single steppable debugger so an experiment is ~10 lines and the load/hook/merge bugs live in one place.) Feature-level interventions are now in too —vm.set_feature(layer, feat, target)/ablate_featureedit individual SAE latents (GPT-2 jbloom / Gemma Scope) directly in the residual;python residual_vm.py --feat-demoshows that ablating the subject’s dominant feature (a France/geography latent promoting “Alps”) at L5 of “ France” dropslogp(" Paris")by ~9 nats — feature-native surgery, the rung from head/MLP-level interventions toward editing the catalogued features themselves. - Composition-DAG extractor — weight-space edge scorer + path-patch gate; auto-recover induction + IOI;
report new sub-DAGs. (Generalizes
path_patch_induction.py/composition_graph.py.) DONE (GPT-2):scripts/disassembly/composition_dag.py— static composition predicts dynamic writer specificity (ρ=+0.37); the canonical induction K-chain and IOI Q-chain are auto-recovered AND live, imposters/random rejected (0% FP); 22 new live edges surfaced (see Composition-DAG section). - MLP ops — neuron key→value catalog + named MLP idioms; add to the DAG + the recompile. DONE (GPT-2):
scripts/disassembly/mlp_ops.py— the recompile now charges for MLPs (M1/bridge kept heads only); MLPs are load-bearing, concentrated in L0 (the detokenizer); head↔MLP composition edges are weight-legible (see MLP ops section). - The ceiling test — reconstruction-coverage plateau vs the tower’s entangled core, same host; the
unifying claim stands or falls. First result DONE (v2, tiny GPT):
scripts/cov95_forge_tax/ceiling_test.py— content/factorability axes decouple; the capability plateau is GPU-scale-gated (see Ceiling test section). - Cross-model — repeat the ceiling on Gemma-2 / Llama-3 / Qwen-2.5 (idea i) to test whether the
decompilable fraction is architecture-invariant like the mechanisms are. DONE (op-selection ceiling, 5
models incl. the gpt2-medium control):
scripts/gemma/cross_model_ceiling.py— the named circuit beats random everywhere (mechanisms invariant) but the decompilable fraction is not invariant; the gpt2-medium control disentangles it — the high fraction tracks the absolute-position family, not scale (see Cross-model ceiling section). The forge-basis ceiling stays SAE/GPU-gated for non-GPT-2. - Executable decompilation & knowledge, across six models — DONE. The reconstruction-coverage idea (M1) run as a sufficiency test over GPT-2 ×3 + Gemma/Llama/Qwen: no small head-set is sufficient for induction (≤30% even under resample-ablation; seed-stable ±0–1%) and even IOI’s 26-head circuit isn’t, in isolation (reconstruction, substrate). Knowledge ported cross-model: ROME causal tracing recovers an architecture-invariant early-MLP-store → late-attention-readout flow, and the store is editable by activation patch (100% fact-transplant in 5/6, generalizing across relations; Gemma’s storage is distributed) (tracing, transplant). Nuance vs M5: the forge-basis decompilable fraction (M5) is absolute-position-family-specific, but the head-ablation reconstruction coverage / circuit distributedness (M6) tracks scale — different metrics, both true (scaling synthesis): small models are unusually localized, so a faithful decompilation is the budget curve, not a tiny op-graph.
First result (milestone 1, GPT-2)
residual_vm.py on GPT-2 (Shakespeare; floor = KL(host ‖ all-144-heads-mean-ablated) = 1.92): keep a head-set
at full fidelity, mean-ablate the complement, sweep the budget B by marginal ablation importance vs a random
control + the named induction circuit.
- Attention is distributed / redundant. The coverage curve rises gradually — 128 of 144 heads are needed for 90% coverage; no small set reconstructs the forward pass. The single most-important head in isolation is even net-negative (keep 1, ablate 143 → slightly below the all-ablated floor): heads interact. (Same program-wide redundancy seen in the circuit work.)
- But the named catalog is coverage-efficient. Top-B beats random-B at every budget except B=1 (largest gap mid-range, Δ≈+0.31 at B=24–32), and the 5-head induction circuit (prev-token 4.11 + induction 5.0/5.5/6.9/7.11) reconstructs +0.164 coverage vs +0.032 for a random 5-head set — ~5×. The disassembly’s named ops are disproportionately load-bearing (importance ranks: 7.11 #2, 4.11 #8). The op-catalog buys real reconstruction.
- Scope (honest). v1 keeps the kept heads at full fidelity (exact weights), so coverage → 1 as B → all
by construction — it measures which/how-many ops matter (op-selection coverage), not the
entangled-core ceiling. The plateau-below-1 prediction (forge tax as decompilation ceiling) needs the
feature-basis recompilation (sae-forge
NativeModel, milestone 4), where kept ops must be expressed in a clean basis and composition bottlenecks. Milestone 1 delivers the interpreter + metric + the op-selection result; the ceiling test is the next build.runs/disassembly/residual_vm_gpt2_summary.json.
Composition-DAG extractor (milestone 2) — first result (GPT-2)
composition_dag.py unifies the two precursors into one extractor that reads the call graph, not one
idiom. composition_graph.py gave the static adjacency (Elhage Q/K-composition on raw weights, mean-write
removed) but only validated the single prev-token→induction K-edge; path_patch_induction.py gave the dynamic
gate but measured an induction-specific collapse, so it could only confirm induction. M2 generalizes both:
score the full K/Q composition DAG over all causal head pairs, then gate the strongest edges with an
idiom-agnostic dynamic metric — the mean total-variation change in the reader’s attention pattern when the
writer is removed from that port (ΔTV), defined for any reader. GPT-2, Shakespeare, weights + two forward
passes; 170 gated edges (105 K / 65 Q); attention recompute is exact (max|Δ| = 9.85e-7 vs the model).
The one methodological move that makes this work: raw ΔTV grows with reader depth/magnitude, so a global random null is confounded (early-layer write-hubs dominate). The fix is a reader-matched null — for each reader, compare the real writer against random causal writers into the same reader head (the path-patching null that isolates writer specificity from reader depth). An edge is “live” if its ΔTV beats its reader’s matched 2σ null; specificity = ΔTV − matched null.
- Static composition predicts dynamic liveness across the whole graph. The headline (cleaner, depth-
unconfounded) metric is Spearman(static, reader-matched specificity) = +0.37; the raw
Spearman(static, ΔTV) = +0.52 is higher but inflated by the depth co-scaling both quantities share, so the
specificity correlation is the one to trust. Either way the weight-space score is a graph-wide predictor of
which writes actually shape which reads — the broad version of
path_patch_induction’s induction-only ρ, not a single-idiom result. - The induction K-chain is auto-recovered AND live. Static prev-token→induction K-composition 0.069 vs causal baseline 0.042 (1.6×) vs random 0.039. Dynamically, the canonical prev-token head 4.11 → inductors is 4/5 live (4.11→5.5/5.0/7.11/6.9 clear their matched nulls; 4.11→5.1 marginal), the strong-edge median Δinduction is +0.015, and the top edge collapses 56% of induction attention under key-path patching (the original strong readout, retained for induction edges).
- The IOI Q-chain is auto-recovered. Static S-inhibition→name-mover Q-composition 0.065 vs causal 0.042 (1.5×); the recovered chain is duplicate-token (3.0) → S-inhibition (8.10/8.3/10.0) → name-mover (9.9/10.0/11.2). Dynamically 5/11 S-inhib→name-mover Q-edges are live — the real S-inhibition heads 8.10/8.3 reshape the name-movers’ query attention; the spurious 6.7 (a Q-composition false-positive) does not (negative specificity).
- The gate is selective — it rejects imposters. Across the named cross-product, 43% of edges are live vs 0% random false-positives. The selectivity is the point: the non-canonical high-prev-token writers (2.2/3.2/3.7, which have high prev-token attention but aren’t the prev-token head) → inductors are mostly dead, and the spurious S-inhib head is dead. The extractor keeps the real sub-circuit and discards the cross-product noise.
-
22 new live edges surfaced above their reader-matched 2σ nulls, not in induction/IOI — dominated by early-layer write-hubs feeding many readers (consistent with positional / duplicate-token hubs). The highest-specificity candidates:
port edge ΔTV reader null specificity K 0.9→2.9 0.444 0.035 +0.410 K 0.9→2.5 0.449 0.044 +0.404 K 1.8→10.9 0.293 0.007 +0.287 K 1.8→9.3 0.333 0.088 +0.245 Q 0.9→1.3 0.259 0.018 +0.240 K 1.8→3.2 0.362 0.133 +0.230 Q 1.10→2.11 0.270 0.055 +0.215 K 1.8→10.5 0.198 0.021 +0.177 Two heads recur as hubs: 0.9 (a layer-0 writer dominating several layer-1/2 readers) and 1.8 (a layer-1 writer reaching long-range into late-layer keys, 9.3/10.5/10.9). These are candidate sub-DAGs, not validated circuits — behavioral labeling (and targeted single-edge path-patching, as for induction) is the obvious follow-up; the full ranked list is in the summary JSON’s
novel_live_edges.
Feeds the recompile harness (milestones 1/4) — now tested, see the next section. The live edges this
extractor confirms are exactly the keepable ops the ResidualVM reconstruction-coverage interpreter
(residual_vm.py) should retain: M1 selects heads by marginal ablation importance, but the DAG supplies the
structured op-set (which writer→reader wires carry the computation), so a DAG-guided keep-set is the
natural upgrade to M1’s flat head-budget — and the recompile-KL then verifies the extracted sub-DAG executes.
The M1↔M2 bridge below confirms this. Completing the DAG with V-composition + MLP nodes (milestone 3) is what
makes that op-set whole.
Scope (honest). (a) The dynamic gate runs on natural text, so the IOI Q-edges are confirmed by generic
attention reshaping (ΔTV), not the IOI-task logit-difference — that task-specific causal validation already
lives in ioi_causal.py; M2’s contribution is the unified static→dynamic recovery. (b) ΔTV measures whether
an edge reshapes the reader’s attention pattern (Q/K composition); V-composition (writing values without
moving attention) needs a different readout, not done here. (c) MLP nodes are absent — they are milestone 3.
Compute: GPT-2, CPU-feasible — weights + two forward passes over the corpus (one for behavioural labels,
one for the path-patch gate); the full run is ~70 s wall-clock and ~5 GB RAM on CPU (no GPU), so it scales
to any HF model the box can hold a forward pass of. runs/disassembly/composition_dag_summary.json (re-run the
script to regenerate the figure).
Validating the new write-hub edges (follow-up to M2)
validate_new_edges.py takes the 22 new live edges to the next rung: a targeted single-edge path-patch with
a behavioural readout (the induction-style strong test, generalized). For each edge A→B it surgically removes
A’s output from B’s port, recomputes B’s attention, and measures the collapse of B’s named components —
{prev-token, duplicate, induction, sink} — against a reader-matched random-writer null. A pattern collapse
beyond the null names the edge’s function; ΔTV with no named collapse is real-but-unlabeled shaping.
-
The write-hubs are early SINK heads broadcasting a positional signal. All three hubs (0.11, 0.9, 1.8) and the minor ones (1.3, 1.7, 1.9) self-label sink (their own attention parks on position-0). 13/22 edges resolve to a named function — 9 prev-token, 3 sink, 1 duplicate. Removing these hubs from a downstream key collapses that head’s prev-token attention: e.g.
0.9→2.5−56%,1.8→9.3 / 1.8→10.9−45%, and — the headline —1.3→4.11−11% and1.8→4.11−8% into the canonical prev-token head itself.edge writer→reader pattern rel-collapse (beats null) K 0.9→2.5 prev-token −56% K 1.8→9.3 prev-token −45% K 1.8→10.9 prev-token −45% Q 0.9→1.3 sink −42% Q 0.9→1.10 prev-token −23% Q 0.9→1.2 prev-token −23% K 1.8→11.8 prev-token −14% K 1.3→4.11 prev-token −11% K 0.11→1.1 prev-token −10% K 1.8→4.11 prev-token −8% - The prev-token mechanism is not self-contained in 4.11. It reads a positional signal piped in from early sink heads; remove that input and 4.11’s previous-token addressing degrades. This edge-resolves the disassembly’s position/structure register and reframes the sink: a sink head is a no-op in where it reads (parks on pos-0) but its OV-write is a load-bearing positional broadcast — attention-pattern and write-content are decoupled. Hypothesis (consistent with the sink-ablation result — GPT-2 is the only family member that depends on its sink, position-independently = the absolute-positional-embedding signature): these hubs propagate GPT-2’s learned absolute positions; the exact encoding pathway is left open.
-
Honest scope. (a) The reader-matched null includes other early/sink heads that also carry positional signal, so the test is conservative — several of the 9 “unlabeled” edges still collapse prev-token (e.g.
0.9→2.9−55%) but don’t beat that positional null (those readers’ prev-token mass is fragile to any key perturbation), so they are real shaping not attributable to one writer. (b) The readout is attention-pattern collapse (like M2’s ΔTV), evidence of a positional-broadcast role, not a task-level loss metric.Next (sharpen the mechanism): (a) task-level readout — re-do the edge patch as a forward-pass intervention and read next-token KL / induction-NLL, turning “reshapes the pattern” into “changes the output”; (b) the positional-embedding test — re-run the patch with
wpezeroed/randomized: if these hubs propagate GPT-2’s absolute positions, their prev-token collapse should vanish withoutwpe; (c) cross-model prediction — RoPE models (Gemma-2 / Llama-3 / Qwen-2.5) do not depend on their sink (sink-ablation result), so they should show no such absolute-positional-broadcast circuit — the prev-token signal is carried by RoPE in the QK directly, not piped from a sink head. A clean falsifier of the “GPT-2 absolute-positions” hypothesis.runs/disassembly/validate_new_edges_summary.json(~40 s on CPU; re-run to regenerate the figure).
Cross-model: the positional broadcast is GPT-2-specific (forward-pointer (c), done)
scripts/gemma/cross_model_positional.py runs the cross-model test. A key-only causal path-patch that
respects each model’s RoPE is non-trivial to do faithfully across architectures, so the test goes to the
representational question the broadcast hypothesis really turns on: is the prev-token head’s position
carried in the key content (absolute, must be written into the residual) or in the QK rotation (RoPE,
applied at attention time)? For each model it finds the top prev-token head, captures its pre-rotation
keys over the corpus, and decomposes the key variance into the fraction explained by absolute position vs by
token identity. The prediction: GPT-2 position-dominated (its keys must encode where — exactly what the
sink heads broadcast), the RoPE models token-dominated (keys encode what; position is the rotation).
Method: a between-group variance decomposition on the head’s key vectors. position_fraction = the
between-position-index variance (variance of the mean key at each absolute position) over the total key-
covariance trace; token_fraction = the same over token identity (mean key per frequent token). Both are
fractions of the same total, so pos/tok is a clean within-head ratio comparable across architectures.
| model | pos. encoding | prev-token head | key var: position | key var: token | pos/tok |
|---|---|---|---|---|---|
| GPT-2 | absolute (wpe) |
4.11 | 59% | 18% | 3.3 |
| Gemma-2-2B | RoPE | 21.7 | 7% | 23% | 0.30 |
| Llama-3.2-1B | RoPE | 0.2 | 2% | 64% | 0.04 |
| Qwen-2.5-1.5B | RoPE | 13.4 | 9% | 25% | 0.35 |
CONFIRMED — a clean 1-vs-3 split (~10×). Only GPT-2’s prev-token key is position-dominated; every RoPE
model’s is token-dominated. This is the cross-model explanation that ties the positional thread together: GPT-2
encodes absolute position as key content → its prev-token head must read that content → it depends on the early
sink heads that broadcast it (the validate_new_edges collapse) → and GPT-2 is the only family member that
depends on its sink (the sink-ablation result, position-independently = the absolute-positions signature). The
RoPE models need none of this — position rides in the rotation — so they have no sink dependence and no
positional-broadcast circuit. The figure (cross_model_positional.png, regenerable) shows the bars flip:
position > token only for GPT-2. Note Llama-3.2-1B’s prev-token head is in layer 0 (head 0.2) and is the
most token-pure of all (pos/tok 0.04) — it reads the raw token embedding directly and leans entirely on RoPE for
position, the cleanest case of the RoPE pattern.
Scope. This is the representational confirmation (the key is position-encoded only in GPT-2),
corroborating the causal GPT-2 result; a faithful key-only causal path-patch across RoPE models
(forward-pointer (a)) is the heavier next step — now done (next subsection). Also next: re-run on an
oracle-supervised host (#19/#20) — does training a more legible model shift the prev-token key’s
position-vs-token content, i.e. does supervision touch the positional machinery or only the feature substrate?
~33 s for all four models. runs/gemma/cross_model_positional_summary.json.
Cross-model: the causal key-only path-patch (the heavier confirmation)
key_patch_cross_model.py does the causal complement #26 deferred. The representational test said GPT-2’s
prev-token key carries position-content and RoPE’s doesn’t; the causal test asks: does removing an upstream
head’s key content collapse the prev-token attention? It’s a forward-pass intervention so each model applies
its own RoPE — for the top prev-token head B, replace B’s key input with norm(resid − A_out) (q untouched →
key-only) for each upstream head A, and re-read B’s attention. RoPE rotates the patched key, so the relative
match is preserved; only the key content changes. The intervention is exact: the zero-patch sanity is
0.00e+00 on every model (q/v untouched).
| model | pos. enc. | prev-token head | top key-patch collapser | prev-token collapse | robust z |
|---|---|---|---|---|---|
| GPT-2 | absolute | 4.11 | sink head 1.3 | −22% | 118.8 |
| Gemma-2-2B | RoPE | 21.7 | 5.4 (non-sink) | −0% | 4.8 |
| Qwen-2.5-1.5B | RoPE | 13.4 | 0.0 (non-sink) | −0% | 1.8 |
CONFIRMED causally — GPT-2’s prev-token attention is carried by KEY CONTENT; the RoPE models’ by the rotation.
Removing the sink head 1.3 from GPT-2’s prev-token key collapses its prev-token attention −22%, a huge
standout (z 118.8 vs a median 0.1% over the other upstream heads) — and the collapser is a sink head, the
positional broadcaster from #25. In the RoPE models, no upstream head’s key-content removal collapses
prev-token (max −0%): their rotation, untouched by the content patch, still aligns q−1. This is the decisive
causal pairing for #26’s representational split, and the third+fourth causal/representational signatures of
GPT-2’s learned absolute positions (with the sink-dependence and the cross-model ceiling). For scale: the −22%
comes from removing one upstream head’s key-content with the rest of the model fully intact — vs a 0.1%
median over the other ~47 upstream heads (the figure key_patch_cross_model.png is the lone red GPT-2 bar
beside the flat RoPE bars). The zero-patch sanity (0.0) is the numerical-fidelity check: norm(resid − A_out)
reproduces the clean key exactly when A_out is forced to zero.
Scope. Llama-3.2’s prev-token head is in layer 0 (no upstream heads to patch), so the RoPE side rests on
Gemma + Qwen — a real limitation for any model whose prev-token head sits at the very bottom (the patch needs an
upstream writer). The patch is the direct A→B key path (resid − A_out at B’s layer). Next: the same
key-only / value-only forward patch generalizes to any circuit — induction heads, the validated V-edges
(#28) — to probe content-vs-rotation dependence beyond prev-token; and re-running it on an oracle-supervised host
tests whether supervision (#19/#20) shifts the key-content dependence. key_patch_cross_model.py,
runs/gemma/key_patch_cross_model_summary.json (~85 s, 4 models).
Generalizing the patch across circuits & channels — match (key) vs move (value)
circuit_content_patch.py runs the same faithful key-only patch on three circuits — prev-token (positional:
attend to q−1), induction (content: attend to the key whose predecessor token == the current token),
duplicate (content: attend to an earlier occurrence of the same token). The sharp question: is the
key-content-vs-rotation split (#26/#31) about the architecture, or about the addressing type? RoPE’s rotation
supplies position, never token content — so a content match must live in the key content in every
architecture, while only positional matching can move to the rotation.
| circuit | type | GPT-2 | Gemma-2-2B | Llama-3.2-1B | Qwen-2.5-1.5B |
|---|---|---|---|---|---|
| prev-token | positional | +22% ✓ | +0% | skip (L0) | +0% |
| induction | content | +17% ✓ | +18% ✓ | +70% ✓ | +89% ✓ |
| duplicate | content | skip (L0) | +3% | +9% | +13% ✓ |
(✓ = the top upstream head’s key-content removal collapses that circuit ≥10% and ≥3× the upstream-head median; every zero-patch sanity is 0.0.)
GENERALIZED — key-content dependence is about the ADDRESSING TYPE, not the architecture. The positional circuit (prev-token) is key-content-dependent only in GPT-2 (the RoPE models read position from the rotation, −0%), but the content circuit (induction) is key-content-dependent in every model (+17…+89%): removing the upstream predecessor-writer from the induction head’s key collapses induction everywhere — the rotation cannot supply the predecessor token, so it must be in the key content. So #26/#31’s GPT-2-vs-RoPE split is specifically the positional register; the content instruction set (token-identity matching) is universal — exactly consistent with the mechanism-invariance results (induction is causally load-bearing in all four). A nuance: in GPT-2 the induction collapser is its prev-token head (4.11 — the canonical K-composition writer), but in the RoPE models it is an early head (their dominant prev-token head is late — e.g. Gemma’s is layer 21, downstream of its layer-4 induction head — so induction is fed by a separate early predecessor-writer); the universal fact (induction is key-content-fed) holds, the specific writer differs.
The move (value) channel — universal even where the key isn’t. The same forward patch runs value-only:
feed norm(resid − A_out) to the reader’s value and measure the change in its OUTPUT (ΔV-out, the #28 readout).
RoPE rotates Q/K but never the value — so what each circuit moves should be content-dependent in every
architecture, even for the positional circuit whose key is rotation-only in RoPE.
| circuit | type | GPT-2 | Gemma | Llama | Qwen |
|---|---|---|---|---|---|
| prev-token | positional | 0.22 ✓ | 0.05 | skip | 0.11 ✓ |
| induction | content | 0.26 ✓ | 0.12 ✓ | 0.17 ✓ | 0.24 ✓ |
| duplicate | content | skip | 0.19 ✓ | 0.28 ✓ | 0.12 ✓ |
(top value-patch ΔV-out; ✓ = >0.05.) The move channel is universal — value-content dependence everywhere, including prev-token (GPT-2 0.22, Qwen 0.11) whose KEY collapses only in GPT-2. So the architecture-specific positional register is confined to the key/match (score) channel; what heads move is content in every model because the value is never rotated. The value channel is also markedly more distributed than the key channel — the top value-mover is only ~2–3× the upstream median (vs ~10–100× for keys), so no single head dominates what a circuit moves (the redundancy theme — cf the V-edges adding nothing to the recompile keep-set, #30).
NET (the addressing register, decomposed): only positional matching is architecture-specific (GPT-2 puts
it in the key content via the sink broadcast; RoPE in the rotation); content matching (induction/duplicate
keys) and all moving (every circuit’s value) are universal — consistent with mechanism-invariance. Scope.
Induction is the clean content circuit; duplicate’s readers are early/layer-0 (GPT-2 skips, noisier elsewhere);
prev-token skips for Llama (layer-0 reader). Every zero-patch sanity is 0.0. circuit_content_patch.py,
runs/gemma/circuit_content_patch_summary.json (~4 min, 4 models × 3 circuits × 2 channels).
Circuit-structured keep-set selection (M1↔M2 bridge) — first result (GPT-2)
dag_recompile.py closes the loop between the two milestones: it feeds the M2-extracted live sub-DAG into M1’s
reconstruction-coverage harness (same mean-ablation metric, coverage = 1 − KL(host‖keep)/KL(host‖all-ablated))
and asks whether the weight-cheap DAG (weights + 2 forward passes) picks the keep-set as well as M1’s
expensive marginal-ablation importance ranking (one forward pass per head). GPT-2, Shakespeare, floor KL
1.82; ~95 s on CPU (most of it the 144-head importance ranking the DAG sidesteps).
- The path-patch-confirmed circuit IS the recompile keep-set — and it beats the greedy importance set at equal size. The 12-head induction+IOI live sub-DAG reconstructs +0.333 coverage vs +0.230 for the 12 individually-most-important heads (greedy top-B) and +0.038 for random-12 — i.e. 145% of the greedy-optimal at equal budget, +0.295 over random. It beats greedy importance because marginal ablation is myopic (ranks heads by their individual effect) while the DAG selects a coordinated interacting circuit — exactly the regime M1 flagged (“heads interact; the single most-important head in isolation is net-negative”). So the auto-extracted circuit is a coverage-efficient keep-set without the per-head ablation sweep.
-
But raw connectivity is NOT a generic output-importance proxy — the honest dissociation. Ranking all heads by DAG-connectivity (summed incident live-edge specificity) only weakly tracks marginal importance: Spearman = +0.24 over all 144 heads but +0.02 among the heads the DAG actually gated. And the larger keep-set that adds the new write-hubs (
dag_all_live, 30 heads) reaches only 67% of the greedy-optimal coverage (+0.361 vs +0.541). Reason: ΔTV measures attention-reshaping, not output-importance — the new early-layer write-hubs (0.9→2.x, 1.8→{9.3,10.5,10.9}) strongly shape downstream attention yet are output-redundant (mean-ablating them barely moves next-token KL). Mechanistic hypothesis: this is the program-wide redundancy seen throughout the stack (substrate/core redundancy in the tower; the redundant prev-token population feeding a bottleneck inductor in rung-3) — an early write-hub broadcasts positional / duplicate-token signal along many parallel paths, so removing any one (or even the hub’s whole output, mean- ablated) leaves the downstream readers able to recover it elsewhere; high attention-influence, low marginal output-importance.As an ordering, DAG-connectivity still beats random at every budget (it front-loads the circuit heads) but lags the marginal-importance ordering at small budgets — consistent with “connectivity finds the circuit, not the importance rank”:
budget B top-importance DAG-connectivity random 4 +0.153 +0.054 +0.025 12 +0.230 +0.186 +0.082 24 +0.391 +0.416 +0.170 48 +0.610 +0.636 +0.408 64 +0.715 +0.690 +0.426
Takeaway. The bridge confirms the program’s central use of the extractor: a path-patch-confirmed sub-DAG is the structured op-set the recompiler should keep (rivals/beats greedy importance, ≫ random, no ablation sweep) — but the gate’s ΔTV is a circuit-liveness signal, not a drop-in importance score, so the keep-set must come from the confirmed circuit, not from thresholding raw connectivity. This is the M1→M4 hand-off: the DAG keep-set is what the feature-basis recompilation (milestone 4) should express in a clean basis.
Next: (a) feed validated new edges (single-edge path-patch of the write-hub candidates from M2) into this
keep-set so the recompiler grows beyond the two textbook circuits; (b) make the gate jointly attention-liveness
and output-importance (e.g. an attribution / logit-effect term alongside ΔTV) so connectivity becomes a true
keep-set score; (c) re-run on an oracle-supervised host (#19/#20) — a more legible host should yield a cleaner
live DAG and a higher-coverage keep-set. The script takes --dag-summary so any of these DAGs (other corpora,
supervised models) drops straight in. runs/disassembly/dag_recompile_summary.json (re-run to regenerate the
figure).
Adding the value pathway (V-edges) to the bridge
The bridge now also consumes the V-composition DAG (vcomposition.py, --v-summary). The K/Q circuit covers
attention routing; do the composed-OV virtual heads (the layer-6 value-readers, induction content re-read
as a value) carry reconstruction the K/Q circuit missed? We add the 22 live V-edge heads to the 12-head K/Q
circuit and measure the incremental coverage against the same number of random and importance-optimal additions:
| keep-set | coverage | Δ vs K/Q |
|---|---|---|
| K/Q circuit (12 heads) | +0.333 | — |
| + 22 V-edge heads | +0.338 | +0.004 |
| + 22 random heads | +0.412 | +0.079 |
| + 22 top-importance heads | +0.532 | +0.198 |
No — the value-pathway heads are output-redundant. Adding the 22 V-edge virtual heads lifts coverage by only
+0.004, less than adding 22 random heads (+0.079), and their marginal-importance ranks are mid-pack
(23–134 of 144). So ΔV-out, like ΔTV, is an edge-COUPLING score, not head output-importance: the V-edges are
a real composed-OV coupling (#28 — removing the writer changes the reader’s output), but those virtual heads
are not load-bearing for the model’s next-token output (keeping them barely helps; the reader is driven by the
writer yet itself contributes little to the logits). This unifies the bridge’s lesson across all three Elhage
ports — K/Q (ΔTV) and V (ΔV-out) composition-coupling both fail to predict output-importance; the recompile
keep-set is the path-patch-confirmed named circuit, full stop, not any coupling score. (Consistent with the
program-wide redundancy theme: composition is distributed; a few named heads are load-bearing, the rest is
redundant coupling.) runs/disassembly/dag_recompile_summary.json, value_pathway field.
MLP ops in the DAG + the recompile (milestone 3) — first result (GPT-2)
mlp_ops.py adds the COMPUTE instruction class. M1 and the bridge kept/ablated attention heads only — MLPs
ran at full fidelity, so the coverage metric never charged for them — yet MLPs carry real computation
(greater-than is MLP-dominated; mlp_catalog.py read the neuron key→value vocabulary). M3 extends the
mean-ablation harness to keep/ablate MLP layers as well as heads (floor = all heads and all MLPs ablated),
adds head↔MLP composition edges to the DAG, and names the load-bearing MLPs. GPT-2, Shakespeare, floor KL
3.39.
- MLPs are load-bearing — and the load is concentrated in L0. Removing all MLPs (heads intact) collapses
coverage to −0.019 (≈ floor): attention alone cannot reconstruct the forward pass. The single most
important op in the whole recompile is the layer-0 MLP (marginal importance +0.772; next are L11 +0.08,
L1 +0.07) — GPT-2’s detokenizer, the same
e_ext = e + MLP0(·)enrichment the QK/copy disassembly already reads through. Its top neurons read sentence-boundary punctuation (. ; ?) and write structural/line-start tokens (newline,I,First) — detokenization / boundary formatting. -
A few MLPs reconstruct most (attention intact). Sweeping the MLP budget with all heads kept, the top-importance MLPs dominate a random-MLP control at every budget (L0 first):
MLPs kept top-importance random 1 +0.118 −0.002 2 +0.257 −0.004 4 +0.436 +0.313 8 +0.819 +0.595 A combined sparse op-set of just the 12 M2 circuit heads + the top-4 MLP layers reaches +0.286 coverage — a tiny MOVE+COMPUTE program (16 ops of 156).
- The DAG gains MLP nodes. Head→MLP read edges (
‖OV_a · W_in^L‖, mean-write removed) and MLP→head write edges (‖W_out^L · W_{Q/K}^B‖) are weight-legible; top edges e.g.2.1→L2,11.0→L11(read) andL1→2.2,L0→4.11(write). So the call graph now has typed head↔MLP edges, not just head↔head.
Scope (honest). (a) Mean-ablating all MLPs is severe (L0 dominates), so “attention-only / MLP-only” are
necessity statements, not a clean attention-vs-MLP credit split — attention’s reconstruction value is
the MLP-intact bridge (#23), where circuit heads reconstruct with MLPs on. (b) Static head↔MLP composition does
not rank MLP recompile-importance (Spearman = −0.43): the most important MLPs are early (L0) and have
the fewest incoming head→MLP edges (a depth confound), so the DAG edges give structure, importance comes
from the recompile — the same lesson as the bridge’s “ΔTV ≠ KL-importance.” (c) The full per-neuron catalog +
low-rank/named-idiom analysis lives in mlp_catalog.py; M3 reuses its read→write naming for the load-bearing
layers only. (d) MLP→MLP dynamic gating remains future work (V-composition is now done — next section).
runs/disassembly/mlp_ops_summary.json (re-run to regenerate the figure).
V-composition — the value pathway (completes the DAG edge types) — first result (GPT-2)
M2 scored K- and Q-composition and gated them with ΔTV — the change in the reader’s attention pattern. By
construction ΔTV cannot see V-composition (Elhage’s third edge type): head A’s output feeding head B’s
value changes what B moves, not where B attends. V-composition is how heads chain OV circuits (“virtual
heads” / composed copies), so the DAG was incomplete. vcomposition.py adds it with the matching readout —
static comp_V(A→B) = ‖OV_A·W_V^B‖/(‖OV_A‖‖W_V^B‖) (mean-write removed) + a dynamic ΔV-out: remove A from
B’s value, recompute B’s output contribution (attention fixed), measure the relative change in B’s residual
write; reader-matched null. GPT-2, faithful patches (recompute vs model 9.85e-7).
- A clean K/V double dissociation — V is a separable pathway, not a relabelled K-edge. The strongest V-edges
change B’s output (median ΔV-out 0.214) but barely its attention (ΔTV 0.020); the strongest
K-edges are the mirror (ΔTV 0.142, ΔV-out 0.067). The scatter is L-shaped: V-edges top-left
(output-moving), K-edges bottom-right (attention-shaping), null in between (figure
runs/disassembly/vcomposition.png). This is exactly the pathway M2’s ΔTV readout was blind to. (ΔV-out is normalized to B’s own output norm, so the median 0.214 means removing one writer shifts ~21% of B’s residual write — a large single-edge effect for the value pathway, ~3× the K-edges’ incidental 0.067.) - Static V-composition predicts dynamic ΔV-out (ρ +0.36) — the value pathway is weight-legible like K/Q.
-
The top V-edges are composed-OV “virtual heads,” and they’re interpretable — induction heads (layer 5) feed layer-6 values: the induction-moved content is re-read as a value by a layer-6 head and moved onward (a 2-hop OV circuit). For
5.9→6.7, ΔV-out 1.32 means removing 5.9 changes 6.7’s output by >100% of its norm — 5.9 dominates 6.7’s value.V-edge (A→B) static V-comp ΔV-out ΔTV reading 5.9→6.70.070 1.32 0.043 induction 5.9 → 6.7’s value (composed OV) 5.5→6.70.074 0.87 0.032 induction 5.5 → 6.7’s value 5.5→6.60.072 0.72 0.046 induction 5.5 → 6.6’s value 5.9→6.00.077 0.60 0.033 induction 5.9 → 6.0’s value 5.9→7.30.076 0.54 0.039 induction 5.9 → 7.3’s value 3.0→4.30.081 0.44 0.026 early duplicate 3.0 → 4.3’s value - V-composition is weaker/secondary to attention routing — mean V/K composition 0.80, and K has the stronger top edges (0.114 vs 0.081), consistent with Elhage’s finding that GPT-2 composition is mostly Q/K. The value pathway is real but the minority edge type.
Scope (honest). (a) The readout is the change in B’s direct output (value patch, attention fixed) — the
value analog of ΔTV, not a task-level loss metric. (b) The spearman_staticK_vs_dTV reported here (−0.17) is
range-restricted (computed over only the top-static-K slice, a narrow static range) and is not the
canonical K static→dynamic agreement — that is M2’s +0.37 over a broad top+random edge set; here the K-edges are
only the dissociation control. (c) Q-value (Q-composition’s value analog) and MLP→head V-edges are natural
extensions. The DAG now carries K, Q and V head-edge types. runs/disassembly/vcomposition_summary.json
(~75 s on CPU; re-run to regenerate the figure).
Ceiling test (milestone 4) — first result (v2, tiny GPT)
ceiling_test.py recompiles the tiny GPT by forcing computation through the SAE feature basis (sae-forge
NativeModel, native_in_basis), sweeping SAE width 1–8×, measuring three things at once: forged-model
output faithfulness (KL vs host; unigram floor 2.17), feature-content retention (mAUC), and monosemantic
factorization (cov95).
| width | forged-model KL | cov95 host→forged | mAUC host→forged |
|---|---|---|---|
| 1× | 43.3 | 0.60→0.00 | 0.92→0.67 |
| 2× | 5.11 | 0.64→0.00 | 0.92→0.78 |
| 4× | 5.08 | 0.64→0.12 | 0.93→0.84 |
| 8× | 5.08 | 0.60→0.16 | 0.92→0.85 |
The single-ceiling prediction is refined into two axes — and the capability axis is scale-confounded:
- Feature content reconstructs; monosemantic factorization does not. mAUC retention ~86% (rises with width) vs cov95 retention ~11% (collapses) — the forge tax in the recompilation frame: the basis carries what the model represents, not a monosemantic factorization of it.
- The forged model’s output is globally broken at every width (KL 5–43 ≫ unigram 2.17 → negative capability coverage; 1× catastrophic, 2–8× plateau at KL≈5.08). This is the known tiny-whole-model-forge artifact, not the entangled-core ceiling — so this substrate cannot isolate the capability plateau-vs-core (the doc’s central prediction). Settling that needs a high-quality GPU-scale forge (SAELens + polygram compression) the 8 GB box can’t run.
So milestone 4 builds the harness and settles the content/factorability axis (decoupled, robust across
widths) and identifies the capability-ceiling test as GPU-scale-gated — an honest partial result: the
unifying “one ceiling” claim is wrong as stated (≥2 axes), and the clean capability test is deferred to better
forge hardware. (The tower’s ~24% irreducible core is the target a GPU-scale capability curve would be
compared against.) ceiling_test.py, runs/cov95_forge_tax/ceiling_test_summary.json.
Cross-model ceiling (milestone 5) — first result (5 models, abs-pos vs RoPE)
The forge-basis ceiling (M4) needs per-layer SAEs + sae-forge forging, which Llama-3.2-1B / Qwen-2.5-1.5B don’t
have and which is globally broken at whole-model scale — so the cross-model ceiling goes to the op-selection
metric, which is arch-generic: cross_model_ceiling.py runs M1’s reconstruction-coverage harness
(coverage = 1 − KL(host‖keep)/KL(host‖all-ablated)) on five models — two absolute-position (GPT-2 144h,
gpt2-medium 384h) and three RoPE (Gemma-2-2B, Qwen-2.5-1.5B, Llama-3.2-1B) — and compares the named
induction circuit (prev-token + induction heads, found behaviorally) to an equal-size random keep-set. The
gpt2-medium control is the key: it is larger than two of the RoPE models, so it separates scale from
architecture-family. The question: is the decompilable fraction architecture-invariant like the mechanisms
are?
| model | pos. enc. | heads | induction circuit | random | lift | ratio |
|---|---|---|---|---|---|---|
| GPT-2 | absolute | 144 | +0.202 (20% of pass) | +0.049 | +0.153 | 4.1× |
| gpt2-medium | absolute | 384 | +0.197 (20%) | +0.012 | +0.185 | 16.9× |
| Gemma-2-2B | RoPE | 208 | +0.031 (3%) | +0.022 | +0.009 | 1.4× |
| Qwen-2.5-1.5B | RoPE | 336 | +0.090 (9%) | +0.031 | +0.060 | 2.9× |
| Llama-3.2-1B | RoPE | 512 | +0.034 (3%) | −0.000 | +0.034 | n/a |
The decompilable fraction is not architecture-invariant — and the gpt2-medium control disentangles why: it’s the absolute-position FAMILY, not scale. The named circuit beats random in every model (lift > 0 → the op-catalog is real and the mechanisms are invariant). But its coverage-share splits cleanly by position-encoding: the absolute-position GPT-2 family reconstructs ~20% of the pass (4–17× random) regardless of size, while every RoPE model sits at 3–9% (1.4–2.9×). The control is decisive — gpt2-medium has 384 heads, more than Qwen (336) and Gemma (208), yet keeps the full 20% / 16.9× fraction the larger RoPE models lack. So the earlier GPT-2-smallest-and-only-absolute confound resolves in favour of family: GPT-2’s absolute positions concentrate the induction circuit into a few load-bearing heads; RoPE distributes it across many (more redundant), so a fixed named circuit explains proportionally less. The random-budget curves echo it: the absolute-pos curves rise with heads kept, the RoPE curves stay flat/declining (random head-sets add ~nothing). This is the same GPT-2-family-is-special pattern as the sink (only GPT-2 depends on it) and the positional-broadcast circuit (only GPT-2 has it) — three independent signatures of GPT-2’s learned absolute positions.
Scope (honest). (a) Op-selection ceiling (heads at full fidelity, complement mean-ablated); the
forge-basis ceiling (M4) stays SAE/GPU-gated for non-GPT-2 — when per-layer SAEs land for the RoPE models it
can be run apples-to-apples. (b) gpt2-large (720 heads) was tested and excluded — its 5-head circuit
collapses to ~0% because the behavioral induction-head identification fails at that head count on the small eval
(it picks a last-layer “inductor”); the disentanglement rests on gpt2-medium, where the circuit is clean. (c)
The circuit is a fixed ~5 heads (behavioral); the size-controlled lift makes the conclusion robust to the exact
heads (--n-prev/--n-ind expose it for sensitivity). cross_model_ceiling.py,
runs/gemma/cross_model_ceiling_summary.json (~3 min, 5 models).
Reachability — host-width × oracle-supervision (first result)
The ceiling test asks what’s achievable; this asks what’s reachable by training. The entanglement-tower
retrain no-go (“you can’t train interpretability in without losing capability”) used a reconstruction
bottleneck — the mAUC axis that already survives forging, not cov95. host_width_sweep.py retries on the
right axis: train tiny GPTs from scratch across host widths, with/without an auxiliary oracle-feature-
recovery loss (a linear head from the residual must predict the exact per-token oracle labels), and measure
native cov95 + capability.
| host width | params | LM-loss unsup→sup | cov95 unsup→sup | Δcov95 |
|---|---|---|---|---|
| 32 | 1.7M | 6.54→6.42 | 0.48→0.62 | +0.14 |
| 64 | 3.4M | 6.21→6.16 | 0.62→0.79 | +0.17 |
| 128 | 7.2M | 5.99→5.99 | 0.69→0.76 | +0.07 |
| 256 † | 16.0M | 6.04→6.05 | 0.45→0.69 | +0.24 |
† w256 is undertrained (16M params on 107k tokens; its unsup LM loss 6.04 is worse than w128’s 5.99) — its low unsupervised cov95 is a compute-budget artifact, not scarcity counter-evidence.
- Reachability — CONFIRMED. Oracle-supervision lifts native cov95 at every width (+0.07…+0.24, mean +0.155) at zero/negative capability cost (mean −0.037 nats — it slightly helps LM loss). So interpretable, equally-capable solutions are not only existent (superposition is linear compression — decompressing preserves the function) but reachable by training pressure, with the manufactured-oracle substrates serving as the training signal, not just the grader. This is the constructive counter to the tower retrain no-go: the no-go used the wrong (reconstruction) axis; supervising the oracle-feature axis lifts monosemanticity for free.
- Scarcity — partially supported. Unsupervised cov95 rises with host width in the well-trained regime (0.48→0.62→0.69 for 32→64→128), consistent with superposition being capacity-driven; it drops at 256 (0.45), but that host is undertrained (16M params on 107k tokens — its LM loss is also worse than w128), a budget artifact, not counter-evidence. So the forge tax is partly a capacity-scarcity artifact — relieved by width up to the training budget, and relieved more cheaply by supervision.
Feeds milestone 1: supervision yields cleaner, more monosemantic low-χ residuals — which are exactly the “registers” the ResidualVM interpreter reads, so a supervised host should decompile further (higher reconstruction-coverage at lower op-budget) than an unsupervised one. That’s the direct hand-off to the recompile harness.
What kind of pressure lifts cov95? (aux-mode comparison)
Is the lift real monosemanticity or just linear recoverability? monosemantic_aux.py compares aux modes at a
fixed well-trained width (128), training from scratch, over 3 seeds (per-seed model init + batch order +
eval-SAE init):
| aux mode | LM-loss | cov95 (mean ± std, 3 seeds) |
|---|---|---|
| none | 6.02 | 0.68 ± 0.04 |
| linear (recoverability) | 6.01 | 0.76 ± 0.00 |
| decorr (orthogonal read-directions) | 6.00 | 0.76 ± 0.00 |
| dedicated (one-neuron-per-feature) | 6.00 | 0.69 ± 0.00 |
| sparsedict (full in-loop aligned SAE) | 6.16 | 0.48 ± 0.07 |
Paired across seeds: linear > none in 3/3 (+0.080 ± 0.043); sparsedict < none in 3/3 (−0.195 ± 0.086);
linear > sparsedict in 3/3 (+0.276) — every signal survives the noise.
The simple linear-recoverability proxy is robustly the best — and every “more direct” monosemanticity
objective fails to beat it: orthogonalizing the probe’s read-directions is inert (decorr ≡ linear, identical
every seed), dedicated raw neurons sit below none’s mean, and the full sparse-dictionary-in-the-loop (a
jointly-trained TopK SAE on the residual, reconstruction + sparsity, first F latents aligned to the oracle) is
robustly the worst — cov95 0.48 (below none in 3/3) and the only one with a real capability cost
(+0.16 nats). The heavier the direct pressure, the worse it gets. So the cov95 lift comes from making features
linearly prominent (so the downstream SAE can isolate them), not from forcing axis-alignment /
sparse-coding in the host residual: the SAE does the factorization; an in-loop dictionary’s reconstruction
pressure distorts the representation in a way that doesn’t transfer to the fresh eval SAE. This answers “is it
just recoverability?” — yes, and recoverability is the effective lever for SAE-measured cov95. Bonus:
supervision also makes cov95 variance-free (linear pins exactly 22/29 features over threshold every seed,
vs none’s noisy 0.62–0.72) — it lifts cov95 and makes it reproducible. monosemantic_aux.py,
runs/cov95_forge_tax/monosemantic_aux_summary.json.
Is the lift real monosemanticity, or the eval-SAE finding what we planted? (non-SAE cross-check)
cov95 fits a TopK SAE — so does the linear aux just make features recoverable in a way a linear-ish SAE
prefers (circular)? legibility_crosscheck.py scores the none→linear lift in three bases (same
symmetric-AUC scorer, only one involving an SAE), width 128, 3 seeds:
| basis | none → linear | Δ (mean ± std) | up in |
|---|---|---|---|
| sae (fitted TopK dictionary) | 0.68 → 0.76 | +0.08 ± 0.04 | 3/3 |
| neuron (raw residual dims, no fit) | 0.68 → 0.76 | +0.08 ± 0.07 | 2/3 |
| pca (rotated basis, fit without labels) | 0.36 → 0.69 | +0.33 ± 0.09 | 3/3 |
Corroborated — the lift is genuine monosemanticity, not circular. Both SAE-free metrics rise too: features
become single-detector-isolable by raw neurons (+0.08) and, most strongly, by PCA components (+0.33). So
supervision doesn’t merely make features recoverable in a way the eval-SAE prefers — it makes them
axis-isolable in the natural and rotated bases as well. (For the unsupervised model, neuron-cov95 equals
sae-cov95 exactly — the fitted SAE adds nothing over raw neurons here, so it can’t be inflating the result.)
Mechanism: the biggest lift is in PCA — unsupervised pca-cov95 is only 0.36 (the oracle features sit off
the high-variance axes), so supervision pushes the features into the residual’s principal (high-variance)
directions, where any axis-aligned probe (neuron, PCA, or SAE) isolates them. That’s a basis-independent
signature of real monosemanticity, and it explains how recoverability becomes monosemanticity: prominence =
high-variance placement. legibility_crosscheck.py, runs/cov95_forge_tax/legibility_crosscheck_summary.json.
Caveats + scope: the aux loss pressures linear recoverability of the oracle (which, per the aux-mode
comparison above, is the effective lever — direct decorr/dedicated/sparsedict objectives don’t beat it, and
the in-loop sparse dict actively hurts). The scarcity trend is only cleanly visible
in the well-trained regime (≤w128 here); confirming it at w256+ needs more tokens/steps (compute scaling),
not more width. Tiny hosts, short training. Status of the open levers: the training-pressure axis is
settled (decorr/dedicated/sparse-dict all tested, none beats linear-recoverability; multi-seed-confirmed) and
the measurement worry is resolved (non-SAE bases — raw-neuron + PCA — corroborate the lift, see cross-check
above → genuine monosemanticity). Remaining follow-ups: (1) multi-seed +
adequately-trained wide hosts to clean the scarcity curve; (3) richer oracles (spaCy POS/NER) + curriculum
annealing of the aux weight; (4) polygram geometry penalties. But the direction is clear and the cost is
~zero, so the reachability lever is real. host_width_sweep.py,
runs/cov95_forge_tax/host_width_sweep_summary.json.
Does feature legibility buy circuit legibility? (oracle-supervised DAG)
The reachability lever makes the feature substrate more legible (cov95 up). oracle_supervised_dag.py asks the
sharp follow-up: does it also make the composition more legible — or are knowledge (features) and computation
(the DAG) the separate axes the forge tax says they are? Train tiny GPTs unsupervised vs oracle-supervised
(the linear lever), 3 seeds, and on each model measure feature legibility (cov95) alongside circuit legibility
(static→dynamic composition ρ; prev-token→induction recovery) and the prev-token head’s key position-vs-token
content (the #26 probe), paired by seed.
| metric | none → linear | seeds + |
|---|---|---|
| cov95 (feature) | 0.690 → 0.736 (Δ +0.046) | 2/3 |
| induction-recovery (circuit) | 1.18 → 1.35 (Δ +0.16) | 3/3 |
| static→dynamic ρ (circuit) | 0.44 → 0.49 (Δ +0.04 ± 0.42) | noise-dominated |
| prev-token key position fraction | 0.33 → 0.13 (Δ −0.20) | 0/3 (drops) |
PARTIAL / SUBSTRATE-DOMINATED — largely separate axes. Supervision robustly reshapes what the residual
represents: cov95 lifts, and the prev-token head’s key shifts token-ward every seed (Δpos −0.20) — the
feature-recovery loss injects token/lexical content into the residual, which the key inherits, making it less
positional. Its effect on the composition is at most marginal: induction-recoverability lifts a small but
consistent +0.16 (3/3), while the broad static→dynamic agreement is noise-dominated (±0.42) on a 16-head host.
So the lever acts mainly on the feature substrate, with only a small spillover to circuit recoverability —
consistent with the program’s knowledge-≠-computation thesis (you cannot supervise circuits into existence with
a feature loss). Scope (honest): the tiny 4-layer host is underpowered for circuit metrics — its induction
is marginal (1.1–1.4× baseline) and its prev-token key is token-dominated even unsupervised (unlike GPT-2’s 4.11
at 59%), so the static→dynamic ρ is high-variance; a definitive circuit-legibility test needs a host with real
circuits (GPT-2 scale), which the reachability lever can’t retrain on this budget. oracle_supervised_dag.py,
runs/cov95_forge_tax/oracle_supervised_dag_summary.json.
Instruction reuse vs specialization — is the op-catalog an ISA? (the “LLM-as-VM” test)
The disassembly reads the named ops as a reusable instruction set. But are the same instructions recruited
across different tasks (genuine reuse → one ISA), or does each task have dedicated heads (specialization)? We’ve
shown the idioms are invariant across languages and architectures, never across tasks within a model —
instruction_reuse.py is that test. It builds the head-class × task causal matrix: mean-ablate each named
op-class and measure the damage to three distinct programs — generic LM (held-out next-token NLL),
induction (NLL on the 2nd copy of a repeated random sequence), IOI (logit(IO)−logit(S)). A class
“serves” a task if ablating it damages it beyond a random-head control.
The matrix is run with five tasks — generic, induction-copy, copy-names (a 2nd copy task, different content), successor (consecutive-number runs → increment), IOI:
| op-class | generic | induction | copy-names | successor | IOI | serves |
|---|---|---|---|---|---|---|
| prev-token | +0.01 | +0.65 ✓ | +1.69 ✓ | +0.45 | +0.16 | copy family |
| induction | +0.03 | +6.53 ✓ | +8.03 ✓ | +5.46 ✓ | +0.26 ✓ | ALL 4 in-context |
| duplicate-token | +0.05 | +0.73 ✓ | +7.46 ✓ | +22.7 ✓ | −0.05 | copy + successor |
| name-mover | −0.00 | +0.10 | +0.50 | +0.42 | +0.06 | (self-repair) |
| S-inhibition | +0.00 | −0.10 | −0.13 | −0.19 | +0.58 ✓ | IOI |
| negative-mover | +0.00 | −0.27 | −0.46 | −0.32 | −0.60 | (writes against IO) |
(values = ablation damage, + hurts the task; ✓ = beyond the random-head control.)
A reused low-level core + task-specific output heads. Thickening the matrix from 3 to 5 tasks flips the read toward reuse:
- None of the named ops are load-bearing for generic LM (every generic cell ≈ 0) — the catalog is in-context-task instructions recruited on demand, not an always-on ISA.
- The low-level copy/addressing ops are genuinely reused across the copy-family:
inductionis load-bearing for all four in-context tasks; prev-token + duplicate serve the copy+successor family. So the shared substrate ops transfer. - Only the output head is task-specific: S-inhibition serves IOI alone.
- A bonus:
successoris carried by the copy ops (induction +5.46, duplicate +22.7), not a dedicated successor head — the model increments partly by in-context copying.
So the named ops are a reusable instruction set for the shared substrate, composed into task-specific
circuits at the output — closer to the VM “shared ISA + per-task programs” than the 3-task version suggested,
but with the architecture-specific caveat that none are always-on. Caveat: name-movers read ~0 only because
mean-ablation triggers the IOI self-repair (quantified in self_repair.py) — not genuine unimportance.
instruction_reuse.py, runs/disassembly/instruction_reuse_summary.json.
Self-repair, made causal (self_repair.py). The name-mover caveat above is quantified: ablating the
primary name-movers (9.6/9.9/10.0) drops the IOI logit-diff by −0.002 (the circuit looks robust), but
ablating primaries and backups (9.0/9.7/10.x/11.2) together drops it −1.04 (+2.57 → +1.53). The backups
are causally idle when primaries are present (Δ +0.26 from removing them) and carry the logit-diff once
primaries are gone (Δ +1.04 — 4× larger). So the op is load-bearing, but a redundant backup pathway masks
it under single-class ablation — a clean instance of the program-wide redundancy (the named circuit ships hot
spares), and exactly why mean-ablation under-counts name-movers. runs/disassembly/self_repair_summary.json.
Does the in-context-copy op survive a non-attention mixer? — SSM vs attention sweep (data, not a verdict)
instruction_reuse.py found induction (in-context copy) to be the one genuinely reused low-level op across
tasks. The disassembly reads that op as attention (a QK predecessor-match feeding an OV copy). Does anything
like it appear when the sequence-mixer is not attention at all? Mamba is a pure state-space model — no
heads, no QK match; mixing is a learned linear recurrence (a scan). ssm_induction.py runs this as an
exhaustive sweep, reporting raw measurements rather than a clean conclusion: two families across three sizes
each (Mamba 130m/370m/790m; GPT-2 small/medium/large), each measured for (a) induction GAIN = (1st-copy NLL −
2nd-copy NLL) on repeated random sequences, over 3 seeds × 3 context lengths; and (b) a per-layer
localization profile = mean-ablate each layer’s mixer (Mamba mixer.out_proj / GPT-2 attn.c_proj), record
the induction-NLL increase.
| model | mixer | L | hidden | induction gain (μ±σ) | gain @ len 12/24/48 | top layer (depth, ΔNLL, conc) |
|---|---|---|---|---|---|---|
| gpt2 | attention | 12 | 768 | +12.54±0.20 | +12.8 / +12.5 / +12.3 | L5 (d0.45, +0.90, c0.27) |
| gpt2-medium | attention | 24 | 1024 | +12.45±0.21 | +12.7 / +12.5 / +12.2 | L3 (d0.13, +0.52, c0.21) |
| gpt2-large | attention | 36 | 1280 | +12.38±0.18 | +12.6 / +12.4 / +12.2 | L0 (d0.0, +15.85, c0.92 — artifact) |
| mamba-130m | SSM | 24 | 768 | +12.10±0.35 | +12.4 / +12.2 / +11.7 | L0 (d0.0, +8.73, c0.47) |
| mamba-370m | SSM | 48 | 1024 | +12.33±0.27 | +12.5 / +12.4 / +12.1 | L0 (d0.0, +4.70, c0.38) |
| mamba-790m | SSM | 48 | 1536 | +12.28±0.30 | +12.6 / +12.4 / +11.9 | L1 (d0.02, +4.43, c0.32) |
What the data shows (descriptive):
- The in-context-copy GAIN is present and large in both families, in a narrow band — +12.10..+12.54 — across 6 models, 3 sizes/family, 3 seeds, 3 lengths. The behaviour that the disassembly attributes to attention is produced just as strongly by a model with no attention. The gain shrinks slightly with context length (12 > 24 > 48) in every model — a consistent secondary trend.
- The localization differs by family — but the headline number is confounded by coarse ablation. Mamba
concentrates the effect on an early layer (L0/L1, large ΔNLL +4.4..+8.7) plus a mid-deep secondary peak
(L17/L34/L23, visible in
runs/gemma/ssm_induction.png); GPT-2 small/medium distribute it across early-mid layers with small per-layer magnitudes (concentration 0.27/0.21). gpt2-large’s L0 spike (+15.85, conc 0.92) is an artifact: mean-ablating L0 in a 36-layer model destroys the detokenizer, not “induction” — so the auto-summary’s “attention concentration 0.47” is dragged up by that one outlier (gpt2/medium are 0.21–0.27). Within Mamba, concentration falls with size (0.47 → 0.38 → 0.32).
What is NOT concluded (the held-off pattern). This is evidence the in-context-copy capability is
architecture-invariant — it does not show the mechanism is the same. (1) the GAIN is a behavioural NLL
effect; (2) whole-layer mean-ablation removes everything a layer does, an over-estimate of “induction
localization”; (3) the two families’ baselines/magnitudes are not directly comparable — compare within family
across size, not across families by magnitude; (4) single seed for the localization profile; (5) the SSM has no
heads, so there is no head-level resolution — we cannot point to a “Mamba induction head,” only a layer. So:
the copy op survives the loss of attention behaviourally, and even survives the loss of heads as a unit of
analysis; whether the SSM implements it by anything resembling a QK-match→OV-copy is unverified here and
would need an SSM-native circuit probe (the recurrence’s state-update, not a head). This complements the
cross-model ceiling result (mechanisms invariant, positional register absolute-position-family-specific): here
the mixer class itself changes and the copy capability persists. ssm_induction.py,
runs/gemma/ssm_induction_summary.json (~SSM models run on the sequential Mamba kernel — slow but exact).
Deep per-operator dossier — the full battery on ONE instruction (induction, first)
The disassembly studied each named op piecemeal: induction lived in composition_dag/ssm_induction,
prev-token in cross_model_positional/key_patch_cross_model, name-movers in self_repair, the key/value
channel in circuit_content_patch, reuse-across-tasks in instruction_reuse. operator_dossier.py inverts
that: pick one operator and run every measurement on it in a single report — --op induction (default),
extensible to prevtok|duplicate|name_mover|s_inhibition. Six sections (A identity · B causal×tasks · C K/V
channels · D composition in/out · E redundancy curve · F cross-model). First dossier — induction on GPT-2:
- A · IDENTITY (behavioural, not hardcoded). Top heads by attention mass on the induction pattern: 5.1 (.81), 5.5 (.78), 6.9 (.77), 7.10 (.75), 7.2 (.73) (then 5.0 .57). The literature set 5.0/5.1/5.5/6.9 is recovered, but behavioural ranking puts 7.10/7.2 ahead of the literature’s 7.11 on this probe — a head-set the downstream sections then inherit (see the B caveat).
- B · CAUSAL × TASKS. Ablating those 5 heads: generic +0.01, induction +6.39*, copy-names +14.42*,
successor +0.01, IOI +0.28* (* = beyond the random-head bar). Serves induction-copy + copy-names + IOI;
not generic. Honest discrepancy:
instruction_reuse.pyfound induction served successor (+5.46) with the literature set (incl. 7.11/5.0); the behavioural top-5 here does not (+0.01). So which heads you call “induction” changes the causal story — the successor-by-copying claim is sensitive to the 7.11-vs-7.10 head choice. A real caveat the piecemeal scripts hid by each fixing its own set. - C · CHANNELS (match vs move, in one view). Reader 5.1 (zero-patch sanity 0.0): KEY/match collapses
+43% when the prev-token head 4.11 is removed from its key (median +0%, 90.8× concentration — sharp,
sparse, one writer); VALUE/move is distributed (top mover 1.10, ΔV-out 0.22 vs 0.08 median). The K/V
dissociation
circuit_content_patchfound across circuits, here localized to the one op. - D · COMPOSITION (local call-graph). IN→key dominated by 4.11 (.101) — the same K-composition edge C
confirms causally — then 4.7/1.0/3.7; OUT→value feeds layer-6/7 heads (6.7/6.8/6.6/7.0), the composed-OV
“virtual heads”
vcomposition.pynamed (induction-moved content re-read as a value downstream). - E · REDUNDANCY (the ablation curve). Primary task induction (baseline +0.674). Per-head solo: 5.1 +1.70,
7.2 +0.22, 6.9 +0.21, 5.5 +0.08, 7.10 −0.03. Cumulative: 1h +1.70 → 2h +2.25 → 3h +3.82 → 4h +5.77 → 5h
+6.39. The full-set effect (+6.39) vastly exceeds the best single head (+1.70) and the sum of solos
(~2.18) → strongly superadditive: the induction heads are a jointly-necessary / synergistic population,
not a single bottleneck and not independent — removing them together hurts far more than their individual
removals predict. (This is the per-op generalization of
self_repair’s redundancy contrast and the progressive-ablation “redundancy depth” probe, folded into the dossier.) - F · CROSS-MODEL. Induction behavioural signal + gain elsewhere: gpt2 .81; gpt2-medium .97 / gain +12.60; Qwen-2.5-1.5B (RoPE) 1.00 / gain +14.07. Strong everywhere, including RoPE — consistent with induction being the content-universal op (the cross-model ceiling + SSM results).
Net (descriptive): consolidating the scattered measurements onto one operator reproduces each prior thread
and surfaces what the piecemeal layout obscured — the head-set sensitivity of the causal profile (B) and the
superadditivity of the redundancy (E). SAE-feature operands (what induction reads/writes in feature space —
the “subword name-completion” finding) are the one battery section not run here (needs a SAE); flagged as the
next layer. operator_dossier.py --op induction, runs/disassembly/operators/dossiers/induction/gpt2.json. The
harness runs the same six sections for the other operators.
The operator CATALOG — every operator class, surveyed across every model
The dossier goes deep on one op; the catalog goes wide and turns the survey into a browsable artifact
(docs/operators/, tree under runs/disassembly/operators/). Two instruments:
operator_atlas.py (the cross-model matrix) + operator_dossier.py (the deep per-op pages), stitched into
docs by operator_catalog_doc.py. The key framing the survey forced: each “operator” is a CLASS — a family
of heads — not a single head. The catalog reports per-(class, model): behavioural signal (max head mass on the
op’s pattern), membership (# heads carrying it), top head + depth, and a uniform causal ΔNLL (mean-ablate
top-3, generic prose).
The catalog matrix — 7 universal/addressing operator classes × 6 models (GPT-2 small/medium/large + Gemma-2-2B / Llama-3.2-1B / Qwen-2.5-1.5B). Signal (membership in parens for GPT-2):
| class | kind | gpt2 | -medium | -large | gemma-2 | llama-3.2 | qwen-2.5 |
|---|---|---|---|---|---|---|---|
| prevtok | positional | .96 (31h) | .99 | .96 | .84 | .68 | .77 |
| induction | content | .92 (22h) | .91 | .96 | .94 | .94 | .99 |
| duplicate | content | .62 (4h) | .86 | .96 | .85 | .74 | .97 |
| sink | addressing | .95 (117h) | .96 | .96 | .07 (0h) | 1.00 | 1.00 |
| self | addressing | .83 (10h) | .45 | .55 | .95 | .96 | 1.00 |
| local | positional | .34 (16h) | .34 | .34 | .31 | .27 | .28 |
| structural | structural | .19 (2h) | .25 | .38 | .23 | .36 | .16 |
What the survey shows (descriptive):
- Induction is the most universal class — signal .91–.99 in all six (the content-copy op the SSM port + cross-model ceiling already flagged as architecture-invariant). prev-token and duplicate are present everywhere too (prev-token weaker in RoPE, .68–.84).
- The sink is the one class that splits by architecture — and it splits 5-vs-1, not GPT-2-vs-rest: present
in the GPT-2 family (.95–.96, 117–553 heads) AND Llama/Qwen (1.00, 292–472 heads), but absent in Gemma-2
(.07, 0 heads). So “attention sink” is common but not universal; Gemma trained without one. And it is
not load-bearing on prose anywhere (causal ΔNLL ≈ 0 every model) — present ≠ depended-on (the
sink_ablationmagnitude-vs-dependence point, now in the matrix; GPT-2’s deeper dependence needs the blocked-attention probe, not mean-ablation). - RoPE models lean on
selfwhere GPT-2 leans on the sink — self signal 1.00/.96/.95 (RoPE) vs .45–.83 (GPT-2), and self is the only universally load-bearing class on prose (causal ΔNLL Qwen +3.77, Gemma +0.51) — the diagonal/current-token read is a RoPE workhorse. structuralis sparse (1–7 heads) but occasionally load-bearing — Llama’s lone structural head ΔNLL +1.47.
The deep dossiers — 9 GPT-2 operator classes (docs/operators/<op>.md): the 7 idiom/addressing classes
behaviourally found + the IOI circuit classes (name-mover, backup name-mover, negative/copy-suppression,
S-inhibition, coreference), which are GPT-2-only (literature DLA head-sets, no published set in the RoPE
models) so they sit outside the cross-model matrix. Highlights the catalog surfaced: the sink “class” is largely
content heads in their idle state (its top-mass heads are 5.1/6.9/7.10 — the induction heads — so ablating
“sink” hurts induction +4.6, not generic); copy-suppression ablation raises IOI (−0.54, it writes against
IO); backup name-movers serve successor; and the head-set sensitivity the piecemeal scripts hid (the
behavioural induction top-5 omits 7.11, so it does not damage successor where instruction_reuse’s literature
set did +5.46).
Taxonomy (answering “N operators or N classes?”): classes. (1) class = the operation (these rows);
(2) instance = an individual head (the disassemble_gpt2.py per-head listing; each dossier §A lists members);
(3) variant = intra-class structure (induction writer-branching; token vs subword-name-completion inductors;
sink-as-idle-state). Documented gaps: succession/greater-than is MLP-dominated (no clean attention head —
carried by the copy ops); SSM/Mamba has no heads (induction present only behaviourally, ssm_induction.py).
Not yet run: the SAE-feature operands per class (needs a SAE); the deep dossier §C/§D/§E ported to the RoPE
models (the atlas covers them at class level; full-depth cross-model is the next layer). operator_atlas.py,
operator_dossier.py, operator_catalog_doc.py; docs/operators/; runs/disassembly/operators/atlas_summary.json.
The CIRCUIT catalog — composed circuits, surveyed & collected across models
Operators are single head-classes; circuits are their compositions (a writer-op feeding a reader-op’s K/Q/V
port, chained). The same cataloging machinery applied one level up: circuit_atlas.py (cross-model edge liveness +
harvest of the GPT-2 discovery artifacts) + circuit_catalog_doc.py → docs/circuits/. The primitive is the
edge (writer-op → reader-op via a port); the discovery gate (composition_dag ΔTV path-patch vs a
reader-matched null) is what collects them. 7 circuits catalogued from two sources.
Cross-model circuit-edge liveness (remove the writer from the reader’s key → % attention collapse, 6 models):
| circuit | defining edge | gpt2 | -medium | -large | gemma-2 | llama-3.2 | qwen-2.5 |
|---|---|---|---|---|---|---|---|
| induction | prev-tok head –K–> induction | +17% | +23% | +8% | +18% | +70% | +89% |
| positional_broadcast | sink/hub –K–> prev-tok key | +22% | +32% | +0% | +0% | (skip) | +0% |
| duplicate | same-token reader (writer often L0) | (skip) | +0% | +4% | +3% | +9% | +13% |
- The induction edge (prev-token → induction) is live in every model — and stronger in RoPE (Llama +70%, Qwen +89% vs GPT-2 +8–23%): content matching lives in the key everywhere, so removing the predecessor-writer collapses the induction reader’s attention universally. (Cross-model mechanism-invariance, at the edge level now, not just the node.)
- positional_broadcast is GPT-2-small/medium-only (+22%/+32%, ≈0 in gpt2-large/Gemma/Qwen, skip Llama) — the same absolute-position signature as the operator catalog’s sink: RoPE reads position from the rotation, so the prev-token key has no upstream writer to remove. The decompilable plumbing circuit is family-specific even though the content circuit is universal.
GPT-2 discovered / circuit-specific (harvested from the committed discovery runs, no recompute — GPT-2-only):
the IOI Q-chain duplicate→S-inhibition→name-mover (5 Q-edges live; causal z from ioi_causal: negative/
copy-suppression z=62 writes against IO, duplicate z=6.0; self-repair: −primaries ΔLD −0.002 but −both
−1.04, backups are hot spares); the V-composition “virtual heads” (induction 5.9 → layer-6 value 6.7,
ΔV-out 1.32 — composed-OV, changes what is moved); the induction K-chain weights (canonical writer 4.11,
4/5 edges live); and the 22 NOVEL-LIVE discovered edges (13 behaviourally named — mostly early sink/write-hub →
prev-token-key broadcasters: the collection-goal output, validate_new_edges).
Taxonomy & gaps. Levels: circuit (a DAG of operator nodes) → edge (writer→reader via a port) → operator class
at each node. Gaps: succession/greater-than is MLP-dominated (no clean attention-composition circuit);
SSM/Mamba has no heads (no composition edges; induction only behavioural). Not yet run: the IOI Q-chain /
V-composition cross-model (no published head-sets off GPT-2); per-edge path-patch of all 22 discovered edges on
the RoPE models. circuit_atlas.py, circuit_catalog_doc.py; docs/circuits/;
runs/disassembly/circuits/atlas_summary.json.
ResidualVM debugger — a programmatic discovery engine (the catalog’s growth tool)
The catalogs above are surveys of what we named; the debugger is the tool that finds what we have not. It is
programmatically steppable (not a human REPL — it returns structured data; a UI could sit on top): it
instruments the forward pass as a steppable interpreter and exposes trace (per-head/MLP residual writes),
intervene (mean-ablate any heads/MLPs → next-token KL), logit_lens_step (per-layer KL-to-final: where the
answer is decided), attribution_sweep (ablate every head + every MLP one-at-a-time, rank by causal
effect on a behaviour, and flag the strong components the catalog has NOT named = candidate new operators), and
edge_probe (path-patch each upstream component out of a reader → candidate circuit edges). main() drives
it; it is the engine for being exhaustive, not a place to hand-poke.
Run on GPT-2 across three behaviours (induction / IOI / generic LM), it immediately produced new catalog work:
- MLP0 had the largest single-component causal effect across all three behaviours (induction ΔNLL +7.8, IOI +7.5, generic +1.8) — far above any attention head. The discovery engine’s first finding is that the biggest operator in the model is an MLP we have not catalogued (the L0 detokenizer): a direct pointer to the MLP/COMPUTE gap (the attention catalog is only half the instruction set).
- Candidate UNNAMED operators, by behaviour: induction → 7.6 (+0.36, more load-bearing than the prev-token head 4.11) and 2.11/8.1/8.5; IOI → 5.9 (+1.00), 8.3, 0.10, 1.3; generic → none above +0.05 (it is distributed + MLP-carried — corroborates the operator catalog’s “no named attention op serves generic LM”).
- A discovered circuit: edge-probing the discovered op 7.6 shows it is fed by the induction heads (5.1 ΔNLL +1.26, 6.9) + the prev-token head (4.11 +0.40) + the candidate writers 2.11 / 5.9 — i.e. 7.6 is a downstream induction-consumer (a late induction-output mover) the literature head-set omits. A new circuit node + its in-edges, found mechanically.
- logit-lens step: induction next-token KL-to-final falls 5.8 → 0.3 by layer L6 (decided exactly where the induction heads fire), 0.1 by L9.
So the debugger closes the loop the goal asks for: it generates candidate operators (7.6, 5.9, 8.3, MLP0) and
candidate circuit edges (2.11→7.6, 5.9→7.6) that are not yet in the catalog — the next things to dossier.
residual_vm_debugger.py, runs/disassembly/residual_vm_debugger_summary.json (GPT-2; intervention harness is
arch-generic, the QK edge-probe is GPT-2-specific here).
The other instruction class — MLP / COMPUTE, across models
Attention MOVES operands; the MLP COMPUTES on them — and a salient discovery-sweep result was that
MLP0 had the largest single-component causal effect measured. The operator catalog was attention-only; mlp_atlas.py adds
the COMPUTE class, surveyed across architectures: per (model, layer) it mean-ablates the whole MLP block and reads
the causal ΔNLL on generic prose + induction. (Mamba/SSM is excluded — no separate MLP block, the COMPUTE analog
of “no attention heads”.)
| model | L | all-MLP ablated ΔNLL (generic) | top generic MLP (depth, Δ) | top induction MLP (depth, Δ) |
|---|---|---|---|---|
| gpt2 | 12 | +2.09 | L0 (d0.0, +1.70) | L0 (d0.0, +11.72) |
| gpt2-medium | 24 | +2.69 | L0 (d0.0, +7.32) | L0 (d0.0, +20.94) |
| gpt2-large | 36 | +5.28 | L0 (d0.0, +3.67) | L0 (d0.0, +13.57) |
| gemma-2-2b | 26 | +10.74 | L25 (d1.0, +0.84) | L0 (d0.0, +4.25) |
| llama-3.2-1b | 16 | +4.18 | L1 (d0.07, +7.35) | L1 (d0.07, +12.65) |
| qwen-2.5-1.5b | 28 | +4.29 | L1 (d0.04, +7.64) | L2 (d0.07, +13.91) |
What the survey shows (descriptive):
- COMPUTE concentrates on an early MLP — the detokenizer — in 5 of 6 models (GPT-2 family L0, Llama L1, Qwen L1–L2): the single biggest COMPUTE op sits at depth ~0, and its ablation is catastrophic for induction (+11.7…+20.9 ΔNLL — far above any single attention head, confirming the discovery engine’s MLP0 finding).
- Gemma-2 is the exception — distributed COMPUTE: no single MLP dominates (top generic only +0.84), yet the whole-stack ablation is the largest of all (+10.74). Its COMPUTE is spread across layers, not concentrated in a detokenizer. (A second cross-architecture split, alongside Gemma’s missing sink in the operator catalog.)
- The whole-MLP-stack ablation is large in every model — COMPUTE is load-bearing everywhere, unlike any single attention-op class (no named attention op damages generic LM; the MLP stack always does).
GPT-2 deep characterization (harvested): the COMPUTE vocabulary is low-rank (mlp_catalog.py: transform
participation 22 vs 1666 random — a small reused set of read→write templates, heavier-tailed than attention’s ~5);
MLPs carry the reconstruction coverage (mlp_ops.py: MLP-only coverage +0.46 vs attention-only ≈0 — they
interact); head↔MLP composition edges exist in weight space. Per-neuron read→write idioms are GPT-2-only (the
cheap token-unembedding basis); the cross-model rows are per-layer causal profiles. mlp_atlas.py,
runs/disassembly/operators/mlp_compute_summary.json, docs/operators/mlp_compute.md.
Boundaries / risks
- Recompile faithfulness is OOD-sensitive (partial reconstructions are low-norm inputs to
lm_head); use norm-preserving mean-ablation + a random-op-budget control (the lesson from the entanglement-tower ablations) so the curve measures structure, not norm artifacts. - Path-patching cost scales with edges; the weight-space edge scorer must prune before patching.
- The operand basis is the bottleneck (the P2 lesson recurs): a wrong basis caps coverage regardless of the algorithm. Expect to co-design the basis (U_C / polygram) with the extractor.
- Single-token-prediction framing — a faithful decompilation of next-token logits is not a proof of the model’s mechanism off-distribution; scope claims to the measured distribution.
Dependencies (cross-repo)
sae-forge (recompile/verify — the metric), polygram (operand-dictionary geometry — the basis), n-orca
(emit the circuit DAG as a typed graph), larql (decompile→query→recompile as a queryable index). The
disassembly toolkit here (scripts/disassembly/, scripts/gemma/) is the front-end that produces the ops.