TorchDynamo初探:Python ByteCode的动态修改

2023-02-01 0 912

TorchDynamo初探:Python ByteCode的动态修改

译者|strint

1

大背景

广度自学架构校对强化时,须要先依照排序方法论逐步形成两个方法论排序图,接着再重写排序图,最终继续执行重写后的排序图。当中聚合方法论排序图形式有三种。

一类排序图聚合是如前所述 trace tensor 的,追踪 tensor 的继续执行方向。tensor 继续执行时,如前所述抒发式空载,能落在全力支持 tensor 排序的架构自订抒发式,该抒发式通常是 c++ 层的。c++ 层的自订抒发式中,机能是用作聚合两个 Operation 的记号抒发。比如说两个对乘法演算,trace 是历史记录两个概念化的乘法微分。这般连串的演算就被切换了概念化的排序图。

除此之外一类排序图聚合是如前所述 AST(抽象化句法树) 导出的。在代码继续执行前,间接依照 Python 文档标识符获得 Python AST,接着依照 AST 来译成排序图(也叫作KParts IR)。

Python(专指 CPython)正则抒发式继续执行,第二阶段林美珠把 Python 源标识符导出成 AST,第二阶段依照 AST 聚合和强化 ByteCode(二进制码),第二阶段在软件包中继续执行 ByteCode。

如前所述 AST 导出的排序图聚合,发生在这里的第二阶段;如前所述 trace tensor 的排序图聚合,发生在第二阶段之后。

TorchDynamo 特别的地方在于其工作在第二阶段,静态修正 Python ByteCode,这样第二阶段继续执行的已经是修正后的 ByteCode了。

2

TorchDynamo 概述

