mirror of
https://github.com/openmlsys/openmlsys-zh.git
synced 2026-04-09 21:57:56 +08:00
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:
@@ -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)详读。
|
||||
|
||||
|
||||

|
||||
@@ -185,5 +178,3 @@ Computing,
|
||||
HPC)的优化思路,这种情况称为借助专家经验进行优化。另外算子编译器面对的后端AI芯片的体系结构的不同,如重点的单指令多数据和单指令多线程为代表的两种后端体系结构,决定了优化过程中更多偏向于生成对单指令多数据友好的加速指令,或者生成对单指令多线程友好的多线程并行计算模型。
|
||||
而后者面向的问题是更加通用的标量计算行为和计算机控制命令,往往在优化中围绕寄存器的使用和分支预测准确性等进行优化。
|
||||
总之,由于需要解决的问题不同,算子编译器和传统编译器在优化算法的具体实现上有着一定的区别,但是在算法设计时也有互相借鉴的机会。
|
||||
|
||||
[^1]: 在CPU上优化矩阵乘运算的实例教程:<https://tvm.apache.org/docs/how_to/optimize_operators/opt_gemm.html>
|
||||
Reference in New Issue
Block a user