Coming from torch.fx¶
This page is for people who use torch.fx.symbolic_trace to get a graph of a model's operations
and want to know what a TorchLens trace gives instead. The examples were written against the
PyTorch 2.x torch.fx that ships with torch, and the current TorchLens release. Checked
2026-11-03.
FX symbolic tracing produces a static GraphModule by running the model on proxy values;
TorchLens runs the eager model on a concrete input and records what actually executed.
The same task in both¶
The task: run a small model through the traced graph and read its output.
In torch.fx¶
# migration-test: tool=fx expected=[[2.5, 2.5]]
import torch
from torch import nn
from torch.fx import symbolic_trace
class Tiny(nn.Module):
def __init__(self):
super().__init__()
self.proj = nn.Linear(2, 2)
with torch.no_grad():
self.proj.weight.copy_(torch.eye(2))
self.proj.bias.copy_(torch.tensor([0.5, -0.5]))
def forward(self, x):
return torch.relu(self.proj(x))
gm = symbolic_trace(Tiny())
RESULT = gm(torch.tensor([[2.0, 3.0]])).detach().tolist()
In TorchLens¶
# migration-test: tool=torchlens expected=[[2.5, 2.5]]
import torch
from torch import nn
import torchlens as tl
class Tiny(nn.Module):
def __init__(self):
super().__init__()
self.proj = nn.Linear(2, 2)
with torch.no_grad():
self.proj.weight.copy_(torch.eye(2))
self.proj.bias.copy_(torch.tensor([0.5, -0.5]))
def forward(self, x):
return torch.relu(self.proj(x))
log = tl.trace(Tiny(), torch.tensor([[2.0, 3.0]]))
RESULT = log["linear_1_1"].out.detach().tolist()
Translation table¶
| torch.fx construct | TorchLens equivalent |
|---|---|
symbolic_trace(model) followed by graph_module(x) |
tl.trace(model, x) and inspect the eager-operation log (trace[label].out) |
graph_module.graph.nodes (node names, targets) |
trace.layers (labels such as linear_1_1, one row per executed operation); tl.compat.from_fx(graph_module) lists FX node names as suggested layer lookups |
Graph rewriting (graph.node_copy, replace_all_uses_with) |
no equivalent; TorchLens does not rewrite graphs. Edit values with tl.when(site, action) or trace.fork().do(site, action) |
What TorchLens adds¶
- Concrete values: each recorded operation carries its output tensor for the input you ran, not a symbolic node.
- Data-dependent control flow: the branch the input took is what is recorded (see the fence below), where symbolic tracing has no value to branch on.
- Replay verification (
tl.validate), source lines and containing-module stack per operation, loop and pass identity. - Value edits on the recorded graph (
trace.fork().do(...)) and capture-time interventions (tl.trace(..., intervene=...)).
What torch.fx does that TorchLens does not¶
- A static graph independent of any input. An FX
GraphModuleis a reusable program: it can be transformed, saved as code and recompiled, which a TorchLens trace (one concrete run) is not. - Graph transformations and passes (rewriting, fusion, quantization and export pipelines build on FX). TorchLens records and edits values; it does not produce a transformed module.
- For other destinations see where to go instead.
Using both¶
torch.fx feeds TorchLens¶
A GraphModule is an nn.Module, so tl.trace(graph_module, x) is the plain call. The migration
helper tl.compat.from_fx(graph_module) returns a payload (schema, model, layers) whose
layers default to the FX node names; it does not run a capture.
# migration-test: tool=fx expected=('torchlens.fx_migration.v1', True)
import torch
from torch import nn
from torch.fx import symbolic_trace
import torchlens as tl
class Tiny(nn.Module):
def __init__(self):
super().__init__()
self.proj = nn.Linear(2, 2)
def forward(self, x):
return torch.relu(self.proj(x))
payload = tl.compat.from_fx(symbolic_trace(Tiny()))
RESULT = (payload["schema"], "proj" in payload["layers"])
TorchLens feeds torch.fx¶
There is no bridge. Tensors from trace[label].out are plain torch.Tensor objects. TorchLens
does not emit an FX graph; to get one, run symbolic_trace on the source model.
Semantic differences¶
- A symbolic trace cannot branch on a tensor value; TorchLens records the branch the input took. The same model, traced both ways:
# migration-test: tool=fx expected=('TraceError', [1.0, 1.0])
import torch
from torch import nn
from torch.fx import symbolic_trace
import torchlens as tl
class Branchy(nn.Module):
def forward(self, x):
if x.sum() > 0:
return torch.relu(x)
return -x
try:
symbolic_trace(Branchy())
raised = None
except Exception as exc:
raised = type(exc).__name__
trace = tl.trace(Branchy(), torch.ones(2))
RESULT = (raised, trace[trace.output_layers[0]].out.tolist())
- An FX graph is the same for every input; a TorchLens trace is one input's execution, so a model with input-dependent control flow gives a different trace for a different input.