PyTorch TorchDynamo 源码深潜:从 CPython 字节码到 FX 图的编译器前端完整剖析
【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch
TorchDynamo(下文简称 Dynamo)是torch.compile的跟踪器(tracer),负责在执行时理解任意的 Python 程序、记录其中的 PyTorch 算子调用并产出 FX 图,是torch.compile中绝大多数疑难 backtrace 的来源。本篇基于 Dynamo Deep-Dive 原文,结合本仓库torch/_dynamo/与torch/csrc/dynamo/的真实源码,从零剖析 Dynamo 的内部设计:它如何借助 CPython 的 frame evaluation API 接管函数执行、如何在 Python 层重写一套字节码解释器、如何用VariableTracker建立中间表示、如何通过 guards 与 symbolic shapes 实现"按需重编译",以及如何通过 graph break 把无法跟踪的代码"切"成多张图。读完本文,你将具备定位torch.compile报错、解读TORCH_LOGS日志并理解重编译原因的能力。
温和的 Dynamo 入门
Dynamo 是一个跟踪器:给定一个函数和它的输入,它会真正执行这个函数,并把执行过程中发生的一串无控制流的线性指令记录成一张图。例如下面这段程序:
import torch @torch.compile def mse(x, y): z = (x - y) ** 2 return z.sum() x = torch.randn(200) y = torch.randn(200) mse(x, y)把它保存为example.py并运行:
TORCH_LOGS=graph_code python example.py可以看到 Dynamo 跟踪出的输出:
def forward(l_x_: torch.Tensor, l_y_: torch.Tensor): # File: example.py:5, code: z = (x - y) ** 2 sub = l_x_ - l_y_ z = sub ** 2 # File: example.py:6, code: return z.sum() sum_1 = z.sum() return (sum_1,)这被称为"函数在给定输入下的图(graph / trace)",它以FX graph的形式表示——可以把 FX 图简单理解成一个存储了一系列函数调用的容器。注意图中是一条线性的 PyTorch 算子序列:Dynamo 把z = (x - y) ** 2拆成了sub与z两步操作。
"线性"意味着图中没有任何分支或控制流。看这个例子:
import torch @torch.compile def fn(x, n): y = x ** 2 if n >= 0: return (n + 1) * y else: return y / n x = torch.randn(200) fn(x, 2)同样用TORCH_LOGS=graph_code执行,得到:
def forward(l_x_: torch.Tensor): # File: example.py:5, code: y = x ** 2 y = l_x_ ** 2 # File: example.py:7, code: return (n + 1) * y mul = 3 * y return (mul,)Dynamo 把if语句整个从 trace 中删掉了,只记录输入实际走的那条路径。由此可以得出两个重要结论:
- 函数的 trace 依赖输入。图不是在写
@torch.compile时生成的,而是在用真实参数调用fn(x, 2)时才生成。 - 非张量值会被当作常量特化。第二个参数
n没有出现在图中,Dynamo 把它当作常量,并把n + 1的结果(即3)直接记录进图。
Dynamo 会把任何非张量值视为常量……唯独整数是例外。整数与形状的"动态跟踪"能力(symbolic shapes)正是 Dynamo 避免无谓重编译、支持生产环境任意尺寸模型的关键:典型场景包括训练时 batch size 固定、推理时 batch size 任意,以及文本/音频处理中可变的序列长度。
把上面的例子多调用几次:
import torch @torch.compile def fn(x, n): y = x ** 2 if n >= 0: return (n + 1) * y else: return y / n x = torch.randn(200) fn(x, 2) fn(x, 3) fn(x, -2)TORCH_LOGS=graph_code会额外生成两张图:
# Graph for n==2 omitted def forward(self, l_x_: torch.Tensor, l_n_: torch.SymInt): # File: a.py:5, code: y = x ** 2 y = l_x_ ** 2 # File: a.py:7, code: return (n + 1) * y add = l_n_ + 1 mul = add * y return (mul,)def forward(self, l_x_: torch.Tensor, l_n_: torch.SymInt): # File: a.py:5, code: y = x ** 2 y = l_x_ ** 2 # File: a.py:9, code: return y / n truediv = y / l_n_ return (truediv,)Dynamo 检测到第二次调用时整数n的值变了,于是开始以符号形式跟踪它,图中出现了torch.SymInt类型的参数。这两张图是通用的:如果之后再调用fn(x, 4),Dynamo 不会重新编译,而是直接复用已经跟踪好的图。
把入门内容总结成四点:
- Dynamo 是一个 Python 跟踪器;
- 给定输入,它返回一张记录已执行 PyTorch 函数的 FX 图;
- 当检测到整数在多次调用间发生变化时,它可以符号化地跟踪整数;
- 任何既非张量也非标量的值都会被特化(specialize)。
当然 Dynamo 还做了更多事情:判断何时需要重新 trace、重写函数字节码、实现 graph break 等等,下文将逐一展开。需要说明的是,Dynamo 生成的不只一张图,而是多张图——这一点在 Graph Break 一节 中会详细解释。
PEP 523:CPython 的 frame 求值 API
设想由我们来实现 Dynamo,该从哪里入手?幸运的是,Python 3.6 引入的PEP 523为第三方实现 Python 的 JIT 编译器而设计,正好提供了切入点。
关于 CPython 的背景:CPython 内部是一个栈式虚拟机。Python 程序先被编译成字节码(bytecode),再由解释器逐条执行(标准库dis模块可以查看这些字节码)。PEP 523 暴露了一个 API,允许用户为函数安装自定义的解释器;之后 CPython 在执行该函数时会调用这个自定义解释器而不是自己的解释器。为了让自定义解释器能够执行函数,CPython 在进入函数时会提供:
- 函数的字节码;
- 函数参数(即局部变量)的值与名字;
- 全局变量的值与名字;
- 内建函数(如
abs、print)。
所有这些绑定代码都位于本仓库的 torch/csrc/dynamo/eval_frame.c 中——这是 Dynamo 中唯一与 CPython 直接打交道的 C 代码。CPython 提供了执行函数所需的全部信息(在 CPython 的术语里,这些对象的集合被称为一个frame)。
有了这个 API,实现跟踪器的方法就清晰了:实现一个解释器,运行代码并在图中记录执行过程中出现的所有 PyTorch 算子——这正是 Dynamo 所做的。Dynamo 用该 CPython API 解析这些对象并打包成 Python 结构,然后立刻从 C 层回到 Python 层。除了这一小段与 CPython 通信的代码外,Dynamo 几乎完全用 Python 实现。
顺带澄清:@torch.compile装饰器的职责是安装必要的脚手架,把字节码、参数、全局变量等在函数被调用时传递给 Dynamo——@torch.compile本身并不编译任何东西。
在 Python 中重写 CPython
回到 Python 世界。此时我们拿到了函数的字节码和全部执行上下文,落脚点在 torch/_dynamo/convert_frame.py 中的_convert_frame_assert(当前仓库中由ConvertFrameAssert类与convert_frame_assert工厂函数实现)。它正是torch.compile装饰器返回的函数;而torch.compile只是_dynamo.optimize之上的一层友好 API(见 torch/_dynamo/eval_frame.py)。
在实现 Python 解释器之前,先要定义一套中间表示(IR):把局部变量与全局变量都包装进 Dynamo 自己的内部类中。这样既能更好地跟踪对象,又能把"在 Dynamo 眼中可同等对待"的对象归为一组。
内部类体系的父类是VariableTracker,它表示 Dynamo 能够理解的各种 Python 对象:
ListVariable表示list对象,内部维护一个VariableTracker列表(见 torch/_dynamo/variables/lists.py);ConstantVariable包装所有被 Dynamo 视为常量的对象(见 torch/_dynamo/variables/constant.py);- 需要特殊对待的对象有专门子类,比如张量对应的
TensorVariable(见 torch/_dynamo/variables/tensor.py)。
所有这些内部类都定义在 torch/_dynamo/variables 目录下。Python 对象在 torch/_dynamo/variables/builder.py 的VariableBuilder._wrap中被包装成对应的VariableTracker——这是一个超长的elif链,递归地把 Python 输入模式匹配成合适的VariableTracker类型。
调试技巧:当 Dynamo 给出出乎意料的结果时,常常是 builder 的逻辑出了问题——builder 逻辑错误时,Dynamo 可能把变量包装成错误的VariableTracker类型,为后续埋下隐患。遇到 Dynamo 报错时,值得留意错误信息中出现的VariableTracker类型,以及抛出异常的VariableTracker方法。特别是当我们发现某个对象被跟踪成了UserDefinedObjectVariable(Dynamo 的"兜底"类),而它本应被跟踪成更具体的类型时,问题往往出在VariableBuilder的逻辑上。
调试技巧:用TORCH_LOGS=dynamo运行程序时,会打印出如下形式的行:
TRACE LOAD_GLOBAL y [TorchInGraphFunctionVariable(<built-in method any>), TensorVariable()]这是原始程序的字节码及该时刻的栈状态,对定位"某个对象没有跟踪成正确的VariableTracker"非常有帮助。
有了 IR,剩下的就是"重新实现 CPython 的栈式虚拟机"。这由 torch/_dynamo/symbolic_convert.py 中的InstructionTranslatorBase(当前仓库第 1465 行附近)完成。该类约有 200 个方法,几乎实现了全部 Python 字节码。以BUILD_LIST为例:
def BUILD_LIST(self, inst): items = self.popn(inst.argval) self.push(ListVariable(items, mutation_type=ValueMutationNew()))BUILD_LIST是l = [2, 3, 4]这类构造生成的字节码:三个元素对应BUILD_LIST 3,即从栈顶弹出 3 个元素,把由它们构成的 list 压回栈顶。该实现位于当前仓库 torch/_dynamo/symbolic_convert.py 第 4075 行附近。
生成输出图
有了符号化执行 Python 代码的能力,就可以在给定输入的程序执行过程中提取 PyTorch 算子。这由 Dynamo 的OutputGraph对象实现(见 torch/_dynamo/output_graph.py,当前仓库第 690 行开始)。OutputGraph与InstructionTranslator对象绑定,跟踪生成最终 FX 图所需的全部数据。
FX 图中的所有输入与中间元素都是fx.Node。在 Dynamo 中,fx.Node被包装成fx.Proxy:fx.Proxy负责构建 FX 图,会把作用在其上的每一次 PyTorch 操作记录进图。调用OutputGraph.create_proxy(torch/_dynamo/output_graph.py)可以创建要加入图的新操作,再通过 torch/_dynamo/variables/builder.py 中的wrap_fx_proxy把它真正加进图。
图既存储张量上的算子,也存储符号整数上的算子。下面先讨论 Dynamo 如何解决一个相当重要的正确性问题,再回到符号整数。
让 Dynamo 保持正确:Guards
至此我们有了一个完全无视控制流的跟踪器,为此还重写了整个 CPython……这听起来有点小题大做,实际上确实如此——torch.jit.trace 不需要这么多机制也能做类似的事。问题在于:正如其文档所警告的,torch.jit.trace只在被跟踪程序不依赖数据(即程序本身是线性的)时才正确,这意味着不能写 if-else、for/while 循环和异常,连第三方库也不能使用任何控制流。在 Python 这种动态语言中不用控制流,是极其苛刻的限制。
JAX 的解法是每次重新 trace 并缓存图;Dynamo 则用guards(守卫)来避免每次都重 trace 整个程序。
guard 是一条假设(关于输入的布尔表达式),其作用是让一个 frame 针对一组示例输入做特化。只有这些假设对新输入仍然成立时,复用已跟踪的图才是合法的。
例如,任何常量输入(比如字符串)都会安装一条 guard:要求该输入的类型是str且值等于传入的字符串。运行:
import torch @torch.compile def fn(a, b): return a * len(b) fn(torch.arange(10), "Hello")用TORCH_LOGS=guards打印(其中一部分 guard):
___check_type_id(L['b'], 94334122025024) L['b'] == 'Hello'含义是"局部变量b的类型应是指定类型(这里为str,用常量9433...表示),且其值应为'Hello'"。如果换一个参数再调用:
import torch @torch.compile def fn(a, b): return a * len(b) fn(torch.arange(10), "Hello") fn(torch.arange(10), "Hi")用TORCH_LOGS=recompiles可以看到是哪条 guard 失败了:
Recompiling function fn in script.py:3 triggered by the following guard failure(s): - L['b'] == 'Hello'Guards 在两类时机被累积:一是函数输入在 builder 中被包装时(见 torch/_dynamo/variables/builder.py),二是程序执行过程中(例如 torch/_dynamo/variables/dicts.py 中的字典操作)。
Source:guard 如何"指回"原始对象
source跟踪的是:如何从进入当前 frame 时的原始局部/全局变量(以及它们包含的对象)重构出某个变量。例如:
def foo(x: Tensor, y: List[Tensor]): a = x * y[0] return a * x其中x、y的 source 是LocalSource,y[0]的 source 是GetItemSource(内部存了一个LocalSource);而a没有 source,因为它只是 FX 图内部的中间变量。所有这些类都定义在 torch/_dynamo/source.py 中。
GetItemSource生成的 guard 可以从下面这个例子看到:
import torch @torch.compile def fn(x, l): return x * len(l[0]) fn(torch.randn(8), ["Hi", "Hello"])生成的 guards:
___check_type_id(L['l'], 94439025877664) len(L['l']) == 2 ___check_type_id(L['l'][0], 94439025840192) L['l'][0] == 'Hi' ___check_type_id(L['l'][1], 94439025840192) L['l'][1] == 'Hello'这里能看到GetItemSource([0]、[1])包裹LocalSource(L['l'])生成的代码。
有了 sources 和 guards,就能实现一个避免每次重 trace 的缓存系统。但细心的读者会发现:上面的 guard 都只依赖输入对象本身,完全可以在执行函数之前预先计算——也就是说,这套 guard 系统完全可以搭建在torch.jit.trace之上,用少得多的功夫达到同样效果。为什么非要重新实现 Python 解释器?答案在symbolic shapes。
符号形状(Symbolic Shapes)
为了实现整数的符号化跟踪,Dynamo 使用符号类torch.SymInt(见 torch/init.py):它行为像int,但会把作用于其上的所有操作记录进输出 FX 图。与之类似的还有SymBool与SymFloat类(后者目前使用不多)。下面讨论符号形状跟踪的三个核心属性及其实现。
默认静态(Static by default)
Dynamo 默认假设每个整数(无论是输入还是张量形状)都是静态的:函数第一次执行时不会跟踪任何整数;只有当它检测到某个整数或形状在后续执行中改变了值,才会对它做符号化跟踪,并生成对该变量通用的图。
前面整数例子已展示过该行为,这里看张量形状的例子:
import torch @torch.compile def fn(a, b): return a.shape[0] * a * b fn(torch.randn(4, 3), torch.randn(4, 3)) fn(torch.randn(8, 3), torch.randn(8, 3))用TORCH_LOGS=graph_code运行,两次调用分别被跟踪为:
def forward(self, l_a_: torch.Tensor, l_b_: torch.Tensor): mul = 4 * l_a_ mul_1 = mul * l_b_ return (mul_1,) def forward(self, s0: torch.SymInt, l_a_: torch.Tensor, l_b_: torch.Tensor): size = l_a_.size() getitem = size[0] mul = getitem * l_a_ mul_1 = mul * l_b_ return (mul_1,)第一张图里形状被当作常量4;形状一旦变化,第二张图就用SymInt(s0)符号化地跟踪它。更直观地查看中间值的形状,可以运行TORCH_LOGS=graph_sizes:
TRACED GRAPH TENSOR SIZES ===== __compiled_fn_1 ===== l_a_: (s0, 3) l_a_ (concrete): (8, 3) l_b_: (s0, 3) l_b_ (concrete): (8, 3) mul: (s0, 3) mul (concrete): (8, 3) mul_1: (s0, 3) mul_1 (concrete): (8, 3)可以看到两个张量参数的第一维由s0表示,是动态的。
Dynamo 是如何实现这一点的?用TORCH_LOGS=guards查看:
# Guards first call check_tensor(L['a'], torch.float32, device=None, requires_grad=False, size=[4, 3], stride=[3, 1]) check_tensor(L['b'], torch.float32, device=None, requires_grad=False, size=[4, 3], stride=[3, 1]) # Guards second call check_tensor(L['a'], torch.float32, device=None, requires_grad=False, size=[None, 3], stride=[3, 1]) check_tensor(L['b'], torch.float32, device=None, requires_grad=False, size=[None, 3], stride=[3, 1]) L['b'].size()[0] == L['a'].size()[0] 2 <= L['a'].size()[0]第一次调用时,guards 检查张量具有固定的 size 与 stride;这些 guard 在第二次执行时失败,于是 Dynamo 重新 trace。由于失败的是int类 guard,第二次迭代就把该整数符号化跟踪,并为这个更通用的 kernel 安装更一般的 guards。
编译性能建议:如果你预先知道某个维度的大小会变化,可以在调用torch.compile之前先调用 torch._dynamo.decorators.mark_dynamic 把它标记为动态,这样能省掉第一次静态形状的编译。类似的工具还有maybe_mark_dynamic、mark_static。也可以用torch.compile(dynamic=True)让所有整数和形状都被跟踪(主要适用于调试)。
0 与 1 永远被特化
无论是否把某个维度标记为动态,只要传入的输入中该维度是 0 或 1,Dynamo 都会把它当作非动态来跟踪,并生成专属的图。这就是上面例子中出现2 <= L['a'].size()[0]这类 guard 的原因。该策略主要有两个考量:
- 张量是空的,当且仅当它的某个维度为 0;
- 张量只有在其某个 stride 为 1 时才可能是连续的。
这一策略不适用于普通 Python int:如果我们认为某个 Python int 应该被动态编译,默认不会特化它,而是根据其具体使用方式决定是否特化。
Duck shaping(鸭子塑形)
Dynamo 会执行所谓的 "duck shaping":如果两个动态整数在 trace 时值相同,就假设它们相等并据此安装 guard。效果就是上例中不会出现s0、s1两个符号,而是统一成s0,并带一条 guardL['b'].size()[0] == L['a'].size()[0]。这样既能在编译器中做融合,又能生成足够通用的 kernel。
符号整数上的 guards
理解了符号形状的高层实现与属性,再回到那个问题:为什么符号形状逼迫我们去接管 CPython 解释器?看这个例子:
import torch @torch.compile(dynamic=True) def fn(a): if a.shape[0] * 2 < 16: return a else: return a + 1 fn(torch.randn(8))这段代码有一条形如2*L['a'].size()[0] >= 16的 guard。它是关于函数输入的非平凡表达式,却是在程序执行的中途注册的——我们必须看到那个以SymNodeVariable参数为条件的if语句,才知道需要这条 guard。这类条件对torch.jit.trace是不可见的,必须深度分析 Python 代码才能发现。
调试技巧:用TORCH_LOGS=dynamo运行这段代码,会告诉我们这条 guard 是在哪里添加的:
eval 2*s0 >= 16 [guard added] at script.py:5 in fn (_dynamo/variables/tensor.py:812 in evaluate_expr)在那里下一个断点、查看 backtrace,对理解 guard 的来源非常有用。
让 Dynamo 完备:Graph Breaks
到这里,我们有了一个能跟踪张量与整数上的 PyTorch 算子、并带缓存系统的跟踪器——而且能执行任意的 Python 代码。但"执行任意 Python 代码"这个说法可能太夸张了:Dynamo 实现了 Python 的很大一部分,但它实现协程或 async 这类复杂特性吗?它理解整个 Python 标准库吗?NumPy 也有 Python API,torch.compile理解 NumPy 吗?那 Django 呢?
Python 生态极其庞大,其中很大一部分是用 C++、Rust 等更高效的语言实现、只暴露 Python 绑定。对于用 C++ 实现的 Python 对象,Dynamo 根本无法穿透。那么,当跟踪器遇到它不理解的算子时该怎么办?
常见的做法是告知用户它卡在了哪个算子上,然后彻底放弃跟踪。但这对 PyTorch 是严重的可用性问题——用户早已习惯了 PyTorch 的灵活性(一个真实例子是doctr_det_predictor模型在推理后处理中使用 NumPy 和cv2库)。
这正是"能接触到 CPython"的另一大价值所在:与其报错,Dynamo 可以让 CPython 去执行那段问题代码!做法是:在 trace 时,为问题代码之前的所有算子生成一张图,为之后的所有算子生成另一张图(如果问题代码有多处,Dynamo 可以按需把代码切成尽可能多的图);运行时先执行第一张图,然后让 CPython 执行问题代码,最后执行第二张图。这种停止跟踪并生成多张图的过程称为 graph break。
坦白说,前面各节为了叙述方便撒了一个小谎:Dynamo 生成的不是一张图,而是多张图!在 graph break 之后重新开始跟踪,实际上可以理解为开始跟踪一个新函数:新图有自己的 guards、自己的一组局部变量,等等。
要理解 graph break 的实现,需要重新审视 Dynamo 与 CPython 的交互。PEP 523 允许用户使用自己的 frame 求值机制,但 CPython 同时也暴露了它自己的 frame 求值接口供他人调用。Dynamo 正是利用这一点,让快速的 CPython 解释器去运行编译后的代码。对于一个没有 graph break 的函数,两次以相同参数调用它的完整过程如下:
- 第一次调用:
- Dynamo 把函数跟踪成 FX 图;
- FX 图随后被编译器(Inductor)编译成高效的低层代码——那是另一篇文章的故事;
- Dynamo 重写函数字节码,使其直接调用编译后的函数;
- Dynamo 把新字节码交给 CPython 执行(见 torch/csrc/dynamo/eval_frame.c)。
- Dynamo 把函数跟踪成 FX 图;
- 第二次调用:
- 用第一次调用产生的 guards 检查新参数,参数相同则通过;
- 让 CPython 执行与这些 guards 关联的字节码。
这个流程看起来过于复杂:为什么不直接创建一个指向编译后函数的 C++ 绑定然后执行它?恰恰是这个"生成新字节码再交给 CPython 执行"的模式,让我们得以实现 graph break!graph break 生成的字节码具有如下结构:
- 执行第一张图的字节码;
- 把栈恢复到"CPython 执行完第一张图后"应有的样子,并重放此刻可见的局部/全局变量修改;
- 导致 graph break 的那段字节码;
- 执行第二张图的字节码。
用一个简单例子验证:
import torch @torch.compile def fn(a): b = a + 2 print("Hi") return b + a fn(torch.randn(4))用TORCH_LOGS=bytecode运行,可以看到初始字节码与修改后的字节码:
MODIFIED BYTECODE fn script.py line 3 0 LOAD_GLOBAL 1 (__compiled_fn_0) 2 LOAD_FAST 0 (a) 4 CALL_FUNCTION 1 6 STORE_FAST 3 (graph_out_0) 8 LOAD_GLOBAL 0 (print) 10 LOAD_CONST 2 ('Hi') 12 LOAD_FAST 3 (graph_out_0) 14 LOAD_CONST 3 (0) 16 BINARY_SUBSCR 18 STORE_FAST 1 (b) 20 CALL_FUNCTION 1 22 LOAD_GLOBAL 2 (__resume_at_14_1) 24 ROT_TWO 26 LOAD_FAST 0 (a) 28 LOAD_FAST 1 (b) 30 CALL_FUNCTION 3 32 RETURN_VALUE MODIFIED BYTECODE resume_in_fn script.py line 6 0 LOAD_GLOBAL 1 (__compiled_fn_2) 2 LOAD_FAST 2 (b) 4 LOAD_FAST 1 (a) 6 CALL_FUNCTION 2 8 UNPACK_SEQUENCE 1 10 RETURN_VALUE修改后的字节码被拆成两个函数:原函数fn,以及 Dynamo 创建的resume_in_fn。后者是 Dynamo 为实现"从 graph break 处继续执行程序"而生成的函数,通常被称为续延函数(continuation),其作用就是用正确的参数调用第二张编译图。原函数的字节码被重写为前文描述的策略:
- L0-4:调用编译函数(
a + 2); - L6:把结果存到局部变量
graph_out_0(它是一个 tuple); - L8-18:把栈恢复到 graph break 发生时应有的状态;
- L20:执行导致 graph break 的代码(
print("Hi")); - L22-32:调用编译好的续延函数(
a + b)。
字节码的栈生成逻辑委托给了各个VariableTracker子类:每个VariableTracker对象都有一个reconstruct方法(见 torch/_dynamo/variables/lists.py),生成重建它表示的 Python 对象所需的字节码。
调试技巧:graph break 会损害性能,应当尽量避免。用TORCH_LOGS=graph_breaks运行程序,可以查看程序命中了多少次 graph break。其输出以VariableTracker对象的形式给出,所以上文关于VariableTracker的调试技巧有时也能帮你找出 graph break 的根源。
结论
Dynamo 是一块复杂的软件——一旦决定实现一个 CPython 解释器,你就知道这是一场硬仗。希望本文能帮你揭开它的神秘面纱。
Dynamo(大部分)用 Python 实现。文中给出了大量源码链接,阅读这些代码、grep 调用它们的位置、或者在上面打断点查看调用栈,都有助于理解其余部分。当然,学习软件的最好方式是扩展它——对 Dynamo 而言,可以关注仓库中torch/_dynamo/下标记了 dynamo 相关的 issue,很多只需要对代码做很小的改动。
附录:Dynamo 关键源码索引
- 与 CPython 交互的 C 绑定:torch/csrc/dynamo/eval_frame.c
torch.compile的入口与_dynamo.optimize:torch/_dynamo/eval_frame.py- frame 转换入口
_convert_frame_assert/ConvertFrameAssert:torch/_dynamo/convert_frame.py - 符号化解释器
InstructionTranslatorBase(约 200 个字节码方法):torch/_dynamo/symbolic_convert.py - 输出图
OutputGraph与create_proxy:torch/_dynamo/output_graph.py - IR 基类与各
VariableTracker子类:torch/_dynamo/variables(其中 builder.py 负责_wrap包装与wrap_fx_proxy,lists.py 有ListVariable与reconstruct,constant.py 定义常量,tensor.py 有TensorVariable与evaluate_expr,dicts.py 在字典操作中累积 guard) - sources(
LocalSource、GetItemSource等):torch/_dynamo/source.py - 符号整数
SymInt:torch/init.py - 动态形状标记
mark_dynamic等:torch/_dynamo/decorators.py TORCH_LOGS调试开关速查:graph_code(查看跟踪出的图)、graph_sizes(查看图中张量形状)、guards(查看安装的 guard)、recompiles(查看重编译原因)、bytecode(查看修改前后的字节码)、graph_breaks(查看 graph break 位置)、dynamo(查看字节码与栈状态的逐步 trace)。
【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考