TVM Python与Cpp绑定机制解析
本文以TVM v0.20.0为例,分析TVM中Python前端与C++后端的绑定机制。绑定机制主要包括三个部分:类的注册、对象的初始化以及成员函数的绑定。本文将以DataflowBlockRewrite类为例,逐步解析这一过程。
为行文方便,前端默认指TVM python前端,后端默认指TVM C++后端
一、前端注册类
在TVM前端中,需要对每一个类进行注册。
以DataflowBlockRewrite为例,注册代码如下
1 |
|
为了行文流畅,本文中大多数的类、文件没有注明具体路径,读者可以使用VSCode
Ctrl + P搜索文件,侧边栏搜索类名进行检索。
接下来,我们分析tvm._ffi.register_object的执行流程,该函数定义如下:
1 | def register_object(type_key=None): |
当传入字符串参数"relax.DataflowBlockRewrite" 时, tvm._ffi.register_object("relax.DataflowBlockRewrite")返回一个注册函数register。
再看register(DataflowBlockRewrite)
1 | def register(cls): |
函数 _register_object的定义在object.pxi中,以_register_object(240, DataflowBlockRewrite)的执行流程为例:
Cython编译流程:
python/tvm/_ffi/_cython中base.pxi,ndarray.pxi,object.pxi,packed_func.pxi描述了与C++的接口,core.pyxinclude了这些*.pxi文件。TVM项目编译后,
python/tvm/_ffi/_cython中的core.pyx被编译成python/tvm/_ffi/_cy3中的.so文件(例如core.cpython-312-x86_64-linux-gnu.so)。(Cython具体编译配置见python/setup.pyL161)。完成编译后,便可以在python文件中调用.so中的C++函数/类。例如python/tvm/_ffi/registry.py中from ._cy3.core import _register_object
1 | """Maps object type index to its constructor""" |
- 前端和后端中相对应的类的
type_index相等,从而实现一一对应。
二、前端对象初始化
仍然以DataflowBlockRewrite为例
1 |
|
DataflowBlockRewrite前端初始化函数__init__所需的参数与后端C++构造函数一致。 binding_rewrite.h:
1 | class DataflowBlockRewrite : public ObjectRef { |
python对象初始化时,通过self.__init_handle_by_constructor__创建了C++实例。
object.pxi中定义了类ObjectBase和方法__init_handle_by_constructor__:
1 | cdef class ObjectBase |
类的初始化发生在函数ConstructorCall。此处函数调用栈比较深,可以简单理解为:使用构造函数fconstructor和参数args,构造了一个C++的DataflowBlockRewrite类,并用临时指针chandle指向这个C++实例,再用self.chandle指向该C++实例。
DataflowBlockRewrite继承自ObjectBase:
1 | classDiagram |
因此,python类DataflowBlockRewrite有一个成员变量self.chandle指向C++对象DataflowBlockRewrite,即python DataflowBlockRewrite.chandle -> &(C++ DataflowBlockRewrite),从而实现了python对象与C++对象的绑定。
三、TVM前端对象的成员函数与后端对象的成员函数绑定
仍然以DataflowBlockRewrite为例,它的replace_all_uses方法与后端C++函数ReplaceAllUses进行了绑定:
1 |
|
1 | /*! \brief Statement rewriter for relax.DataflowBlock. */ |
python类DataflowBlockRewrite调用_ffi_api.dfb_rewrite_replace_all_uses,实际在调用后端的注册函数relax.dfb_rewrite_replace_all_uses:
1 | TVM_REGISTER_GLOBAL("relax.dfb_rewrite_replace_all_uses") |
函数体内再调用rwt->ReplaceAllUses(old_var, new_var);
所以,_ffi_api.dfb_rewrite_replace_all_uses是如何与后端注册的函数relax.dfb_rewrite_replace_all_uses进行绑定的呢?_ffi_api是一个module,为什么会有一个dfb_rewrite_replace_all_uses方法呢?这一功能的实现依赖于Python一个特性,运行时对象属性更新(Dynamic Attribute Assignment).
在binging_rewrite.py中,通过from . import _ffi_api导入_ffi_api,这个导入操作会执行_ffi_api.py中的tvm._ffi._init_api("relax", __name__),这行代码将后端注册的、relax namspace中的函数写入了_ffi_api这个module。
1 | tvm._ffi._init_api("relax", __name__) |
registry.py
1 | def _init_api(namespace, target_module_name=None): |
Tips:
- 后端
relax.dfb_rewrite_replace_all_uses的注册
1 | TVM_REGISTER_GLOBAL("relax.dfb_rewrite_replace_all_uses") |
list_global_func_names()返回的列表中有一个字符串为relax.dfb_rewrite_replace_all_uses
- 经过以上步骤,
_ffi_api具有属性dfb_rewrite_replace_all_uses,_ffi_api.dfb_rewrite_replace_all_uses是一个python函数dfb_rewrite_replace_all_uses, 它里面包装了对应的名为relax.dfb_rewrite_replace_all_uses的后端注册的函数。