TorchDynamo 是 PyTorch 新实验的 JIT 校对接口,全力支持使用 Python 在运行时修正静态继续执行方法论,修正的时机是 CPython 的 ByteCode 继续执行前。这个思想类似 DynamoRIO(https://dynamorio.org) 项目,DynamoRIO 能静态的修正 x86 机器码。

CPython 的每次抒发式调用会聚合两个 Frame(或者叫 Stack),Frame 中带有的标识符部分是 ByteCode。CPython 运行时全力支持如前所述现有的 Frame 去设置两个自订的 Frame,接着后面继续执行的是自订的 Frame。

TorchDynamo 的工作原理是在运行时设置两个自订的 Frame,该 Frame 中的 ByteCode 全力支持 CallBack 到 Python 层去修正。其提供的典型的修正接口是 FX Graph,也是说 TorchDynamo 会分析 ByteCode,聚合对应的 FX Graph,接着提供 FX Graph 的接口供用户自订排序图。这种做法有如下优点:

能全力支持所有的 Python 句法,因为如果在自订 Frame 过程中的任何一点发现不全力支持,都能选择不修正 Frame 而回退到原 Frame;

开销少,劫持发生在 Python 继续执行比较早的阶段(ByteCode 聚合和强化阶段),而非 Python ByteCode 继续执行后的阶段,有时能减少 Python ByteCode 的继续执行开销(猜测如果很多次 ByteCode 层面的抒发式调用被融合层成一次抒发式调用,的确能缩减开销);

能做到不增加校对带来的延迟(之前的如前所述 tensor trace 或者 ast 导出的做法,通常都有先校对继续执行所以校对开销无法掩盖,但是重写 ByteCode 这个做法,猜测是能在识别出热点标识符后,单独开两个线程去做校对,而不影响主线程工作。Python ByteCode 重写的 API 中有这种延迟校对的样例,peps.python.org/pep-052 )。

之前排序图聚合机制(如前所述 trace tensor、如前所述 AST 导出的)中的几个问题,获得了缓解:

存在无法静态化的操作,之前通常须要显式的移除静态化作用域,现在总是允许不做校对,间接继续执行原 Python 标识符,这样使得静态化标注变得简单;

打开静态图校对强化,之前校对时通常无法掩盖,现在有办法部分掩盖;

静态 shape 问题,因为有了校对时和运行时的掩盖,也能获得缓解。

这种尽量强化、静态强化的设计,最大程度了照顾了标识符开发的体验,让校对强化上手变得更简单了。这是 TorchDynamo 带来的最主要的好处。这种做法非常符合 PyTorch 的 Python First、Eager First、User Experience First的偏好。但是这个设计对寻求最好的性能、最方便的静态化部署这两个目标并没有改善。

3

CPython 的标准继续执行流程

上文提到了 CPython 的继续执行从 Python 文档代码,到 AST,到 ByteCode。这里用两个示例展开看一下。Python 的标准组件非常易用,能在 Python 层用 ast 组件来查看 AST,能用 compile 内置抒发式来校对 ByteCode,能用 exec 系统抒发式来继续执行 ByteCode。我们先在标识符开头导入相关组件:

import astimport disimport sys

接着我们构造两个 python 标识符,能看到 src_code 是普通的字符串。当中包含了一段普通的 python 内置的乘法,一段广度自学的 tensor scalar 乘法,最终一段是当前Python Frame 中的 ByteCode 关联对象的打印(用作两个检验,后面会提到)。

print(“=== source code ===”)src_code = “””# normal python operationx = 1x = x * 2# tensor operationy = dl_framework.ones((1, 2))z = x + yprint(z)# print python framef = sys._getframe()# print the code objectprint(f.f_code)”””print(src_code)

接着使用 ast 组件来聚合这段标识符的 AST。

print(“=== source code to ast ===”)# 把源标识符导出成 ASTast_obj = ast.parse(src_code)# 打印 ASTprint(ast.dump(ast_obj))

能获得 AST,这里展示的结果额外做了格式化,除此之外删减掉了和排序方法论无关的打印 frame 的部分,标识符和其 AST 的对应关系参见注释。AST导出是纯文档层面的,`dl_framework` 还没有被 import 进来,AST导出仍然能正常工作。AST 基本是两个多叉树的结构,每个节点对应两个抒发式,节点子节点代表子抒发式。以 `x = x + 2` 为例,Assign 是两个节点,是赋值演算,被赋值的是 `x`,赋值的值是两个二元乘法演算。

Module(body=[ # x = 1 Assign(targets=[Name(id=x, ctx=Store())], value=Constant(value=1, kind=None), type_comment=None), # x = x * 2 Assign(targets=[Name(id=x, ctx=Store())], value=BinOp(left=Name(id=x, ctx=Load()), op=Mult(), right=Constant(value=2, kind=None)), type_comment=None), # y = dl_framework.ones((1, 2)) Assign(targets=[Name(id=y, ctx=Store())], # dl_framework.ones((1, 2)) value=Call(func=Attribute(value=Name(id=dl_framework, ctx=Load()), attr=ones, ctx=Load()), args=[Tuple(elts=[Constant(value=1, kind=None), Constant(value=2, kind=None)], ctx=Load())], keywords=[]), type_comment=None), # z = x + y Assign(targets=[Name(id=z, ctx=Store())], # x + y value=BinOp(left=Name(id=x, ctx=Load()), op=Add(), right=Name(id=y, ctx=Load())), type_comment=None), # print(z) Expr(value=Call(func=Name(id=print, ctx=Load()), args=[Name(id=z, ctx=Load())], keywords=[])), # 省略了打印 frame 的标识符],type_ignores=[])

Python AST 聚合后,能利用系统抒发式 `compile` 把它转成 ByteCode 二进制码。正则抒发式继续执行也存在校对的环节,只不过是校对成二进制码。

print(“=== ast to bytecode ===”)# 校对成 ByteCodecode_obj = compile(ast_obj, filename=””, mode=”exec”)print(code_obj)# 展示 ByteCode 的句法糖byte_obj = dis.Bytecode(code_obj)print(byte_obj.dis())

`print(code_obj)`的结果是 `<code object <module> at 0x7ff79bb5c660, file “”, line 3>`,这里能看到聚合的 code object 对象的指针是 `0x7ff79bb5c660`,后面我们在继续执行二进制码时,会再次看到这个指针。

`print(byte_obj.dis())` 的结果如下,每一行对应一条二进制码,也即一条指令, 通过字面含义基本能看出是在做什么:

# x = 1 3 0 LOAD_CONST 0 (1) 2 STORE_NAME 0 (x) # x = x * 2 4 4 LOAD_NAME 0 (x) 6 LOAD_CONST 1 (2) 8 BINARY_MULTIPLY 10 STORE_NAME 0 (x) # y = dl_framework.ones((1, 2)) 7 12 LOAD_NAME 1 (dl_framework) 14 LOAD_METHOD 2 (ones) 16 LOAD_CONST 2 ((1, 2)) 18 CALL_METHOD 1 20 STORE_NAME 3 (y) # x = x + y 8 22 LOAD_NAME 0 (x) 24 LOAD_NAME 3 (y) 26 BINARY_ADD 28 STORE_NAME 4 (z) # print(z) 9 30 LOAD_NAME 5 (print) 32 LOAD_NAME 4 (z) 34 CALL_FUNCTION 1 36 POP_TOP # 省略了打印 frame 的标识符

获得 ByteCode 之后,就能传递给 Python VM 继续执行了。在真正继续执行前,先做了一下 ByteCode 中指令的打印,实际 Python VM 继续执行时,也基本是这样遍历每一行指令,接着继续执行指令。能想象,如果这些指令被修正,就能让 Python VM 继续执行自订的指令了。

print(“=== execute bytecode ===”)# print instructionfor instr in byte_obj: print(instr.opname, instr.opcode)# You can also do `import torch as dl_framework“import oneflow as dl_framework# execute bytecodeexec(code_obj)

二进制码的继续执行结果如下。只须要在真正继续执行前,把 `dl_framework`导入就好,接着能看到 tensor 排序的结果,是符合预期的。

frame(或者叫 stack)是运行时的对象,对应两个抒发式调用的栈,在继续执行时被创建。frame 中要继续执行的指令是之前创建的 ByteCode。

在运行时之前,像我们之前看到的,存在两个校对时进行 AST 和 ByteCode 的校对,之前校对时聚合的 code object 对象的指针是 `0x7ff79bb5c660`。

通过 `frame.f_code`拿到当前 frame 里面包含的 ByteCode(即 code object),能发现它的指针是之前校对时聚合的那个。

# print(z) 的结果tensor(# print(f.f_code)<code object <module> at 0x7f5cea7f1660, file “”, line 3>

到此,窥见了一下 Python 源标识符到 AST, AST 到 ByteCode,ByteCode 到 Frame 继续执行这个默认的 Python 继续执行流程。TorchDynamo 用下图做了简单的介绍:

TorchDynamo初探:Python ByteCode的动态修改

当中 foo 对应两个 Python 抒发式,即上文介绍的 Python Source Code。PyCodeObject 是上文介绍的 code object (ByteCode)在 C 标识符层面对应的类。PyFrameObject 是上文介绍的 Frame 在 C 标识符层面对应的类,它包含了标识符段 PyCodeObject。_PyEval_EvalFrameDefault 对应上文介绍的 exec,它继续执行两个 Frame,即运行 Frame 带有的 `PyCodeObject`。

现在我们看一下 CPython 在 C 层面的继续执行 Frame 的实现,对应 _PyEval_EvalFrameDefault(https://github.com/python/cpython/blob/d48ecebad5ac78a1783e09b0d32c211d9754edf4/Python/ceval.c#L757)。它的主方法论是取 ByteCode 指令和继续执行指令(https://github.com/python/cpython/blob/d48ecebad5ac78a1783e09b0d32c211d9754edf4/Python/ceval.c#L1080):

co = f->f_code; // 从 PyFrameObject* f 中取出 PyCodeObject* ,放到 co 中 names = co->co_names; consts = co->co_consts; fastlocals = f->f_localsplus; freevars = f->f_localsplus + co->co_nlocals; // 从 co 中取出第一条指令 first_instr = (_Py_CODEUNIT *) PyBytes_AS_STRING(co->co_code); next_instr = first_instr;#define NEXTOPARG() do { \ _Py_CODEUNIT word = *next_instr; \ opcode = _Py_OPCODE(word); \ oparg = _Py_OPARG(word); \ // 指向下一条指令 next_instr++; \ } while (0) // 循环继续执行指令 for (;;) { // 从当前的指令 next_instr

每个指令类型对应两个 opcode,它是两个数值,继续执行 opcode(https://github.com/python/cpython/blob/d48ecebad5ac78a1783e09b0d32c211d9754edf4/Python/ceval.c#L1266),这里的 opcode 能清晰的看到和之前我们打印的 ByteCode 的类型对应关系:

#define TARGET(opcode) \ case opcode: switch (opcode) { // TARGET 是两个 case // load TARGET(LOAD_FAST) { PyObject *value = GETLOCAL(oparg); if (value == NULL) { format_exc_check_arg(PyExc_UnboundLocalError, UNBOUNDLOCAL_ERROR_MSG, PyTuple_GetItem(co->co_varnames, oparg)); goto error; } Py_INCREF(value); PUSH(value); FAST_DISPATCH(); } // store TARGET(STORE_FAST) { PyObject *value = POP(); SETLOCAL(oparg, value); FAST_DISPATCH(); } // 二元乘法 TARGET(BINARY_ADD) { PyObject *right = POP(); PyObject *left = TOP(); PyObject *sum; if (PyUnicode_CheckExact(left) && PyUnicode_CheckExact(right)) { sum = unicode_concatenate(left, right, f, next_instr); /* unicode_concatenate consumed the ref to left */ } else { sum = PyNumber_Add(left, right); Py_DECREF(left); } Py_DECREF(right); SET_TOP(sum); if (sum == NULL) goto error; DISPATCH(); } // 抒发式调用 TARGET(CALL_FUNCTION) { PyObject **sp, *res; PCALL(PCALL_ALL); sp = stack_pointer; res = call_function(&sp, oparg, NULL); stack_pointer = sp; PUSH(res); if (res == NULL) { goto error; } DISPATCH(); } }

以上总结了 Python的默认继续执行流程。

4

TorchDynamo 的工作流程

TorchDynamo 在标准的 Python 继续执行流程中做的主要改变是全力支持修正 Frame 继续执行前的 ByteCode。我们暂时不关注 AST 聚合,看 Python 的继续执行流程,是 Python Source Code -> ByteCode -> Evaluate. TorchDynamo 全力支持 Python Source Code -> ByteCode -> [ByteCode rewrite] -> Evaluate。

ByteCode rewrite 的工作形式是把一段 ByteCode 转成 FX Graph,接着调用用户自订的 FX Graph 重写继续执行方法论,聚合两个能经过校对的继续执行抒发式。接着把该段 ByteCode 替换成抒发式调用 ByteCode,而调用的抒发式是经过校对的继续执行抒发式。从而实现校对强化的机能。

FX Graph 全力支持了在 Python 层做标识符重写,提高了写校对 Pass 的便利性,这里不做深入,能参考资料1(

https://pytorch.org/docs/stable/fx.html)和2(https://zhuanlan.zhihu.com/p/416165157)。

ByteCode rewrite 发生在 ByteCode 继续执行前。同样的 Source Code,每次继续执行都会走到这个步骤,都能选择是否进行 ByteCode rewrite,或者选择进行什么样的 rewrite,还能全力支持 rewrite 结果的缓存和复用。这体现了 Dynamo 的静态性。

下面看两个 TorchDynamo 下 fn() 抒发式校对的的例子:

# 两个普通的抒发式def fn(a, b): x = a + b x = x / 2.0 if x.sum() < 0: return x * -1.0 return x # torchdynamo 抒发式接口with torchdynamo.optimize(custom_compiler): fn(torch.randn(10), torch.randn(10))

fn() 抒发式对应的原始的 python ByteCode,和标识符对应的关系参见当中的注释:

# x = a + b 0 LOAD_FAST 0 (a) 2 LOAD_FAST 1 (b) 4 BINARY_ADD 6 STORE_FAST 2 (x) # x = x / 2.0 8 LOAD_FAST 2 (x) 10 LOAD_CONST 1 (2.0) 12 BINARY_TRUE_DIVIDE 14 STORE_FAST 2 (x) # if x.sum() < 0: 16 LOAD_FAST 2 (x) 18 LOAD_METHOD 0 (sum) 20 CALL_METHOD 0 22 LOAD_CONST 2 (0) 24 COMPARE_OP 0 (<) 26 POP_JUMP_IF_FALSE 36 # return x * -1.0 28 LOAD_FAST 2 (x) 30 LOAD_CONST 3 (-1.0) 32 BINARY_MULTIPLY 34 RETURN_VALUE # return x 36 LOAD_FAST 2 (x) 38 RETURN_VALUE

经过 TorchDynamo 静态重写后的 ByteCode:

# x = a + b # x = x / 2.0 # x.sum() < 0 # 上面两行被切换成了 __compiled_fn_0 # __compiled_fn_0 会返回 x 和 x.sum() < 0 组成的 tuple 0 LOAD_GLOBAL 1 (__compiled_fn_0) 2 LOAD_FAST 0 (a) 4 LOAD_FAST 1 (b) 6 CALL_FUNCTION 2 8 UNPACK_SEQUENCE 2 10 STORE_FAST 2 (x) 12 POP_JUMP_IF_FALSE 22 # x * -1.0 被切换成了 __compiled_fn_1 14 LOAD_GLOBAL 2 (__compiled_fn_1) 16 LOAD_FAST 2 (x) 18 CALL_FUNCTION 1 20 RETURN_VALUE # return x 22 LOAD_FAST 2 (x) 24 RETURN_VALUE

能看到新增了两个抒发式调用, `__compiled_fn_0` 和 `__compiled_fn_1` ,这两个抒发式对应的标识符方法论参见 bytecode 中的注释。这两个抒发式对应的 fx graph 如下:

__compiled_fn_0:opcode name target args kwargs————- ——- ————————— —————- ——–placeholder a_0 a_0 () {}placeholder b_1 b_1 () {}call_function add <built-in function add> (a_0, b_1) {}call_function truediv <built-in function truediv> (add, 2.0) {}call_method sum_1 sum (truediv,) {}call_function lt <built-in function lt> (sum_1, 0) {}output output output ((truediv, lt),) {}__compiled_fn_1:opcode name target args kwargs————- —— ———————– ———– ——–placeholder x_4 x_4 () {}call_function mul <built-in function mul> (x_4, -1.0) {}output output output (mul,) {}

在 ByteCode rewrite 的最终,TorchDynamo 为这一段标识符的输入创建两个 Guard:

局部参数 a 必须是两个 Tensor

局部参数 b 必须是两个 Tensor

该 fn 抒发式被再次调用时,如果符合这两个条件,则能命中缓存的 TrochDynamo 处理结果;否则下次 fn 继续执行时,会触发新的 ByteCode 分析和变换。

除此之外,对和 tensor 无关的、比较特别的 python 标识符,其 ByteCode 会保持原状。这样就达到了不须要用户标注区域、自动寻找强化机会的设计目标。

现在看下 TorchDynamo 继续执行的流程总结:

TorchDynamo初探:Python ByteCode的动态修改

能看到它把原来的 PyFrameObject 替换成了 Patched PyFrameObject,这个是 CPython 全力支持的特性。这个 Patched PyFrameObject 中最主要的改动是 Frame 中的 ByteCode (即 PyCodeObject)被修正了,原来的 PyCodeObject 变成了 Transformed PyCodeObject。而这个被重写的 PyCodeObject 如上文和上图所示,主要是部分 ByteCode 被替换成了调用被校对过抒发式。这个被校对过的抒发式,全力支持自订校对方法论,当前默认的校对接口是 FX Graph。

这部分基本参考了Dynamo的官方介绍(https://dev-discuss.pytorch.org/t/torchdynamo-an-experiment-in-dynamic-python-bytecode-transformation/361)。

5

TorchDynamo 修正 Python ByteCode 的实现

Python ByteCode 修正主要依赖 PEP 523(https://peps.python.org/pep-0523/) 提供的继续执行自订 Frame Evaluation API。默认的 Eval Frame 方法论入口抒发式是 _PyEval_EvalFrame,默认情况,它会间接调用 _PyEval_EvalFrameDefault() 来处理没被修正的 frame,但是如果发现存在两个自订的 Eval Frame 抒发式,就会继续执行自动线的抒发式。

CPython _PyEval_EvalFrame 抒发式实现(https://github.com/python/cpython/blob/76449350b3467b85bcb565f9e2bf945bd150a66e/Include/internal/pycore_ceval.h#L84),所以只要在 ByteCode 继续执行前,设置两个自订的 eval frame 抒发式即可:

static inline PyObject*_PyEval_EvalFrame(PyThreadState *tstate, struct _PyInterpreterFrame *frame, int throwflag){ EVAL_CALL_STAT_INC(EVAL_CALL_TOTAL); if (tstate->interp->eval_frame == NULL) { // 这是默认的 eval frame return _PyEval_EvalFrameDefault(tstate, frame, throwflag); } // 如果存在 eval_frame 就会被继续执行 return tstate->interp->eval_frame(tstate, frame, throwflag);}

能看到 TorchDynamo 正是这么做的。第一步,在 Python 层如前所述 ContextManger 在进入 Dynamo 作用域时,就触发 eval_frame 的设置,实现(https://github.com/pytorch/pytorch/blob/4068c5467d496cd3c09a841f40adacedf3ab41a0/torch/_dynamo/eval_frame.py#L128):

# torch._dynamo.optimize(…) 对应的 context manager.class _TorchDynamoContext: def __init__( self, callback: DynamoCallback, ): super().__init__() assert callable(callback) or callback is False or callback is None self.callback: DynamoCallback = callback self.prior: Union[Unset, DynamoCallback] = unset def __enter__(self): # 设置 eval_frame,历史记录之前的 eval frame self.prior = set_eval_frame(self.callback) def __exit__(self, exc_type, exc_val, exc_tb): assert self.prior is not unset # 恢复之前的 eval frame set_eval_frame(self.prior)

这里先大致认为设置的 DynamoCallback 对应两个自订的 eval frame 所需的参数,通常是自订的 eval frame 中所需的校对方法论。

看下 set_eval_frame ,C 标识符层面的实现(https://github.com/pytorch/pytorch/blob/eaf4fe3d2b7096579b05b52d543756f74d0e91e7/torch/csrc/dynamo/eval_frame.c#L446),它有点绕但最终走到了这里(https://github.com/pytorch/pytorch/blob/eaf4fe3d2b7096579b05b52d543756f74d0e91e7/torch/csrc/dynamo/eval_frame.c#L121),也是设置 tstate->interp->eval_frame ,把 eval_frame 设置成自订的 custom_eval_frame_shim:

// custom_eval_frame_shim 是自订的 frameinline static void enable_eval_frame_shim(PyThreadState* tstate) { if (tstate->interp->eval_frame != &custom_eval_frame_shim) { // First call // 设置自订的 eval frame tstate->interp->eval_frame = &custom_eval_frame_shim; }}

现在回头看一下 PEP 523 提供的 Python JIT 校对器的自订 frame 继续执行的样例,它提供了两个比较标准的模版(注意笔者对例子做了微调,原文有多余和不合理的地方)。在自订 eval frame 之前,通常还须要自订两个存放自订 ByteCode 的数据结构,可以认为是自订校对结果,比如说样例中自订校对结果包括3个字段:

exec_count, 代表改 frame 被继续执行的次数;

jit_failed, 代表之前 jit 校对是否失败过;

jit_code,代表 jit 校对过后的自订 ByteCode;

据此,来看下自订 eval frame 的样例:

frame 中的 code object 中的存放自订校对结果的字段 pyjion_code = frame.code.co_extra if not pyjion_code: # 不如不存在,就设置两个空的默认值 frame.code.co_extra = PyjionJittedCode() elif not pyjion_code.jit_failed: # 如果之前 jit 继续执行成功 if pyjion_code.jit_code: # 如果存在 jit 聚合的 bytecode,就继续执行它 return pyjion_code.eval(pyjion_code.jit_code, frame) elif pyjion_code.exec_count > 20000: # 没有 jit 校对过,且 frame 被继续执行超过 20000 次,就尝试进行 jit 校对 # 如果不存在 jit 聚合的 bytecode,就 jit 校对聚合它 if jit_compile(frame): # 如果 jit 校对成功,就继续执行 jit 校对的 bytecode return pyjion_code.eval(pyjion_code.jit_code, frame) else: # 如果 jit 校对失败,就历史记录下,后面不再校对 pyjion_code.jit_failed = True # 增加 frame 继续执行次数计数 pyjion_code.exec_count += 1 # 继续执行默认的 frame return _PyEval_EvalFrameDefault(frame, throw_flag)

下面接着看 TorchDynamo 自订 evale frame 的实现。在了解具体的自订 frame 继续执行方法论前,有个前置知识是 PyFrameObject 中的 PyCodeObject 为了继续执行自订 frame 增加了两个 co_extra 字段,用来让用户放置自订的数据,通常是存放自订校对

结果(

https://peps.python.org/pep-0523/#expanding-pycodeobject)。
typedef struct { … void *co_extra; /* 自订的 frame 须要的自订数据 */} PyCodeObject;

TorchDynamo 在自订校对结果的类型是 CacheEntry,当中最重要的字段是 code,是被校对器修正后的 ByteCode:

typedef struct cache_entry { // check the guards: lambda: <locals of user function>: bool PyObject* check_fn; // modified user bytecode (protected by check_fns guards) PyCodeObject* code; // on a cache miss, linked list of next thing to try struct cache_entry* next;} CacheEntry;

现在看下自订的 eval frame 方法论 custom_eval_frame_shim(https://github.com/pytorch/pytorch/blob/eaf4fe3d2b7096579b05b52d543756f74d0e91e7/torch/csrc/dynamo/eval_frame.c#L342):

static PyObject* _custom_eval_frame(PyThreadState* tstate, PyFrameObject* frame, in CacheEntry* extra = get_extra(frame->f_code); // callback 即上文说的自订校对器 // 使用 callback 进行 bytecode 的修改,即校对 // 校对结果写在了 frame->f_code中的 extra 中 PyObject* result = call_callback(callback, (PyObject*)frame, cache_size(extra)); if (result != Py_None) { // 缓存校对结果 extra = create_cache_entry(extra, result); Py_DECREF(result); // 继续执行自订的 frame // eval_custom_code 最终会调用 CPython 接口 _PyEval_EvalFrameDefault 来继续执行排序 // 当中 extra->code 中存放的就自订校对器聚合的 ByteCode // 所以最终 _PyEval_EvalFrameDefault 继续执行的是校对器聚合的 ByteCode return eval_custom_code(tstate, frame, extra->code, throw_flag); }}inline static PyObject* eval_custom_code(PyThreadState* tstate, PyFrameObject* frame, PyCodeObject* custom_code, int throw_flag) { // 使用 custom_code 创建两个自订的 frame PyFrameObject* shadow_frame = PyFrame_New(tstate, custom_code, frame->f_globals, NULL); // 调用 Python 的 frame 继续执行自订 frame return _PyEval_EvalFrameDefault(tstate, shadow_frame, throw_flag);}

到这里,已经清楚了修正 Python ByteCode 继续执行的主线方法论。

6

小结

这里对 Python 的继续执行和 TorchDynamo 的主要原理做了探析,主要是自订 Eval Frame 的实现技巧。其它相关的 Python ByteCode 标准,ByteCode 到 FX Graph 的切换,ByteCode 的重写等内容还没涉及。

参考资料

tenthousandmeters.com/b (https://tenthousandmeters.com/blog/python-behind-the-scenes-1-how-the-cpython-vm-works/)

peps.python.org/pep-052 (https://peps.python.org/pep-0523/)

dev-discuss.pytorch.org (https://dev-discuss.pytorch.org/t/torchdynamo-an-experiment-in-dynamic-python-bytecode-transformation/361)

(原文:https://zhuanlan.zhihu.com/p/589115427)

欢迎 Star、试用 OneFlow 最新版本:https://github.com/Oneflow-Inc/oneflow/

举报/反馈

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务