TVM PackedFunc 实现

为了便于 Python 和 C++ 混合编程,TVM 使用了统一的 PackedFunc 机制。PackedFunc 可以将 C++ 中的各类函数打包成统一的函数接口,并自动导出到 Python 模块中进行调用,并且也支持从 Python 中注册一个函数,并伪装成 PackedFunc 在 C++ 和 Python 中调用。

TVM PackedFunc实现机制 - 图1

预备知识

Python ctypes 混合编程

ctypes 是 Python 自带的跨语言函数调用库,ctypes 提供了简单的 C 数据类型,可以将 C/C++ 动态库中的函数包装成 Python 函数进行调用。

  • 导出 C++ 函数
    首先在 C++ 中定义一个全局函数,并编译生成 C++ 动态库。
    1. // test.h
    2. extern "C" {
    3. int add(int a, int b);
    4. }
  1. // test.cc
  2. #include "test.h"
  3. int add(int a, int b) {
  4. return a + b;
  5. }


用 ctypes 模块在 Python 中加载生成的动态库(test.so),并调用 C++ 中的函数。

  1. import ctypes
  2. # Load shared library
  3. _LIB = ctypes.CDLL("./test.so", ctypes.RTLD_GLOBAL)
  4. a = ctypes.c_int(1)
  5. b = ctypes.c_int(2)
  6. # Call C func in Python
  7. print(_LIB.add(a, b))
  8. # Or
  9. print(_LIB.add(1, 2))
  • 传递 Python 函数到 C++
    ctypes 也支持将 Python 函数转换成 C 类型的函数,并在 C/C++ 中进行调用。
    1. def add(a, b):
    2. return a + b


Python add 有两个参数 a 和 b,返回值类型与 a 和 b 的类型一致。在 C++ 中可以为 Python add 定义一个函数原型 int(int, int)。

  1. extern "C" {
  2. typedef int (*PyCFunc)(int, int);
  3. int call_py_func(PyCFunc f, int a, int b);
  4. }
  1. #include "test.h"
  2. int call_py_func(PyCFunc f, int a, int b) {
  3. return f(a, b);
  4. }


使用 ctypes 将 Python 函数转换成 C function,传入 C++ 中进行调用。

  1. import ctypes
  2. cfunc = ctypes.CFUNCTYPE(
  3. ctypes.c_int, # return type
  4. ctypes.c_int, # arg0 type
  5. ctypes.c_int # arg1 type
  6. )
  7. f = cfunc(add)
  8. # CFUNCTYPE is callable in Python
  9. print(f(5, 1))
  10. # Call Python func in C
  11. print(_LIB.call_py_func(f, 5, 1))

PackedFunc 实现

PackedFunc 定义

ctypes 可以很方便的将 C/C++ 中的函数导出到 Python,调用时直接传入对应的参数即可,但如果需要将 Python 函数导入到 C/C++,则需要在 C/C++ 中提前定义好对应的函数原型(比如上面的 PyCFunc),并提供对应函数的调用入口(call_py_func)。为了支持更加灵活的函数定义,TVM 将不同类型的函数包装成统一的函数原型。

  1. void(TVMArgs args, TVMRetValue *rv);

统一的函数原型被封装成 PackedFunc 对象,提供通用的调用接口,直接与调用者进行交互。

  1. class PackedFunc {
  2. public:
  3. using FType = std::function<void (TVMArgs args, TVMRetValue* rv)>;
  4. template<typename... Args>
  5. inline TVMRetValue operator()(Args&& ...args) const;
  6. inline void CallPacked(TVMArgs args, TVMRetValue* rv) const;
  7. private:
  8. /*! \brief internal container of packed function */
  9. FType body_;
  10. };

当获得一个 PackedFunc 对象时,我们就可以像调用普通函数一样调用 PackedFunc 打包的函数。比如:

  1. PackedFunc f;
  2. // f(1, 2)首先会自动将参数1,2打包成TVMArgs,接着调用CallPacked,CallPacked最终的执行体是body_
  3. TVMRetValue ret = f(1, 2);

函数打包

TVM 支持对各类函数进行打包,包括一般的函数、类的成员函数以及 lamda 表达式。

  • 函数原型萃取
    萃取函数原型是为了得到函数的参数和返回值类型。TVM 中使用 decltype 和模版结构体 function_signature 来实现。
    比如定义一个简单的 C 函数,
    1. int add(int a, int b) {
    2. return a + b;
    3. }


接下来就可以使用如下的代码来萃取 add 的函数原型,

  1. template <typename R, typename ...Args>
  2. struct function_signature<R(Args...)> {
  3. using FType = R(Args...);
  4. };
  5. // 萃取add的函数原型
  6. using FType = function_signature<decltype(add)>::FType;


