日常生活非常感谢昇腾CTOZOMI酱
(我愿称作肝帝!)的DT重大贡献,热烈欢迎全屏三连
ZOMI酱的Nikto-ZOMI酱Blog-bilibilibilibili音频
他的github门牌号
GitHub – chenzomi12/DeepLearningSystem: Deep Learning System core principles introduction.
第二节-AIC++后端强化如是说
强化,那时将注意力再进一步,让他们看一看后端强化是是不是玩的,所以其间端强化有甚么差别呢?
比如说他们看一看当中那个传递函数演算法:
他们能对SCSI和外部循环式形式、缓存出访形式展开各式各样的强化。
接下去看一看后端强化的两个主要就关键步骤:
具体来说把排序图高阶ir切换为合情理ir:
接著后端强化会把它变为更合情理的ir,接着展开许多DDL到具体内容的硬体上展开继续执行。
这里面的relay.add就是TVM IR的一个定义,最后由TVM的编译器生成底层外部的底层IR。
接下去再看一看具体内容的算子相关问题:(共两类,访存密集和排序密集型)
假如他们有一个向量vec a以及向量vec b,都是从缓存取出来的,继续执行了alu的乘法后再放入cache(内存),接著再从缓存读出来再读下一次;这需要大量频繁的访存:
第一个表前面是传递函数的参数,右边是排序量;不同传递函数的入参排序量也是不同的,这里的排序量比他们的访存量大了很多,这就是排序密集型:
对于这两种算子优化还是有着很多挑战的:
出访和存储是天平的两端,这实际上是一个复杂的组合强化问题。。。
Cudnn其实早期只有100多个算子,那时又接近200多个算子。
所以问题就来了,AI算子迭代所以快,是不是赶得上更新的速度?
所以他们需要AIC++,有人提出了自动kernel生成的形式,目标是给定演算法自动生成目标平台上的高性能实现。
(下图上面部分提到了许多不错的文章能去看一看)
第二节-算子排序与调度
接下去看一个算子计算的具体内容例子:(注意甚么叫做不同的调度形式与唯一定义)
但是高斯滤波这种图像模糊方法,有很多种的调度形式:
我能先对横坐标展开迭代,再对纵坐标展开迭代;他们也能先对纵坐标展开迭代再看横坐标,这里体现的就是调度形式的不同。
排序和调度解耦的好处是能通过不同硬体特点好处对具体内容演算法排序展开排序强化。
右边就是加速强化的高斯滤波,做了很多分片(没有大循环式了)与数据展开(buffer),这就是调度的强化。
接下来看一看神经网络算子的排序特征:
他们再来看一下根据算子排序特征抽象后的高斯滤波的代码:
大部分的算子调度都是由缓存分配、循环式、排序实现的。所以他们就能根据那个去定义调度树:
他们能根据形状不同去差别不同的阶段,他们能用调度树很好的表达算子排序原语(类似画算子的状态机),真正loop结束后就是排序节点菱形。所以他们该如何理解调度树的语义?他们能对调度树展开优先遍历DFS接着切换为对应的程序代码。AIC++后端的目标就是在算子原语不变的情况下去强化调度树。
接下去看几种对调度树修改的方法(不影响算子原语):
这里的内联的意思是把f*x放到后续的排序,接着把gx,hx往前提。
所以基于调度树理解这些有啥用?—— search algorithm for schedules
左下角是他们调度树节点排序的耗时,他们需要根据整体,找到满足代价函数最优的情况,这时候就是最好的调度策略。
实际的调度策略可能是比上面复杂得多的多的。
具体内容的许多调度方法其实还能去看一看halide(http://halide-lang.org/),能很方便的实现许多GPU API的调度方法。https://zhuanlan.zhihu.com/p/346468141
Halide是用C++作为宿主言语的一个图画处理相关的DSL(Domain Specified Language)言语,全程范畴专用言语。首要的效果为在软硬层面上(与演算法自身的规划无关)对演算法的底层加快。在OpenCV(传统图画处理库)中部分演算法运用了Halide后端,而TVM(神经网络C++)也是用了Halide的思维去强化神经网络算子。
具体内容也能看openmmlab的一期开放麦对它展开了如是说:
【社区开放麦#27 | 部署神器Halide, 实现高性能演算法】 https://www.bilibili.com/video/BV1Tg411B7ch/?share_source=copy_web&vd_source=b42f227a4f2d413fbde18499d83227cf
第三节-算子强化手工形式
他们来看一看算子强化在不同框架的强化形式:
再展开具体内容这些东西是啥之前,他们先来看一个例子——基于源码的修改:
看到上面,他们能看到先遍历一个10000再遍历200,外层循环式遍历比较多,这里可能会导致缓存消耗比较大。那如果他们先遍历200再遍历10000,每次在堆栈迭代下一个i,能很好的利用缓存空间。
另一个就是循环式变量的实例化:
对于许多重复排序的表达式能先构建好。这里是空间换时间的方法,后在循环式就能通过读取缓存或cache的形式直接快速读取到tmp里面,不用每次都展开一个排序。
另外他们能通过把许多循环式中用到的东西提前到外面实例化处理,避免非必要的函数调用开销。(不过其实我觉得很多情况下C++也会对这些展开强化了。。)
另外,大家不要觉得AI编译器里面每一层我都要去实现自己的操作,其实每一层都有可替换方案和对应的开源项目。
另外,他们能把算子调度强化方法分成三大类型:
为甚么循环式强化有所以多内容呢?那是因为他们的排序特征以多重循环式为特点,多维张量排序为主要就数据结构。所以算子强化大部分集中在循环式loop的强化。
如果想要理解更深入能看一看参考文献的许多内容:
第四节-算子循环式强化
4.1 循环式展开
这部分看起来还是有点抽象,我咨询了一下我的朋友:
其实就是乱序的一种情况, 一个循环式里面的有很多指令,后面的指令先继续执行,在循环式里能体现为下一个(甚至下两个)循环式的某条指令先于当前指令继续执行,接着总体上来看就像循环式里的指令重新排序了. 当然因为同一个循环式里指令所指定的寄存器名字是一样的,为了解决那个冲突需要在寄存器组里使用虚拟化技术,硬体上叫寄存器重命名. 相当于不同的循环式用了同样名字的寄存器,大家都叫xxx,但是实际上硬体会自动把他们处理成不同的寄存器. 还有一点不重要的,循环式最后一条是跳转,本来要循环式10次的,把两次loop合并为一次相当于跳转次数减半了。不过那个强化比较小,而且有延迟槽+转移预测(那个命中率特别高),所以我直接把他忽略了,面试还是要说的,但是工业上那个提升有没有都无所谓,当然如果是代码会在循环式的不同层级和其他代码块之间频繁互相跳转就要另外说,但是我要强调那个情况是极其极其丑陋的代码习惯(说的就是goto)风险大,强化难。
接下来他们一起看一看甚么叫做循环式展开:(PS这其实是比较基础的,更详细的要看体系结构量化方法或者国科大的体系结构教材)
这里j在循环式的时候跳两个位,里面继续执行了两个数据的操作。
4.2 循环式分块
分块的原因是cache显存缓存是有限的。了解这些特性是为了方便他们更好的写AIC++自动调度的策略。分块与缓存块的大小应该一致,能提高处理器整体的效率。(对齐)实现思路如下:
那个具体内容做法看起来有点抽象,一起来看个具体内容的例子:
本来是从0到m,他们把循环式按照T大小展开了一个分块,每一次对分块后的j_0开始一直从j_0加到j_0+T。j_o是out loop,里面的j_i叫做inner loop。那个数据就能塞到处理器里面,而不会导致在迭代的时候导致cache missing,需要重新对数据调入调出,能最大程度利用芯片的缓存空间。
其实实际问题比理想的情况更复杂:
4.3 循环式重排
他们再来看一看循环式的排布策略:
假设这里的m非常非常大,n相对小。这时候需要把里面的数据全部塞入cache其实是不可能的,所以他们能把m放到外面,比较小的n放到里面。假设那个n只有100,和cache的空间相同,提高了cache的命中率也就提高了芯片的满载负荷。
具体内容例子能参考:
排序机系统基础学习讲义(4)-Cache友好代码https://blog.csdn.net/qq_43336390/article/details/106071998
4.4 循环式融合
接下去再看一看循环式融合:
这里上面的循环式的边界条件是不同的,所以他们要先展开一个对齐,让他们能融合到下面的同样的从1到n-1。这种方法就能加强软件的流水线并行效率。这里计算的数据还是要满足cache 友好的形式才能让排序效率最大化。
4.5 循环式拆分
并行的处理器是不擅长处理控制流的。 而下面的继续执行比较慢,他们就让他单独继续执行(15-17行),接着单独判断,这能加快整体处理器的效率。
最后他们复习一下今天学习了甚么:
第五节-指令和存储强化
接下去先看一看指令强化部分(向量化与张量化)
5.1 向量化
他们以那个图为例,假设想要排序四个字节/单位的数据,具体来说从缓存的低位开始读取,一次性读取四个数据;接著再从下一个低位读取四个数据,和之前的四个数据一起排序,不断循环式这种+操作,被称为向量化操作——一次处理多个数据。
在没有继续执行向量化的时候,第2到5行的代码主要就是对数组展开单纯的遍历求和。
下面的部分是用了向量化指令的结果。
(这里可能是伪代码,他们看一看一个实际情况下的向量化)
现代C++也已经有各种自动向量化的操作,例如:
详细能参考:
向量化代码实践与思考:如何借助向量化技术给代码提速
https://blog.csdn.net/AlibabaTech1024/article/details/126359743
具体内容请以手册为准。
5.2 张量化
接下去看一看张量化:
他们来看一看左侧的图,这里的左边小部分是很多cuda core 右边是tensor core。
Tensor core主要就就是用来做张量排序,考虑到神经网络的gemm等排序原理,实际上硬体设计也设计成了对应的形式。
接下去看看许多主流厂商是是不是做的:
这里给出对应的官方门牌号:
https://docs.nvidia.com/cuda/cublas/
https://developer.nvidia.com/cudnn
https://github.com/oneapi-src/oneDNN
cuBLAS是线性代数库,适合做矩阵乘;传递函数能用矩阵乘法的形式实现,这也是cuDNN排序传递函数的方法之一,不过cudnn有更加深度的强化。
对于局限性,所以他们需要aiC++提供增量化的指令或调度语言。
5.3 访存延迟
接下来他们再看一看存储强化相关(分为访存延迟和存储分配):
在ai展开训练的时候有大量的核或者线程来继续执行他们的算子或排序,但是他们的排序是严重依赖memory的,排序结果中间结果都需要存储;这时候存储和排序就会重叠;他们能尽量让他们重叠,最大提高硬体设备的利用率,这时候就需要访存延迟。(避免都在访存没人在排序)
接下去让他们看一看gpu的访存延迟是是不是产生和解决的:
里面的cache分为三层,第一层是dram,第二个是 l2 最后一个是 l1,l1就离他们的排序单元cuda core很近了,具体内容排序单元到缓存之间有一个 warp scheduler,专门管理各种线程。
下面就是warp scheduler 和他们具体内容指令之间的关系:warp scheduler会对他们的指令做好分发和预分配,接着给他们的instruction dispatch unit(IDU),接著IDU就会把具体内容的线程分发到不同的worker上继续执行。
假设他们有四个warp,每个warp都有一个指令,那时在一个读数据的过程当中,如果warp在读取数据,就会导致他们整个系统或者说线程的阻塞。这时候warp1和warp2不会因为warp0 的继续执行而阻塞;因为gpu 会根据 warp schedule去解决访存延迟的问题。
具体内容关键步骤也能参考:
CUDA——SM中warp调度器调度机制&&访存延迟隐藏
https://blog.csdn.net/weixin_44444450/article/details/118058031
访存是有先后顺序的(SM、调度器是有限的)。
(1)应该让先访存完毕的warp去继续执行尽可能多的指令(不然运算单元空着也是浪费啊),去隐藏其它warp的访存时间。
(2)增加active warps的数量,让尽可能多的warp去隐藏访存延迟。(那个有局限性,warps是有限的)接下去他们看一看DAE,解耦出访和继续执行的架构:那个就是大部分NPU CPU所采用的形式
接下去看一看软件部分实现:
TVM这么切换的好处是pipeline是明确的(下图的monolithic pipeline),能通过软件去控制,但是这里有个问题,硬体的正确继续执行顺序需要软件控制而且需要合情理的同步来实现。实现的演算法好坏直接决定访存延迟性能。
5.4 存储分配强化
接下去他们一起看一看缓存分配:(局部变量,全局变量,堆变量)
所以有同学问了,到底啥是堆变量?
接下去看一个简单的缓存排布(详细直接去看程序的装载映像缓存分配)
那个只是传统的cpu和C++结合的形式。在神经网络跑的地方(AI加速器等)里面就复杂多了,这里不仅有L1 L2 还有dram等。。。。面向不同的ai加速器不同的缓存分配形式其实很复杂,人工控制cuda会很麻烦,所以交给AIC++解决。
第六节-Auto-Tuning原理
其实传统C++早就有了auto tuning的概念。以下是传统C++对应的概念:
下面出自一个文章,对gemm矩阵乘展开强化;完美运用了现代处理器的四级缓存:(L3 L2 L1 还有alu旁边的许多寄存器展开了加速)
传统C++发展后分为两个方法:强化选择 optimize selection 强化顺序 optimize sequence 。
那时他们从传统C++回到AI C++,看一看有如何特点。
大量相类似的算子排序模式比如说不同的Normalization。。。pooling等。
那个问题其实非常难,需要大量的大神去研究。
那时一般来说总结为三个关键步骤:
接下去先看一看参数化的内容:不管是循环式强化、指令强化还是缓存强化都能组成许多相对固定的指令结构,变为能调度的原语。TVM能对调度模板展开建模。
下图中的意思:s[C]是数据内容展开split。循环式切分因子是64,意思是可能的参数空间、范围。Factor是能作为搜索空间。
有了参数化后就需要建立成本函数:
有了成本函数后就是开始搜索,对他们参数化的内容调度形式展开搜索,以便找到最好的参数化配置方案:
接下去他们看一看TVM里面的一个栈是是不是实现的:
那个大哐哐里面基本就是 auto tuning的内容。这里的Declarative Tensor Expressions 是参数化,把tensor的表达形式抽离,类似halide对排序和调度逻辑展开解耦,右边是对许多硬体感知强化的原语。
有了参数化后就能很好的对我们的排序和硬体展开一个表示,有了那个表示后就建立了他们的成本模型,接着用ML based automated optimizer,后就对loop program不断搜索让他们找到最优的成本函数。找到对应策略后就用C++生成对应的指令代码给真正的硬体展开部署使用。
来看一看ansor(TVM的最新版)
具体来说他们拿到AI框架表达排序图的子图, 接着对子图展开切分获得重要的子图,拿到重要的子图就需要去 4、5关键步骤;比如说section 4:确定强化的结构,随机注释(主要就是对应右边看到很多不同的循环式和形式,这里会展开随机的初始化,产生了很多配置的形式);接下去就是启发式搜索与cost model等等。。。。最后迭代到比较好的策略后就能把他部署到实际在硬体上能继续执行的指令(黑盒测试),再迭代回来不停的强化。
完结撒花~