the pytorch path · 0/12
start the path

the pytorch path · chapter 07 of 12 · part i, the model

Graphs

Whatever captures your model, dynamo's guarded graph or torch.export's whole program, what's underneath is the same ATen vocabulary eager PyTorch already speaks.

the goal Given a torch.export call, read the resulting graph line by line, and predict which programs export accepts outright and which ones it refuses instead of silently splitting them.

mastery work · this chapter0/4
manual items are your word; auto items complete from your streaks, labs, and can-you ticks · stored in your browser only
§ 01

Below dynamo, ATen

Dynamo captures your tensor math into a graph, chapter 6's whole subject, but the graph itself is not exotic. It's FX, and every node in it calls an ATen op: the same operator library eager PyTorch calls under the hood, whether or not dynamo ever ran. Compile a function and print its captured ops, and you're reading the same vocabulary a plain, uncompiled forward pass already spoke.

That graph doesn't stop at the forward pass either. aot_autograd traces the backward through the same machinery, so what dynamo hands the compiler already covers a full training step, forward and backward both, one FX graph each, not something bolted on after the fact. This is why torch.compile speeds up training loops, not only inference calls.

One more pass runs before the graph settles: decompositions rewrite the larger ATen surface down toward a smaller core set of ops, fewer signatures for a compiler backend to implement, more of them expressible as combinations of the ones that remain. A backend author who supports the core set gets most of the rest for free.

Below dynamo, everything is ATen.
three artifacts, increasing in strictness, ending at the one a compiler can take whole
FX graph ATen calls, stitchedaround graph breaks aot_autograd forward and backwardtraced together ExportedProgram whole graphno Python left decompositions shrink the op set toward a core ATen read it the waythe jax path readsa jaxprexport is what the TPU bridges want, and chapter 10 explains why
§ 02

Torch.export: the whole graph, no Python left

Dynamo's graphs are stitched: wherever a guard might miss or Python control flow resists capture, a graph break splits the function into pieces and Python fills the gap between them, chapter 6's whole story. torch.export refuses that compromise. Call torch.export.export on a module and you get back an ExportedProgram: one standalone graph for the whole function, with no Python left in it at all, and no seam where a break could have happened.

That completeness is what makes an ExportedProgram portable. It's the artifact you hand to a serving stack running with no Python interpreter alongside it, and it's the same artifact chapter 10 picks up and carries across the bridge to StableHLOThe portable, versioned tensor IR that JAX and PyTorch both lower into; chapter 03 reads it line by line.taught in /l/stablehlo →, the wire format a compiler on the other side of that bridge actually reads. Where dynamo optimizes the training loop you're already running, export produces something you can hand off entirely.

run it: exporting a two-op module (torch 2.2.2, Python 3.11)
import torch

class Sin(torch.nn.Module):
    def forward(self, x):
        return torch.sin(x).sum()

ep = torch.export.export(Sin(), (torch.randn(4),))
print(ep.graph)
§ 03

Reading the graph

The call above prints exactly the text below, and reading it takes a habit the jax path already built for jaxprsThe traced program: one equation per primitive in single-assignment form, every shape and dtype stated.taught in /l/jaxpr →: find the placeholders, the inputs coming in, then follow one operation per line down to the outputs returned at the end. The IR is different, ATen calls here, JAX primitives there, but the fluency is the same skill wearing a different vocabulary.

Read it line by line and there's nothing left to guess at. %l_x_ is the placeholder, the function's one input. %sin calls torch.ops.aten.sin.default on it, and %sum_1 calls torch.ops.aten.sum.default on whatever %sin produced. The graph returns sum_1, and that's the whole function: two ATen calls, one input, one output, exactly what Sin.forward wrote in Python, with nothing hidden and nothing implicit left standing between them.

the verbatim output of the export call above
graph():
    %l_x_ : [num_users=1] = placeholder[target=l_x_]
    %sin : [num_users=1] = call_function[target=torch.ops.aten.sin.default](args = (%l_x_,), kwargs = {})
    %sum_1 : [num_users=1] = call_function[target=torch.ops.aten.sum.default](args = (%sin,), kwargs = {})
    return (sum_1,)
assigned

Readings