Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 7

Verifying Intervention Specificity

Recommended resources

In our last lesson, we took our first step as active experimenters, learning to use PyTorch hooks to patch activations and test causal hypotheses. We saw how replacing an activation from a "destination" run with one from a "source" run could causally steer the model's final output. This is a powerful technique, but with great power comes the need for great rigor.

This lesson is all about ensuring our experiments are scientifically valid. We will learn how to verify that our interventions do exactly what we intend, and nothing more. This involves two critical checks. First, we must confirm that we are only changing the model's transient activations, not its permanent, learned parameters. Second, we need to understand how the rest of the model might react to our intervention in unexpected ways, a crucial step in correctly interpreting our results. Mastering these verification steps will move you from simply running interventions to designing robust, trustworthy interpretability experiments.

The First Rule of Intervention: Do No Harm to Parameters

When we perform an activation patch, our goal is to study the model's computation as a fixed function. We are asking "what if" about the data flowing through the network, not about the network itself. Accidentally modifying the model's weights or biases during an experiment would be like a biologist accidentally mutating a specimen's DNA while trying to observe its behavior. The results would be invalid.

The primary mechanism that could cause such an accidental change is PyTorch's automatic differentiation engine, or autograd. During a standard training loop, PyTorch builds a computational graph that tracks every operation. When .backward() is called on a loss tensor, gradients are calculated and flow backward through this graph to update the model's parameters (.grad attributes).

This diagram illustrates the kind of graph PyTorch's `autograd` engine builds. Each operation (like `AddBackward` or `SinBackward`) is a node, linking output tensors back to the inputs that created them. The `.grad_fn` attribute of a tensor points to the function that will be used to compute gradients.

Even though we are only doing inference and not explicitly calling .backward(), it is best practice to design our hooks to be "autograd-proof." This is where the .detach() method, which you saw in the previous lesson's code, is essential. When we call .detach() on a tensor, we create a new tensor that shares the same underlying data but is severed from the computational graph. No gradients can flow back through it.

The following video gives a comprehensive explanation of how autograd and hooks work. While we are focused on the forward pass, understanding the backward pass mechanics will clarify why detach() is so important for preventing unintended consequences.

PyTorch Hooks Explained - In-depth Tutorial

This video, "PyTorch Hooks Explained", provides a deep dive into the autograd system.

Watch the segment from tensor hooks for a detailed walkthrough of how gradients flow through the computational graph. This will give you a solid mental model of the structure that detach() allows us to break.

While .detach() is our main tool, we can also add a simple, explicit check to our code to be absolutely certain that no parameters have changed. We can do this by saving a copy of the model's state_dict (a dictionary of all its parameters) before our intervention and comparing it to the state_dict after.

Here is how you would add this verification to the patching code from our last lesson:

import torch
import torch.nn as nn
import copy




# (Model definition, inputs, etc. from previous lesson)
model = nn.Sequential(
    nn.Linear(2, 10), nn.ReLU(), nn.Linear(10, 10), nn.ReLU(), nn.Linear(10, 2)
)
target_layer = model[1]
source_input = torch.tensor([[10.0, 0.1]])
destination_input = torch.tensor([[0.1, 10.0]])




# --- Record source activation (as before) ---
activation_cache = {}
def get_activation(name):
    def hook(model, input, output):



        # Crucially, we detach the activation when we cache it.
        activation_cache[name] = output.detach()
    return hook
record_handle = target_layer.register_forward_hook(get_activation('source_activation'))
_ = model(source_input)
source_activation_patch = activation_cache['source_activation']
record_handle.remove()




# --- The Verification Step ---
# 1. Save a snapshot of the model's parameters BEFORE the intervention.
pre_intervention_state = copy.deepcopy(model.state_dict())




# --- Run the Patched Pass (as before) ---
def patch_activation_hook(module, input, output):
    return source_activation_patch.to(output.device)

patch_handle = target_layer.register_forward_hook(patch_activation_hook)
patched_output = model(destination_input) 
patch_handle.remove()




# 2. Compare the current parameters to the saved snapshot.
post_intervention_state = model.state_dict()

all_params_unchanged = all(
    torch.equal(pre_intervention_state[key], post_intervention_state[key])
    for key in pre_intervention_state
)

if all_params_unchanged:
    print("Verification successful: Model parameters were not changed by the intervention.")
else:
    print("Verification FAILED: Model parameters were modified!")

assert all_params_unchanged

This state_dict check provides a foolproof guarantee that your experiment is only studying the model's forward computation, not altering its learned knowledge.


Verifying the Computational Effect: The Hydra of Self-Repair

We've verified that we haven't changed the model's parameters. But have we truly changed "only the intended computation"? This is a much more subtle and profound question.

Our naive assumption might be that when we patch an activation at layer L, the computation up to L is from the destination run, the computation at L is replaced by the source activation, and the rest of the model passively processes this new value. This is often not the case. The model is a highly non-linear, dynamic system. Downstream layers can react to a patched activation, sometimes in ways that actively counteract our intervention. This phenomenon is known as self-repair or downstream compensation.

The following reading is essential for understanding this critical concept. It introduces the idea of self-repair and explains why it's a major blind spot for simple intervention experiments.

Activation Patching and Causal Interventions | Learn Mechanistic Interpretability

This article from the Learn Mechanistic Interpretability series explains why activation patching results can sometimes be misleading.

Please read the section on confounds and self-repair. Focus on understanding why the effect we measure from an intervention is often a lower bound on a component's true importance. Note the two mechanisms mentioned: LayerNorm rescaling and backup components.

As the article explains, if we damage or "noise" a component (patching a corrupted activation into a clean run), backup systems in the model might activate to compensate, making the original component seem less important than it is. This is like a city's power grid having redundant generators; knocking one offline doesn't cause a blackout, but that doesn't mean the generator was useless.

This has two major implications for verifying our experiments:

  1. Ablation is a lower bound: The performance drop you see from ablating a component should be interpreted as "at least this much" of a contribution, with the true contribution potentially being higher and masked by self-repair.
  2. Noising vs. Denoising: The direction of your patch matters. "Noising" (corrupted-into-clean) tests necessity and is vulnerable to compensation. "Denoising" (clean-into-corrupted) tests sufficiency and can often identify important components even when backups exist.

Verification, in this context, isn't a single assert statement. It's a conceptual check. It means interpreting your results with the knowledge that the model is not a passive pipeline but an adaptive system. When you see a small effect from an intervention, you must ask: "Is this component truly unimportant, or is the model just repairing the damage I caused?"


Conclusion

In this lesson, we've added crucial layers of rigor to our experimental toolkit. You now know how to ensure your interventions are clean and how to interpret their results with the necessary nuance.

Here are the key takeaways for verifying your interventions:

  • Protect parameters: Always use .detach() when caching activations for patching. For absolute certainty, verify that the model's state_dict is unchanged after an intervention.
  • Intervention changes only activations: An activation patch is a change to the data flowing through the model, not the model itself.
  • Beware of self-repair: The model can compensate for your interventions. This means the measured effect of an ablation or patch is a lower bound on the component's true causal importance.
  • Verification is conceptual: Understanding downstream compensation is key to verifying and correctly interpreting the meaning of your experimental results.

So far in this course, we've built a foundational toolkit for looking inside neural networks. We can record activations, patch them to perform causal experiments, and verify the validity of those experiments. Now, we are ready to apply these tools to the dominant architecture in modern AI. In the next module, we will begin our deep dive into the inner workings of the Transformer.