GEMM与AutoKernel算子优化
GEMM与AutoKernel算子优化
随着AI技术的快速发展,深度学习在各个领域得到了广泛应用。深度学习模型能否成功在终端落地应用,满足产品需求,一个关键的指标就是神经网络模型的推理性能。一大波算法工程师为了算法的部署转岗算子优化工程师。优化代码并不是一件简单的事,要求工程师既要精通计算机体系架构,又要熟悉算法的计算流程,稍微有经验的深度学习推理优化工程师都成了各家公司争抢的“香饽饽”。需求多,算子优化自动化成为了未来的一大趋势。
为了方便更多的工程师进行推理优化,一个致力于降低优化门槛,提升优化开发效率的算子自动优化工具AutoKernel宣布正式开源!
AutoKernel特色:
低门槛: 无需底层优化汇编的知识门槛简单易用: 提供docker环境,无需安装环境,plugin一键集成到推理框架Tengine高效率: 无需手写优化汇编,一键生成优化代码,一键部署AutoKernel使用业界广泛使用的自动代码生成项目Halide,通过输入计算描述和调度策略,自动生成底层代码。AutoKernel支持以plugin的形式,将生成的自动优化算子一键部署到推理框架Tengine中。
本文将带领大家一步步优化矩阵乘法GEMM。无需手工撸代码,编写繁杂冗长的底层汇编代码,只需十几行简洁的调度代码。
优化的本质。优化的时候,计算机底层做了什么?优化的”瓶颈“是什么?为什么通过一波”优化操作“,性能就能提升呢?AutoKernel使用的Halide是如何实现自动优化的呢?
需要了解一下硬件的基础的体系结构,了解硬件如何工作,才能在软件上实现算法的时候,尽可能去考虑利用硬件的一些特性,来做到高效的、极致的优化。
上图是典型的存储理器层次结构:主存容量大,访问速度慢,寄存器和缓存读取速度快,但容量有限。在寄存器的层级上,CPU可以在一个时钟周期内访问它们,如果CPU去访问外部的DDR的话,延迟是非常大的,大概是200个时钟周期左右。如果CPU去访问cache的话,一般需要6到12个cycle就够了。所以,一个很重要的一个优化宗旨是:优化内存访问,充分利用寄存器和高速缓存去存数据。
第二个优化宗旨则是提高并行性:充分利用SIMD进行指令向量化和多核心并行。大部分现代CPU支持SIMD(Single Instruction Multiple Data,单指令流多数据流)。在同一个CPU循环中,SIMD可在多个值上同时执行相同的运算/指令。在4个数据点上进行向量化,一次计算四个数据,理论上就可以实现4倍的加速。
运行环境搭建
AutoKernel提供了docker镜像,docker里已经配置好运行环境,进入docker即可直接运行demo代码:
# 拉取镜像docker pull openailab/autokernel# 启动容器,进入开发环境docker run -it openailab/autokernel /bin/bash# 获取代码git clone https://github.com/OAID/AutoKernel.gitcd AutoKernel/doc/tutorials/data/目录下的build.sh是demo的执行脚本,运行需要指定优化步骤step,可选的step是从1 到7,其中step= 1 是默认不优化的,step=7是最极致优化的。
优化效果
# 执行demo./build.sh 1./build.sh 7
下图展示了在Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz的电脑上的优化效果,无需手工撸代码,无需编写繁杂冗长的底层汇编代码,只需十几行简洁的调度代码, 就能性能优化200+倍~
优化步骤
以下是更为详细的优化步骤:
STEP1
第一个步骤是不带任何优化的。用Halide语言直接描述GEMM的计算过程。
Var x,y; RDom k(0, K); Func gemm("gemm"); gemm(x, y) += A(k, y) * B(x, k);
计算M=N=K=640的矩阵乘法。运行脚本第一个参数指定step=1。耗时结果如下:
root@bd3faab0f079:/AutoKernel/doc/tutorials/data# ./06_build.sh 1step = 1M N K = 640 640 640 err 0.00 [rep 50] autokernel | blas 240.8523 ms 1.1376 ms
STEP2
这一步采用分块tile。分块的目的是为了充分利用缓存。如果原来的循环较大,tile分块改成小块数据去计算,可以使得每次计算的数据都比较舒适地呆在缓存里,不用经历重复的驱逐(在缓存中重复的添加和删除数据)。分块后进行reorder操作,交换两个嵌套循环的顺序,目的是最内层的内存访问友好。按照x,y维度划分成16x8的小分块去计算:
.gemm.update .tile(x, y, xo, yo, xi, yi, 16, 8) .reorder(xi, yi, k, xo, yo);
执行结果如下:
root@bd3faab0f079:/AutoKernel/doc/tutorials/data# ./06_build.sh 2step = 2M N K = 640 640 640 err 0.00 [rep 50] halide | blas 81.8148 ms 1.1281 ms
性能从240ms优化到82ms,提升了近3倍。
STEP3
在上一步的基础上增加向量化vectorize。向量化是把几个标量计算(scale)转换为一个向量计算(vector),充分利用SIMD向量指令。大部分现代CPU支持SIMD(Single Instruction Multiple Data,单指令流多数据流)。在同一个CPU循环中,SIMD可在多个值上同时执行相同的运算/指令。
gemm.update .tile(x, y, xo, yo, xi, yi, 16, 8) .reorder(xi, yi, k, xo, yo) .vectorize(xi, 8);
执行结果:
root@bd3faab0f079:/AutoKernel/doc/tutorials/data# ./06_build.sh 3step = 3M N K = 640 640 640 err 0.00 [rep 50] autokernel | blas 27.5433 ms 1.1445 ms
性能从82ms优化到27ms,又加速了接近3倍。可以看到,围绕前面提到的两条优化宗旨:优化内存访问和提高并行性,从step1到step3,性能已经提升了近9倍。
STEP4
调度策略在step3的基础上增加并行化parallel。对一个循环并行化是把循环的每次迭代分给多个线程或者处理器去同时处理,每个线程处理通过代码段(loop body),但是处理不同的数据。
gemm(x, y) += A(k, y) * B(x, k); gemm.update .tile(x, y, xo, yo, xi, yi, 16, 8) .reorder(xi, yi, k, xo, yo) .vectorize(xi, 8) .parallel(yo);
执行结果:
root@bd3faab0f079:/home/chunying/AutoKernel/doc/tutorials# ./06_build.sh 4step = 4M N K = 640 640 640 err 0.00 [rep 50] autokernel | blas 7.2605 ms 1.1605 ms
增加并行化后,build.sh默认指定四线程,性能直接翻了近4倍,从27ms到7.3ms.
STEP5
调度策略在上一步的基础上增加unroll展开。如果循环体内的语句没有数据相关依赖,循环展开可以增加并发执行的机会,使得更充分利用寄存器,减少循环时每个操作内存加载和保存的次数。
gemm.update .tile(x, y, xo, yo, xi, yi, 16, 8) .reorder(xi, yi, k, xo, yo) .vectorize(xi, 8) .parallel(yo) .unroll(xi) .unroll(yi,2);
执行结果:
root@bd3faab0f079:/AutoKernel/doc/tutorials/data# ./06_build.sh 5step = 5M N K = 640 640 640 err 0.00 [rep 50] autokernel | blas 4.7617 ms 1.1597 ms
unroll展开后,性能从7.3ms优化到4.8ms.
STEP6
前面的分块成 16 x 8的小kernel, 这一步先划分成 16 x 32的分块,然后把每个分块再分成 16 x 8的子分块。把最外层的两层循环合并到一层,并对这一层进行并行化。这一步计算描述多了一个prod函数来定义子分块的计算,prod函数的计算公式和总的gemm是一样的,通过 compute_at指定在 yi维度之下计算prod,则prod计算的是 16x8的小kernel, 大致逻辑如下:
总的代码如下:
Func prod; prod(x, y) += A(k, y) * B(x, k); gemm(x, y) = prod(x, y); gemm.tile(x, y, xi, yi, 16, 32) .fuse(x, y, xy).parallel(xy) .split(yi, yi, yii, 4) .vectorize(xi, 8) .unroll(xi) .unroll(yii); prod.compute_at(gemm, yi) .vectorize(x, 8).unroll(y); prod.update .reorder(x, y, k) .vectorize(x, 8) .unroll(x) .unroll(y) .unroll(k, 2);
执行结果
root@bd3faab0f079:/AutoKernel/doc/tutorials/data# ./06_build.sh 6step = 6M N K = 640 640 640 err 0.00 [rep 50] autokernel | blas 3.1824 ms 1.1373 ms
这一步距离STEP1性能已经优化了近80倍了,性能越来越接近OpenBlas了。
STEP 7
这一步添加的操作是对矩阵B进行数据重排,使得在计算小kernel 16x8时,内存读取更顺畅。因为小kernel的x维度是按照16划分的,因此重排数据B的x维度也是按照16重排。
总的代码如下:
Func B_interleave("B"), Bs("Bs"); Bs(x, y, xo) = B(xo * 16 + x, y); B_interleave(x, y) = Bs(x % 16, y, x / 16); Func prod; prod(x, y) += A(k, y) * B_interleave(x, k); gemm(x, y) = prod(x, y); gemm.tile(x, y, xi, yi, 16, 32)
.fuse(x, y, xy).parallel(xy) .split(yi, yi, yii, 4) .vectorize(xi, 8) .unroll(xi) .unroll(yii); prod.compute_at(gemm, yi) .vectorize(x, 8).unroll(y); prod.update .reorder(x, y, k) .vectorize(x, 8) .unroll(x) .unroll(y) .unroll(k, 2); Bs.compute_root .split(y, yo, yi, 16) .reorder(x, yi, xo, yo) .unroll(x)
.vectorize(yi).parallel(yo, 4);
执行结果:
root@bd3faab0f079:/AutoKernel/doc/tutorials/data# ./06_build.sh 7step = 7M N K = 640 640 640 err 0.00 [rep 50] autokernel | blas 1.1957 ms 1.1425 ms
至此,的每一步调优策略始终都围绕两条优化宗旨“优化内存访问”,“提高并行性”展开优化,到最后性能已经与OpenBlAS差不多了,距离STEP1已经加速了200+倍了。
GEMM与AutoKernel算子优化的更多相关文章
- Spark为什么只有在调用action时才会触发任务执行呢(附算子优化和使用示例)?
Spark算子主要划分为两类:transformation和action,并且只有action算子触发的时候才会真正执行任务.还记得之前的文章<Spark RDD详解>中提到,Spark ...
- 深度学习算子优化-FFT
作者:严健文 | 旷视 MegEngine 架构师 背景 在数字信号和数字图像领域, 对频域的研究是一个重要分支. 我们日常"加工"的图像都是像素级,被称为是图像的空域数据.空域数 ...
- spark算子优化
一.在聚合前在map端先预聚合 使用reduceByKey/aggregateByKey代替groupByKey 二.一次处理一个分区的数据,不过要注意一个分区里的数据不要太大,不然会报oom * 使 ...
- BigData-‘基于代价优化’究竟是怎么一回事?
本文由 网易云发布. 本文具体讨论了Join基础算法的一种优化方案 – Runtime Filter,在本文最后还引申地聊了聊谓词 下推技术.同时,在本文文章开头,笔者引出了两个问题,SQL执行引 ...
- spark核心优化详解
大家好!转眼又到了经验分享的时间了.吼吼,我这里没有摘要也没有引言,只有单纯的经验分享,请见谅哦! 言归正传,目前在大数据领域能够提供的核心计算的工具,如离线计算hadoop生态圈的mr计算模型,以及 ...
- flink任务性能优化
如何提高 Flink 任务性能 一.Operator Chain 为了更高效地分布式执行,Flink 会尽可能地将 operator 的 subtask 链接(chain)在一起形成 task,每个 ...
- CUDA 矩阵乘法终极优化指南
作者:马骏 | 旷视 MegEngine 架构师 前言 单精度矩阵乘法(SGEMM)几乎是每一位学习 CUDA 的同学绕不开的案例,这个经典的计算密集型案例可以很好地展示 GPU 编程中常用的优化技巧 ...
- Spark 3.x Spark Core详解 & 性能优化
Spark Core 1. 概述 Spark 是一种基于内存的快速.通用.可扩展的大数据分析计算引擎 1.1 Hadoop vs Spark 上面流程对应Hadoop的处理流程,下面对应着Spark的 ...
- Halide应用开发
Halide应用开发 1. 基本原理 1.1.介绍 随着人工智能的普及,深度学习网络的不断涌现,为了让各硬件(CPU, GPU, NPU,...)能够支持深度学习应用,各硬件芯片需要软件库去支持高性能 ...
随机推荐
- pyqt5 多线程+定时器+读取本地图片
前言 一个程序界面有多个button 按钮时,单击一个按钮,若此按钮对应的信号正在执行,且还未执行完毕: 此时再次单击另外一个按钮,就会出现假死状态. 这个时候我们就需要使用 多线程去解决 多线程+定 ...
- hdu4370 比较抽象的最短路
题意: 给你一个n*n的矩阵,然后让咱们构造另一个n*n的矩阵,构造的矩阵有如下要求, 1.X12+X13+...X1n=1. 2.X1n+X2n+...Xn-1n=1. 3.for ea ...
- LA3644简单并查集判环
题意: 有n个化合物,每个化合物是两种元素组成,现在要装车,但是一旦车上的化合物中的某几个化合物组成这样一组关系,有n个化合物正好用了n中元素,那么就会爆炸,输入的顺序是装车的顺序,对于每 ...
- C#-播放器相关
axWindowsMediaPlayer1.URL= 设置路径 axWindowsMediaPlayer1.Ctlcontrols.play();开始 axWindowsMediaPlayer1.Ct ...
- Andrew Ng机器学习算法入门((六):多变量线性回归方程求解
多变量线性回归 之前讨论的都是单变量的情况.例如房价与房屋面积之前的关系,但是实际上,房价除了房屋面积之外,还要房间数,楼层等因素相关.那么此时就变成了一个多变量线性回归的问题.在实际问题中,多变量的 ...
- python工业互联网应用实战15-前后端分离模式1
我们在13章节里通过监控界面讲了如何使用jquery的动态加载数据写法,通过简单案例来说明了如何实现动态的刷新监控界面的数据,本章我们将演示如何从Django模板加载数据逐步演化到前后端分离的异步数据 ...
- FROM-4-TO-6!!!!!!!!! - OO第二单元总结
电梯的这三次作业是对并发编程的一次管窥,感觉收获还是蛮多的.在设计上有好的地方也有不足,这里简单回顾总结一下 设计总述 电梯这个问题由于比较贴近真实生活,所以需求还是很好理解的.总的来说,我的数据处理 ...
- python将控制台输出保存到文件
python将控制台输出保存到文件 在平时工作中,有时我们需要将控制台输出保存到文件 1.命令行用>覆盖写入和>>追加写入 for i in range(10000): prin ...
- centos保存rpm到本地以及使用yum完全卸载软件包
目录 保存安装的rpm到本地 方法一: 方法二(推荐): yum卸载软件包包括依赖 保存安装的rpm到本地 方法一: [root@ServerA ~]# vim /etc/yum.conf [main ...
- Linux_交换分区SWAP
一.交换分区SWAP 1️⃣:交换分区SWAP就是LINUX下的虚拟内存分区,它的作用是在物理内存使用完之后,将磁盘空间(也就是SWAP分区)虚拟成内存来使用. 2️⃣:交换分区一般指定虚拟内存的大小 ...