Skip to main content

Welcome to a Grasp lesson

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

Lesson 5

Implementing PyTorch Forward Hooks for Activation Extraction

Recommended resources

In our last lesson, we saw how to use Singular Value Decomposition (SVD) to analyze a matrix of activations and find the principal directions of variation. This is a powerful technique for understanding what features a model has learned. However, it relies on one key assumption: that we already have access to that activation matrix. Today, we'll address the crucial preceding step: how do we actually extract these internal activations from a model during its forward pass?

This lesson will introduce you to PyTorch hooks, a fundamental mechanism for instrumenting your models. Our goal is to register a forward hook that records and safely detaches an intermediate activation. By mastering this, you'll gain the ability to non-invasively "listen in" on the computations happening inside a neural network, a prerequisite for nearly all mechanistic interpretability work.

1. The Need for a "Stethoscope": Introducing Hooks

Imagine you're trying to understand how a complex engine works. You can't just look at the final output (the car moving); you need to listen to the individual components, measure pressures, and track temperatures. Modifying the model's source code by adding print() statements everywhere is like drilling holes in the engine block—it's messy, invasive, and not sustainable.

PyTorch provides a much more elegant solution: hooks. Hooks are functions that you can attach to any nn.Module (like a linear layer or an attention block). PyTorch will then automatically execute your function at a specific point in the computation—either right before or right after the module's forward pass. This allows you to inspect, record, or even modify the tensors flowing through the network without altering the model's definition.

The following video provides an excellent tour of where hooks fit into the PyTorch nn.Module source code, which helps build a solid mental model of how they work.

Visualizing activations with forward hooks (PyTorch)

Watch this video from "mildlyoverfitted" to understand the mechanism behind hooks.

Focus on these key segments: The introduction explains what hooks are and shows exactly where they are executed within the nn.Module's __call__ method, right after the forward method completes. This demystifies the "magic." The video sets up a simple custom neural network. Pay attention to how a standard nn.Module is defined, as we will use a similar structure in our own code.


2. The Anatomy of a Forward Hook

We'll be focusing on register_forward_hook, which attaches a function to be executed immediately after a module's forward() method has been computed.

To use it, you need to define a hook function with a specific signature:

def my_hook_function(module, inputs, output):
# ... your code here ...

Let's break down these arguments:

  • module: This is the instance of the nn.Module itself (e.g., the specific nn.Linear layer that you attached the hook to). You can use this to get information about the layer, like its name or parameters.
  • inputs: This is a tuple containing the positional arguments that were passed to the module's forward method. For a simple linear layer, this will typically be a tuple with a single tensor.
  • output: This is the tensor that the module's forward method returned. This is usually what we're most interested in for recording activations.

The video below gives a very clear explanation of the inputs and outputs of both forward and pre-forward hooks.

PyTorch Hooks Explained - In-depth Tutorial

This clip from Elliot Waite's tutorial provides a clear, practical demonstration of the hook signature.

Watch the section on module hooks. It shows how the module, inputs, and output arguments are passed to the hook function during the forward pass. Note how you can even modify the output by returning a new value from the hook, which is a concept we'll explore in the next lesson.


3. A Practical Recipe for Recording Activations

Now let's build a complete, hands-on example. Our goal is to capture the output activation of a specific layer in a simple network.

The blog post "Intermediate Activations — the forward hook" lays out a clean and effective pattern for this, which we will implement. The core idea is to use a dictionary to store the activations, with a key corresponding to the layer name.

Intermediate Activations — the forward hook

This blog post provides a concise recipe for capturing activations.

First, read the section Forward Hooks 101 for a conceptual summary. Then, carefully study the code pattern shown under Using the forward hooks. This pattern, which uses a closure to create the hook function, is a common and powerful technique you will use frequently.

Let's implement this pattern ourselves.

Step-by-Step Implementation

We'll follow these steps:

  1. Define a simple neural network.
  2. Create a dictionary to act as our activation cache.
  3. Define a "factory" function that creates our hook function (using a closure, as shown in the blog post). The hook will save the detached output to the cache.
  4. Register the hook to a specific layer.
  5. Run a forward pass to trigger the hook.
  6. Inspect the cache to verify the activation was saved.
  7. Remove the hook to clean up.

