Coming from forward hooks¶
This page is for people who save activations with register_forward_hook and want to know when
a TorchLens capture is the better fit. The examples were written against PyTorch 2.x and the
TorchLens version on this branch. Checked 2026-11-03.
register_forward_hook ships with torch, adds nothing to your dependency tree, and is the right
tool for always-on training instrumentation: a scalar norm per module per step costs almost
nothing and never needs a capture. If that is your whole ask, keep your hooks.
The same task in both¶
The task: save the output of the ReLU in a small network.
In forward hooks¶
# migration-test: tool=hooks expected=(2, 8)
import torch
import torch.nn as nn
torch.manual_seed(0)
model = nn.Sequential(nn.Linear(8, 8), nn.ReLU(), nn.Linear(8, 4)).eval()
x = torch.randn(2, 8)
saved = {}
def save_output(module, args, output):
saved["relu"] = output.detach()
handle = model[1].register_forward_hook(save_output)
model(x)
handle.remove()
RESULT = tuple(saved["relu"].shape)
In TorchLens¶
# migration-test: tool=torchlens expected=(2, 8)
import torch
import torch.nn as nn
import torchlens as tl
torch.manual_seed(0)
model = nn.Sequential(nn.Linear(8, 8), nn.ReLU(), nn.Linear(8, 4)).eval()
x = torch.randn(2, 8)
log = tl.trace(model, x, save=tl.func("relu"))
relu_out = log["relu_1_2"].out
RESULT = tuple(relu_out.shape)
log.cleanup()
Translation table¶
| Hook idiom | TorchLens spelling |
|---|---|
module.register_forward_hook(save_output) |
log = tl.trace(model, x, save=tl.in_module("encoder")) then log["encoder"].out |
| Hook on every ReLU | tl.trace(model, x, save=tl.func("relu")) |
| Hook dict keyed by module name | log[label].out; labels are stable, pass-qualified, and listable |
register_full_backward_hook |
capture=tl.options.CaptureOptions(backward_ready=True) then trace.backward(loss); per-fire grad records with node identity (see backward.md) |
| Mutating outputs inside a hook | intervene=tl.when(tl.func("relu"), tl.zero_ablate()): declared, audited, replayable |
| Hook that only computes a running scalar | Keep the hook: it is cheaper, and correct for always-on telemetry |
What TorchLens adds¶
- Functional operations.
F.scaled_dot_product_attention,torch.matmul,x + yand.softmax()are notnn.Moduleboundaries, so module hooks never see them. TorchLens records every wrapped tensor operation (tl.func("relu")selects by operation name). - Per-call identity. A weight-tied block or a loop-invoked cell fires one hook per call with no
identity attached. TorchLens labels each call pass-qualified (
attn_1_1:2is the second pass), with the containing-module stack per call. - The executed dataflow graph (which operation consumed which), source lines per operation,
selective capture predicates, interventions, replay and validation (
tl.validate). A hook dictionary does not grow into these.
What forward hooks do that TorchLens does not¶
- Hooks cost close to nothing per step. A TorchLens capture is instrumented execution whose overhead depends on model, shape and save tier, and is published only as a measured matrix (see native-torch.md). For always-on scalar telemetry, use hooks.
- Hooks can change the live forward pass in training with no capture step, and they compose with any other PyTorch tooling that registers hooks. TorchLens interventions are declared and replayable, which is a different contract.
- For tools that attach to training loops or profile kernels, see landscape.md.
Using both¶
Forward hooks feed TorchLens¶
There is no bridge. A tensor a hook saved is an ordinary tensor: compare it with
log[label].out from a capture of the same model and input, or keep both in one script.
TorchLens feeds forward hooks¶
There is no bridge. Tensors from log[label].out are plain torch.Tensor objects, so any code
that consumed a hook's saved dictionary can take them directly.
Semantic differences¶
- A hook fires on
nn.Modulecalls only. A functional operation has no module to hook, so the same ReLU written astorch.relu(x)insideforwardis invisible to a module hook and is recorded by TorchLens:
# migration-test: tool=torchlens expected=True
import torch
import torch.nn as nn
import torchlens as tl
class Net(nn.Module):
def __init__(self):
super().__init__()
self.fc = nn.Linear(8, 8)
def forward(self, x):
return torch.relu(self.fc(x))
torch.manual_seed(0)
model = Net().eval()
x = torch.randn(2, 8)
log = tl.trace(model, x, save=tl.func("relu"))
RESULT = log["relu_1_2"].out.shape == (2, 8)
log.cleanup()
- Choose by the question. Always-on scalar telemetry: hooks. What actually ran, at operation resolution, with identity: a capture.