fix some reference issues (#446)

* fix some reference issues

* mis-remove of code

---------

Co-authored-by: Tanzhipeng <Rudysheeppig@users.noreply.github.com>
This commit is contained in:
Pei Mu
2023-03-31 11:49:10 +01:00
committed by GitHub
parent 5ac39809e9
commit 245bc4099f

View File

@@ -1,4 +1,5 @@
## 算子编译器
:label:`ch05-sec-op_operator`
算子编译器,顾名思义,即对算子进行编译优化的工具。这里所谓的"算子"可以来自于整个神经网络中的一部分也可以来自于通过领域特定语言Domain
Specific Language,
@@ -49,7 +50,7 @@ DSL实现的代码。而所谓编译通俗来说起到的是针对目标
我们以形式为乘累加计算的代码为例简要分析描述这一算法。该代码的核心计算逻辑为首先对张量C进行初始化之后将张量A与张量B相乘后结果累加到张量C中。
``` {#lst:before_tvm caption="乘累加计算代码" label="lst:before_tvm"}
```c++
for (m: int32, 0, 1024) {
for (n: int32, 0, 1024) {
C[((m*1024) + n)] = 0f32
@@ -62,18 +63,11 @@ for (m: int32, 0, 1024) {
}
```
假定数据类型为浮点型Float此时张量A、B、C的大小均为1024 $\times$
1024三者占用的空间共为1024 $\times$ 1024 $\times$ 3 $\times$
sizeof(float) = 12MB。这远远超出了常见缓存的大小如L1
Cache为32KB。因此按照此代码形式要将整块张量A、B、C一起计算只能放入离计算核更远的内存进行计算。其访存效率远低于缓存。
假定数据类型为浮点型Float此时张量A、B、C的大小均为1024 $\times$ 1024三者占用的空间共为1024 $\times$ 1024 $\times$ 3 $\times$ sizeof(float) = 12MB。这远远超出了常见缓存的大小如L1 Cache为32KB。因此按照此代码形式要将整块张量A、B、C一起计算只能放入离计算核更远的内存进行计算。其访存效率远低于缓存。
为了提升性能提出使用平铺Tile循环移序Reorder和切分Split的调度策略。由于L1缓存大小为32KB为了保证每次计算都能够放入缓存中我们选取因子Factor为32进行平铺使得平铺后的每次计算时只需要关注m.inner
$\times$
n.inner构成的小块Block即可而其他的外层循环不会影响最内层小块的访存。其占用内存大小为32
$\times$ 32 $\times$ 3 $\times$ sizeof(float) =
12KB足够放入缓存中。如下代码展示了经过该策略优化优化后的变化。
为了提升性能提出使用平铺Tile循环移序Reorder和切分Split的调度策略。由于L1缓存大小为32KB为了保证每次计算都能够放入缓存中我们选取因子Factor为32进行平铺使得平铺后的每次计算时只需要关注m.inner $\times$ n.inner构成的小块Block即可而其他的外层循环不会影响最内层小块的访存。其占用内存大小为32 $\times$ 32 $\times$ 3 $\times$ sizeof(float) = 12KB足够放入缓存中。以下代码展示了经过该策略优化优化后的变化。
``` {#lst:after_tvm caption="子策略组合优化后的代码" label="lst:after_tvm"}
```c++
// 由for (m: int32, 0, 1024)以32为因子平铺得到外层循环
for (m.outer: int32, 0, 32) {
// 由for (n: int32, 0, 1024)以32为因子平铺得到外层循环
@@ -110,17 +104,16 @@ for (m.outer: int32, 0, 32) {
}
```
本示例参照TVM提供的"在CPU上优化矩阵乘运算的实例教程"[^1]中的第一项优化,读者可深入阅读后续优化内容。
本示例参照TVM提供的"[在CPU上优化矩阵乘运算的实例教程](https://tvm.apache.org/docs/how\_to/optimize\_operators/opt\_gemm.html)"中的第一项优化,读者可深入阅读后续优化内容。
### 调度空间算法优化
算子编译器的另外一种优化思路是:通过对调度空间搜索/求解自动生成对应算子调度。此类方案包括多面体模型编译Polyhedral
Compilation基于约束对调度空间求解和Ansor调度空间搜索等。这类方法的好处是提升了算子编译的泛化能力缺点是搜索空间过程会导致编译时间过长。
算子编译器的另外一种优化思路是:通过对调度空间搜索/求解自动生成对应算子调度。此类方案包括多面体模型编译Polyhedral Compilation基于约束对调度空间求解和Ansor调度空间搜索等。这类方法的好处是提升了算子编译的泛化能力缺点是搜索空间过程会导致编译时间过长。
以多面体模型编译技术将代码的多层循环抽象为多维空间,将每个计算实例抽象为空间中的点,实例间的依赖关系抽象为空间中的线,主要对循环进行优化。该算法的主要思想是针对输入代码的访存特点进行建模,调整循环语句中的每一个实例的执行顺序,使得新调度下的循环代码有更好的局部性和并行性。
我们以如下代码为例介绍该算法。
``` {#lst:before_poly caption="待优化代码" label="lst:before_poly"}
```c++
for (int i = 0; i < N; i++)
for (int j = 1; j < N; j++)
a[i+1][j] = a[i][j+1] - a[i][j] + a[i][j-1];
@@ -135,14 +128,14 @@ for (int i = 0; i < N; i++)
再进行复杂的依赖分析和调度变换之后得到一个符合内存模型的最优解。如下代码显示了经过多面体模型优化后得到的结果。
``` {#lst:after_poly caption="多面体模型算法优化后的代码" label="lst:after_poly"}
```c++
for (int i_new = 0; i_new < N; i_new++)
for (int j_new = i+1; j_new < i+N; j_new++)
a[i_new+1][j_new-i_new] = a[i_new][j_new-i_new+1] - a[i_new][j_new-i_new] + a[i_new][j_new-i_new-1];
```
观察得到的代码,发现优化后的代码较为复杂。但是仅凭肉眼很难发现其性能优势之处。仍需对此优化后的代码进行如算法描述那样建模,并分析依赖关系后得出结论,如 :numref:`ch05-poly`所示:经过算法优化后解除了原代码中的循环间的依赖关系,从而提高了并行计算的机会。即沿着 :numref:`ch05-poly`中虚线方向分割并以绿色块划分后,可以实现并行计算。
该算法较为复杂,限于篇幅,在这里不再详细展开。读者可移步到笔者专门为此例写的文章-《深度学习编译之多面体模型编译------以优化简单的两层循环代码为例》详读。
该算法较为复杂,限于篇幅,在这里不再详细展开。读者可移步到笔者专门为此例写的文章-[《深度学习编译之多面体模型编译------以优化简单的两层循环代码为例》](https://zhuanlan.zhihu.com/p/376285976)详读。
![多面体模型优化结果](../img/ch05/poly.png)
@@ -185,5 +178,3 @@ Computing,
HPC的优化思路这种情况称为借助专家经验进行优化。另外算子编译器面对的后端AI芯片的体系结构的不同如重点的单指令多数据和单指令多线程为代表的两种后端体系结构决定了优化过程中更多偏向于生成对单指令多数据友好的加速指令或者生成对单指令多线程友好的多线程并行计算模型。
而后者面向的问题是更加通用的标量计算行为和计算机控制命令,往往在优化中围绕寄存器的使用和分支预测准确性等进行优化。
总之,由于需要解决的问题不同,算子编译器和传统编译器在优化算法的具体实现上有着一定的区别,但是在算法设计时也有互相借鉴的机会。
[^1]: 在CPU上优化矩阵乘运算的实例教程<https://tvm.apache.org/docs/how_to/optimize_operators/opt_gemm.html>