Here is the full code. You can run this directly in a Google Colab notebook.

import torch
import torch.nn as nn




# 1. Define a simple model
model = nn.Sequential(
    nn.Linear(10, 20),
    nn.ReLU(),
    nn.Linear(20, 5)
)




# We want to inspect the output of the ReLU activation
target_layer = model[1] 




# 2. Create the activation cache
activation_cache = {}




# 3. Define the hook factory and the hook function
def get_activation(name):
    def hook(model, input, output):



        # Detach the tensor from the computation graph and save it
        activation_cache[name] = output.detach()
    return hook




# 4. Register the forward hook
# The register_forward_hook method returns a handle that we can use to remove the hook later
hook_handle = target_layer.register_forward_hook(get_activation('relu'))

print("Hook registered.")




# 5. Run a forward pass
input_tensor = torch.randn(1, 10)
output = model(input_tensor)

print("\nForward pass complete.")




# 6. Inspect the cache
print(f"Keys in cache: {activation_cache.keys()}")
print(f"Shape of cached 'relu' activation: {activation_cache['relu'].shape}")




# Verify that the hook did its job
assert 'relu' in activation_cache
assert activation_cache['relu'].shape == (1, 20)




# 7. Remove the hook
hook_handle.remove()
print("\nHook removed.")




# Clear the cache and run another forward pass to confirm the hook is gone
activation_cache.clear()
output = model(input_tensor)
print("\nSecond forward pass complete.")
print(f"Keys in cache after removal: {list(activation_cache.keys())}")
assert 'relu' not in activation_cache

Why .detach()?

The line output.detach() is critical. In PyTorch, every tensor that results from an operation on another tensor requiring gradients becomes part of a computation graph. This graph is what allows automatic differentiation to work for backpropagation.

By calling .detach(), we create a new tensor that shares the same data but is explicitly excluded from this graph. For interpretability, this is vital for two reasons:

  1. Memory Efficiency: It prevents PyTorch from storing the entire history of computations needed for gradient calculation, which can consume a lot of memory, especially with large models and datasets.
  2. Preventing Errors: It ensures that our analysis tools don't accidentally interfere with the gradient calculations if we were to perform backpropagation later.

4. A More General Approach

Registering hooks one by one is fine for a single layer, but for analyzing a whole model, we need a more scalable approach. We can write a helper function that iterates through the model's layers and attaches hooks to the ones we specify.

The following resource presents a very clean implementation of this idea.

Saving PyTorch activations w/ forward hooks - lyndonduong.com

This article from lyndonduong.com demonstrates a more robust and reusable pattern for registering hooks.

Read through the code for the save_activations hook function and the register_activation_hooks helper. Notice how it uses model.named_modules() to iterate through layers and functools.partial to pass the layer name to the hook. This is an alternative to the closure method we used earlier, with the same result. The full example at the end shows how to apply this to a simple CNN.

This pattern of programmatically attaching and managing hooks is what enables powerful analysis tools. In fact, libraries like TransformerLens, which we will use later in this course, are built around this very principle of managing hooks to give you easy access to any internal activation.


Conclusion

You have now acquired a core skill for peering inside neural networks. We've moved beyond treating models as black boxes and have a precise, clean mechanism for extracting their internal states.

Here are the key takeaways from today's lesson:

  • PyTorch hooks are functions that can be registered to nn.Modules to execute code during a forward or backward pass without modifying the model's source.
  • register_forward_hook attaches a function that runs after a module's forward method and has the signature hook(module, inputs, output).
  • When recording activations, it is crucial to call .detach() on the output tensor to remove it from the computation graph, saving memory and preventing side effects.
  • register_forward_hook returns a handle whose .remove() method must be called to de-register the hook and prevent memory leaks or unintended behavior.

In this lesson, we used hooks for passive observation. But what if we want to intervene? In our next lesson, we will build directly on this foundation to do just that. You will learn how to use a hook to not just record an activation, but to actively replace it with a different value during inference. This is the gateway to causal interventions, the experimental bedrock of mechanistic interpretability.