跳转至

多面体模型设计初衷是更智能的在深度学习编译器内,对多种算子进行很好的融合(fuse),以及优化

场景引入

我们举一个简单的场景,下面是一段伪代码,代码分别做了以下三个事情

  1. 量化操作
  2. 卷积操作
  3. ReLU激活操作
for(h=0;h<H;h++)
    for(w=0;w<W;w++)
        A[h][w] = Quant(A[h][w]) /*S0*/
for(h=0;h<H;h++)
    for(w=0;w<W;w++){
        C[h][w] = 0; /*S1*/
        for(kh=0;kh<kH;kh++)
            for(kh=0;kh<kH;kh++) 
                C[h][w] += A[h+kh][w+kw]*B[kh][kw]; /*S2*/ 这里就是在做一个卷积操作
    }
for(h=0;h<H;h++)
    for(w=0;w<W;w++)
        C[h][w] = ReLU(C[h][w]) /*S3*/ 这里在做ReLU激活

我们可以把上述的操作,分成四个阶段,从S0开始到S3

可以发现不同阶段的代码是有先后顺序的

第一阶段,我们需要先对A数组做一个量化操作。

第二阶段,需要对特征图数组做初始化,才能允许后面卷积正常执行。

第三阶段是做卷积操作,依赖于前两个阶段的完成。

第四阶段是读取特征图数组,做ReLU激活。

根据读写顺序,我们可以抽象出一个依赖关系

我们给每个语句,做一个时间戳处理

[S0(h, w)->(0, h, w);S1(h, w)->(1, h, w, 0, 0);S2(h, w, kh, kw)->(1, h, w, 1, kh, kw);S3(h, w)->(2,h,w)]

X - > Y,右边Y是一个最大为6维的时间节点

比如我们看第一个S0和第二个S1

按照字典序,第一个0小于第二个的1。因此S0是要在S1之前做的

然后看S1第四个数值是0,而S2第四个数值为1,因此S1需要在S2之前做。

这里我们是跟原始程序的运行顺序是一致的,只不过是换了种表示方式

多面体模型要做的就是根据原始的调度顺序,求解出一个新的,更优的指令调度顺序

在这里插入图片描述

这个是新的调度结果,这个调度顺序会有更好的并行性能,后面我们会讲解。

初步优化

首先我们看S3,这是个RELU语句,我们完全可以在卷积的那个循环里,就做RELU激活,因此S3的循环是可以放到S1, S2那部分的

在这里插入图片描述

我们现在暂时少去了一个循环,优化了访存性能

补充说明:

可以观察到第一个S0由两个for循环,变成了四个for循环。

这实际上是做了个分块处理

放到GPU里是怎么做的?

GPU里面是有两个层级的并行硬件

一个是Block,一个是Thread

在这里插入图片描述

如果将指令映射到对应的节点上,那么我们程序就会获得并行性能

前面我们讲到了S0的四层分块循环,我们就可以很好的指令分散到Block和Thread两种并行硬件

在这里插入图片描述

前面两个分块的for循环分配到了Grid,后面两个for循环被分配到了Block上

数据交换发生

S0这部分做完以后,数据是放在GPU显存上的。

由于后面S1 S2 S3需要用到这部分,所以GPU的数据要出来,放到CPU的主存上,让后面的指令来取数据。

这里就发生了数据交换

所以我们在想,能不能把S0和后面的S1,S2,S3给合并,这样我们数据一次都不需要取,减少了访存消耗。

数据空间的冲突

上述的操作有量化和卷积,我们可以作为一个二维空间进行展示

在这里插入图片描述

在分块之前,整个操作都是一个二维空间(我们先只关心最外2层的for循环)

为什么我们不能合并操作呢?主要是数据空间上发生了冲突

我们假设对量化的部分做分块,是4x4大小。对卷积的部分做分块,是2x2大小,那么效果如下

在这里插入图片描述

如果能融合,就需要保证相同分块,所访问到的数据是一致的

但在这个例子里,两个空间的红色方块对应的数据空间是不一致的,也就导致为什么无法融合

在这里插入图片描述

转换下分块思维

我们不同时对两个空间做分块,只考虑卷积部分做分块

在这里插入图片描述

进而,我们可以得知需要多少数据量

在这里插入图片描述

可以看到每一个块需要的是4x4数据空间,其中有数据交叉部分,这也是有一定的性能损耗原因

通过这个数据空间的情况,我们再逆推S0的空间分块

在这里插入图片描述

可以看到逆推的结果跟之前的不一样,同样存在数据交叉的情况

调度树

我们可以将调度方式,以一个树的形式来描述

在这里插入图片描述

这样我们优化的就是树结构,不需要看原始代码

如何做fusion融合操作?

原始的树结构为

在这里插入图片描述

首先我们对S1,S2,S3做一个分块

在这里插入图片描述

这就是我们前面提到的对外层两层for循环做分块

接着是对内循环优化

我们先将S0节点拷贝过来

在这里插入图片描述

右边S1,S2,S3子树是没有S0的,现在为了让S1,S2,S3认识S0,我们将其当作一个节点复制过来

叫 extension 节点

这样根据前面推导我们完成了融合,但是左边的树还存在S0,显然调度两次是不正确的,因此我们给左边的S0标记一个skipped,告诉编译器跳过

在这里插入图片描述

最终代码对应结果

在这里插入图片描述


本文总阅读量