# How To Do Patching Fast

This post outlines an efficient implementation of Edge Patching that massively outperforms common hook-based implementations. This implementation is available to use in my new library, [AutoCircuit](https://ufo-101.github.io/auto-circuit), and was first introduced by [Li et al. (2023)](https://github.com/xanderdavies/circuit-breaking/blob/f2cdc4fc95581f1bc82bc16b44184ef8cbb67c1d/toxicity/transformer.py#L198).

What is activation patching?
============================

I introduce new terminology to clarify the distinction between different types of activation patching.

Node Patching
-------------

Node Patching (aka. “normal” activation patching) is when some activation in a neural network is altered from the value computed by the network to some other value. For example we could run two different prompts through a language model and replace the output of `Attn 1` when the model is given some `input 1` with the output of the head when the model is given some other `input 2`.

We will use the running example of a tiny, 1-layer transformer, but this approach generalizes to any transformer and any residual network.

![](https://lh7-us.googleusercontent.com/2KLYAerwc5EJVPY8kaYnZL0kBHYTN2vNyEg6g9m4KPv6GQImf87wY64D4YzGG743RNu3--5sYm5agFxcyYBxMUQ3kpQIfE6fKhkmhjpCsCEYhWhbn5IO088RFNSeKE0z8H-2WCkviabo4by_K-TXJ_0)

All the nodes downstream of `Attn 1` will be affected by the patch.

Edge Patching
-------------

If we want to make a more precise intervention, we can think about the transformer [differently](https://transformer-circuits.pub/2021/framework/index.html), to isolate the interactions between components.

![](https://lh7-us.googleusercontent.com/7JJukdg_9R_0NwWxZ3BbE1CgR48M1bKYUDH2PolZuXyfovQpd5YvDQavpQZARSxwfBsXEgn_iUrbua4gR4v1olSmq9dR3qeNecQidVjWQyLzzaRg9ICNcQHZlfiR6Rd5R4tzCsik6sRtjGkKZdhg0AU)

Now we can patch the edge `Attn 1 -> MLP` and only nodes downstream of `MLP` will be affected (eg. `Attn 1->Output` is unchanged). Edge Patching has not been explicitly named in any prior work.

![](https://lh7-us.googleusercontent.com/D_xoWZ0FRQhpY1syHE6DOK_x2QQv8l8gX-l-M6Z9CdWvVouJRt7wkzfJHFPFPQTLngIw_CMKPqJVqsTWzNJFVMDAZ15-7J9McVxW626EOKYxsVGonXSvlsbtPRogrS6-ccVD1DpDU0YLraaUORFHhX4)

Path Patching
-------------

[Path Patching](https://arxiv.org/abs/2304.05969) refers to the intervention where an input to a path is replaced in the ‘treeified’ view of the model. The treeified view is a third way of thinking about the model where we separate each path from input to output. We can implement an equivalent intervention to the previous diagram as follows:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/8240b36209b4db5584954bce32db6e8bdd37310536a3acce.png)

In the [IOI paper](https://arxiv.org/abs/2211.00593), ‘Path Patching’ the edge `Component 1 -> Component 2` means Path Patching all paths of the form

    Input -> ... -> Component 1 -> ... -> Component 2 -> ... -> Output

where all components between `Component 1` and `Component 2` are `MLP`s[^dvqpldkcvb]. However, it can be easy to confuse Edge Patching and Path Patching because if we instead patch all paths of the form

    Input -> ... -> Component 1 -> Component 2 -> ... -> Output

this is equivalent to Edge Patching the edge `Component 1->Component 2`.

Edge Patching all of the edges which have some node as source is equivalent to Node Patching that node. [AutoCircuit](https://ufo-101.github.io/auto-circuit) does not implement Path Patching, which is much more expensive in general. However, as explained in the appendix, Path Patching is sometimes equivalent to Edge Patching.

Fast Edge Patching
==================

We perform two steps.

*   First we gather the activations that we want to patch into the model. There’s many ways to do this, depending on what type of patching you want to do. If we just want to do zero ablation, then we don’t need to even run the model. But let’s assume we want to patch in activations from a different, corrupt input. We create a tensor, `Patch Activations`, to store the outputs of the source of each edge and we write to the tensor during the forward pass. Each source component has a row in the tensor, so the shape is `[n_sources, batch, seq, d_model]`.[^wn9s101prlm]
    

![](https://lh7-us.googleusercontent.com/OmteRLE1QxDfkW0g9uj1uxW7sxwDEkn24eava_WK4NeCWRL17KAosNtc88x4VxQ-7e0AhzxBuk2jmmKoyFyL0ljGhhdBESHUi3cyIUtRAZC4tCtWu__0AhyRUTs-KhVyD1Ubhf2dprGi45xYpP-QQBk)

*   Now we run the forward pass in which we actually do the patching. We write the outputs of each edge source to a different tensor, `Current Activations`, of the same shape as `Patch Activations`. When we get to the input of the destination component of the edge we want to patch, we add the difference between the rows of `Patch Activations` and `Current Activations` corresponding to the edge’s source component output.

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/934980fae2b278376530f7cdef0b4fad8f7161bae14ca086.png)

This works because the difference in input to the edge destination is equal to the difference in output of the source component.[^xk7eji38h8f] Now it’s straightforward to extend this to patching multiple edges at once by subtracting the entire `Current Activations` tensor from the entire `Patch Activations` tensor and multiplying by a `Mask` tensor of shape `[n_sources]` that has a single value for each input edge.

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/0a8d7d454f2a965fc8ba8771127faf61b8625ffc98a76241.png)

By creating a `Mask` tensor for each destination node we can patch any set of edges in the model. Crucially, the entire process is vectorized so it’s executed very efficiently on the GPU.

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/9dc8f718babef806e2bef67da25ef4fd9c3ccf184fc945b7.png)

Performance Comparison
----------------------

We [test the performance](https://colab.research.google.com/drive/1Fb6GFPwK872H24_stEUfMDjq10h85nAS?usp=sharing) using the ACDC circuit discovery algorithm, which iteratively patches every edge in the model. We compare the performance of AutoCircuit's implementation to the [official ACDC](https://github.com/ArthurConmy/Automatic-Circuit-Discovery/tree/main) hook-based implementation. We run ACDC using both libraries at a range of thresholds for a tiny 2-layer model with only 0.5 million parameters[^djlo3xlrvwu] and measure the time taken to execute.[^ulaa4nnm8tt]

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/78de9173eb26144e5b60efd1b4444133dc01a88a27c7c981.png)

Different numbers of edges are included at different thresholds in the ACDC algorithm.[^5g87u9oaqhg] While this greatly affects the performance of the hook-based implementation, it doesn't change the fast implementation because mask parameters for all edges are always included.

Mask Gradients
==============

In [AutoCircuit](https://ufo-101.github.io/auto-circuit), masks are implemented not using hooks, but by injecting new PyTorch `Module`s that wrap the existing node modules and perform the Edge Patching. The `Mask` tensors are `Parameter`s of the wrapper `Module`s. This means that we can compute the gradients of the model output with respect to the `Mask` values using the normal AutoGrad system.

So we can ‘train’ a circuit by optimizing the `Mask` parameters using gradient descent[^n4y1foshcwf]. We can also compute the [attribution](https://www.neelnanda.io/mechanistic-interpretability/attribution-patching) of each edge very easily. If we set all `Mask`s to `0`, the attribution is simply the gradient of output with respect to the mask.

Proof:

> Let $\alpha \in [0, 1]$ interpolate between the clean and corrupt edge activation $e_{\text{clean}}$ and $e_{\text{corr}}$:
> 
> $e_{\alpha} = e_{\text{clean}} + \alpha \times (e_{\text{corr}} - e_{\text{clean}})$
> 
> Then
> 
> $\frac{\partial F(e_{\alpha})}{\partial \alpha} = \frac{\partial F(e_{\alpha})}{\partial e_{\alpha}} \frac{\partial e_{\alpha}}{\partial \alpha}$
> 
> $= \frac{\partial F(e_{\alpha})}{\partial e_{\alpha}} \frac{\partial [e_{\text{clean}} + \alpha \times (e_{\text{corr}} - e_{\text{clean}})]}{\partial \alpha}$
> 
> $= \frac{\partial F(e_{\alpha})}{\partial e_{\alpha}} (e_{\text{corr}} - e_{\text{clean}})$
> 
> Set $\alpha = 0$, ie. $e_{\alpha} = e_{clean}$
> 
> $= (e_{\text{corr}} - e_{\text{clean}}) \frac{\partial F(e_{\text{clean}})}{\partial e_{\text{clean}}}$
> 
> Which is the definition of (edge) attribution patching.

Intuition:

> The gradient of the output with respect to the activation is the amount that the output would change if you add $\delta x$ to the activation, divided by $\delta x$. So we need to multiply by $e_{\text{corr}} - e_{\text{clean}}$ to estimate the effect of patching.
> 
> The gradient of the output with respect to the mask is the amount that the output would change if you add $\delta x \times (e_{\text{corr}} - e_{\text{clean}})$ to the activation, divided by $\delta x$. So the full effect of patching is already accounted for.

Appendix: Path Patching vs. Edge Patching
=========================================

In general 'treeified' interventions have time complexity exponential in the number of layers of the model, because each node sees a different “copy” than its siblings of the subtree upstream of itself, and each copy can have different inputs. However, there is a special class of treeified interventions which can be implemented using Edge Patching.

Starting with a simple example, we have already seen that the path

    Input -> Attn 1 -> MLP -> Output

can be patched by Edge Patching the edge `Attn 1 -> MLP`.

Now consider a transformer with an extra attention layer.

![](https://lh7-us.googleusercontent.com/6wQT1YkhEkpfW95B3gvmXxzoipuj_iAxSWmcIFFfeiZZXJ-Xm4p-0Xl7q-hqh8m9mF6c7d-nSjrpd-g64BuR7YwdPrLFhPxj3Bbyag5YtcFYPGtHEjsqe9KVEgY2fBKlmYe8wSpSaQ-wJOowRcVqTlA)

Not all edges are shown

Say we want to patch the path

    Input -> Attn 0.0 -> MLP -> Attn 1.0 -> Output

This can be implemented in the treefied view with Path Patching as follows:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/a28d42c0c9391bff4d41a0b9699a4535f75ba326197a3260.png)

However, if we just Edge Patch `Attn 0.0 -> MLP` (or `Input -> Attn 0.0`), we will get a different output because there is a downstream effect on `Attn 1.1`.

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/7b7da4038ee39f6f9a01a0c29fc2219c6b17b7f319b3fdff.png)

Not all edges are shown

If we instead change the input to the corrupt prompt and patch in clean activations to the *complement* of the circuit, then we can achieve the desired intervention.

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/64bca598680ee61b94ac0cd5f7fa748ee12acef53ec0724f.png)

Not all edges are shown

In general, Edge Patching can be used to implement any treeified hypothesis in which all instances of a node have the same input. This means that any [Causal Scrubbing](https://www.alignmentforum.org/posts/JvZhhzycHu2Yd57RN/causal-scrubbing-a-method-for-rigorously-testing) hypothesis which just specifies a set of important and unimportant *edges* (and a single clean and corrupt prompt pair) can be implemented with fast Edge Patching.

But a circuit hypothesis which specifies a set of important and unimportant *paths* cannot always be implemented with Edge Patching. 

For example, if we want to patch the paths

    Input -> Attn 0.0 -> MLP -> Attn 1.0 -> Output
    Input -> Attn 0.1 -> MLP -> Attn 1.1 -> Output

this can only be expressed in the treeified model, because it requires the output of the MLP to be computed on two different inputs, and for both outputs of the MLP to be propagated to the output.

![](https://lh7-us.googleusercontent.com/zaAEsoQD37fYefKixc73c3FmnPN9ryfBf6CX3Ikn1TMBCbkeEOf0_bKHmKuCZWQDGZR3RILKdKz0H1zTP8lKPpj3FEZsNj7RXKMh0oddCwytCXZMwk6Z4xCObV5zheJ0kZx89S3asTp4SuRoCgmfzhs)

*Thanks to Bilal Chughtai for his extensive feedback. Thanks to Nix Goldowsky-Dill and Arthur Conmy for their comments. Thanks to Sam Marks for the example of a treeified intervention that cannot be implemented by Edge Patching.*

[^dvqpldkcvb]: They hypothesize that the task is mostly performed by attention heads, at it only requires moving information around. 

[^wn9s101prlm]: n_sources is the number of source nodes in the model. batch is the number of elements in the current input batch. seq is the length of the prompts in the batch. d_model is the size of the model activations. 

[^xk7eji38h8f]: This will always be the case for the edges in this diagram, but it won’t work if you consider MLPs to be included in the direct edges between attention heads, as they do in the IOI paper (which is why that is Path Patching, not Edge Patching). 

[^djlo3xlrvwu]: For larger models, both implementations will take longer to execute. The ratio of execution time between the two probably remains similar. But ACDC becomes sufficiently slow that this is annoying to test for eg. >50% of edges included in GPT-2, so we're not certain what the curve looks like. 

[^ulaa4nnm8tt]: Note that the AutoCircuit library contains other optimizations besides the fasting patching method. In particular, in the ACDC algorithm we cache layers during forward passes that patch edges to nodes in later layers.So this is not a fair comparison for measuring the speedup from the fast patching method alone. However, the ACDC repo is the most popular library for patching and the ACDC algorithm is one of the most common use-cases where you would want to patch most of the edges in a model so it seems like a useful metric anyway. 

[^5g87u9oaqhg]: Note that ACDC and AutoCircuit count the number of edges differently (AutoCircuit doesn't include 'Direct Computation' or 'Placeholder' edges) so we compare the proportion of edges included. The underlying computation graphs are equivalent. 

[^n4y1foshcwf]: Also done by Li et al. (2023). 

[^a0t9aaibb7r]: Note that the AutoCircuit library contains other optimizations besides the fasting patching method. In particular, we cache layers during forward passes that patch edges to nodes in later layers.So this is not a fair comparison for measuring the speedup from the fast patching method alone. However, the ACDC repo is the most popular library for patching and the ACDC algorithm is one of the few use-cases where you would want to patch most of the edges in a model so it seems like a useful metric anyway.