TVM apps extension示例扩展库

TVM apps extension示例扩展库

此文件夹包含TVM的示例扩展库。演示了其它库如何在C++和Python API中扩展TVM。

该库扩展了TVM的功能。

python模块加载新的共享库,可以使用TVM的python API进行插值。

https://github.com/apache/tvm/tree/main/apps/extension

 

 

 test_ext.py修改了一些代码

 

 

  

 

  

 

  

 

 运行结果

 

 

 TVM Runtime System

TVM支持多种编程语言用于编译器堆栈的开发和部署。将解释TVMRuntime的关键元素。

 

  需要满足许多有趣的要求:

 部署:从python/javascript/c++语言调用编译后的函数。

 调试:在python中定义一个函数,从编译后的函数调用该函数。

 链接:编写驱动程序代码,调用设备特定代码(CUDA),从编译后的主机函数中调用。

 原型:从Python定义IR传递,从C++后端调用。

 暴露:C++开发的编译器栈到前端(即,Python)

 实验:将编译后的函数发送到嵌入式设备,直接运行。

从任何语言定义函数,从另一种语言调用函数。希望Runtime核心最小,部署到嵌入式设备。

PackedFunc

PackedFunc是一个简单但优雅的解决方案,可以解决列出的挑战。单个PackedFunc对象,表示调用者和被调用者可能使用不同语言的函数调用。

下面的代码块提供了C++中的一个例子

#include <tvm/runtime/packed_func.h>

 

void MyAdd(TVMArgs args, TVMRetValue* rv) {

  // automatically convert arguments to desired type.

  int a = args[0];

  int b = args[1];

  // automatically assign value return to rv

  *rv = a + b;

}

 

void CallPacked() {

  PackedFunc myadd = PackedFunc(MyAdd);

  // get back 3

  int c = myadd(1, 2);

}

在上面的代码块中,定义了一个PackedFunc MyAdd。有两个参数:args表示输入参数,rv表示返回值。函数被类型擦除,这意味着函数签名不限制要传入的输入类型或要返回的输入类型。在后端,当调用PackedFunc时,将输入参数打包到堆栈上的TVMArgs,通过TVMRetValue返回结果。

 

由于C++中的模板技巧,可以调用PACKEDFUNC,就像普通函数一样。由于类型擦除特性,可以从动态语言(如python)调用PackedFunc,无需为创建的每个新类型函数,添加额外的粘合代码。下面的示例在C++中注册PackedFunc,从Python调用。

// register a global packed function in c++
TVM_REGISTER_GLOBAL("myadd")
.set_body(MyAdd);
import tvm
 
myadd = tvm.get_global_func("myadd")
# prints 3
print(myadd(1, 2))

PackedFunc的大部分魔力在于TVMArgs和TVMRetValue结构。限制可以传递的可能类型的列表。以下是常见的:

  • · int, float and string
  • · PackedFunc itself
  • · Module for compiled modules
  • · DLTensor* for tensor object exchange
  • · TVM Object to represent any object in IR

该限制使得实现简单,无需序列化。尽管PackedFunc是最小的,对于深度学习部署的用例已经足够了,因为大多数函数只接受DLTensor或数字。

由于一个PACKEDFUNC可以采取另一个PACKEDFUNC作为参数,所以可以将函数从Python(如PackedFunc)传递到C++。

TVM_REGISTER_GLOBAL("callhello")

.set_body([](TVMArgs args, TVMRetValue* rv) {

  PackedFunc f = args[0];

  f("hello world");

});

import tvm

 

def callback(msg):

  print(msg)

 

# convert to PackedFunc

f = tvm.convert(callback)

callhello = tvm.get_global_func("callhello")

# prints hello world

callhello(f)

TVM提供了一个最小的C API,它允许将PackedFunc嵌入到任何语言中。除了python,支持java和javascript。嵌入式API的这个概念很像Lua,只是没有新的语言,但是使用C++。

PackedFunc用于编译器和部署堆栈。

TVM的所有编译器pass函数,都作为PackedFunc公开给前端

编译后的模块将编译后的函数作为PackedFunc返回

为了保持Runtime最小值,将IR对象支持与部署Runtime隔离。根据包含多少Runtime驱动程序模块(如CUDA),生成的Runtime大约需要200K-600K。

 与普通函数相比,调用PackedFunc的开销很小,因为只在堆栈上保存了一些值。因此,只要不包装小函数就可以了。总之,PackedFunc是TVM中的通用粘合剂,在TVM中广泛使用,支持编译器和部署。

Module

由于TVM支持多种类型的设备,需要支持不同类型的驱动程序。必须使用驱动程序API加载内核,以压缩格式设置参数,执行内核启动。需要修补驱动程序API,以便公开的函数是线程安全的。因此,经常需要在C++中实现这些驱动程序GLUE,公开给用户。当然不能对每种类型的函数都这样做,所以PackedFunc也是答案。

TVM将编译对象定义为模块。用户可以从模块中以PackedFunc的形式获取编译后的函数。生成的编译代码可以在Runtime从模块中动态获取函数。在第一次调用中缓存函数句柄,在后续调用中重用。将设备代码和回调从生成的代码链接到任何PackedFunc(如python)中。

