@jino_rohit: https://x.com/jino_rohit/status/2071247775837356399
摘要
一篇博文,解释 PyTorch FX 图,这是 PyTorch 2.0 编译生态系统中使用的一种中间表示。它涵盖了核心对象 Graph、Node 和 GraphModule,以及如何理解和使用它们。
查看缓存全文
缓存时间: 2026/06/29 04:23
Torch 内部机制(第一部分)—— FX 图
这是一个新的博客系列,我将尝试深入解析 PyTorch 和 PyTorch Compile 的生态系统。我一直想找个理由深入研究,弄清楚 PyTorch 为什么这么做、怎么做,以及它做得有多出色。希望到这个系列结束时,你能更深入地理解 PyTorch 2.0 生态系统的运作方式,并能更从容地调试程序。
到底什么是 FX 图?
什么是 FX 图?我为什么要关心它?我想读 torch.compile,跳过这个无聊的图。坏消息是:理解 FX 图是理解编译生态系统中所有其他组件的基础。从 Dynamo 到 Inductor,每个组件都以某种方式使用了 FX 图。好消息是:我将在一篇博客里全部分析清楚!
FX 图是 PyTorch 模块提供的一种中间表示(IR),它将 Python 代码转换成一个有向无环图(DAG)。
经典的 DAG
DAG 代表有向无环图,基本由节点和有向边组成。这是一个酷炫的 DAG——
三个核心对象
FX 图有三个你需要理解的概念——Graph、Node 和 GraphModule。我来展示一下我的意思。下面是一个用 torch.fx.symbolic_trace 追踪的简单模型:
import torch
import torch.fx
class MyModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.linear = torch.nn.Linear(4, 8)
def forward(self, x):
return torch.relu(self.linear(x))
gm = torch.fx.symbolic_trace(MyModel())
gm.graph.print_tabular()
打印结果:
opcode name target args kwargs
------------- ------ --------------- ---------- --------
placeholder x x () {}
call_module linear linear (x,) {}
call_function relu torch.relu (linear,) {}
output output output (relu,) {}
FX 图将 Python 代码展平为一系列节点。乍看可能觉得信息很多,但别担心,我们会逐一解析每个字段。
Graph
torch.fx.Graph 是一个计算图,内部保存着一系列节点。你可以打印它来查看完整结构:
graph():
%x : [num_users=1] = placeholder[target=x]
%linear : [num_users=1] = call_module[target=linear](args = (%x,), kwargs = {})
%relu : [num_users=1] = call_function[target=torch.relu](args = (%linear,), kwargs = {})
return relu
这是 FX 图的文本表示形式。下一节会有更多介绍。
Node
节点是表示输入、函数调用或输出的基本单元。每个节点主要有 6 个你会用到的字段。
node.op
node.op 告诉你节点的大类。有且仅有六个可能的值:
placeholder- 图输入get_attr- 例如读取参数call_function- 调用 torch 运算或 Python 函数call_method- 对张量调用方法(如.view())call_module- 调用子模块(如self.linear)output- 图输出
node.name
node.name 是打印图时看到的以 % 为前缀的临时变量名。例如:
%add = call_function[target=operator.add](args = (%x, %y), kwargs = {})
# node.name == "add"
node.args
node.args 是一个元组,包含该节点依赖的节点。这就是你追踪数据流的方式:
%add = call_function[target=operator.add](args = (%x, %y), kwargs = {}) # add 依赖于 x 和 y
%relu = call_function[target=torch.relu](args = (%add,), kwargs = {}) # relu 依赖于 add
node.kwargs
与 args 类似,但用于关键字参数。
node.users
node.users 是该节点被其他节点使用的次数。
# 输入 x 被 2 个节点使用
%x : [num_users=2] = placeholder[target=x]
# ...
%add = call_function[target=operator.add](args = (%x, %y))
%neg = call_function[target=torch.neg](args = (%x,))
# node.users[x] 会映射到 {add: 1, neg: 1}
这对图变换至关重要。删除节点前,必须检查 len(node.users) == 0,或者先将它的用户重定向到另一个节点。这也是检测死代码的方式。
node.target
这是最重要的字段——也是最容易混淆的,因为它的含义取决于 node.op。你不能孤立地读取 target。比如 target="linear"。如果 op 是 call_module,意思是“调用 self.linear”。如果 op 是 get_attr,意思是“读取 self.linear”(这会出错,因为 linear 是模块,不是张量)。所以一定要先检查 op 再检查 target。
node.meta
node.meta 保存节点的元数据信息。
FX 图是如何构建的
到目前为止,我们看到 torch.fx 神奇地将 Python 代码变成了这个图,但它是怎么做到的?它通过一种叫符号追踪的概念来实现。
符号追踪
首先,为什么我们需要符号追踪?当普通的 Python 代码运行时,它立即执行代码,执行完后你得到了结果,但不知道你是如何得到这个结果的——基本上就是计算图。因此我们转向符号追踪。
通过符号追踪,torch.fx 拦截这个调用,说“嘿,我看到了一个 relu 操作”,然后创建一个 Node。没有实际计算发生。它只模拟计算。这种拦截是通过 Proxy 对象完成的。
Proxy 包装每个输入值,并重写所有可能在前向传播中使用的 Python 特殊方法:
__add__、__mul__、__sub__用于算术运算__getattr__用于属性访问,如.shape、.view()__getitem__用于索引,如x[0]__torch_function__用于 torch 运算,如torch.relu(x)__call__用于子模块调用,如self.linear(x)
每当这些钩子触发时,Proxy 就会创建一个新的 Node 并记录操作。Node 存储操作码、目标函数和输入(args/kwargs)。当 forward 返回时,我们就得到了一个完整的图。
import torch
import torch.fx
class MyModel(torch.nn.Module):
def forward(self, x):
z = x + 1 # Proxy.__add__ 触发 → 创建 Node(op=call_function, target=operator.add)
return torch.relu(z) # __torch_function__ 触发 → 创建 Node(op=call_function, target=torch.relu)
这太棒了!但这种 Proxy 方法也有一些局限性。
符号追踪的局限性
1. 动态控制流——如果条件依赖于运行时变化(基于变量、数据或用户输入)的程序执行路径,则无法追踪,因为 Proxy 没有实际值来评估条件:
def forward(self, x):
if x.sum() > 0: # proxy 无法评估这个!
return torch.relu(x)
else:
return torch.neg(x)
2. 非 torch 的 Python 函数——普通的 Python 函数不会被拦截:
from math import sqrt
def normalize(x):
return x / sqrt(len(x))
traced = torch.fx.symbolic_trace(normalize) # 失败!
3. 静态控制流没有问题——如果条件在初始化时已知,追踪可以完美工作:
class MyModule(torch.nn.Module):
def __init__(self):
super().__init__()
self.do_activation = False
self.linear = torch.nn.Linear(512, 512)
def forward(self, x):
x = self.linear(x)
if self.do_activation: # 静态的——不依赖数据
x = torch.relu(x)
return x
# 这个追踪正确
总结
理解 FX 图是理解整个 PyTorch 2.0 编译栈的第一步。所以,恭喜你读到了这里。在下一篇文章中,我们将讨论 TorchDynamo,一种字节码级别的 JIT,以及它如何处理这些不同之处。
相似文章
@0xkozue: https://x.com/0xkozue/status/2072607035624247732
一份简洁的一页速查表,涵盖PyTorch张量、模型、矩阵乘法以及用于深度学习训练流程的自动求导。
@JaydevTonde: https://x.com/JaydevTonde/status/2068361821002846418
有关在LLM推理服务器Tokn中实现CUDA Graphs的详细教程,涵盖FastAPI服务器设置、引擎初始化以及用于优化解码阶段的CUDA Graph捕获。
@jino_rohit: 理解 torch.compile 堆栈 torch.compile 是一种加速 PyTorch 代码的技术。torch.compile 通过…
本文解释了 PyTorch 中的 torch.compile 堆栈,详述了从 API 到 Dynamo、FX 图、ATen 操作以及用于 JIT 编译的 Torch Inductor 的各个步骤。
@gordic_aleksa: 新深度博文时刻:Inside the Transformer: The Life of a Token 对现代密集Transformer的深入探讨,我…
一篇深入探讨现代密集Transformer内部工作原理的博文,涵盖YaRN(位置信息)、混合注意力(实现160k上下文长度)、soft capping、QK归一化,以及Transformer数学(包括FLOPs/Token公式和集群规模估算)。
PyTorch 生态圈
PyTorch 生态系统的概述,涵盖关键工具、库和社区资源。