Proposal
Add a self-contained tools/analysis/ module that estimates the causal effect of every node and edge in a model's computational graph in a single 2-forward-plus-1-backward pass, instead of one forward pass per intervention. The module implements three layered methods on the same substrate:
- Attribution patching — a first-order Taylor estimate of activation patching:
effect ≈ (clean_act − corrupt_act) · grad_of_metric, computed for all components at once from one gradient cache.
- EAP (Edge Attribution Patching) — score every edge in the residual-stream computational graph (node = head / MLP / input at a position; edge = residual write→read), not just nodes, yielding a ranked edge list.
- EAP-IG — replace the single saturating gradient with an integrated-gradients path between corrupt and clean activations, giving markedly more faithful circuits (the ablate-everything-outside-the-circuit metric).
- Faithfulness evaluation — given a candidate edge set, mean-ablate every edge outside it and report how much of the task metric (e.g. IOI logit-diff) is recovered.
- A demo on
gpt2-small IOI reproducing the known circuit from EAP/EAP-IG scores alone.
This is the linearized-patching substrate that ACDC (Automatic Circuit Discovery — a separate, not-yet-filed sibling proposal for a patch-and-prune circuit-discovery tool that would consume this module's ranked edge list as its pre-filter) and attribution-graph work build on — the single highest-leverage addition to TL's circuit-discovery toolkit.
Status: exploratory proposal, no code changes.
Suggested labels: enhancement, tooling, TransformerBridge, complexity: moderate-to-high
Motivation
What TL has today. TL already exposes every primitive this method needs but assembles none of them into an all-at-once attributor. run_with_cache caches activations and can retain their gradients for a manual backward; get_act_patch_direct_path (in tools/analysis/direct_path_patching.py) does a forward-mode linear perturbation of a single A→B edge (no gradients); patching.py:generic_activation_patch does exact (expensive) node patching; direct_logit_attribution maps a component to the logit; the hook graph enumerates the nodes. What is missing is the tool that reads one gradient cache and produces every node/edge score in a single sweep.
Why it extends the existing baseline. Exact activation patching (generic_activation_patch) needs one forward pass per patched node/edge — quadratic in graph size and infeasible for full-graph edge discovery. Attribution patching is its gradient-based linearization: two forward passes (clean, corrupt) plus one backward pass estimate all effects simultaneously (Nanda 2023). get_act_patch_direct_path already scores a single edge, but by a forward-mode linear perturbation (no gradients/backward) that coincides with the first-order gradient estimate only when the intervening LayerNorm is treated as linear; EAP instead reads the full edge-scoring sweep from one gradient cache. EAP-IG then fixes attribution patching's known failure mode — gradient saturation near the clean point makes the first-order estimate unfaithful — by integrating the gradient along a corrupt→clean path.
Why no maintained TL implementation exists. Three reference implementations exist; none is TransformerLens. Live-checked 2026-09-03:
All three are single-paper research scripts, not packaged as a reusable library API, with no hook-native TransformerLens integration — confirming the audit's original claim. hannamw/eap-ig's recent activity and permissive MIT license make it the strongest pinned oracle of the three (upgrade from an EAP-only oracle). None of this supersedes filing: the method exists elsewhere, but the TL-native tool does not.
SAELens boundary. This tool operates entirely on the model's own activations and gradients through the residual-stream hook graph. It trains no dictionary and loads no external artifact, so there is no SAELens dependency. (Edge attribution into SAE features is a separate, artifact-side extension and is explicitly out of scope.)
Pitch
Proposed API
Names adjustable to maintainer preference.
from transformer_lens import HookedTransformer # or TransformerBridge
from transformer_lens.tools.analysis import (
attribution_patch, EdgeAttributionConfig, faithfulness,
)
model = HookedTransformer.from_pretrained("gpt2") # Bridge works unchanged
clean = model.to_tokens("When John and Mary went to the store, John gave a drink to")
corrupt = model.to_tokens("When John and Mary went to the store, Mary gave a drink to")
def metric(logits): # IOI logit-diff (Mary - John) on final pos
return logits[0, -1, mary_id] - logits[0, -1, john_id]
# EAP: score every edge in the computational graph, all at once.
result = attribution_patch(
model, clean, corrupt, metric,
config=EdgeAttributionConfig(
method="EAP-IG", # {"attribution", "EAP", "EAP-IG"}
ig_steps=5, # integrated-gradients path steps (EAP-IG only)
granularity="edge", # {"node", "edge"}
),
)
result.edge_scores # sparse [n_edges] signed effect estimates
result.top_edges(k=50) # ranked (src_node, dst_node, score)
result.node_scores # [n_nodes] aggregated
# Build a circuit by thresholding, then measure faithfulness.
circuit = result.top_edges(k=50)
faith = faithfulness(
model, clean, corrupt, metric, circuit,
ablation="mean", # mean-ablate everything OUTSIDE the circuit
)
faith.recovered # fraction of clean-vs-corrupt metric recovered by circuit
faith.full_metric, faith.corrupt_metric # baselines reported alongside
Design (algorithm)
Let m be the task metric, a_clean/a_corrupt the activation at a node under the clean/corrupt run.
- Two forward + one backward. Run clean (cache activations), run corrupt (cache activations, retaining their gradients), then compute the scalar
m = metric(logits) and call m.backward() manually to populate ∂m/∂a at every node. (run_with_cache(..., incl_bwd=True) only backpropagates the model's own scalar output and does not support a custom non-scalar metric such as logit-diff, so the metric backward is done by hand.)
- Node attribution (Taylor).
effect(node) ≈ (a_clean − a_corrupt) · ∂m/∂a — first-order estimate of the exact patch m(patch node to clean) − m(corrupt).
- Edge attribution (EAP). In the residual-stream graph, an edge
u→v carries u's write into v's read. Its score is (a_clean[u] − a_corrupt[u]) · ∂m/∂(input of v), evaluated with the gradient at v's read point. Sweeping all (u, v) from the two caches gives every edge score at once.
- EAP-IG (faithful gradient). Replace the single gradient at the clean point with the integrated gradient along the straight path
a(α) = a_corrupt + α·(a_clean − a_corrupt), α ∈ (0,1]: IG = (a_clean − a_corrupt) · (1/N) Σ_k ∂m/∂a |_{a(α_k)}. This averages out gradient saturation and is what buys faithfulness.
- Faithfulness metric. Given a candidate edge set
C, mean-ablate every edge outside C (replace with dataset-mean activation) and measure the recovered fraction of the clean→corrupt metric gap. This is the paper's headline evaluation and the honest check that a high-scoring edge set is actually a circuit.
- Dataset averaging. Steps 1–4 as described operate on a single clean/corrupt pair. A single pair is too noisy to recover the canonical IOI circuit (check 4) — the original IOI and EAP work average scores/gradients over many templates and name pairs (15+ templates, multiple names each).
attribution_patch must accept a batch of clean/corrupt pairs and average per-edge scores across the batch before ranking; this is part of the vertical slice, not a follow-up.
Reuse-map table
| Sub-step |
TL primitive |
file:symbol |
| Clean/corrupt activation caches |
run_with_cache |
transformer_lens/HookedTransformer.py:run_with_cache |
Gradient cache (∂m/∂a) |
grad-retaining forward + manual metric.backward() (not incl_bwd=True, which only backprops the scalar model output) |
transformer_lens/HookedRootModule.py:run_with_cache (incl_bwd mechanics), exposed via transformer_lens/HookedTransformer.py:run_with_cache |
| Per-head Q/K/V and output hooks (required for edge granularity) |
use_split_qkv_input=True (separate hook_q_input/hook_k_input/hook_v_input per head) and use_attn_result=True (per-head hook_result before the sum into resid) — both must be set on the model config or these hook points don't exist |
HookedTransformerConfig |
| Single-edge forward-mode linearization (context; EAP is gradient-based) |
get_act_patch_direct_path |
transformer_lens/tools/analysis/direct_path_patching.py |
| Enumerate nodes/edges of the graph |
hook graph / HookPoint, get_full_resid_decomposition |
transformer_lens/hook_points.py, transformer_lens/ActivationCache.py |
| Exact-patch oracle (validation) |
generic_activation_patch + setters |
transformer_lens/patching.py:generic_activation_patch |
| Metric readout (logit-diff) |
logit_attrs, W_U + final LN |
transformer_lens/ActivationCache.py:logit_attrs |
| IG path interpolation on activations |
setter hooks via add_hook |
transformer_lens/hook_points.py:HookPoint.add_hook |
| Which heads/edges are canonical (context) |
detect_head |
transformer_lens/head_detector.py |
Main correctness risk (named)
Risk 1 — Edge-graph bookkeeping: attributing an edge's effect to the wrong (src, dst) pair, or double-counting a residual write. Because every node writes into the residual stream and every downstream node reads the sum, the edge decomposition must map each writer's contribution to each reader's gradient exactly once, with consistent position/head indexing between the activation cache and the gradient cache. A silent off-by-one in layer/position/head indexing, or summing a write into both a direct edge and a through-MLP edge, produces plausible-looking but wrong scores that no smoke test catches.
Mitigations (raise/validate, do not merely warn):
- Sum-to-total assertion. For a small graph, assert that the summed node attributions reconstruct the first-order metric change
m(clean) − m(corrupt) to a tolerance; raise on mismatch — this catches double-counting and index misalignment directly.
- Exact-patch parity on a tiny model. On a 2-layer toy model, assert every edge's attribution matches the sign (and, within tolerance, the magnitude) of the exact
generic_activation_patch effect for that edge.
- Explicit graph object. Represent nodes/edges as a typed graph with asserted
(layer, position, head) keys shared by both caches; raise if a cache is missing a required hook (e.g. hook_pattern/hook_result) rather than silently skipping edges.
- Honesty in the return type. Attribution scores are estimates; faithfulness (ablate-outside-circuit) is the causal check and is reported next to a random-edge-set baseline.
Risk 2 — Edge-level ablation has no ready-made hook. Standard TL hook points (e.g. hook_resid_post) expose the summed contribution of every upstream writer; there is no existing hook that lets you zero out one writer→reader edge while leaving every other edge into that reader untouched. faithfulness() therefore cannot be built by toggling existing node hooks — it needs the same per-edge patching machinery get_act_patch_direct_path uses for a single edge (reconstruct a component's input as the sum of only the included edges' contributions, substituting the corrupt-run value for excluded edges), generalized to sweep every edge in the circuit at once. This is harder than the indexing risk above and should be scoped and estimated explicitly during implementation, not treated as a detail of the bookkeeping fix.
Risk 3 — Attention-pattern nonlinearity degrades edges into Q/K. Edges that feed a head's query or key input pass through the softmax before affecting the metric; the first-order (or IG) linear estimate is measurably less faithful there than for edges into V or MLP inputs, a caveat the EAP paper itself notes. Faithfulness scores should be expected to be worse for pattern-affecting edges, and this should be reported as a known limitation rather than debugged as a bug if the demo shows it.
Validation plan (falsifiable)
- Reconstruction (unit). On a tiny synthetic model, summed node attributions ≈
m(clean) − m(corrupt) to atol (linearization identity).
- Exact-patch parity (unit). On a 2-layer toy model with a small edge set, each edge's attribution-patch score matches the exact
generic_activation_patch effect in sign and within tolerance in magnitude.
- IG improves faithfulness (integration). On
gpt2-small IOI, a fixed-size circuit built from EAP-IG scores achieves higher ablate-outside faithfulness than one built from plain EAP scores at the same edge budget.
- IOI circuit recovery (oracle,
@pytest.mark.slow). Top-scoring edges, averaged over a batch of IOI templates/name pairs (not a single clean/corrupt pair), on gpt2-small IOI overlap the established IOI head set (name-movers 9.9/9.6/10.0, S-inhibition 7.3/7.9/8.6/8.10, etc.) above a threshold, compared against the pinned reference repo's scores.
- Baseline honesty (integration). A random equal-size edge set recovers markedly less metric than the discovered circuit; both numbers reported.
Scope of the first PR (vertical slice)
Attribution patching + EAP edge scores + EAP-IG + a faithfulness check on gpt2-small IOI, plus one demo. Exact paths:
transformer_lens/tools/analysis/attribution_patching.py — new module (attribution_patch, EdgeAttributionConfig, faithfulness, result dataclass with .top_edges()).
transformer_lens/tools/analysis/__init__.py — export.
tests/unit/tools/test_attribution_patching.py — synthetic 2-layer fixtures (checks 1–2), no HF downloads.
tests/integration/test_attribution_patching.py — gpt2-small IOI EAP vs EAP-IG faithfulness + random baseline (checks 3, 5).
tests/integration/test_attribution_patching_oracle_parity.py — slow IOI recovery vs pinned reference (check 4), @pytest.mark.slow.
demos/Attribution_Patching_Demo.ipynb — IOI edge scoring → circuit → faithfulness (nbval).
Follow-up work
Full auto-pruning into a minimal subgraph is deliberately deferred to the separate ACDC proposal noted above (this tool is its fast pre-filter). Attribution graphs, SAE-feature edges, multi-task datasets, and speed optimizations are tiered as follow-up work in my own commit plan (not part of this issue; I'll open a tracking issue for these once the vertical slice lands).
Model coverage
- CI reference:
gpt2-small — cacheable, canonical IOI circuit for the oracle.
- Demo/extension: Pythia-160m noted as an extension (follow-up).
- Honesty caveat: small open models will not reproduce large-model paper figures verbatim; the demo reports the discovered circuit's faithfulness next to the random-edge-set baseline and the full/corrupt metric, and states that circuit quality is measured by faithfulness, not by score magnitude alone.
Alternatives
- Exact activation patching for every edge (
generic_activation_patch). Rejected: one forward pass per edge is quadratic in graph size and infeasible for full-graph discovery; it is kept only as the correctness oracle.
- Node-only attribution patching (Nanda 2023). Rejected as the sole deliverable: it misses edge structure that ACDC/EAP need; we include it as the base case but ship edge scoring.
- Plain EAP without integrated gradients. Rejected as the headline method: gradient saturation makes it less faithful (the EAP-IG paper's central finding); we ship EAP as a baseline and EAP-IG as the default.
- SAE-feature edge attribution. Rejected for the slice: requires trained dictionaries (SAELens artifact side); orthogonal to the residual-stream substrate this PR establishes.
Correctness oracle
Two-tier:
- Brute-force / analytic reference (primary, fast unit): on a tiny synthetic model, (i) summed node attributions must reconstruct the first-order metric change, and (ii) each edge's attribution-patch score must match the exact
generic_activation_patch effect in sign and within tolerance. Deterministic, no downloads. This is the mandatory oracle where no cheap external reference applies.
- External frozen impl (slow,
@pytest.mark.slow): hannamw/eap-ig (MIT, most actively maintained — preferred), with Aaquib111/edge-attribution-patching and hannamw/MIB-circuit-track as secondary references, pinned and installed test-only, used for a threshold-based comparison of edge scores / recovered IOI circuit on gpt2-small. Tolerant comparison (rank overlap above threshold), not bit-exact. MIB-circuit-track runs on nnsight; treat only as a cross-check of expected head sets, not an installable oracle dependency.
Additional context
- Papers: Neel Nanda, "Attribution Patching: Activation Patching At Industrial Scale" (2023 blog). Aaquib Syed, Can Rager, Arthur Conmy, "Attribution Patching Outperforms Automated Circuit Discovery," arXiv:2310.10348. Michael Hanna, Sandro Pezzelle, Yonatan Belinkov, "Have Faith in Faithfulness: Going Beyond Circuit Overlap When Finding Model Mechanisms" (EAP-IG), arXiv:2403.17806.
- External implementation status: three research repos, none TransformerLens (live-checked 2026-09-03):
hannamw/eap-ig (MIT, 86 stars, pushed 2026-05-23 — strongest oracle), hannamw/MIB-circuit-track (Apache-2.0, 24 stars, pushed 2025-06-30, nnsight-based), Aaquib111/edge-attribution-patching (unlicensed, 49 stars, pushed 2024-05-31, stale). All single-paper scripts, no packaged API, no TL hook integration; suitable only as pinned, test-only oracles, not as a substitute for this proposal.
- Repo scope fit confirmed: matches
AGENTS.md (repo root) §1's definition of TL — "exposes internal activations through a hook system for caching, editing, and ablating intermediate state" — via existing run_with_cache/ActivationCache/generic_activation_patch primitives, no new runtime dependency.
- Artifact sources: none — method uses only activations + gradients from the model's own hook graph; no dictionaries, no registry for the vertical slice.
- Algorithm note for reviewers. Attribution patching is a first-order Taylor estimate of exact patching; it is exact only in the linear limit and saturates near the clean point. EAP-IG's integrated-gradients path is what restores faithfulness and must be the default. The edge decomposition requires each residual write to be attributed to each downstream read exactly once — the sum-to-total assertion is the guard.
- Provisional-citation flags: arXiv IDs
2310.10348, 2403.17806, and 2211.00593 are pre-2026 and established; no [verify] needed. IOI head set is Wang, Variengien, Conmy, Shlegeris, Steinhardt 2022, arXiv:2211.00593.
Checklist
Reviewer note
cc @jlarson4 — could you take a look and let me know if this scope and API shape look right?
If this gets a green light, I'd like to pick it up myself. Could you assign this issue to me once it's approved?
Thank you
Proposal
Add a self-contained
tools/analysis/module that estimates the causal effect of every node and edge in a model's computational graph in a single 2-forward-plus-1-backward pass, instead of one forward pass per intervention. The module implements three layered methods on the same substrate:effect ≈ (clean_act − corrupt_act) · grad_of_metric, computed for all components at once from one gradient cache.gpt2-smallIOI reproducing the known circuit from EAP/EAP-IG scores alone.This is the linearized-patching substrate that ACDC (Automatic Circuit Discovery — a separate, not-yet-filed sibling proposal for a patch-and-prune circuit-discovery tool that would consume this module's ranked edge list as its pre-filter) and attribution-graph work build on — the single highest-leverage addition to TL's circuit-discovery toolkit.
Status: exploratory proposal, no code changes.
Suggested labels:
enhancement,tooling,TransformerBridge,complexity: moderate-to-highMotivation
What TL has today. TL already exposes every primitive this method needs but assembles none of them into an all-at-once attributor.
run_with_cachecaches activations and can retain their gradients for a manual backward;get_act_patch_direct_path(intools/analysis/direct_path_patching.py) does a forward-mode linear perturbation of a single A→B edge (no gradients);patching.py:generic_activation_patchdoes exact (expensive) node patching;direct_logit_attributionmaps a component to the logit; the hook graph enumerates the nodes. What is missing is the tool that reads one gradient cache and produces every node/edge score in a single sweep.Why it extends the existing baseline. Exact activation patching (
generic_activation_patch) needs one forward pass per patched node/edge — quadratic in graph size and infeasible for full-graph edge discovery. Attribution patching is its gradient-based linearization: two forward passes (clean, corrupt) plus one backward pass estimate all effects simultaneously (Nanda 2023).get_act_patch_direct_pathalready scores a single edge, but by a forward-mode linear perturbation (no gradients/backward) that coincides with the first-order gradient estimate only when the intervening LayerNorm is treated as linear; EAP instead reads the full edge-scoring sweep from one gradient cache. EAP-IG then fixes attribution patching's known failure mode — gradient saturation near the clean point makes the first-order estimate unfaithful — by integrating the gradient along a corrupt→clean path.Why no maintained TL implementation exists. Three reference implementations exist; none is TransformerLens. Live-checked 2026-09-03:
hannamw/eap-ighannamw/MIB-circuit-tracknnsight, not TL; MIB benchmark harnessAaquib111/edge-attribution-patchingAll three are single-paper research scripts, not packaged as a reusable library API, with no hook-native TransformerLens integration — confirming the audit's original claim.
hannamw/eap-ig's recent activity and permissive MIT license make it the strongest pinned oracle of the three (upgrade from an EAP-only oracle). None of this supersedes filing: the method exists elsewhere, but the TL-native tool does not.SAELens boundary. This tool operates entirely on the model's own activations and gradients through the residual-stream hook graph. It trains no dictionary and loads no external artifact, so there is no SAELens dependency. (Edge attribution into SAE features is a separate, artifact-side extension and is explicitly out of scope.)
Pitch
Proposed API
Names adjustable to maintainer preference.
Design (algorithm)
Let
mbe the task metric,a_clean/a_corruptthe activation at a node under the clean/corrupt run.m = metric(logits)and callm.backward()manually to populate∂m/∂aat every node. (run_with_cache(..., incl_bwd=True)only backpropagates the model's own scalar output and does not support a custom non-scalar metric such as logit-diff, so the metric backward is done by hand.)effect(node) ≈ (a_clean − a_corrupt) · ∂m/∂a— first-order estimate of the exact patchm(patch node to clean) − m(corrupt).u→vcarriesu's write intov's read. Its score is(a_clean[u] − a_corrupt[u]) · ∂m/∂(input of v), evaluated with the gradient atv's read point. Sweeping all(u, v)from the two caches gives every edge score at once.a(α) = a_corrupt + α·(a_clean − a_corrupt),α ∈ (0,1]:IG = (a_clean − a_corrupt) · (1/N) Σ_k ∂m/∂a |_{a(α_k)}. This averages out gradient saturation and is what buys faithfulness.C, mean-ablate every edge outsideC(replace with dataset-mean activation) and measure the recovered fraction of the clean→corrupt metric gap. This is the paper's headline evaluation and the honest check that a high-scoring edge set is actually a circuit.attribution_patchmust accept a batch of clean/corrupt pairs and average per-edge scores across the batch before ranking; this is part of the vertical slice, not a follow-up.Reuse-map table
run_with_cachetransformer_lens/HookedTransformer.py:run_with_cache∂m/∂a)metric.backward()(notincl_bwd=True, which only backprops the scalar model output)transformer_lens/HookedRootModule.py:run_with_cache(incl_bwdmechanics), exposed viatransformer_lens/HookedTransformer.py:run_with_cacheuse_split_qkv_input=True(separatehook_q_input/hook_k_input/hook_v_inputper head) anduse_attn_result=True(per-headhook_resultbefore the sum into resid) — both must be set on the model config or these hook points don't existHookedTransformerConfigget_act_patch_direct_pathtransformer_lens/tools/analysis/direct_path_patching.pyHookPoint,get_full_resid_decompositiontransformer_lens/hook_points.py,transformer_lens/ActivationCache.pygeneric_activation_patch+ setterstransformer_lens/patching.py:generic_activation_patchlogit_attrs,W_U+ final LNtransformer_lens/ActivationCache.py:logit_attrsadd_hooktransformer_lens/hook_points.py:HookPoint.add_hookdetect_headtransformer_lens/head_detector.pyMain correctness risk (named)
Risk 1 — Edge-graph bookkeeping: attributing an edge's effect to the wrong (src, dst) pair, or double-counting a residual write. Because every node writes into the residual stream and every downstream node reads the sum, the edge decomposition must map each writer's contribution to each reader's gradient exactly once, with consistent position/head indexing between the activation cache and the gradient cache. A silent off-by-one in layer/position/head indexing, or summing a write into both a direct edge and a through-MLP edge, produces plausible-looking but wrong scores that no smoke test catches.
Mitigations (raise/validate, do not merely warn):
m(clean) − m(corrupt)to a tolerance;raiseon mismatch — this catches double-counting and index misalignment directly.generic_activation_patcheffect for that edge.(layer, position, head)keys shared by both caches;raiseif a cache is missing a required hook (e.g.hook_pattern/hook_result) rather than silently skipping edges.Risk 2 — Edge-level ablation has no ready-made hook. Standard TL hook points (e.g.
hook_resid_post) expose the summed contribution of every upstream writer; there is no existing hook that lets you zero out one writer→reader edge while leaving every other edge into that reader untouched.faithfulness()therefore cannot be built by toggling existing node hooks — it needs the same per-edge patching machineryget_act_patch_direct_pathuses for a single edge (reconstruct a component's input as the sum of only the included edges' contributions, substituting the corrupt-run value for excluded edges), generalized to sweep every edge in the circuit at once. This is harder than the indexing risk above and should be scoped and estimated explicitly during implementation, not treated as a detail of the bookkeeping fix.Risk 3 — Attention-pattern nonlinearity degrades edges into Q/K. Edges that feed a head's query or key input pass through the softmax before affecting the metric; the first-order (or IG) linear estimate is measurably less faithful there than for edges into V or MLP inputs, a caveat the EAP paper itself notes. Faithfulness scores should be expected to be worse for pattern-affecting edges, and this should be reported as a known limitation rather than debugged as a bug if the demo shows it.
Validation plan (falsifiable)
m(clean) − m(corrupt)toatol(linearization identity).generic_activation_patcheffect in sign and within tolerance in magnitude.gpt2-smallIOI, a fixed-size circuit built from EAP-IG scores achieves higher ablate-outside faithfulness than one built from plain EAP scores at the same edge budget.@pytest.mark.slow). Top-scoring edges, averaged over a batch of IOI templates/name pairs (not a single clean/corrupt pair), ongpt2-smallIOI overlap the established IOI head set (name-movers 9.9/9.6/10.0, S-inhibition 7.3/7.9/8.6/8.10, etc.) above a threshold, compared against the pinned reference repo's scores.Scope of the first PR (vertical slice)
Attribution patching + EAP edge scores + EAP-IG + a faithfulness check on
gpt2-smallIOI, plus one demo. Exact paths:transformer_lens/tools/analysis/attribution_patching.py— new module (attribution_patch,EdgeAttributionConfig,faithfulness, result dataclass with.top_edges()).transformer_lens/tools/analysis/__init__.py— export.tests/unit/tools/test_attribution_patching.py— synthetic 2-layer fixtures (checks 1–2), no HF downloads.tests/integration/test_attribution_patching.py—gpt2-smallIOI EAP vs EAP-IG faithfulness + random baseline (checks 3, 5).tests/integration/test_attribution_patching_oracle_parity.py— slow IOI recovery vs pinned reference (check 4),@pytest.mark.slow.demos/Attribution_Patching_Demo.ipynb— IOI edge scoring → circuit → faithfulness (nbval).Follow-up work
Full auto-pruning into a minimal subgraph is deliberately deferred to the separate ACDC proposal noted above (this tool is its fast pre-filter). Attribution graphs, SAE-feature edges, multi-task datasets, and speed optimizations are tiered as follow-up work in my own commit plan (not part of this issue; I'll open a tracking issue for these once the vertical slice lands).
Model coverage
gpt2-small— cacheable, canonical IOI circuit for the oracle.Alternatives
generic_activation_patch). Rejected: one forward pass per edge is quadratic in graph size and infeasible for full-graph discovery; it is kept only as the correctness oracle.Correctness oracle
Two-tier:
generic_activation_patcheffect in sign and within tolerance. Deterministic, no downloads. This is the mandatory oracle where no cheap external reference applies.@pytest.mark.slow):hannamw/eap-ig(MIT, most actively maintained — preferred), withAaquib111/edge-attribution-patchingandhannamw/MIB-circuit-trackas secondary references, pinned and installed test-only, used for a threshold-based comparison of edge scores / recovered IOI circuit ongpt2-small. Tolerant comparison (rank overlap above threshold), not bit-exact.MIB-circuit-trackruns onnnsight; treat only as a cross-check of expected head sets, not an installable oracle dependency.Additional context
hannamw/eap-ig(MIT, 86 stars, pushed 2026-05-23 — strongest oracle),hannamw/MIB-circuit-track(Apache-2.0, 24 stars, pushed 2025-06-30,nnsight-based),Aaquib111/edge-attribution-patching(unlicensed, 49 stars, pushed 2024-05-31, stale). All single-paper scripts, no packaged API, no TL hook integration; suitable only as pinned, test-only oracles, not as a substitute for this proposal.AGENTS.md(repo root) §1's definition of TL — "exposes internal activations through a hook system for caching, editing, and ablating intermediate state" — via existingrun_with_cache/ActivationCache/generic_activation_patchprimitives, no new runtime dependency.2310.10348,2403.17806, and2211.00593are pre-2026 and established; no[verify]needed. IOI head set is Wang, Variengien, Conmy, Shlegeris, Steinhardt 2022, arXiv:2211.00593.Checklist
grepoftools/analysis/; only patching-family +direct_path_patching+ J-lens present, none doing full-graph edge scoring).gpt2-smallIOI. Auto-pruning deferred to the separate ACDC proposal (pre-filter consumer, not yet filed); attribution graphs / SAE edges / multi-task deferred to a tiered follow-up issue I'll open post-merge.hannamw/eap-ig(primary),Aaquib111/edge-attribution-patching/hannamw/MIB-circuit-track(secondary) (slow, threshold).Reviewer note
cc @jlarson4 — could you take a look and let me know if this scope and API shape look right?
If this gets a green light, I'd like to pick it up myself. Could you assign this issue to me once it's approved?
Thank you