Several recent SAE variants (Matryoshka, Temporal) aim to recover hierarchical structure from language-model activations, but there is little quantitative testing of whether the recovered features form coherent parent-child relationships. This project builds a coverage-based diagnostic for that question, validates it on synthetic ground truth and on negative controls, and applies it to a real Gemma-2-2b Matryoshka SAE (SOAR project I-6).
A coherent hierarchy has three measurable signatures: coverage (P(parent|child) ~ 1, the child
implies its parent), asymmetry (coverage parent|child >> child|parent, distinguishing hierarchy
from symmetric co-occurrence), and sibling exclusivity (children of one parent partition its
activations, so high sibling overlap signals feature absorption). Because a frequently-firing parent
covers everything by chance, the diagnostic always reports lift (P(parent|child)/P(parent)) and
compares against a shuffled-pair null and a permutation p-value.
A planted 2-level tree (5 parents, 4 children each; 25 features, 20 edges), 4,000 samples, base rate
0.069, with tunable noise (p_drop, unrelated firing, and a p_absorb knob that injects absorption).
Recovers planted hierarchy. True edges vs random pairs: coverage 0.814 vs 0.042, lift 4.33, asymmetry 0.646, separation AUC 0.980, significant by permutation (observed 0.646 vs null 0.150, p = 0.003). Each child's best candidate parent is its true parent in 100% of cases, and unsupervised discovery recovers the full 20-edge tree at recall 1.00.
Does not hallucinate hierarchy. On independent features the diagnostic reports chance (asymmetry 0.00, AUC 0.54). On symmetrically co-firing pairs it reports high coverage (1.00) but ~zero asymmetry - the concrete demonstration that raw coverage alone would be fooled and the asymmetry term does real work.
Detects absorption / robust. Sibling overlap rises monotonically with injected absorption (0.006 -> 0.283); coverage falls gracefully under parent-drop noise while AUC holds ~0.98; a 3-level hierarchy shows transitive ancestor->descendant coverage (AUC 0.918); AUC is stable across thresholds.
Applied to the layer-12 Matryoshka SAE (chanind/...-w-32k-l0-40), coarsest 2,048 latents, ~100 C4
documents. Activations use continuous tokenization (BOS only on the first chunk, to avoid flooding the
statistics with BOS-token features), with stop_at_layer=13 and bfloat16 to fit a single GPU.
The raw signal is confounded. The aggregate cross-group test is weak (outer->inner coverage 0.091
vs null 0.056, AUC 0.605), and that weak signal is itself partly an artifact: coarse Matryoshka latents
fire more often, and P(parent|child) - P(child|parent) is positive whenever the parent's base rate
exceeds the child's, regardless of any real relationship. So coverage and asymmetry cannot be read
directly.
Lift correction + best-parent. Correcting by base rate (lift) and taking each fine latent's single best coarse parent (1,920 children with a candidate parent):
| best-parent lift | Real SAE | Shuffle control |
|---|---|---|
| median | 28.6 | 3.07 |
| mean | 83.7 | 4.37 |
| frac(lift > 2) | 0.99 | 0.77 |
The shuffle control permutes each latent independently - destroying co-activation while preserving base rates and sparsity. The real median best-parent lift (28.6) is about 9x the control (3.07), so coarse latents cover fine ones far above what base rate and sparsity alone produce. A genuine, above-chance co-activation hierarchy exists in the layer-12 Matryoshka latents.
frac(>2) is uninformative because the max-over-candidates selection pushes even the shuffle to
0.77. Reporting the single big number without the control would have overstated the effect.src/
coverage.py # coverage, lift, asymmetry, evaluate_hierarchy, sibling_overlap,
# best_parent, parent_recovery, permutation_pvalue, cross_group_coverage, propose_edges
synthetic.py # planted hierarchy (+ absorption knob), flat & symmetric controls, deep 3-level tree
sae_loading.py # SAELens loader for the Gemma-2-2b Matryoshka SAE + group boundaries -- real path
activations.py # TransformerLens hooks -> SAE feature activations -- real path
tests/
test_diagnostic.py # 9 property tests (recovery, controls, absorption, p-value, robustness)
notebooks/
01_mvp.ipynb # full synthetic validation + controls (+ gated real-SAE path)
figures/
Tests: python tests/test_diagnostic.py (or pytest tests/)
Colab: open notebooks/01_mvp.ipynb, run the clone + pip cell, run all. Synthetic by default
(USE_REAL = False, no downloads). Set USE_REAL = True for the SAE path (network + GPU + HF access).
Local:
pip install -r requirements.txt
jupyter notebook notebooks/01_mvp.ipynb
2 commits
Jupyter Notebook
78.2%
Python
21.8%
Several recent SAE variants (Matryoshka, Temporal) aim to recover hierarchical structure from language-model activations, but there is little quantitative testing of whether the recovered features form coherent parent-child relationships. This project builds a coverage-based diagnostic for that question, validates it on synthetic ground truth and on negative controls, and applies it to a real Gemma-2-2b Matryoshka SAE (SOAR project I-6).
A coherent hierarchy has three measurable signatures: coverage (P(parent|child) ~ 1, the child
implies its parent), asymmetry (coverage parent|child >> child|parent, distinguishing hierarchy
from symmetric co-occurrence), and sibling exclusivity (children of one parent partition its
activations, so high sibling overlap signals feature absorption). Because a frequently-firing parent
covers everything by chance, the diagnostic always reports lift (P(parent|child)/P(parent)) and
compares against a shuffled-pair null and a permutation p-value.
A planted 2-level tree (5 parents, 4 children each; 25 features, 20 edges), 4,000 samples, base rate
0.069, with tunable noise (p_drop, unrelated firing, and a p_absorb knob that injects absorption).
Recovers planted hierarchy. True edges vs random pairs: coverage 0.814 vs 0.042, lift 4.33, asymmetry 0.646, separation AUC 0.980, significant by permutation (observed 0.646 vs null 0.150, p = 0.003). Each child's best candidate parent is its true parent in 100% of cases, and unsupervised discovery recovers the full 20-edge tree at recall 1.00.
Does not hallucinate hierarchy. On independent features the diagnostic reports chance (asymmetry 0.00, AUC 0.54). On symmetrically co-firing pairs it reports high coverage (1.00) but ~zero asymmetry - the concrete demonstration that raw coverage alone would be fooled and the asymmetry term does real work.
Detects absorption / robust. Sibling overlap rises monotonically with injected absorption (0.006 -> 0.283); coverage falls gracefully under parent-drop noise while AUC holds ~0.98; a 3-level hierarchy shows transitive ancestor->descendant coverage (AUC 0.918); AUC is stable across thresholds.
Applied to the layer-12 Matryoshka SAE (chanind/...-w-32k-l0-40), coarsest 2,048 latents, ~100 C4
documents. Activations use continuous tokenization (BOS only on the first chunk, to avoid flooding the
statistics with BOS-token features), with stop_at_layer=13 and bfloat16 to fit a single GPU.
The raw signal is confounded. The aggregate cross-group test is weak (outer->inner coverage 0.091
vs null 0.056, AUC 0.605), and that weak signal is itself partly an artifact: coarse Matryoshka latents
fire more often, and P(parent|child) - P(child|parent) is positive whenever the parent's base rate
exceeds the child's, regardless of any real relationship. So coverage and asymmetry cannot be read
directly.
Lift correction + best-parent. Correcting by base rate (lift) and taking each fine latent's single best coarse parent (1,920 children with a candidate parent):
| best-parent lift | Real SAE | Shuffle control |
|---|---|---|
| median | 28.6 | 3.07 |
| mean | 83.7 | 4.37 |
| frac(lift > 2) | 0.99 | 0.77 |
The shuffle control permutes each latent independently - destroying co-activation while preserving base rates and sparsity. The real median best-parent lift (28.6) is about 9x the control (3.07), so coarse latents cover fine ones far above what base rate and sparsity alone produce. A genuine, above-chance co-activation hierarchy exists in the layer-12 Matryoshka latents.
frac(>2) is uninformative because the max-over-candidates selection pushes even the shuffle to
0.77. Reporting the single big number without the control would have overstated the effect.src/
coverage.py # coverage, lift, asymmetry, evaluate_hierarchy, sibling_overlap,
# best_parent, parent_recovery, permutation_pvalue, cross_group_coverage, propose_edges
synthetic.py # planted hierarchy (+ absorption knob), flat & symmetric controls, deep 3-level tree
sae_loading.py # SAELens loader for the Gemma-2-2b Matryoshka SAE + group boundaries -- real path
activations.py # TransformerLens hooks -> SAE feature activations -- real path
tests/
test_diagnostic.py # 9 property tests (recovery, controls, absorption, p-value, robustness)
notebooks/
01_mvp.ipynb # full synthetic validation + controls (+ gated real-SAE path)
figures/
Tests: python tests/test_diagnostic.py (or pytest tests/)
Colab: open notebooks/01_mvp.ipynb, run the clone + pip cell, run all. Synthetic by default
(USE_REAL = False, no downloads). Set USE_REAL = True for the SAE path (network + GPU + HF access).
Local:
pip install -r requirements.txt
jupyter notebook notebooks/01_mvp.ipynb
2 commits
Jupyter Notebook
78.2%
Python
21.8%