此外只需要特化 function_signature 就可以支持函数指针和 lambda 表达式。注意:TVM function_signature 不支持普通成员函数的类型萃取,因此 TVM 需要借助一个辅助 function_signature_helper 来对 lambda 表达式类型进行萃取,而我们这里的 function_signature 支持普通成员函数,因此 lambda 表达式类型萃取可以通过递归的 function_signature 来实现。

  1. // 普通函数指针
  2. template <typename R, typename ...Args>
  3. struct function_signature<R(*)(Args...)> {
  4. using FType = R(Args...);
  5. };
  6. // 非const类的成员函数指针
  7. template <typename T, typename R, typename ...Args>
  8. struct function_signature<R(T::*)(Args...)> {
  9. using FType = R(Args...);
  10. };
  11. // const类的成员函数指针
  12. template <typename T, typename R, typename ...Args>
  13. struct function_signature<R(T::*)(Args...) const> {
  14. using FType = R(Args...);
  15. };
  16. // lambda表达式
  17. template<typename T>
  18. struct function_signature {
  19. using FType = typename function_signature<decltype(&T::operator())>::FType;
  20. };
  • 函数打包
    一旦萃取到了函数原型,TVM 就利用 TypedPackedFunc 对普通函数或 lambda 表达式进行打包。TypedPackedFunc 只支持对 R(Args…) 类型的函数打包,所以如果被打包的函数是一个函数指针,则需要创建一个 lambda 表达式,转换成 R(Args…) 类型之后再用 TypedPackedFunc 对创建的 lambda 表达式进行打包。
    1. template<typename R, typename ...Args>
    2. class TypedPackedFunc<R(Args...)> {
    3. public:
    4. using TSelf = TypedPackedFunc<R(Args...)>;
    5. template<typename FLambda,
    6. typename = typename std::enable_if<
    7. std::is_convertible<FLambda,
    8. std::function<R(Args...)>
    9. >::value>::type>
    10. TypedPackedFunc(const FLambda& typed_lambda) { // NOLINT(*)
    11. this->AssignTypedLambda(typed_lambda);
    12. }
    13. ...
    14. private:
    15. ...
    16. PackedFunc packed_;
    17. };


当被打包的函数用来实例化 TypedPackedFunc 对象时,会立刻调用 AssignTypedLambda 将被打包的函数打包成 PackedFunc。

  1. template<typename R, typename ...Args>
  2. template<typename FType>
  3. inline void TypedPackedFunc<R(Args...)>::AssignTypedLambda(FType flambda) {
  4. packed_ = PackedFunc([flambda](const TVMArgs& args, TVMRetValue* rv) {
  5. detail::unpack_call<R, sizeof...(Args)>(flambda, args, rv);
  6. });
  7. }


AssignTypedLambda 实际上是将被打包的函数先封装成了一个函数原型为 void(const TVMArgs &args, TVMRetValue *rv) 的 lambda 表达式,然后将这个 lambda 表达式作为 PackedFunc 对象的一个成员,通过设置合适的接口(重载 operator ()),使得 PackedFunc 与被打包的源函数表现的完全一样了。

自动导出函数

TVM 将需要从 C++ 自动导出的函数打包成 PackedFunc,然后通过宏 TVM_REGISTER_GLOBAL 注册到全局的一个 map 中。比如:

  1. TVM_REGISTER_GLOBAL("_Var")
  2. .set_body_typed([](std::string s, DataType t) {
  3. return VarNode::make(t, s);
  4. });

当 Python 加载编译好的动态库时,会自动查询 map 中静态注册的函数,每个函数都包装成 Python 中的 Function 对象,最终添加到 Python 模块中。Function 重定义了函数调用接口,自动完成参数打包过程。
如果是在 Python 中动态注册的函数,则需要在 Python 中通过函数名和来查询 PackedFunc,返回一个 PackedFunc 的 handle(函数指针),并封装成 Function。

  1. def get_global_func(name, allow_missing=False):
  2. handle = FunctionHandle()
  3. check_call(_LIB.TVMFuncGetGlobal(c_str(name), ctypes.byref(handle)))
  4. if handle.value:
  5. return Function(handle, False)
  6. if allow_missing:
  7. return None
  8. raise ValueError("Cannot find global function %s" % name)

注:TVMFuncGetGlobal 是通过 ctypes 导出的 C++ 接口,FunctionHandle 是 ctypes 中表示 void 指针类型(c_void_p)。

从 Python 注册函数

由于 TVM 中 PackedFunc 的精心设计,我们只需要将 Python 中的函数转换成统一的函数原型 void(const TVMArgs, TVMRetValue),然后将函数转换成 PackedFunc 并动态地注册到全局的 map 中。

先将 Python 函数用 ctypes 转成 int(TVMValue , int , int, void , void ) 的 C 函数。

  1. TVMPackedCFunc = ctypes.CFUNCTYPE(
  2. ctypes.c_int,
  3. ctypes.POINTER(TVMValue),
  4. ctypes.POINTER(ctypes.c_int),
  5. ctypes.c_int,
  6. ctypes.c_void_p,
  7. ctypes.c_void_p)

然后通过 TVMFuncCreateFromCFunc 将上面的 C 函数转换成统一的 PackedFunc 函数。

  1. int TVMFuncCreateFromCFunc(TVMPackedCFunc func,
  2. void* resource_handle,
  3. TVMPackedCFuncFinalizer fin,
  4. TVMFunctionHandle *out) {
  5. API_BEGIN();
  6. if (fin == nullptr) {
  7. *out = new PackedFunc(
  8. [func, resource_handle](TVMArgs args, TVMRetValue* rv) {
  9. int ret = func((TVMValue*)args.values, (int*)args.type_codes, // NOLINT(*)
  10. args.num_args, rv, resource_handle);
  11. if (ret != 0) {
  12. throw dmlc::Error(TVMGetLastError() + ::dmlc::StackTrace());
  13. }
  14. });
  15. } else {
  16. ...
  17. }
  18. API_END();
  19. }

最后通过接口 TVMFuncRegisterGlobal 注册到全局的 map 中。下面是从 Python 中注册一个函数,并在 Python 中调用的例子。

  1. targs = (10, 10.0, "hello")
  2. @tvm.register_func
  3. def my_packed_func(*args):
  4. assert(tuple(args) == targs)
  5. return 10
  6. # Get it out from global function table
  7. f = tvm.get_global_func("my_packed_func")
  8. assert isinstance(f, tvm.nd.Function)
  9. y = f(*targs)
  10. assert y == 10

https://hjchen2.github.io/2020/01/10/TVM-PackedFunc%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6/