编者按:半年前,上海交大陈天奇团队开源了端到端ir堆栈工具tvm,可以帮用户优化深度学习过程中的硬件配置,缓解了当前大多数计算机gpu在面对深度学习时表现出来的性能不足。近日,团队的一名学生郑怜悯带来了项目的新进展,他将tvm用于移动端常见的arm gpu,提高了移动设备对深度学习的支持能力。
以下是论智对原文的翻译:
随着深度学习不断取得进展,开发者们对在移动设备上的部署神经网络的需求也与日俱增。和我们之前在桌面级gpu上做过的尝试类似,把深度学习框架移植到移动端需要做到这两点:够快的inference速度和合理的能耗。但是,现在的大多数dl框架并不能很好地支持移动端gpu,因为它们和桌面级gpu在架构上存在巨大差异。为了在移动端做深度学习,开发者们往往要对gpu做一些特殊优化,而这类额外工作也加大了对gpu的压力。
tvm是一个端到端的ir堆栈,它可以解决学习过程中的资源分配问题,从而轻松实现硬件优化。在这篇文章中,我们将展示如何用tvm/nnvm为arm mali gpu生成高效kernel,并进行端到端编译。在对mali-t860 mp4的测试中,我们的方法在vgg-16上比arm compute library快了1.4倍,在mobilenet上快了2.2倍。这些提升在图像处理和运算上均有体现。
mali midgard gpu
目前,移动领域最常见的3大图形处理器为高通的adreno、英国powervr和arm的嵌入式图形处理器mali。我们的测试环境是配有mali-t860 mp4 gpu的开发板firefly-rk3399,所以下面我们主要关注mali t8xx的表现。
架构
t860和t880是mali系列的两款高端gpu,下图是具体配置。它们有16个着色器核心(shader core),每个核心内包含2—3条运算管道、1条加载/存储管道和1条纹理管道(即triple pipeline架构)。其中运算管道中的alu(算数逻辑单元)又包含4个128-bit的矢量单元和一个标量单元。
我们用opencl编写程序。当映射到opencl模型时,每个着色器核心会执行一个或多个工作组,它们的上限是并行执行384个线程,通常一个工作组对应一个线程。mali系列gpu使用的是vliw架构(超长指令集架构),因此每个指令包含多个操作;同时,它也用了simd(单指令流多数据流),所以大多数运算运算指令可以同时执行多个数据流。
和nvidia gpu的区别
在用tvm优化gpu前,我们先看一看mali gpu和nvidia gpu的区别:
nvidia gpu的存储系统架构一般分为全局内存、共享内存、寄存器三层,在实践中我们通常会把数据复制到共享内存;而mali gpu只有一个统一的全局内存,它不需要制作副本提升性能,因为这个内存是和cpu共享的,所以cpu和gpu之间也不需要复制;
mali midgard gpu基于simd设计,所以需要用到矢量;而在nvidia cuda中,gpu的并行处理是通过simt实现的,所以它对矢量没有那么高的要求。需要注意的是,mali bifrost架构的图形处理器新添加了quad based vectorization技术,即允许四个线程一起被执行,它也不太需要矢量;
mali gpu中的每一个线程都有独立的程序计数器,即warp size=1,所以branch divergence不是问题。
优化:以卷积层为例
卷积层是许多深度神经网络的核心,也占用了大部分计算资源。所以我们以卷积层为例,谈谈tvm在pack、tile、unroll、向量化中的优化应用。
im2col+gemm
im2col是卷积计算的一种常用方法,它会把问题转换成一个矩阵,然后调用gemm完成矩阵乘法运算。这种方法的优点是便于和高度优化的blas库结合,缺点是会耗费大量内存。
spatial packing
所以我们换了一种方法,先计算卷积,再逐步应用优化技术。以vgg-16中的卷积层为例(如下图所示),inference的batch size=1。
为了提供一个对照组,我们列出了arm compute library的数据。
pack和tile是两个调整内存的常见指令。其中tile是把数据划分成片,使每一片适合共享内存的使用;而pack则是对输入矩阵重新布局(内存对齐),方便我们按顺序读取数据。
我们在输入图像的宽度和filter矩阵的co维上使用了tile(tvm.compute):
# set tiling factor
vh = 1
vw = vc = 4
# get input shape
_, ci, ih, iw = data.shape
co, ci, kh, kw = kernel.shape
th = ih + 2 * h_pad
tw = iw + 2 * w_pad
# calc output shape
oh = (ih + 2*h_pad - kh) // h_str + 1
ow = (iw + 2*w_pad - kw) // w_str + 1
# data shape after packing
dvshape = (n, th // (vh*h_stride), tw // (vw*w_stride), ci, vh*h_stride+hcat, vw*w_stride+wcat)
# kernel shape after packing
kvshape = (co // vc, ci, kh, kw, vc)
ovshape = (n, co // vc, oh // vh, ow // vw, vh, vw, vc)
oshape = (n, co, oh, ow)
# define packing
data_vec = tvm.compute(dvshape, lambda n, h, w, ci, vh, vw:
data_pad[n][ci][h*vh*h_stride+vh][w*vw*w_stride+vw], name='data_vec')
kernel_vec = tvm.compute(kvshape, lambda co, ci, kh, kw, vc:
kernel[co*vc+vc][ci][kh][kw], name='kernel_vec')
# define convolution
ci = tvm.reduce_axis((0, ci), name='ci')
kh = tvm.reduce_axis((0, kh), name='kh')
kw = tvm.reduce_axis((0, kw), name='kw')
conv = tvm.compute(ovshape, lambda n, co, h, w, vh, vw, vc:
tvm.sum(data_vec[n, h, w, ci, vh*h_stride+kh, vw*w_stride+kw].astype(out_dtype) *
kernel_vec[co, ci, kh, kw, vc].astype(out_dtype),
axis=[ci, kh, kw]), name='conv')
# unpack to correct layout
output = tvm.compute(oshape, lambda n, co, h, w:
conv[n][co//vc][h/vh][w//vw][h%vh][w%vw][co%vc],
name='output_unpack', tag='direct_conv_output')
用以下命令检查定义的ir:
print(tvm.lower(s, [data, kernel, output], simple_mode=true))
选择卷积的部分:
produce conv {
for (co, 0, 64) {
for (h, 0, 56) {
for (w, 0, 14) {
for (vw.init, 0, 4) {
for (vc.init, 0, 4) {
conv[((((((((co*56) + h)*14) + w)*4) + vw.init)*4) + vc.init)] = 0.000000f
}
}
for (ci, 0, 256) {
for (kh, 0, 3) {
for (kw, 0, 3) {
for (vw, 0, 4) {
for (vc, 0, 4) {
conv[((((((((co*56) + h)*14) + w)*4) + vw)*4) + vc)] = (conv[((((((((co*56) + h)*14) + w)*4) + vw)*4) + vc)] + (data_vec[(((((((((h*14) + w)*256) + ci)*3) + kh)*6) + kw) + vw)]*kernel_vec[((((((((co*256) + ci)*3) + kh)*3) + kw)*4) + vc)]))
}
}
}
}
}
}
}
}
}
kernel 1:绑定线程
在tvm中,我们先计算,再计划(schedule),这便于分离算法和实现细节。
如代码所示,我们简单把axes坐标轴对应到gpu线程,之后就能在mali gpu上跑代码了。
# helper function for binding thread
def tile_and_bind3d(s, tensor, z, y, x, z_factor=2, y_factor=none, x_factor=none):
tile and bind 3d
y_factor = y_factor or z_factor
x_factor = x_factor or y_factor
zo, zi = s[tensor].split(z, z_factor)
yo, yi = s[tensor].split(y, y_factor)
xo, xi = s[tensor].split(x, x_factor)
s[tensor].bind(zo, tvm.thread_axis(blockidx.z))
s[tensor].bind(zi, tvm.thread_axis(threadidx.z))
s[tensor].bind(yo, tvm.thread_axis(blockidx.y))
s[tensor].bind(yi, tvm.thread_axis(threadidx.y))
s[tensor].bind(xo, tvm.thread_axis(blockidx.x))
s[tensor].bind(xi, tvm.thread_axis(threadidx.x))
# set tunable parameter
num_thread = 8
# schedule data packing
_, h, w, ci, vh, vw = s[data_vec].op.axis
tile_and_bind3d(s, data_vec, h, w, ci, 1)
# schedule kernel packing
co, ci, kh, kw, vc = s[kernel_vec].op.axis
tile_and_bind(s, kernel_vec, co, ci, 1)
# schedule conv
_, c, h, w, vh, vw, vc = s[conv].op.axis
kc, kh, kw = s[conv].op.reduce_axis
s[conv].reorder(_, c, h, w, vh, kc, kh, kw, vw, vc)
tile_and_bind3d(s, conv, c, h, w, num_thread, 1, 1)
_, co, oh, ow = s[output].op.axis
tile_and_bind3d(s, output, co, oh, ow, num_thread, 1, 1)
虽然有了这个schedule,我们现在可以运行代码了,但它的性能要求还是相当可怕。
kernel 2:unroll
循环展开(loop unrolling)是一个常用的优化方法,它能通过减少循环控制指令降低循环本身的开销,同时因为能消除分支以及一些管理归纳变量的代码,它也可以摊销一些分支开销,此外,它还能掩盖读取内存的延迟。在tvm中,你可以调用s.unroll(axis)实现循环展开。
# set tunable parameter
num_thread = 8
# schedule data packing
_, h, w, ci, vh, vw = s[data_vec].op.axis
tile_and_bind3d(s, data_vec, h, w, ci, 1)
!! add unroll here !!
s[data_vec].unroll(vw)
# schedule kernel packing
co, ci, kh, kw, vc = s[kernel_vec].op.axis
tile_and_bind(s, kernel_vec, co, ci, 1)
!! add unroll here !!
s[kernel_vec].unroll(kh)
s[kernel_vec].unroll(kw)
s[kernel_vec].unroll(vc)
# schedule conv
_, c, h, w, vh, vw, vc = s[conv].op.axis
kc, kh, kw = s[conv].op.reduce_axis
s[conv].reorder(_, c, h, w, vh, kc, kh, kw, vw, vc)
tile_and_bind3d(s, conv, c, h, w, num_thread, 1, 1)
!! add unroll here !!
s[conv].unroll(kh)
s[conv].unroll(kw)
s[conv].unroll(vw)
s[conv].unroll(vc)
_, co, oh, ow = s[output].op.axis
tile_and_bind3d(s, output, co, oh, ow, num_thread, 1, 1)
kernel 3:向量化(vectorization)
如前所述,为了在mali gpu上实现最佳性能,我们还要把数字转成矢量。
# set tunable parameter
num_thread = 8
# schedule data packing
_, h, w, ci, vh, vw = s[data_vec].op.axis
tile_and_bind3d(s, data_vec, h, w, ci, 1)
# unroll
s[data_vec].unroll(vw)
# schedule kernel packing
co, ci, kh, kw, vc = s[kernel_vec].op.axis
tile_and_bind(s, kernel_vec, co, ci, 1)
# unroll
s[kernel_vec].unroll(kh)
s[kernel_vec].unroll(kw)
!! vectorize here !!
s[kernel_vec].vectorize(vc)
# schedule conv
_, c, h, w, vh, vw, vc = s[conv].op.axis
kc, kh, kw = s[conv].op.reduce_axis
s[conv].reorder(_, c, h, w, vh, kc, kh, kw, vw, vc)
tile_and_bind3d(s, conv, c, h, w, num_thread, 1, 1)
# unroll
s[conv].unroll(kh)
s[conv].unroll(kw)
s[conv].unroll(vw)
!! vectorize here !!
s[conv].vectorize(vc)
_, co, oh, ow = s[output].op.axis
tile_and_bind3d(s, output, co, oh, ow, num_thread, 1, 1)
如何设置可调参数
上文中涉及的一些可调参数是可以被计算出来的,如向量vc,如果是float32,|vc|=128/32=4;如果是float16,则是128/16=8。
但由于运行时间过长,很多时候我们会无法确定最佳值。tvm使用的是网格搜索,所以如果用的是python,而不是opencl的话,我们也能快速找到最佳值。
端到端的benchmark
在这一节中,我们比较了一些流行深度神经网络在不同后端上的综合性能,测试环境是:
firefly-rk3399 4g
cpu: dual-core cortex-a72 + quad-core cortex-a53
gpu: mali-t860mp4
armcomputelibrary : v17.12
mxnet: v1.0.1
openblas: v0.2.18
我们使用nnvm和tvm进行端到端编译。
性能
imagenet上不同后端的inference速度
如上图所示,我们在imagenet测试了移动端神经网络的inference速度,发现在firefly-rk3399上,mali gpu可以比6核big.little cpu快2—4倍,我们的端到端编译速度比arm compute library快了1.4—2.2倍。在arm compute library中,我们比较了用gemm计算卷积和直接计算卷积,发现前者速度始终更快,所以在图中只展示了gemm方法的成果。
上图中也有一些数据缺失,如第二幅图不包含arm compute library上的resnet18。这是因为arm compute library的graph runtime目前不支持跳转连接,并且neon在上面的实现性能不太好。这也从侧面反映了nnvm软件栈的优势。
半精度性能
深度神经网络对精度要求不高,尤其是对于计算资源捉襟见肘的移动设备,降低精度可以加快神经网络的inference速度。我们还计算了mali gpu上的半精度浮点数。
magenet上fp16的inference速度
从理论上讲,fp16既可以实现双峰计算,又可以将内存消耗减半,从而使速度提高一倍。但是如果涉及较长的向量化和某些参数的微调,它也需要良好的输入形态。
欲打破垄断格局,国内车企该如何突破
老司机对ETC的办理有什么疑虑
英特尔AMX助华栖云多场景AI推理性能大幅提升多达96倍
小米6已经在国外开卖?高颜值+高通835,价格更给力!
英特尔:Tiger Lake处理器是世界上最适合轻薄笔记本电脑的处理器
将TVM用于移动端常见的ARM GPU,提高移动设备对深度学习的支持能力
基于霓虹灯串行级联LED选择脉冲频率和宽度实现调节电压
苹果股票犹如过山车,瞬间掉入泥潭!
西门子PLC程序的调试方法介绍
自然语言分析(NLA)是什么
使用LTspice表征电容器组
如何将新增的board的代码直接纳入app下
基于HD61202液晶显示控制器和单片机实现接口设计
如果能用光助力人类健康,岂不是很有意义?
vivoZ3确定10月17日发布 将采用水滴屏主打性价比
南卡又出新作,RunnerPro3科技派新旗舰,体验新升级
接地基础知识:PE 和 FG 有何区别?
基于薄板V型刨槽机上的OMRON PLC伺服控制系统设计浅析
索尼Walkman40周年或将现身IFA 2019上推出大量新产品
超五类和六类的水晶头有什么区别