ModuleNode是一个抽象类,可以由每种类型的设备实现。到目前为止,支持CUDA、Metal、OpenCL和加载动态共享库的模块。这种抽象使得引入新设备变得容易,不需要为每种类型的设备重新生成主机代码。

远程部署

PackedFunc和模块系统可以轻松地将功能直接发送到远程设备。在后端,有一个RPCModule,序列化参数以进行数据移动,在远程启动计算。

 

 

 RPC服务器本身是最小的,可以捆绑到Runtime中。可以在iPhone/android/raspberry pi甚至浏览器上,启动最小的TVM RPC服务器。服务器上的交叉编译和用于测试的模块的发布,可以在同一个脚本中完成。有关更多详细信息,请参考交叉编译和RPC。

这种即时反馈给了很多好处。例如,为了在iPhone上测试生成代码的正确性,不再需要从头开始用swift/objective-c编写测试用例——可以使用RPC在iPhone上执行,将结果复制,通过numpy在主机上进行验证。可以使用相同的脚本进行分析。

TVM对象和编译器堆栈

如前所述,在PackedFuncRuntime系统之上,构建编译器堆栈API。为了研究的需要,面临着编译器API的不断变化。每当想要测试新的原语时,都需要一个新的语言对象或IR节点。但是,不希望不时更改API。除此之外,

能够序列化任何语言对象和IRs

能够使用前端语言探索、打印和操作IR对象,进行快速原型制作。

引入了一个名为Object的基类,解决这个问题。编译器堆栈中的所有语言对象都是object的子类。每个对象都包含一个字符串类型type_key,该键唯一标识对象的类型。选择string而不是int作为类型键,可以以分散的方式添加新的对象类,无需将代码添加回中心repo。为了简化调度速度,在Runtime为每个type_key分配一个整数type_index。

 因为通常一个对象可以在语言中的多个位置引用,所以使用一个共享的ptr跟踪引用。使用ObjectRef类表示对对象的引用。可以粗略地将ObjectRef类视为对象容器的shared_ptr。可以定义子类ObjectRef,保存对象的每个子类型。对象的每个子类都需要定义VisitAttr函数。

class AttrVisitor {

public:

  virtual void Visit(const char* key, double* value) = 0;

  virtual void Visit(const char* key, int64_t* value) = 0;

  virtual void Visit(const char* key, uint64_t* value) = 0;

  virtual void Visit(const char* key, int* value) = 0;

  virtual void Visit(const char* key, bool* value) = 0;

  virtual void Visit(const char* key, std::string* value) = 0;

  virtual void Visit(const char* key, void** value) = 0;

  virtual void Visit(const char* key, Type* value) = 0;

  virtual void Visit(const char* key, ObjectRef* value) = 0;

  // ...

};

 

class BaseAttrsNode : public Object {

public:

  virtual void VisitAttrs(AttrVisitor* v) {}

  // ...

};

每个对象子类都将覆盖此项访问成员。下面是TensorNode的一个示例实现。

class TensorNode : public Object {

public:

  /*! \brief The shape of the tensor */

  Array<Expr> shape;

  /*! \brief data type in the content of the tensor */

  Type dtype;

  /*! \brief the source operation, can be None */

  Operation op;

  /*! \brief the output index from source operation */

  int value_index{0};

  /*! \brief constructor */

  TensorNode() {}

 

  void VisitAttrs(AttrVisitor* v) final {

    v->Visit("shape", &shape);

    v->Visit("dtype", &dtype);

    v->Visit("op", &op);

    v->Visit("value_index", &value_index);

  }

};

在上面的示例中,操作和数组<Expr>都是ObjectRef。VisitAttrs提供了一个反射API,访问对象的每个成员。可以使用此函数访问节点,递归序列化任何语言对象。允许用前端语言轻松获取对象的成员。例如,在下面的代码中,访问了TensorNode的op字段。

import tvm

from tvm import te

 

x = te.placeholder((3,4), name="x")

# access the op field of TensorNode

print(x.op.name)

可以在不改变前端运行时的情况下,将新的对象添加到C++,便于对编译器堆栈进行扩展。这不是向成员公开前端语言的最快方法,但可能是最简单的方法之一。主要使用Python进行测试和原型开发,仍然使用C++完成繁重的工作。

实施详情

PackedFunc中的每个参数,都包含一个联合值TVMValue和一个类型代码。这种设计允许动态类型的语言,直接转换为相应的类型,静态类型的语言在转换过程中,进行运行时类型检查。

有关档案如下:

C++ API的PACKEDFUNC.H

c_runtime_api.cc用于c API,如何提供回调。

为了支持扩展类型,使用注册表系统登记类型相关信息,如C++中的任何支持,参阅扩展类型获取更多细节:https://github.com/apache/tvm/tree/main/apps/extension。

 

参考链接:

https://github.com/apache/tvm/tree/main/apps/extension

https://tvm.apache.org/docs/arch/runtime.html

 

posted @ 2021-11-22 06:11  吴建明wujianming  阅读(90)  评论(0编辑  收藏  举报