twinlens 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. twinlens-1.1.0/LICENSE +21 -0
  2. twinlens-1.1.0/PKG-INFO +201 -0
  3. twinlens-1.1.0/README.md +159 -0
  4. twinlens-1.1.0/pyproject.toml +77 -0
  5. twinlens-1.1.0/server/__init__.py +0 -0
  6. twinlens-1.1.0/server/app.py +414 -0
  7. twinlens-1.1.0/server/schemas.py +41 -0
  8. twinlens-1.1.0/server/state.py +43 -0
  9. twinlens-1.1.0/setup.cfg +4 -0
  10. twinlens-1.1.0/tests/test_api.py +143 -0
  11. twinlens-1.1.0/tests/test_api_validation.py +130 -0
  12. twinlens-1.1.0/tests/test_causal.py +95 -0
  13. twinlens-1.1.0/tests/test_cli.py +107 -0
  14. twinlens-1.1.0/tests/test_gpt2.py +59 -0
  15. twinlens-1.1.0/tests/test_metrics.py +103 -0
  16. twinlens-1.1.0/tests/test_qwen.py +107 -0
  17. twinlens-1.1.0/tests/test_reproduction.py +73 -0
  18. twinlens-1.1.0/tests/test_twin.py +116 -0
  19. twinlens-1.1.0/twinlens/__init__.py +40 -0
  20. twinlens-1.1.0/twinlens/causal/__init__.py +0 -0
  21. twinlens-1.1.0/twinlens/causal/attribution.py +97 -0
  22. twinlens-1.1.0/twinlens/causal/interventions.py +142 -0
  23. twinlens-1.1.0/twinlens/causal/patching.py +167 -0
  24. twinlens-1.1.0/twinlens/circuits/__init__.py +0 -0
  25. twinlens-1.1.0/twinlens/circuits/datasets.py +158 -0
  26. twinlens-1.1.0/twinlens/circuits/known.py +53 -0
  27. twinlens-1.1.0/twinlens/cli.py +178 -0
  28. twinlens-1.1.0/twinlens/core/__init__.py +0 -0
  29. twinlens-1.1.0/twinlens/core/gpt2.py +295 -0
  30. twinlens-1.1.0/twinlens/core/qwen3.py +180 -0
  31. twinlens-1.1.0/twinlens/core/weights.py +119 -0
  32. twinlens-1.1.0/twinlens/fidelity/__init__.py +0 -0
  33. twinlens-1.1.0/twinlens/fidelity/engine.py +131 -0
  34. twinlens-1.1.0/twinlens/fidelity/metrics.py +99 -0
  35. twinlens-1.1.0/twinlens/fidelity/signals.py +53 -0
  36. twinlens-1.1.0/twinlens/fidelity/trust.py +91 -0
  37. twinlens-1.1.0/twinlens/fidelity/trust_model.json +52 -0
  38. twinlens-1.1.0/twinlens/twin/__init__.py +0 -0
  39. twinlens-1.1.0/twinlens/twin/replacement.py +452 -0
  40. twinlens-1.1.0/twinlens/twin/transcoders.py +221 -0
  41. twinlens-1.1.0/twinlens.egg-info/PKG-INFO +201 -0
  42. twinlens-1.1.0/twinlens.egg-info/SOURCES.txt +44 -0
  43. twinlens-1.1.0/twinlens.egg-info/dependency_links.txt +1 -0
  44. twinlens-1.1.0/twinlens.egg-info/entry_points.txt +2 -0
  45. twinlens-1.1.0/twinlens.egg-info/requires.txt +22 -0
  46. twinlens-1.1.0/twinlens.egg-info/top_level.txt +2 -0
