技术好文:TVM适配NN编译Compiler缺陷

简介: 技术好文:TVM适配NN编译Compiler缺陷

TVM适配NN编译Compiler缺陷


内容纲要


前言


TVM针对VTA的编译流程


自定义VTA架构:TVM的缺陷与性能瓶颈


TVM缺陷与瓶颈


缺陷一:SRAM配置灵活性差


缺陷二:计算阵列配置僵硬


缺陷三:网络支持少


TVM源码修改之静态调度搜索算法


前言


前文NN编译栈之TVM研究报告深度分析TVM的源码结构,编译器特点。本文介绍TVM的当前缺陷以及如何修改源代码弥补缺陷并适配自己开发的神经网络加速器。不久会在GitHub上开源自己的适配修改工作并向TVM仓库提交新的版本。


现在主流的深度学习训练框架是Caffe/PyTorch/TensorFlow/MxNet等,对CPU/CUDA支持得很好。如果想把训练好的神经网络部署到其它的终端设备,这就带了几个挑战:


主流框架不支持ARM/FPGA/ASIC


嵌入式终端不需要训练功能,对前向推理的速度有极大的要求


嵌入式终端性能/内存/存储有限,主流框架的臃肿不适合部署


终端指令集,架构没有统一标准,开发部署难度很大


TVM 是深度学习系统的编译器堆栈。旨在缩小以算力为重点的深度学习框架,以性能和效率为重点的硬件后端之间的差距。 TVM与深度学习框架协同工作,为不同的后端提供端到端编译。TVM支持主流的深度学习前端框架,包括TensorFlow, MXNet, PyTorch, Keras, CNTK;同时能够部署到宽泛的硬件后端,包括CPUs, server //代码效果参考:http://hnjlyzjd.com/hw/wz_25052.html

GPUs, mobile GPUs, and FPGA-based accelerators。

如果想自己设计一款深度学习处理器(VTA)并兼容TVM编译栈,那么该如何开发?TVM编译器的扩展性如何?如何修改TVM编译器,适配自定义的深度学习处理器?


TVM针对VTA的编译流程


TVM定义了VTA的指令集,体系结构。为了实现硬件的通用化计算,VTA硬件参考RISC指令集,按照Fetch—>Load—>Compute—>Store模式,将所有操作划分到这种粒度的计算,不设计专用复杂的计算模式。


VTA硬件体系结构


同时,TVM定义了VTA硬件的可重构约束。VTA主要对硬件的数据位宽,SRAM 大小,计算阵列大小进行配置,而不能更改大的计算架构,数据流,控制流等。


VTA配置


假设VTA配置已经固定下来,TVM会按照如下流程编译VTA:


定义前端网络:对接MxNet。


1


2


3


4


5


6


7


8


9


10


11


12


13


#"tvm/vta/tutorials/frontend/deploy_resnet_on_vta.py"


# Name of Gluon model to compile


# The start_pack and stop_pack labels indicate where


# to start and //代码效果参考:http://hnjlyzjd.com/hw/wz_25050.html

end the graph packing relay pass: in other words

# where to start and finish offloading to VTA.


model = "resnet18_v1"


start_pack = "nn.max_pool2d"


stop_pack = "nn.global_avg_pool2d"


...


dtype_dict = {"data": 'float32'}


shape_dict = {"data": (env.BATCH, 3, 224, 224)}


# Get off the shelf gluon model, and convert to relay


gluon_model = vision.get_model(model, pretrained=True)


将前端网络转为自己设计的AST Graph/Relay IR:“relay.frontend.from_mxnet“函数


1


2


3


4


5


6


#"tvm/vta/tutorials/frontend/deploy_resnet_on_vta.py"


# Start front end compilation


mod, params = relay.frontend.from_mxnet(gluon_model, shape_dict)


# Update shape and type dictionary


shape_dict.update({k: v.shape for k, v in params.items()})


dtype_dict.update({k: str(v.dtype) for k, v in params.items()})


量化网络:这里应该采用的post-quantization方法,不需要重新训练或autotune。


1


2


3


4


5


#"tvm/vta/tutorials/frontend/deploy_resnet_on_vta.py"


# Perform quantization in Relay


with relay.quantize.qconfig(global_scale=8.0,


skip_conv_layers=【0】):


relay_prog = relay.quantize.quantize(mod【"main"】, params=params)


图打包和常量展开:预先计算常量节点,减少参数量和计算量。


1


2


3


4


5


6


7


8


9


10


11


#"tvm/vta/tutorials/frontend/deploy_resnet_on_vta.py"


# Perform graph packing and constant folding for VTA target


