Vig et al. 2020; ROME, Meng et al. 2022
Activation patching, also called causal mediation, is a core mechanistic-interpretability method. It runs the system once, records an internal value, and then, on a second run, overwrites, or 'patches', that internal value with the one recorded from a different input. The change in the output measures how much that internal value causally mediates the behaviour. In language-model work this is how researchers localise where a fact or a computation lives inside the network. The idea is genuinely causal: it does not correlate, it substitutes a value and observes the effect. We apply it to the VCS by patching a recorded state value, a RAM cell or register, from one run into another and measuring the causal effect on the output. On this machine the operation is special. Clamping a cell to a donor value and re-running the real ROM is literally the same operation the intervention oracle performs. So activation patching cannot disagree with the oracle: it recovers the exact causal-effect table by construction. This makes it the clearest demonstration that where a method's operation is itself a real intervention on a fully observable machine, faithfulness is automatic. The catch, taken up elsewhere, is that recovering the exact effect table is still not the same as recovering what the patched variables mean.
Each point is one intervention site: its exact causal effect from the oracle (x) against the method's recovered/approximate effect (y). Points on the dashed diagonal mean the method recovered the true effect. max_abs_recovered_minus_exact = 0.000 — this example only (pong, state gameplay(seed=0,prefix=90)+15); the audit aggregate is below.
New to explainable AI? This block explains the method as it is used on neural networks, then how we run the very same method on an Atari 2600, where — unlike for a network — we know the right answer. Every implementation step links to the code. See also the XAI primer for the shared vocabulary.
Gradient methods ask “what if I nudged this a tiny bit?”. Activation patching asks a bolder, causal question: what if this internal value had been the one from a different input? You run the network twice. The clean run uses the input you care about (“The Eiffel Tower is in” → “Paris”). The corrupted run uses a slightly different input that gives a different answer. Then you run a third time, and at one chosen place inside the network you overwrite (“patch”) the internal value with the one recorded in the other run. If the answer flips, that place carries the information that decides the answer.
An internal value of a network is called an activation: the output of one neuron, one layer or one attention head. Statisticians call a variable that sits on the causal path from cause to effect a mediator, so the method is also called causal mediation analysis. It is the workhorse of mechanistic interpretability, the attempt to reverse-engineer the algorithm a network runs. Unlike a correlation, a patch is an intervention: it sets a value and watches what happens. On the VCS we can do exactly the same thing to the machine's memory, and it turns out to be identical to our ground-truth oracle.
Let \(x\) be the clean input and \(\tilde x\) the corrupted one. Let \(a_\ell(x)\) be the activation of component \(\ell\) (a neuron, an attention head, or a position in the residual stream: the running vector that every transformer layer reads from and adds to). Write \(y(x \mid a_\ell := v)\) for the output when we run on \(x\) but force \(a_\ell\) to the value \(v\). The patch effect of component \(\ell\) is
\[ \Delta_\ell \;=\; y\big(x \,\big|\, a_\ell := a_\ell(\tilde x)\big) \;-\; y(x). \]This direction (corrupted value into the clean run) is called noising; the reverse (clean value into the corrupted run) is denoising. In ROME’s “causal tracing” the corruption is Gaussian noise on the subject tokens, and the patched-back clean state shows where a fact is recalled. Each \(\Delta_\ell\) costs one extra forward pass.
Toy example. Take a two-step network \(h = 2x_1\), \(g = x_2 + 1\), \(y = h + 3g\). Clean input \(x = (1, 0)\) gives \(h = 2\), \(g = 1\), \(y = 5\). Corrupted input \(\tilde x = (4, 0)\) gives \(h = 8\), \(g = 1\), \(y = 11\). Patch \(h\): run on \(x\) but set \(h := 8\). Then \(y = 8 + 3 = 11\), so \(\Delta_h = 6\): \(h\) mediates the whole difference. Patch \(g\): its corrupted value is also 1, so \(\Delta_g = 0\). The patch tells us which component carries the change between these two inputs, not which components matter in general. A different \(\tilde x\) (say, changing \(x_2\)) would light up \(g\) instead.
| The network and its forward pass | The ROM program running on the bit-exact emulated VCS (jutari). One “layer” is one video frame of 6507 CPU work. |
|---|---|
| The residual stream at layer \(\ell\) | The whole machine state at a frame boundary: the 128 bytes of RAM plus TIA video registers. Every frame reads it and writes it back. |
| A component (neuron / head) | One state site: a single RAM byte that the game uses as a variable (a candidate concept cell such as a ball coordinate), or one TIA register (the background colour COLUBK). |
| Clean run / corrupted run | Clean: the seeded random-action gameplay stream, checkpointed at frame \(f^*{=}90\). Donor: the same stream but with LEFT or RIGHT pressed at the last frame before \(f^*\). |
| The logit / metric | Outputs read 15 frames later: the values of the two most causally active candidate bytes, and the number of screen pixels that differ from the clean frame. |
| Ground truth | The intervention oracle, which pokes a value and re-runs the real ROM. On the VCS that is a patch, so activation patching and the truth coincide. |
The runner sits on the shared testbed that every method uses: a seeded stream of random joystick actions, a checkpoint at frame \(f^*\), and a horizon of 15 frames. Two fresh boots are first asserted to be byte-identical in RAM and screen, so any later difference is caused by our patch and nothing else.
base+17 (the same value the oracle uses), plus one TIA patch
that sets the background colour to white.The difference from LLM practice is worth stating plainly. In a transformer, patching and “the truth” are different things, and we argue about which metric to trust. On the VCS, patching a byte and re-running is the oracle’s own definition of a cause. So this method is the reference the other Phase C methods approximate.
XAI the interpretability method substrate the bit-exact emulator it runs on (jutari, Julia).
activation_patching.jl:282 — The patch itself: copy the checkpoint, write one RAM byte or TIA register, step the tail, snapshot.activation_patching.jl:417 — The donor (“corrupted”) runs: same gameplay prefix, LEFT or RIGHT at the last step.activation_patching.jl:460 — The recovered effect: patch into the clean checkpoint and re-run.activation_patching.jl:466 — The exact effect via an independent fresh boot, so equality is not a shared-code artefact.activation_patching.jl:487 — Site precision/recall of the recovered firing pattern against the exact data-flow.jutari_oracle.jl:176 — The write: one byte of RIOT RAM in the live emulator.jutari_oracle.jl:187 — The same write for a TIA video register (the background-colour patch).shared_testbed_impl.jl:115 — The shared gameplay state: seeded actions, checkpoint at \(f^*\), causes, cause-density gate.StellaEnvironment.jl:259 — One bit-exact video frame of the real machine; every re-run is a loop over this.oracle_intervene.jl:242 — The oracle: apply a cause to a copy of the checkpoint and run the horizon. Same operation as a patch.The headline number is the largest gap between the recovered and the exact effect table. Expect it to be zero on every game: this is a positive result by construction, not a discovery. The more informative parts are the firing pattern and the transient list. Many bytes show no effect because the game recomputes them every frame. Others move a score byte but not the screen, or the reverse. Read the page as a map of which variables mediate which outputs over 15 frames, not as a test the method could fail.
base+17, or Gaussian noise in ROME, may put the system in a state it never visits naturally. The effect is real, but it may not describe normal behaviour.julia --project=jutari tools/xai_study/phaseC_mechanistic/activation_patching.jl --games core
Writes activation_patching_<game>.json/.npz and a core summary into tools/xai_study/phaseC_mechanistic/out/. Each patch is replayed twice (clean checkpoint and fresh boot), so expect minutes per game on one CPU core.
Read next: Attribution / edge patching · Path patching / IOI circuit · Interchange interventions / DAS · Causal scrubbing
The score is the maximum absolute difference between the recovered patch effect and the exact patch effect from the oracle, taken across every site and every core game. The grading is always against the intervention oracle. This method is exact by construction, so the difference is zero. Clamping a cell to a donor value and re-running the real ROM is literally the same operation the intervention oracle performs, so the recovered effect table cannot disagree with the oracle, and site precision and recall are both perfect. Faithfulness for this method is therefore at the ceiling, level with the oracle-as-method positive control. There is no sprite-position hard-case here: because the method intervenes on the real machine rather than taking a derivative, it works on position outputs exactly as it works on content outputs. The important qualification, which the score does not by itself capture, is that recovering the exact effect table is not the same as recovering what the patched variables mean or how they combine into the computed function. The audit box reports the measured triad. This method is the clearest demonstration that when a method's operation is itself a real intervention on a fully observable machine, faithfulness is automatic.
The score is measured against the §1 intervention oracle — never against another interpretability method. F (faithful) is always vs the oracle; see the execution stack. How each of F / S / M is actually computed for this method (and whether it matches the paper) is in the box just below; the numbers are in the In the audit box under it.
The exact formula this method uses for each score, read from its runner, and
whether it matches the paper's §3 (F ∧ S ∧ M triad) definition.
From the committed audit
fsm_math_phaseC.json.
| Axis | Formula (as computed) | What it measures | Matches §3? |
|---|---|---|---|
| F faithfulness | \(F = 1 - \max_u|\,\hat{\Delta y}(u)-\Delta y(u)\,| = 1\) | The patch literally re-runs the exact single-site intervention, so recovered effect equals the oracle effect and the causal-effect agreement is 1. | ✓ matches activation_patching.jl main record (metric max_abs_recovered_minus_exact=0 -> F=1.0); jl:693 position record F = _pearson(rec_px, ex_px). Both are the paper's Phase-C patching F = causal-effect-agreement correlation vs Delta_y(u); here exact by construction. Record: extra.triad.F=1.0 (battery mean F=1.0, n=84). how it's measured — call stack:
|
| S sufficiency | \(S = \dfrac{\#\{\,u_{\mathrm{held}}:\ |\hat{y}-y|\le\varepsilon\,\}}{|\mathrm{held\text{-}out}|}\in[0,1]\) | Held-out predictive sufficiency: fit the attribution->Δy map on a calibration half of the causes, predict the disjoint held-out half, count hits within an ε band. | ✓ matches common/triad_sm.jl sufficiency_score, called via triad_extra_dict (activation_patching.jl). Exactly the paper's held-out predictive estimator on the oracle's per-cause bit-exact re-runs. Record: extra.triad.S (activation_patching_pong.json S=1.0; battery mean S=0.996, n=84). how it's measured — call stack:
|
| M minimality | \(M = |U^{\star}| / |\hat{U}| \in (0,1]\quad U^{\star}=\{u:\Delta y(u)>0\},\ \hat{U}=\{u:|\mathrm{attr}(u)|>\tau\}\) | True minimal cause set size over the method-named set size. | ✓ matches common/triad_sm.jl minimality_score. Exactly the paper's M. Record: extra.triad.M=1.0, M_true_minimal_size / M_named_size (pong |U*|=6/|U_hat|=6; battery mean M=0.973, n=84). how it's measured — call stack:
|
✓ matches = the same quantity as §3; ◐ partial = the same kind of estimator but a differing detail; ✗ does not match = a different quantity (see the note). Definitions: F = agreement with the oracle's true causal effects Δy(u); S = held-out predictive score in [−1, 1]; M = |U*|/|Û| (true-minimal-set size / named-set size).
This is the method's entry in the actual cross-method audit — scored on the paper's correctness triad, each axis a mean over 44 of the 44 scored games (88 committed §R records), not the single example shown above. Tradition: causal. The example figure (Pong) is one of those records.
F faithfulness (scored vs the oracle for every method) · S sufficiency (held-out predictive score in [−1, 1]; a negative value means the explanation predicts held-out interventions worse than the unperturbed baseline; reported for the predictive methods across all three phases where the calibration/held-out split is defined) · M minimality (true-minimal-set / named-set; where the method names a cause set) — n/a otherwise, per the paper's F ∧ S ∧ M triad.
Source: leaderboard.json · the whole leaderboard is on the
methods page and the
Paper 2 audit.
This method's faithfulness on each of the 44 scored games (all-regime F, and the content vs position split). Click a header to
sort. Every number is read from site_data.json
(methods.activation_patching.per_game).
| Game | F (all) | content-F | position-F | S | M | Note |
|---|---|---|---|---|---|---|
| Air Raid | 1.000 | n/a | 1.000 | 1.000 | 0.824 | recovers the true causes |
| Alien | 1.000 | n/a | 1.000 | 0.833 | 1.000 | recovers the true causes |
| Assault | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Asterix | 1.000 | n/a | 1.000 | 1.000 | 0.929 | recovers the true causes |
| Atlantis | 1.000 | n/a | 1.000 | 1.000 | 0.944 | recovers the true causes |
| Bank Heist | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Berzerk | 1.000 | n/a | 1.000 | 1.000 | 0.944 | recovers the true causes |
| Bowling | 1.000 | n/a | 1.000 | 1.000 | 0.955 | recovers the true causes |
| Boxing | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Breakout | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Carnival | 1.000 | n/a | 1.000 | 1.000 | 0.975 | recovers the true causes |
| Centipede | 1.000 | n/a | 1.000 | 1.000 | 0.958 | recovers the true causes |
| Chopper Command | 1.000 | n/a | 1.000 | 1.000 | 0.964 | recovers the true causes |
| Demon Attack | 1.000 | n/a | 1.000 | 1.000 | 0.929 | recovers the true causes |
| Double Dunk | 1.000 | n/a | 1.000 | 1.000 | 0.967 | recovers the true causes |
| Enduro | 1.000 | n/a | 1.000 | 1.000 | 0.900 | recovers the true causes |
| Fishing Derby | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Freeway | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Frostbite | 1.000 | n/a | 1.000 | 1.000 | 0.900 | recovers the true causes |
| Gopher | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Hero | 1.000 | n/a | 1.000 | 1.000 | 0.955 | recovers the true causes |
| Ice Hockey | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Jamesbond | 1.000 | n/a | 1.000 | 1.000 | 0.909 | recovers the true causes |
| Kangaroo | 1.000 | n/a | 1.000 | 1.000 | 0.909 | recovers the true causes |
| Krull | 1.000 | n/a | 1.000 | 1.000 | 0.923 | recovers the true causes |
| Kung-Fu Master | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Montezuma's Revenge | 1.000 | n/a | 1.000 | 1.000 | 0.938 | recovers the true causes |
| Ms. Pac-Man | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Name This Game | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Pacman | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Phoenix | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Pitfall | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Pong | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Private Eye | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Q*bert | 1.000 | n/a | 1.000 | 1.000 | 0.944 | recovers the true causes |
| Riverraid | 1.000 | n/a | 1.000 | 1.000 | 0.940 | recovers the true causes |
| Road Runner | 1.000 | n/a | 1.000 | 1.000 | 0.938 | recovers the true causes |
| Seaquest | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Skiing | 1.000 | n/a | 1.000 | 1.000 | 0.909 | recovers the true causes |
| Space Invaders | 1.000 | n/a | 1.000 | 1.000 | 0.958 | recovers the true causes |
| Tennis | 1.000 | n/a | 1.000 | 1.000 | 0.929 | recovers the true causes |
| Venture | 1.000 | n/a | 1.000 | 1.000 | 0.978 | recovers the true causes |
| Video Pinball | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
| Yars' Revenge | 1.000 | n/a | 1.000 | 1.000 | 1.000 | recovers the true causes |
The path from this method's runner (run_game) into the bit-exact VCS substrate, and the
computation it involves. Every step links to the exact source on main. From
callstack_phaseC.json.
env_reset!(env; boot_noop_steps = 60, boot_reset_steps = 4) jutarienv_step!(env::StellaEnvironment, action::Integer) jutaribuild_shared_testbed(game; ... intervene_ram = intervene_ram!, run_intervention = run_intervention, soft_ram_peek = soft_ram_peek, ...) runnersnapshot(env::StellaEnvironment, frame::Integer) jutarirun_patch(checkpoint, tail, kind, site, value) -> intervene_ram!/intervene_tia! runner intervene_ram! @ activation_patching.jl:285, intervene_tia! @ :287rec_snap = run_patch(clean_ckpt, ...); ex_snap = run_patch(fresh_ckpt, ...) inside _run_game_body runner rec_snap @ :460, ex_snap @ :466Scored against the exact intervention oracle Δy(u): run_intervention(checkpoint, actions, target_frame, horizon, cause).
The figure is generated from the committed record by
docs/gen_method_figures.py; the game frame
and each RAM cell's screen footprint are produced by
render_scenes.jl /
cell_footprints.jl.