twinlens-1.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SpiliosDimakopoulos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: twinlens
3
+ Version: 1.1.0
4
+ Summary: A digital twin of an LLM's internals that measures how far its own explanations can be trusted.
5
+ Author: Spilios Dimakopoulos
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://spiliosdimakopoulos.github.io/twinlens/
8
+ Project-URL: Source, https://github.com/SpiliosDimakopoulos/twinlens
9
+ Project-URL: Issues, https://github.com/SpiliosDimakopoulos/twinlens/issues
10
+ Project-URL: Citation, https://doi.org/10.5281/zenodo.23437556
11
+ Keywords: interpretability,mechanistic-interpretability,transcoders,gpt-2,faithfulness,explanations,llm
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: torch>=2.2
22
+ Requires-Dist: numpy>=1.26
23
+ Requires-Dist: tokenizers>=0.19
24
+ Requires-Dist: huggingface_hub>=0.23
25
+ Requires-Dist: pandas>=2.1
26
+ Requires-Dist: pydantic>=2.5
27
+ Provides-Extra: server
28
+ Requires-Dist: fastapi>=0.110; extra == "server"
29
+ Requires-Dist: uvicorn[standard]>=0.29; extra == "server"
30
+ Provides-Extra: dev
31
+ Requires-Dist: twinlens[server]; extra == "dev"
32
+ Requires-Dist: pytest; extra == "dev"
33
+ Requires-Dist: pytest-timeout; extra == "dev"
34
+ Requires-Dist: httpx; extra == "dev"
35
+ Requires-Dist: ruff; extra == "dev"
36
+ Requires-Dist: matplotlib; extra == "dev"
37
+ Requires-Dist: pyarrow; extra == "dev"
38
+ Requires-Dist: scipy; extra == "dev"
39
+ Requires-Dist: psutil; extra == "dev"
40
+ Requires-Dist: transformers; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ # TwinLens
44
+
45
+ [![ci](https://github.com/SpiliosDimakopoulos/twinlens/actions/workflows/ci.yml/badge.svg)](https://github.com/SpiliosDimakopoulos/twinlens/actions/workflows/ci.yml) [![live demo](https://img.shields.io/badge/live%20demo-open-2a78d6)](https://spiliosdimakopoulos.github.io/twinlens/) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23437556.svg)](https://doi.org/10.5281/zenodo.23437556) [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/SpiliosDimakopoulos/twinlens/blob/main/notebooks/audit_your_prompt.ipynb)
46
+
47
+ **Do attribution graphs predict what a language model actually does?** An attribution graph
48
+ explains a prediction with arrows: "this internal feature pushes the answer up by this much".
49
+ TwinLens tests those arrows. For each one it switches the feature off in GPT-2 and compares the
50
+ graph's predicted effect with the real one.
51
+
52
+ On 8,064 such edits the graph is often wrong, sometimes in the opposite direction. Most of the
53
+ error comes from two parts of the model that the graph holds fixed, LayerNorm and attention.
54
+ Adding their first-order response back keeps the graph linear and raises its rank agreement
55
+ with GPT-2 from ρ = 0.20 to 0.53. The result replicates on Qwen3-0.6B.
56
+
57
+ ![IOI: the attribution graph predicts the edit lowers log p(Mary) by 8.1; GPT-2 actually raises it by 0.23; the graph with the fix predicts +0.32](docs/screenshots/hero.png)
58
+
59
+ *In the IOI prompt above, today's attribution graph predicts that switching off one feature
60
+ lowers the probability of "Mary" by 8.1 nats. GPT-2 actually raises it by 0.23. The same linear
61
+ graph with the two derivatives restored predicts +0.32.*
62
+
63
+ ## Try it
64
+
65
+ **In the browser:** the [live demo](https://spiliosdimakopoulos.github.io/twinlens/) runs on
66
+ precomputed GPT-2 runs. Click a dot in the graph and compare each prediction with what GPT-2 did.
67
+
68
+ **On your own prompt, in Colab:** [`notebooks/audit_your_prompt.ipynb`](https://colab.research.google.com/github/SpiliosDimakopoulos/twinlens/blob/main/notebooks/audit_your_prompt.ipynb)
69
+ (free CPU runtime, about five minutes including downloads).
70
+
71
+ **From a terminal:**
72
+
73
+ ```bash
74
+ pip install torch --index-url https://download.pytorch.org/whl/cpu # CPU build, ~200 MB
75
+ pip install "twinlens @ git+https://github.com/SpiliosDimakopoulos/twinlens"
76
+ twinlens audit "When Mary and John went to the store, John gave a drink to"
77
+ ```
78
+
79
+ The first run downloads GPT-2 small (0.5 GB) and its 12 transcoders (1.8 GB) from the Hugging
80
+ Face Hub into `~/.cache/twinlens` (set `TWINLENS_CACHE` to move it). `twinlens audit` checks the
81
+ strongest arrows of the graph and prints one row per arrow:
82
+
83
+ ```
84
+ layer word feature graph fixed GPT-2 graph verdict fixed verdict trust
85
+ 0 Mary 5545 -8.117 +0.318 +0.228 WRONG SIGN ok 7%
86
+ 10 to 13585 +0.014 +0.031 +0.032 understated ok 98%
87
+ 7 Mary 12771 -0.186 +0.032 +0.034 WRONG SIGN ok 81%
88
+ ...
89
+ Today's graph is off on 3 of 8 edges; with the fix, 0 of 8.
90
+ ```
91
+
92
+ - **graph**: today's attribution graph (LayerNorm and attention frozen);
93
+ - **fixed**: the same linear graph with the LayerNorm and attention derivatives kept;
94
+ - **GPT-2**: the real model with the edit applied;
95
+ - numbers are the change in log-probability of GPT-2's answer (−1 ≈ 2.7× less likely);
96
+ - **trust** estimates, from the unedited run only, how likely the graph's number is to be close.
97
+ It was fitted on ablations and ×2 edits of this study's prompts; treat it as a warning light,
98
+ not a probability you can bank on.
99
+
100
+ Add `--json` for machine-readable output and `--top 20` for more arrows.
101
+
102
+ ## Results
103
+
104
+ From 8,064 edits on GPT-2 small (56 prompts, 8 task families). Full write-up:
105
+ [report](https://spiliosdimakopoulos.github.io/twinlens/report/) ([PDF](docs/TwinLens_report.pdf)).
106
+
107
+ | | today's graph | gradient through real GPT-2 | with the fix |
108
+ |---|---|---|---|
109
+ | rank agreement with the real effect (Spearman ρ) | 0.20 | 0.38 | 0.53 |
110
+ | right direction, on edits that move the answer | 67% | 77% | 86% |
111
+ | badly wrong (off by more than 1 nat) | 2.2% | 0.1% | 0.05% |
112
+
113
+ - **Why the graph fails.** Frozen LayerNorm causes most of the error and nearly all large
114
+ misses, almost all of them for edits in the first three layers. Frozen attention causes most
115
+ of the ranking error.
116
+ - **The fix.** Keeping the LayerNorm and attention-softmax Jacobians gives a graph that is still
117
+ linear and matches the nonlinear twin. It also ranks and signs effects better than attribution
118
+ patching, a gradient through the real model (Δρ +0.15 [0.11, 0.19]). On Qwen3-0.6B the gain
119
+ over today's graph is smaller but holds on every metric (Δρ +0.12 [0.05, 0.20]). IOI prompts
120
+ go from ρ 0.01 to 0.59.
121
+ - **Spotting bad predictions.** A logistic model over nine signals from the unedited run flags
122
+ failures with AUROC 0.84 on edits that move the answer. Disagreement between today's graph
123
+ and the fixed graph is an even better warning (0.94).
124
+ - **Upstream.** The fix is proposed to the open-source
125
+ [circuit-tracer](https://github.com/decoderesearch/circuit-tracer) library as opt-in flags
126
+ ([PR #123](https://github.com/decoderesearch/circuit-tracer/pull/123), under review).
127
+
128
+ <p>
129
+ <img src="experiments/figures/F5_fix.png" width="100%" alt="Keeping two derivatives makes the linear graph as faithful as the nonlinear twin">
130
+ </p>
131
+
132
+ ## Limitations
133
+
134
+ - Two small models (GPT-2 small; Qwen3-0.6B, upper 18 layers only) and per-layer transcoders.
135
+ Cross-layer transcoders, used by the current circuit-tracing work, are untested.
136
+ - 56 short prompts written by the author; one prediction target per prompt.
137
+ - Only feature-to-output effects are measured, not feature-to-feature edges.
138
+ - Most edits change the answer very little; the rank and sign results are the robust ones.
139
+
140
+ Details and the remaining caveats are in §5 of the [report](https://spiliosdimakopoulos.github.io/twinlens/report/).
141
+
142
+ ## How it works
143
+
144
+ GPT-2 runs on a ~250-line PyTorch implementation with hook points
145
+ ([`twinlens/core/gpt2.py`](twinlens/core/gpt2.py)); its logits match Hugging Face to 7×10⁻⁵.
146
+ Before measuring anything new it reproduces known GPT-2 circuits (induction heads, the IOI name
147
+ movers, the sites of factual recall), each pinned as a test. MLPs are replaced by the
148
+ transcoders of Dunefsky, Chlenski & Nanda (2024) plus a fixed error term, and five twin levels
149
+ switch one part of the real model back on at a time, so every gap is attributed to its source.
150
+
151
+ Weights and transcoders are memory-mapped, so the whole system runs in 0.82 GB of RAM on a
152
+ laptop CPU ([`DECISIONS.md`](DECISIONS.md)):
153
+
154
+ | CPU latency (Ryzen 7 5700U, 15 tokens) | |
155
+ |---|---|
156
+ | forward pass | 194 ms |
157
+ | twin trace (12 transcoders) | 1.0 s |
158
+ | what-if, graph twin / nonlinear twin | 0.17 s / 0.45 s |
159
+
160
+ ## Run the dashboard locally
161
+
162
+ ```bash
163
+ python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
164
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
165
+ pip install -e ".[dev]"
166
+ cd web && npm ci && cd ..
167
+ make serve # http://localhost:8000
168
+ ```
169
+
170
+ Locally the dashboard accepts any prompt. `docker compose up` does the same in a container.
171
+
172
+ ## Reproduce the paper
173
+
174
+ The sweep's raw results are committed, so `make analyze` re-derives every number in the report
175
+ in a few minutes without a model. Re-running the sweeps themselves is described in §7 of the
176
+ report. `make test`, `make web-test` and `make e2e` run the test suites.
177
+
178
+ ```
179
+ twinlens/ model, twin levels, interventions, fidelity metrics, trust model, CLI
180
+ server/ FastAPI (REST + WebSocket)
181
+ web/ React + TypeScript dashboard
182
+ experiments/ sweeps, analysis, figures, committed results
183
+ paper/ the technical report
184
+ ```
185
+
186
+ ## Cite
187
+
188
+ ```bibtex
189
+ @techreport{dimakopoulos2026twinlens,
190
+ title = {Attribution graphs fail because of the derivatives they drop, not because they are linear},
191
+ author = {Dimakopoulos, Spilios},
192
+ year = {2026},
193
+ institution = {TwinLens project},
194
+ doi = {10.5281/zenodo.23437556},
195
+ url = {https://github.com/SpiliosDimakopoulos/twinlens}
196
+ }
197
+ ```
198
+
199
+ ## Contributing
200
+
201
+ Issues and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). MIT licence.
@@ -0,0 +1,159 @@
1
+ # TwinLens
2
+
3
+ [![ci](https://github.com/SpiliosDimakopoulos/twinlens/actions/workflows/ci.yml/badge.svg)](https://github.com/SpiliosDimakopoulos/twinlens/actions/workflows/ci.yml) [![live demo](https://img.shields.io/badge/live%20demo-open-2a78d6)](https://spiliosdimakopoulos.github.io/twinlens/) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23437556.svg)](https://doi.org/10.5281/zenodo.23437556) [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/SpiliosDimakopoulos/twinlens/blob/main/notebooks/audit_your_prompt.ipynb)
4
+
5
+ **Do attribution graphs predict what a language model actually does?** An attribution graph
6
+ explains a prediction with arrows: "this internal feature pushes the answer up by this much".
7
+ TwinLens tests those arrows. For each one it switches the feature off in GPT-2 and compares the
8
+ graph's predicted effect with the real one.
9
+
10
+ On 8,064 such edits the graph is often wrong, sometimes in the opposite direction. Most of the
11
+ error comes from two parts of the model that the graph holds fixed, LayerNorm and attention.
12
+ Adding their first-order response back keeps the graph linear and raises its rank agreement
13
+ with GPT-2 from ρ = 0.20 to 0.53. The result replicates on Qwen3-0.6B.
14
+
15
+ ![IOI: the attribution graph predicts the edit lowers log p(Mary) by 8.1; GPT-2 actually raises it by 0.23; the graph with the fix predicts +0.32](docs/screenshots/hero.png)
16
+
17
+ *In the IOI prompt above, today's attribution graph predicts that switching off one feature
18
+ lowers the probability of "Mary" by 8.1 nats. GPT-2 actually raises it by 0.23. The same linear
19
+ graph with the two derivatives restored predicts +0.32.*
20
+
21
+ ## Try it
22
+
23
+ **In the browser:** the [live demo](https://spiliosdimakopoulos.github.io/twinlens/) runs on
24
+ precomputed GPT-2 runs. Click a dot in the graph and compare each prediction with what GPT-2 did.
25
+
26
+ **On your own prompt, in Colab:** [`notebooks/audit_your_prompt.ipynb`](https://colab.research.google.com/github/SpiliosDimakopoulos/twinlens/blob/main/notebooks/audit_your_prompt.ipynb)
27
+ (free CPU runtime, about five minutes including downloads).
28
+
29
+ **From a terminal:**
30
+
31
+ ```bash
32
+ pip install torch --index-url https://download.pytorch.org/whl/cpu # CPU build, ~200 MB
33
+ pip install "twinlens @ git+https://github.com/SpiliosDimakopoulos/twinlens"
34
+ twinlens audit "When Mary and John went to the store, John gave a drink to"
35
+ ```
36
+
37
+ The first run downloads GPT-2 small (0.5 GB) and its 12 transcoders (1.8 GB) from the Hugging
38
+ Face Hub into `~/.cache/twinlens` (set `TWINLENS_CACHE` to move it). `twinlens audit` checks the
39
+ strongest arrows of the graph and prints one row per arrow:
40
+
41
+ ```
42
+ layer word feature graph fixed GPT-2 graph verdict fixed verdict trust
43
+ 0 Mary 5545 -8.117 +0.318 +0.228 WRONG SIGN ok 7%
44
+ 10 to 13585 +0.014 +0.031 +0.032 understated ok 98%
45
+ 7 Mary 12771 -0.186 +0.032 +0.034 WRONG SIGN ok 81%
46
+ ...
47
+ Today's graph is off on 3 of 8 edges; with the fix, 0 of 8.
48
+ ```
49
+
50
+ - **graph**: today's attribution graph (LayerNorm and attention frozen);
51
+ - **fixed**: the same linear graph with the LayerNorm and attention derivatives kept;
52
+ - **GPT-2**: the real model with the edit applied;
53
+ - numbers are the change in log-probability of GPT-2's answer (−1 ≈ 2.7× less likely);
54
+ - **trust** estimates, from the unedited run only, how likely the graph's number is to be close.
55
+ It was fitted on ablations and ×2 edits of this study's prompts; treat it as a warning light,
56
+ not a probability you can bank on.
57
+
58
+ Add `--json` for machine-readable output and `--top 20` for more arrows.
59
+
60
+ ## Results
61
+
62
+ From 8,064 edits on GPT-2 small (56 prompts, 8 task families). Full write-up:
63
+ [report](https://spiliosdimakopoulos.github.io/twinlens/report/) ([PDF](docs/TwinLens_report.pdf)).
64
+
65
+ | | today's graph | gradient through real GPT-2 | with the fix |
66
+ |---|---|---|---|
67
+ | rank agreement with the real effect (Spearman ρ) | 0.20 | 0.38 | 0.53 |
68
+ | right direction, on edits that move the answer | 67% | 77% | 86% |
69
+ | badly wrong (off by more than 1 nat) | 2.2% | 0.1% | 0.05% |
70
+
71
+ - **Why the graph fails.** Frozen LayerNorm causes most of the error and nearly all large
72
+ misses, almost all of them for edits in the first three layers. Frozen attention causes most
73
+ of the ranking error.
74
+ - **The fix.** Keeping the LayerNorm and attention-softmax Jacobians gives a graph that is still
75
+ linear and matches the nonlinear twin. It also ranks and signs effects better than attribution
76
+ patching, a gradient through the real model (Δρ +0.15 [0.11, 0.19]). On Qwen3-0.6B the gain
77
+ over today's graph is smaller but holds on every metric (Δρ +0.12 [0.05, 0.20]). IOI prompts
78
+ go from ρ 0.01 to 0.59.
79
+ - **Spotting bad predictions.** A logistic model over nine signals from the unedited run flags
80
+ failures with AUROC 0.84 on edits that move the answer. Disagreement between today's graph
81
+ and the fixed graph is an even better warning (0.94).
82
+ - **Upstream.** The fix is proposed to the open-source
83
+ [circuit-tracer](https://github.com/decoderesearch/circuit-tracer) library as opt-in flags
84
+ ([PR #123](https://github.com/decoderesearch/circuit-tracer/pull/123), under review).
85
+
86
+ <p>
87
+ <img src="experiments/figures/F5_fix.png" width="100%" alt="Keeping two derivatives makes the linear graph as faithful as the nonlinear twin">
88
+ </p>
89
+
90
+ ## Limitations
91
+
92
+ - Two small models (GPT-2 small; Qwen3-0.6B, upper 18 layers only) and per-layer transcoders.
93
+ Cross-layer transcoders, used by the current circuit-tracing work, are untested.
94
+ - 56 short prompts written by the author; one prediction target per prompt.
95
+ - Only feature-to-output effects are measured, not feature-to-feature edges.
96
+ - Most edits change the answer very little; the rank and sign results are the robust ones.
97
+
98
+ Details and the remaining caveats are in §5 of the [report](https://spiliosdimakopoulos.github.io/twinlens/report/).
99
+
100
+ ## How it works
101
+
102
+ GPT-2 runs on a ~250-line PyTorch implementation with hook points
103
+ ([`twinlens/core/gpt2.py`](twinlens/core/gpt2.py)); its logits match Hugging Face to 7×10⁻⁵.
104
+ Before measuring anything new it reproduces known GPT-2 circuits (induction heads, the IOI name
105
+ movers, the sites of factual recall), each pinned as a test. MLPs are replaced by the
106
+ transcoders of Dunefsky, Chlenski & Nanda (2024) plus a fixed error term, and five twin levels
107
+ switch one part of the real model back on at a time, so every gap is attributed to its source.
108
+
109
+ Weights and transcoders are memory-mapped, so the whole system runs in 0.82 GB of RAM on a
110
+ laptop CPU ([`DECISIONS.md`](DECISIONS.md)):
111
+
112
+ | CPU latency (Ryzen 7 5700U, 15 tokens) | |
113
+ |---|---|
114
+ | forward pass | 194 ms |
115
+ | twin trace (12 transcoders) | 1.0 s |
116
+ | what-if, graph twin / nonlinear twin | 0.17 s / 0.45 s |
117
+
118
+ ## Run the dashboard locally
119
+
120
+ ```bash
121
+ python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
122
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
123
+ pip install -e ".[dev]"
124
+ cd web && npm ci && cd ..
125
+ make serve # http://localhost:8000
126
+ ```
127
+
128
+ Locally the dashboard accepts any prompt. `docker compose up` does the same in a container.
129
+
130
+ ## Reproduce the paper
131
+
132
+ The sweep's raw results are committed, so `make analyze` re-derives every number in the report
133
+ in a few minutes without a model. Re-running the sweeps themselves is described in §7 of the
134
+ report. `make test`, `make web-test` and `make e2e` run the test suites.
135
+
136
+ ```
137
+ twinlens/ model, twin levels, interventions, fidelity metrics, trust model, CLI
138
+ server/ FastAPI (REST + WebSocket)
139
+ web/ React + TypeScript dashboard
140
+ experiments/ sweeps, analysis, figures, committed results
141
+ paper/ the technical report
142
+ ```
143
+
144
+ ## Cite
145
+
146
+ ```bibtex
147
+ @techreport{dimakopoulos2026twinlens,
148
+ title = {Attribution graphs fail because of the derivatives they drop, not because they are linear},
149
+ author = {Dimakopoulos, Spilios},
150
+ year = {2026},
151
+ institution = {TwinLens project},
152
+ doi = {10.5281/zenodo.23437556},
153
+ url = {https://github.com/SpiliosDimakopoulos/twinlens}
154
+ }
155
+ ```
156
+
157
+ ## Contributing
158
+
159
+ Issues and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). MIT licence.
@@ -0,0 +1,77 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "twinlens"
7
+ version = "1.1.0"
8
+ description = "A digital twin of an LLM's internals that measures how far its own explanations can be trusted."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Spilios Dimakopoulos" }]
14
+ keywords = ["interpretability", "mechanistic-interpretability", "transcoders", "gpt-2", "faithfulness", "explanations", "llm"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Science/Research",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
22
+ ]
23
+ dependencies = [
24
+ "torch>=2.2",
25
+ "numpy>=1.26",
26
+ "tokenizers>=0.19",
27
+ "huggingface_hub>=0.23",
28
+ "pandas>=2.1",
29
+ "pydantic>=2.5",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://spiliosdimakopoulos.github.io/twinlens/"
34
+ Source = "https://github.com/SpiliosDimakopoulos/twinlens"
35
+ Issues = "https://github.com/SpiliosDimakopoulos/twinlens/issues"
36
+ Citation = "https://doi.org/10.5281/zenodo.23437556"
37
+
38
+ [project.scripts]
39
+ twinlens = "twinlens.cli:main"
40
+
41
+ [project.optional-dependencies]
42
+ server = ["fastapi>=0.110", "uvicorn[standard]>=0.29"]
43
+ dev = ["twinlens[server]", "pytest", "pytest-timeout", "httpx", "ruff", "matplotlib", "pyarrow", "scipy", "psutil", "transformers"]
44
+
45
+ [tool.setuptools.packages.find]
46
+ include = ["twinlens*", "server*"]
47
+
48
+ [tool.setuptools.package-data]
49
+ twinlens = ["fidelity/trust_model.json"] # so `pip install` users get the trust score
50
+
51
+ [tool.ruff]
52
+ line-length = 100
53
+ target-version = "py311"
54
+
55
+ [tool.ruff.lint]
56
+ select = ["E", "F", "I", "B", "UP", "SIM", "C4", "RUF", "PT"]
57
+ ignore = [
58
+ "E741", # `l` for layer is the field convention
59
+ "PT018", # composite asserts in tests (7 hits); split later
60
+ "RUF001", # intentional unicode (e.g. minus, arrows) in strings (4 hits)
61
+ "RUF002", # same, in docstrings (3 hits)
62
+ "RUF005", # [a] + b style (2 hits); fix later
63
+ "RUF007", # zip(x, x[1:]) (1 hit); fix later
64
+ "C416", # identity comprehensions (3 hits); fix later
65
+ "RUF100", # one stale noqa (1 hit); fix later
66
+ ]
67
+
68
+ [tool.ruff.lint.per-file-ignores]
69
+ "experiments/build_report.py" = ["E501"] # embedded CSS
70
+
71
+ [tool.pytest.ini_options]
72
+ testpaths = ["tests"]
73
+ addopts = "-m 'not slow and not qwen'"
74
+ markers = ["slow: needs extra RAM (HF parity)", "qwen: needs the prepared Qwen3 transcoders (~7 GB)"]
75
+
76
+ [tool.ruff.format]
77
+ quote-style = "double"
File without changes