if target.device_name == "vta":


assert env.BLOCK_IN == env.BLOCK_OUT


relay_prog = graph_pack(


relay_prog,


env.BATCH,


env.BLOCK_OUT,


env.WGT_WIDTH,


start_name=start_pack,


stop_name=stop_pack)


VTA编译,生成Complied PackedFunc动态链接库。


1


2


3


4


5


#"tvm/vta/tutorials/frontend/deploy_resnet_on_vta.py"


with vta.build_config():


graph, lib, params = relay.build(


relay_prog, target=target,


params=params, target_host=env.target_host)


VTA编译这步是研究的重点,分析TVM如何针对特定硬件进行编译,步骤如下:


将网络分解为针对VTA硬件的卷机计算图:


卷机topi.nn.conv2d


移位topi.right_shift


bias相加topi.add


溢出处理my_clip


1


2


3


4


5


6


7


8


9


# tvm/vta/tests/python/integration/test_benchmark_topi_conv2d.py


# Define base computation schedule


with target:


res = topi.nn.conv2d(


data, kernel, (wl.hstride, wl.wstride), (wl.hpad, wl.wpad), (1, 1),


layout, env.acc_dtype)


res = topi.right_shift(res, 8)


res = topi.add(res, bias)


res = my_clip(res, 0, (1 [ env.OUT_WIDTH - 1) - 1)


来查看VTA的卷积是如何定义的:


传入数据(data,kernel)及卷机形状定义(strides,padding)


定义占位符(类似TensorFlow)reduce_axis


卷积张量定义tvm.compute


1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17


18


19


20


21


22


23


24


25


26


27


28


29


30


31


# tvm/vta/python/vta/top/vta_conv2d.py


@autotvm.register_topi_compute(topi.nn.conv2d, 'vta', 'direct')


def _declaration_conv2d(cfg,


data,


kernel,


strides,


padding,


dilation,


layout,


out_dtype):


""" Packed conv2d function."""


if not is_packed_layout(layout):


raise topi.InvalidShapeError()


assert dilation == (1, 1)


d_i = tvm.reduce_axis((0, kshape【2】), name='d_i')


d_j = tvm.reduce_axis((0, kshape【3】), name='d_j')


k_o = tvm.reduce_axis((0, ishape【1】), name='k_o')


k_i = tvm.reduce_axis((0, ishape【-1】), name='k_i')


hstride, wstride = strides


res = tvm.compute(


oshape,


lambda b_o, c_o, i, j, b_i, c_i: tvm.sum(


pad_data【b_o, k_o, ihstride+d_i, jwstride+d_j, b_i, k_i】.astype(out_dtype)


kernel【c_o, k_o, d_i, d_j, c_i, k_i】.astype(out_dtype),


axis=【k_o, d_i, d_j, k_i】),


name="res", tag="conv2d_dense")


cfg.add_flop(2 np.prod(topi.util.get_const_tuple(oshape))


kshape【2】 kshape【3】 ishape【1】 ishape【-1】)


return res


生成卷积层的调度方案


1


2


3


# tvm/vta/tests/python/integration/test_benchmark_topi_conv2d.py


# Derive base schedule


s = topi.generic.schedule_conv2d_nchw(【res】)


看一下生成的调度器是什么样子:


绑定数据与对应buffer


DMA搬运


循环分片:包括Channels、Height、Width</li>


地址生成:数据加载存储地址


虚拟线程:掩盖访存延迟


1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17


18


19


20


21


22


23


24


25


26


27


# tvm/vta/tests/python/integration/test_benchmark_topi_conv2d.py


print(vta.lower(s, 【data, kernel, bias, res】, simple_mode=True))


##调度器部分代码


// attr 【res_conv】 storage_scope = "local.acc_buffer"


// attr 【data_buf】 storage_scope = "local.inp_buffer"


// attr 【kernel_buf】 storage_scope = "local.wgt_buffer"


produce res {


vta.coproc_dep_push(3, 2)


vta.coproc_dep_push(3, 2)


for (ic.outer, 0, 8) {//input channel


// attr 【iter_var(vta, , vta)】 coproc_scope = 1


vta.coproc_dep_pop(2, 1)


produce data_buf {


VTALoadBuffer2D(tvm_thread_context(VTATLSCommandHandle()), data, ((((ic.outer196) + (i2.outer98)) + (max((1 - (i2.outer7)), 0)14)) - 14), 14, ((9 - max((1 - (i2.outer7)), 0)) - max(((i2.outer7) - 6), 0)), 14, 1, max((1 - (i2.outer7)), 0), 1, max(((i2.outer7) - 6), 0), 0, 2)


}


produce kernel_buf {


VTALoadBuffer2D(tvm_thread_context(VTATLSCommandHandle()), kernel, ((i1.outer.outer1152) + (ic.outer9)), 9, 8, 72, 0, 0, 0, 0, 0, 1)


}


vta.coproc_dep_push(1, 2)


// attr 【iter_var(vta, , vta)】 coproc_scope = 1


vta.coproc_dep_pop(2, 1)


produce data_buf {


VTALoadBuffer2D(tvm_thread_context(VTATLSCommandHandle()), data, ((((ic.outer196) + (i2.outer98)) + (max((1 - (i2.outer7)), 0)14)) - 14), 14, ((9 - max((1 - (i2.outer7)), 0)) - max(((i2.outer7) - 6), 0)), 14, 1, max((1 - (i2.outer7)), 0), 1, max(((i2.outer7) - 6), 0), 144, 2)


}


produce kernel_buf {


VTALoadBuffer2D(tvm_thread_context(VTATLSCommandHandle()), kernel, (((i1.outer.outer1152) + (ic.outer9)) + 576), 9, 8, 72, 0, 0, 0, 0, 72, 1)


}


然后看一下调度器是什么计算的


加载默认的分片配置文件cfg


绑定数据与对应buffer:set_scope


DMA:compute_at和cache_read


循环分片:apply


虚拟线程:split


指令生成:pragma


1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17


18


19


20


21


22


23


24


25


26


27


28


29


30


31


32


33


34


# tvm/vta/python/vta/top/vta_conv2d.py


@autotvm.register_topi_schedule(topi.generic.schedule_conv2d_nchw, 'vta', 'direct')


def _schedule_conv2d(cfg, outs):


# 绑定数据与对应buffer


cdata = s.cache_read(data, env.inp_scope, 【conv2d_stage】)


ckernel = s.cache_read(kernel, env.wgt_scope, 【conv2d_stage】)


# DMA cache数据


s【conv2d_stage】.set_scope(env.acc_scope)


# tile 循环分片


x_bo, x_co, x_i, x_j, x_bi, x_ci = s【output】.op.axis


x_co0, x_co1 = cfg【'tile_co'】.apply(s, output, x_co)


x_i0, x_i1 = cfg【'tile_h'】.apply(s, output, x_i)


x_j0, x_j1 = cfg【'tile_w'】.apply(s, output, x_j)


s【output】.reorder(x_bo, x_i0, x_co0, x_j0, x_co1, x_i1, x_j1, x_bi, x_ci)


store_pt = x_j0


# set all compute scopes DMA


for tensor in cache_read_ewise:


s【tensor】.compute_at(s【output】, store_pt)


s【tensor】.pragma(s【tensor】.op.axis【0】, env.dma_copy)


# virtual threading along output channel axes 虚拟线程


if cfg【'ocnthread'】.val > 1:


, v_t = s【output】.split(x_co0, factor=cfg【'oc_nthread'】.val)


s【output】.reorder(v_t, x_bo)


s【output】.bind(v_t, tvm.thread_axis("cthread"))


# Use VTA instructions 指令生成


s【cdata】.pragma(s【cdata】.op.axis【0】, env.dma_copy)


s【ckernel】.pragma(s【ckernel】.op.axis【0】, env.dma_copy)


s【conv2d_stage】.tensorize(x_bi, env.gemm)


s【output】.pragma(x_co1, env.dma_copy)


return s


到这一步Complied PackedFunc已经生成好了,可以给VTA硬件编程使用


1


2


3


#"tvm/vta/tutorials/frontend/deploy_resnet_on_vta.py"


vta.reconfig_runtime(remote)


vta.program_fpga(remote, bitstream=None)


前向测试:resnet18_v1分类结果并统计访存计算数据。


1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


resnet18_v1


Execution statistics:


inp_load_nbytes : 48864512


wgt_load_nbytes : 1695547392


acc_load_nbytes : 6723584


uop_load_nbytes : 36


out_store_nbytes: 1680896


gemm_counter : 1655808


alu_counter : 286160


resnet18_v1 prediction


#1: tiger cat


#2: Egyptian cat


#3: tabby, tabby cat


#4: lynx, catamount


#5: weasel


总的来说,TVM针对前端网络和VTA的编译,调用流程如下。其中重点是调度方案的生成,对不同架构的VTA适配最需要的就是定义对应架构的调度方案。


定义前端网络


将前端网络转为自己设计的AST Graph/Relay IR


量化网络


图打包和常量展开


VTA编译,生成Complied PackedFunc动态链接库


生成卷积层的调度方案


VTA硬件编程


前向测试


自定义VTA架构:TVM的缺陷与性能瓶颈


自定义深度学习处理器VTA架构,可以分为两种设计方案:


魔改VTA架构,指令集:需要对TVM编译器作大规模源码更改,目前TVM的针对VTA的编译是固定指令集与架构,扩展性较差。


保持VTA架构不变,修改配置:修改硬件的数据位宽,SRAM 大小,计算阵列大小进行配置,而不能更改大的计算架构,数据流,控制流等。


本文的目的是分析TVM的缺陷与瓶颈,所以先从最简单的第二种方案研究。VTA架构配置更改在"tvm/vta/config/vta_config.json",参考前文进行对应的修改。


1


2


3


4


5


6


7


8


9


10


11


12


13


{


"TARGET" : "sim",


"HW_VER" : "0.0.1",


"LOG_INP_WIDTH" : 3,


"LOG_WGT_WIDTH" : 3,


"LOG_ACC_WIDTH" : 5,


"LOG_BATCH" : 0,


"LOG_BLOCK" : 4,


"LOG_UOP_BUFF_SIZE" : 15,


"LOG_INP_BUFF_SIZE" : 15,


"LOG_WGT_BUFF_SIZE" : 18,


"LOG_ACC_BUFF_SIZE" : 17


}


TVM缺陷与瓶颈


缺陷一:SRAM配置灵活性差


VTA配置SRAM大小的时候不是任意可配的,假如固定计算位宽和计算规模,理论上讲BUFF_SIZE只要大于一个最小值就可以。但其实不是这样,BUFF_SIZE必须固定,计算方式本文推定如下。总的来说,SRAM配置不够灵活,如果芯片/FPGA资源过小或过大,都不好兼容TVM。


1


2


3


4


VTA_UOP_BUFF_SIZE=VTA_LOG_UOP_BUFF_DEPTH+VTA_LOG_UOP_WIDTH-3=15


VTA_INP_BUFF_SIZE=VTA_LOG_INP_BUFF_DEPTH+VTA_LOG_BLOCK_IN+VTA_LOG_INP_WIDTH-3=


VTA_WGT_BUFF_DEPTH=VTA_LOG_WGT_BUFF_DEPTH+VTA_LOG_BLOCK_OUT + VTA_LOG_BLOCK_IN + VTA_LOG_WGT_WIDTH - 3


VTA_ACC_BUFF_DEPTH=VTA_LOG_ACC_BUFF_DEPTH+VTA_LOG_BATCH + VTA_LOG_BLOCK_OUT + VTA_LOG_ACC_WIDTH - 3


VTA_LOG_UOP_BUFF_DEPTH必须固定为15,没有什么道理可讲。TVM固定UOP_BUFF_DEPTH必须为8192


LOG_INP_BUFF_SIZE/LOG_ACC_BUFF_SIZE必须保持上面的计算方式,不可大于计算值。


LOG_WGT_BUFF_SIZE可小于计算值,灵活性还算可以。


缺陷二:计算阵列配置僵硬


如果修改计算阵列,比如16×16到32×32或者64×64,那么VTA编译依然按照16×16的调度方案编译新的架构,修改的配置毫无意义。同时新的架构性能比16×16还差。如下图所示,32×32访存比16×16高出95倍,带来极大的性能损失。


1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


resnet18_v1:16x16 架构


Execution statistics:


inp_load_nbytes : 5549568


wgt_load_nbytes : 12763136


acc_load_nbytes : 30720


uop_load_nbytes : 1540


out_store_nbytes: 1680896


gemm_counter : 6623232


alu_counter : 572320


resnet18_v1 prediction for sample 0


#1: tiger cat


#2: Egyptian cat


#3: tabby, tabby cat


#4: lynx, catamount


#5: weasel


1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


resnet18_v1:32x32架构


Execution statistics:


inp_load_nbytes : 48864512


wgt_load_nbytes : 1695547392


acc_load_nbytes : 6723584


uop_load_nbytes : 36


out_store_nbytes: 1680896


gemm_counter : 1655808


alu_counter : 286160


resnet18_v1 prediction for sample 0


#1: tiger cat


#2: Egyptian cat


#3: tabby, tabby cat


#4: lynx, catamount


#5: weasel


缺陷三:网络支持少


虽然TVM宣称支持主流的深度学习框架及大量神经网络模型,但是当前TVM(*本文指VTA,ARM/CUDA没有深入研究)支持/适配的网络较少,目前本文在不改变任何代码的情况下只跑通了默认的resnet18_v1。支持其它网络模型较少的原因如下:


Relay量化目前只支持有限的操作类型


VTA目前只支持有限的操作类型


Relay量化Bug:如果部署resnet50_v1,会报如下错误:


1


Check failed: lhs->dtype == dtype (int8 vs. int32)


1


2


3


4


5


6


7


8


9


10


11


Separate quantization code base into different files: partition.cc, annotate.cc, realize.cc


Change rewrite_for_vta to extra partition pass and enable it by default


Change annotation.force_cast(x) to annotation.cast_hint(x, dtype)


Remove qconfig.store_lowbit_output and enable it by default


Fixed accuracy of models like mobilenet:


resnet18_v1(8-16bit): 69.29%


resnet18_v1(8-32bit): 69.29%


resnet34_v1: 73.33%


resnet50_v1: 74.78%


resnet101_v1: 75.66%


mobilenetv2_1.0: 66.64%


TVM源码修改之静态调度搜索算法


VTA的架构配置僵化与性能瓶颈原因在于TVM编译器僵化的调度方案。僵化原因如上文VTA编译流程所说,VTA采用写死的分片配置文件cfg,无论怎么修改架构,分片方案都不变。所以性能瓶颈出现。


1


2


3


# tvm/vta/python/vta/top/vta_conv2d.py


@autotvm.register_topi_schedule(topi.generic.schedule_conv2d_nchw, 'vta', 'direct')


def _schedule_conv2d(cfg, outs):


有解决方案吗?TVM的设计方案是AutoTVM,一种在线的调度空间搜索算法:


修改VTA配置


联机FPGA:不支持仿真模式


对调度方案进行迭代,大概1000次(可配置)


找寻在所有迭代方案中性能最优的调度器。


这种方案不支持仿真模式,因为仿真迭代速度太慢(仿真 VS FPGA类似CPU VS GPU)。本文对这种方案表示不解,原因如下:


如果是做芯片ASIC,设计阶段的在线迭代会很麻烦。


如果硬件VTA的架构固定,调度方案是确定解。完全可以实现一套静态的调度方案,根据VTA配置,在编译阶段就可以确定最优的调度。


本文实现了这种静态调度搜索算法,针对前端网络与VTA硬件在编译阶段搜索最优的调度器。实现性能的大幅提升,不需要FPGA这种在线AutoTVM迭代方案。编译时间的增加可忽略不计。


1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


resnet18_v1:32x32架构 静态调度搜索算法


Execution statistics:


inp_load_nbytes : &

相关文章
|
5月前
|
XML 存储 数据格式
Kotlin Fuel库:图像下载过程中的异常处理
Kotlin Fuel库:图像下载过程中的异常处理
|
网络安全 图形学 UED
Rhino ceros 7.25:3D设计软件的全新进化!+Rhino ceros 全版本安装包
Rhino ceros 7.25:3D设计软件的全新进化!+Rhino ceros 全版本安装包
|
人工智能 JSON 前端开发
一眼就懂的TVM7大功能
简明扼要的介绍TVM的7大功能
|
缓存 并行计算 Java
不惧编译error,编译Tensorflow源码(一)
编译tensorflow遇到的bug本来就多,在Windows平台上bugs更是加大力度。明明官方教程中在配置完环境后只需执行两行bazel命令,第一行命令却产生不少error。笔者踩了不少坑后,总结出了一些解决方法形成此教程。
不惧编译error,编译Tensorflow源码(一)
|
并行计算 TensorFlow 算法框架/工具
不惧编译error,编译Tensorflow源码(二)
编译tensorflow遇到的bug本来就多,在Windows平台上bugs更是加大力度。明明官方教程中在配置完环境后只需执行两行bazel命令,第一行命令却产生不少error。笔者踩了不少坑后,总结出了一些解决方法形成此教程。
不惧编译error,编译Tensorflow源码(二)
|
开发工具 C语言 Windows
恒玄BES2300开发编译环境搭建
恒玄BES2300开发编译环境搭建
461 0
恒玄BES2300开发编译环境搭建
|
编译器 Linux TensorFlow
不惧编译error,编译Tensorflow源码(三)
编译tensorflow遇到的bug本来就多,在Windows平台上bugs更是加大力度。明明官方教程中在配置完环境后只需执行两行bazel命令,第一行命令却产生不少error。笔者踩了不少坑后,总结出了一些解决方法形成此教程。
|
TensorFlow API 算法框架/工具
TF版本升级问题集合:成功解决由于不同的TensorFlow版本之间某些函数的用法引起的错误
TF版本升级问题集合:成功解决由于不同的TensorFlow版本之间某些函数的用法引起的错误
|
并行计算 openCL PyTorch