源码研习 — TVM中的IR设计与技术实现
一、关键问题
TVM中的 IR 是什么,架构设计上分几层?
解答:TVM的整体结构图如下:
概念上,分为两层:上层为面向前端组网的Relay IR, 下层为面向LLVM的底层 IR。
但从设计实现上,底层通过 Object 元类实现统一的AST Node表示,借助一个 IRModule 贯穿上下层。个人理解,TVM的 IR 实现上其实只有一层,只是封装后在直观概念上分为上下层。
IRModule里持有的是
BaseFunction
列表上层
relay::Funtion
继承自BaseFunction
官方解释:
relay::Function
对应于一个end2end的模型。可以简单理解为一个支持控制流、递归、以及复杂数据结构的计算图。下层
tir::PrimFunc
也继承自BaseFunction
官方解释:
tir::PrimFunc
包含了一些底层threading、vector/tensor的"指令"。通常为模型layers中的一个Op执行单元在编译阶段,一个
relay::Function
可能会被lower
成多个tir::PrimFunc
。
TVM架构上主要包含了哪些核心模块和概念?
解答:如下是各个模块的交互图:
从编译流程上来看,涉及的核心数据结构有两个:
IRModule
:包含relay::Function
和tir::PrimFunc
- 此部分也是 Pass 策略的输入输出单元,即
IRModule
→pass
→IRModule
- 传送门:TVM 的 Relay IR设计
- 此部分也是 Pass 策略的输入输出单元,即
runtime::Module
:经过lowering
之后,可执行期的基本单元,包含很多runtime::PackedFunc
(可以理解为KernelFunc
编译时的Pass策略主要在IRModule
数据结构层面进行,分为两方面:
ruled-base
:包括relay/transform
和tir/transform
- 前者多为上层“图”结构上Pass优化,比如常量折叠,fusion
- 后者多为下层偏向编译器方面的Pass优化,比如prefetch注入,unrollLoop
search-based
:包括auto-schedule
和auto-tvm
在前后端交互上,TVM将所有的核心数据结构都暴露到了Python前端,易用性和灵活性极强:
- 所有的核心对象都可以通过Python API直接构造和操作,比如
IRModule
- 支持在前端自定义组合pass和transformation
- 通过TVM的API直接操作 IR,支持Python端写pass
IRMoule是什么样的?
IRModule通过IRModuleNode管理元信息
核心成员:
- Functions
- 表示计算的函数单元,如Conv、log
- Function内部有通过params、body关联Var
- 概念上,对应与AST的Module
- Global_var
import tvm
from tvm import relay
import numpy as np
# step 1: modeling
m,n = 4, 2
x = relay.var("x", shape=(m,n), dtype='float32')
out = relay.nn.softmax(x)
net = relay.Function([x], out) # step 2: build and lowering
module = tvm.IRModule.from_expr(net)
lib = relay.build(module, "llvm") # step 3: input tensor data
ctx = tvm.cpu(0)
x_t = tvm.nd.array(np.random.uniform(size=[m,n]).astype('float32'), ctx)
runtime = tvm.contrib.graph_runtime.GraphModule(lib["default"](ctx))
runtime.set_input("x", x_t)
runtime.run()
print(runtime.get_output(0)) # print(net.body)
'''
fn (%x: Tensor[(4, 2), float32]) {
nn.softmax(%x)
}
''' # print(module)
'''
def @main(%x: Tensor[(4, 2), float32]) {
nn.softmax(%x)
}
'''
- Functions
Relay的pass是如何实现和管理的?
解答:上面提到,概念上讲,TVM可以看做是分两层的:Relay层和tir层,通过IRModule来贯穿。在Pass优化上,TVM也进行了两层的设计:
上层基于“图”的优化
这部分很类似Paddle的pass,主要通过对 AST 的分析,应用一些上层的pass策略,主要包括:
- 常量折叠、DSE、Layout转换、scaling因子折叠
- 最后会应用fuse pass。比如将一个MobileNet表示成很多conv2d-relu 的“段”
- pass的定义见
relay/transform
下层基于“target”的优化
这部分pass主要涉及 lowering到target时需要采取的优化策略,比如如何生成高效执行
conv2d-relu
的代码。主要包括:- Prefetch语句注入、VectorizeLoop、UnrollLoop、RemoveNoOp
- SkipAssert、ThreadSync、HoistIfThenElse等
- 此部分 pass有的可以直接复用底层编译器的pass,如LLVM、CUDA C等编译器。因此TVM主要关注和ML相关、且底层编译器未考虑到的场景
TVM 的 pass是通过遍历AST,进行node修改来实现的(类似paddle的动转静),通过TVM_REGISTER_GLOBAL
注册和暴露支持的pass。
对于开发者来讲,TVM是如何便捷地支持新增一个Pass的呢?
TVM官方给出了一个[常量折叠 Pass的文档](Adding a Compiler Pass to Relay)。由于 TVM 的 IR 比较像AST,因此pass的新增主要包括如下几个步骤:
需要一个
AST Traversers
用于确定哪些node是需要修改。在常量折叠pass中,实现了
ConstantChecker
,通过map结构的memo_
记录哪些node是常量node。这里只涉及两个node的函数重载:ConstantNode和TupleNode需要一个
Expression Mutators
用于修改和替换满足条件的node。在常量折叠pass中,只有三种node涉及折叠:LetNode、TupleItemGetNode和CallNode,因此也需要重载这三个函数即可
TVM的pass设计思想和架构,可以更多的参考Pass Infrastructure文档介绍。整体上借鉴了很多LLVM的pass设计思想。目标很明确,旨在实现如下效果:
- 可以灵活地排布Optimization单元,支持用户随意地进行pass piplines定制
- 提供友好地pass budug体验
- 避免用户去手动处理pass之间的依赖
- 简化开发者新增pass的流程,支持在python端写pass
TVM Pass实现上,可以分为三大类:
- Module-Level Pass
- 利用全局信息进行优化,可以删减
Function
,如 DSE pass - 核心pass函数是
PackedFunc
类型,因此支持python、C++去写pass
- 利用全局信息进行优化,可以删减
- Funtion-Level Pass
- 对Module中的每个
Function
进行优化,只有局部信息 - 不允许删减
Function
- 如公共子表达式替换、vectorization
- 对Module中的每个
- Sequential-Level Pass
- 顺序执行一系列的pass
FusionPass的基本原理:
- 会先将IRModule转为Graph
TVM 中的 auto-tvm的角色是什么?
解答:上面我们介绍的TVM的pass都是rule-based的,意味着开发者在新增pass时,其实是只要匹配什么样的模式,然后替换成什么样的模式。
这导致两个问题:
- pass的数量会很受限
- pass都需要预定义后才能支持
auto-tvm会先定义一些粒度比较小的优化策略,TVM会启发式组合应用、评估这些策略带来的提升,最后使用最佳的组合策略,以实现auto。
Relay结构是执行期的结构么?
解答:Relay的解释器(Interpreter)可以执行relay的表达式,但不适合生产环境部署时使用。原因是:
解释器是通过遍历 AST 来执行程序,遍历过程是很低效的。
无法友好支持动态代码。比如动态schduling、动态Tensor shape、还有控制流。解释器提供了简单的实现方案,但无法高效地编译和优化
静态的代码优点:graphs是固定的,方便大刀阔斧地进行优化,比如内存静态分配,最佳的内存复用等。
TVM 也使用了 graph runtime技术——提供了一种快速执行机制,但仅支持部分Relay的programs
因此,Relay引入了 Virtual Machine,旨在取得部署、执行Relay programs时,性能与灵活性之间的平衡。
从用户的角度,可以通过relay.crete_executor(kind, ctx, target)
接口来创建不同的执行器:
kind
取值为:graph、vm、debug
- 统一实现了
evalutae(expr, *args)
接口
前置知识:VM
- 传统的VM主要操作部分scalar和大量低阶instructions
- 对于ML,主要是Tensor,以及部分的高阶instructions
- 耗时集中在计算密集型Op的调用,如GEMM和Conv
- 设计的核心点是:指令集的选择、指令表示
- op-code 和 data payload
TVM中的VM的指令集的设计:
- 偏向high-level的设计,尽量与Relay层的operation相呼应
- AllocTenor、If、Goto
- 核心的三种object对象:
- NDArray、ADT 和 Closure,分别用于表示Tensor、tuple/list、closure data。
- 栈(Stack)和状态(State)
- 栈帧用于标记当前的函数调用
- 每个函数的寄存器都是在连续空间上申请的
- dispatch loop
- VM实现了switch 和 goto
TVM 的VM compiler设计:
- 作用:将 Relay的IR 编译成字节码序列,即
tvm::relay::Module
→tvm::relay::vm::Executable
→tvm::relay::vm::Function
→tvm::relay::vm::VirtualMachine
TVM 的 VM 对序列化和反序列化的支持:
- Graph Runtime方案中序列化的结果是:
- 权重参数保存为
.weight
文件 - graph保存为
.json
文件 - 计算kernel保存为
.so
库
- 权重参数保存为
- VM 方案中序列化的结果为:
- Relay的 object文件
.o
文件 - 计算kernel保存为
.so
库
- Relay的 object文件
TVM的Runtime模块是什么样的?
解答:先看一个用户侧使用的接口样例:
import tvm
# Example runtime execution program in python, with type annotated
mod: tvm.runtime.Module = tvm.runtime.load_module("compiled_artifact.so")
arr: tvm.runtime.NDArray = tvm.nd.array([1, 2, 3], ctx=tvm.gpu(0))
fun: tvm.runtime.PackedFunc = mod["addone"]
fun(a)
print(a.asnumpy())
Runtime时期的三大核心概念:
runtime.Module
:封装编译DSO的核心单元,包含了很多PackedFunc
,可以根据name
获取函数runtime.PackedFunc
:后端生成的函数,对应于DL中的KernelFuncruntime.NDArray
:封装了执行期的Tensor概念
TVM的target过程做了什么事情?
解答:这个应该是比较明确的,类似很多开源的框架,TVM会将 IRModule
emit 到后端编译器去in-memory
地生成可执行代码。
个人理解,target的过程涉及到编译,这对框架要求很高,在大多数场景下,这个过程应该是超级轻量级的,速度应该越快越好。
通过本地编译安装和试用TVM,发现target的过程超级快,几乎瞬发返回可执行函数。
TVM中编译执行和预测部署是什么样的?
解答:首先需要进行网络的定义:
import tvm
import numpy as np
n = 12
A = te.placeholder((n,), name="A") # Tensor
B = te.compute(A.shape, lambda *i: A(*i) + 1.0, name="B") # Tensor
C = te.compute(A.shape, lambda *i: A(*i) - 1.0, name="C") # Tensor
s = te.create_scheduleC[B.op, C.op]) # schedule
add_func = tvm.build(s, [A, B, C], "llvm", name="add") # compile
# prepare data
ctx = tvm.cpu(0)
a_t = tvm.nd.array(np.random.uniform(size=nn).astype(A.type), ctx)
b_t = tvm.nd.array(np.zeros(nn, dtype=A.dtype), ctx)
c_t = tvm.nd.array(np.zeros(nn, dtype=A.dtype), ctx)
add_func(a_t, b_t, c_t)
对于预测部署,可以将计算逻辑编译为DSO:
from tvm.contrib import cc
# serialization
add_func.save('./add_kernel.o')
cc.create_shared('./for_infer.so', ['./add_kernel.o'])
# load for inference
m = tvm.runtime.load_module('./for_infer.so')
add_func = m['add'] # load add kernel func
add_func(a_t, b_t, c_t) # infer
对于model的序列化和加载的例子:
# Resnet18 workload
resnet18_mod, resnet18_params = relay.testing.resnet.get_workload(num_layers=18)
# build
with relay.build_config(opt_level=3):
_, resnet18_lib, _ = relay.build_module.build(resnet18_mod, "cuda", params=resnet18_params)
# export library
file_name = "./deploy.so"
resnet18_lib.export_library(file_name)
# load it back
loaded_lib = tvm.runtime.load_module(file_name)
#infer
data = np.random.uniform(-1, 1, size=input_shape(mod)).astype("float32")
ctx = tvm.gpu()
gmod = graph_runtime.GraphModule(loaded_lib["default"](ctx))
gmod.set_input("data", data)
gmod.run()
out = gmod.get_output(0).asnumpy()
TVM中对训练是如何支持的?
解答:TVM支持训练包括如下几个核心模块
自动微分 auto-diff
TVM中提供了
grads = te.gradient(out, inputs)
接口,实现反向梯度的自动求导。但目前仍然是只是一个实现性功能
TVM的动态shape是如何实现的?
解答:理解TVM的动态shape实现机制,首先我们先看下:从用户的角度,动态shape怎么使用。
import tvm
import numpy as np
# 组网
n, m = te.size_var("n"), te.size_var("m")
A = te.placeholder((n,m), name="A")
k = te.reduce_axis((0, m), "k")
B = te.compute((n,),lambda i:te.sum(A[i,k], axis=k), name="B")
# 编译
s = te.create_schedule(B.op)
net = tvm.build(s, [A, B, n, m])
# 执行
def run(n, m):
ctx = tvm.cpu(0)
a = tvm.nd.array(np.random.uniform(size=[n,m]).astype(A.dtype), ctx)
b = tvm.nd.array(np.zeros((n,)).astype(A.dtype), ctx)
return net(a, b, n, m)
run(4, 6)
run(10, 16)
TVM提供了便捷的debug机制,可以直接打印查看中间编译的函数代码:
print(str(tvm.lower(s, [A, B])))
"""
primfn(A_1: handle, B_1: handle) -> ()
attr = {"global_symbol": "main", "tir.noalias": True}
buffers = {B: Buffer(B_2: Pointer(float32), float32, [n: int32], [stride: int32], type="auto"),
A: Buffer(A_2: Pointer(float32), float32, [n, m: int32], [stride_1: int32, stride_2: int32], type="auto")}
buffer_map = {A_1: A, B_1: B} {
for (i: int32, 0, n) {
B_2[(i*stride)] = 0f32
for (k: int32, 0, m) {
B_2[(i*stride)] = ((float32*)B_2[(i*stride)] + (float32*)A_2[((i*stride_1) + (k*stride_2))])
}
}
}
"""
也可以查看build之后的LLVM代码:
print(m.get_source())
2. 安装和体验TVM
1. clone代码
拉取仓库
git clone --recursive https://github.com/apache/tvm tvm
拉取子仓库
git submodule init
git submodule update
2. docker镜像
拉取镜像
docker pull tvmai/ci-gpu
或者
docker pull tvmai/ci-cpu
启动容器
cd tvm
./docker/bash.sh tvmai/ci-gpu
3. 编译TVM
- 编译命令
mkdir build
cd build
cp ../cmake/config.cmake .
cmake ..
make -j$(nproc)
4. 配置环境变量
```bash
export TVM_HOME=/workspace/tvm
export PYTHONPATH=$TVM_HOME/python:${PYTHONPATH}
```
源码研习 — TVM中的IR设计与技术实现的更多相关文章
- Mybatis源码研究7:缓存的设计和实现
Mybatis源码研究7:缓存的设计和实现 2014年11月19日 21:02:14 酷酷的糖先森 阅读数:1020 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog ...
- netty源码分析 - Recycler 对象池的设计
目录 一.为什么需要对象池 二.使用姿势 2.1 同线程创建回收对象 2.2 异线程创建回收对象 三.数据结构 3.1 物理数据结构图 3.2 逻辑数据结构图(重要) 四.源码分析 4.2.同线程获取 ...
- 将Android源码导入eclipse中的方法以及编译Android源码指定模块
本文博客地址:http://blog.csdn.net/qq1084283172/article/details/53365659 将android源码导入eclipse.androidstudio. ...
- 源码解析.Net中IConfiguration配置的实现
前言 关于IConfituration的使用,我觉得大部分人都已经比较熟悉了,如果不熟悉的可以看这里.因为本篇不准备讲IConfiguration都是怎么使用的,但是在源码部分的解读,网上资源相对少一 ...
- [源码解析] PyTorch 分布式 Autograd (1) ---- 设计
[源码解析] PyTorch 分布式 Autograd (1) ---- 设计 目录 [源码解析] PyTorch 分布式 Autograd (1) ---- 设计 0x00 摘要 0x01 分布式R ...
- mysql-5.5.28源码安装过程中错误总结
介绍一下关于mysql-5.5.28源码安装过程中几大错误总结,希望此文章对各位同学有所帮助.系统centOS 6.3 mini (没有任何编译环境)预编译环境首先装了众所周知的 cmake(yum ...
- TCP/IP源码(59)——TCP中的三个接收队列
http://blog.chinaunix.net/uid-23629988-id-3482647.html TCP/IP源码(59)——TCP中的三个接收队列 作者:gfree.wind@gmai ...
- 2018-10-08 Java源码英翻中进展-内测上线
创建了一个子域名: http://translate.codeinchinese.com/ 欢迎试用, 如有建议/发现问题欢迎在此拍砖: program-in-chinese/code_transla ...
- 2018-09-24 Java源码英翻中网页演示
在线演示地址: 源代码翻译 两部分如下. 独立的Java代码翻译库 续前文代码翻译尝试-使用Roaster解析和生成Java源码 源码库: program-in-chinese/java_code_t ...
- Spring源码解析一:IOC容器设计
一.IOC接口设计 IOC容器设计的源码主要在spring-beans.jar.spring-context.jar这两个包中.IOC容器主要接口设计如下: 这里的接口设计有两条主线:BeanFact ...
随机推荐
- 学习Source Generators之输出生成的文件
上一篇文章学习了通过获取和解析swagger.json的内容,来生成API的请求响应类. 但是其中无法移动与编辑. 那么本文将介绍如何输出生成的文件. EmitCompilerGeneratedFil ...
- 【开源三方库】Aki:一行代码极简体验JS&C++跨语言交互
开源项目 OpenHarmony 是每个人的 OpenHarmony 一.简介 OpenAtom OpenHarmony(以下简称"OpenHarmony")的前端开发语言是A ...
- Go 语言数组基础教程 - 数组的声明、初始化和使用方法
数组用于在单个变量中存储相同类型的多个值,而不是为每个值声明单独的变量. 声明数组 在Go中,有两种声明数组的方式: 使用var关键字: 语法 var array_name = [length]dat ...
- centos部署Django三:编写相关配置文件及启动服务
1. 进入到项目的根目录,编写 uwsgi.xml 配置文件 *:centos用的不是 uwsgi.ini,而是 uwsgi.xml <uwsgi> <socket>127.0 ...
- Blocks(单调栈)
题干中说每次选择一个大于k的数,还要选他左右两个数其中之一加上一,最后问你最长的每个数不小于K的子序列. 这些都是障眼法,其实就是问你最长的平均值大于或等于K的最长子序列,这样就明朗了. 接下来就是找 ...
- mmcls/mmdet模型部署至 TorchServe
mmcls/mmdet模型部署至 TorchServe 官方教程:模型部署至 TorchServe - MMClassification 0.23.2 文档 接口说明: serve/inference ...
- Swin Transformer安装记录(国内源,飞快)
0. 设备环境 ubuntu--20.10 GPU--3080 cuda--11.0 torch--1.7.0 mmcv--1.3.8 mmdetection--2.11.0 所有的git的项目,都可 ...
- vue2.0中watch用法
watch:观测Vue实例上的数据变动,对应一个对象,键:就是需要监测的那个东西,值:1.可以是当键变化时执行的函数,有两个参数,第一个是变化前的值,第二个是变化后的值.2.可以是函数名,得用单引号包 ...
- boltdb 介绍
介绍 BoltDB 是一个用 Go 语言编写的嵌入式键/值数据库.以下是关于 BoltDB 的一些基本介绍: 键/值存储: BoltDB 为应用程序提供了简单的键/值存储接口. 事务: BoltDB ...
- 力扣166(java)-分数到小数(中等)
题目: 给定两个整数,分别表示分数的分子 numerator 和分母 denominator,以 字符串形式返回小数 . 如果小数部分为循环小数,则将循环的部分括在括号内. 如果存在多个答案,只需返回 ...