fix bug for ch05 (#444)

Co-authored-by: chujinjin <chujinjin52@huawei.com>
Co-authored-by: Tanzhipeng <Rudysheeppig@users.noreply.github.com>
This commit is contained in:
zjuchujinjin
2023-03-31 17:40:13 +08:00
committed by GitHub
parent 04285985a3
commit cef59fa233
4 changed files with 22 additions and 26 deletions

View File

@@ -126,9 +126,9 @@ compute(y, z)
上述代码表达了如下计算逻辑:
```text
x = y
x = z
x = x - y
x = y
x = z
x = x - y
```
这段简单的计算逻辑翻译到计算图上可以表示为 :numref:`side_effect_1`所示。

View File

@@ -11,7 +11,7 @@
访存密集型算子这些算子的时间绝大部分花在访存上他们大部分是Element-Wise算子例如 ReLU、Element-Wise Sum等。
在典型的深度学习模型中一般计算密集型和访存密集型算子是相伴出现的最简单的例子是“Conv + ReLU”。Conv卷积算子是计算密集型ReLU算子是访存密集型算子ReLU算子可以直接取Conv算子的计算结果进行计算因此可以将二者融合成一个算子来进行计算从而减少内存访问延时和带宽压力提高执行效率。
例如“Conv + Conv + Sum + ReLU”的融合图\ref{fig:ch07/ch07-compiler-backend-03}中可以看到融合后的算子减少了两个内存的读和写的操作优化了Conv的输出和Sum的输出的读和写的操作。
例如“Conv + Conv + Sum + ReLU”的融合 :numref:`conv_sum_relu`中可以看到融合后的算子减少了两个内存的读和写的操作优化了Conv的输出和Sum的输出的读和写的操作。
![Elementwise算子融合](../img/ch05/conv_sum_relu.png)
:width:`800px`

View File

@@ -1,11 +1,11 @@
## 内存分配
:label:`ch05-sec-memory_pool`
内存在传统计算机存储器层次结构中有着重要的地位它是连接高速缓存和磁盘之间的桥梁有着比高速缓存更大的空间比磁盘更快的访问速度。随着深度学习的发展深度神经网络的模型越来越复杂AI芯片\footnote{与前文中的硬件加速器指意相同业内习惯称为AI芯片}上的内存很可能无法容纳一个大型网络模型。因此,对内存进行复用是一个重要的优化手段。此外,通过连续内存分配和 In-Place内存分配还可以提高某些算子的执行效率。
内存在传统计算机存储器层次结构中有着重要的地位它是连接高速缓存和磁盘之间的桥梁有着比高速缓存更大的空间比磁盘更快的访问速度。随着深度学习的发展深度神经网络的模型越来越复杂AI芯片上的内存很可能无法容纳一个大型网络模型。因此对内存进行复用是一个重要的优化手段。此外通过连续内存分配和 In-Place内存分配还可以提高某些算子的执行效率。
### Device内存概念
在深度学习体系结构中通常将与硬件加速器如GPU、AI芯片等相邻的内存称之为设备Device内存而与CPU相邻的内存称之为主机Host内存。如:numref:`host-device-memory`所示CPU可以合法地访问主机上的内存而无法直接访问设备上的内存同理AI芯片可以访问设备上的内存却无法访问主机上的内存。因此在网络训练过程中往往需要从磁盘加载数据到主机内存中然后在主机内存中做数据处理再从主机内存拷贝到设备内存中最后设备才能合法地访问数据。算子全部计算完成后用户要获取训练结果又需要把数据从设备内存拷贝到主机内存中。
在深度学习体系结构中通常将与硬件加速器如GPU、AI芯片等相邻的内存称之为设备Device内存而与CPU相邻的内存称之为主机Host内存。如 :numref:`host-device-memory`所示CPU可以合法地访问主机上的内存而无法直接访问设备上的内存同理AI芯片可以访问设备上的内存却无法访问主机上的内存。因此在网络训练过程中往往需要从磁盘加载数据到主机内存中然后在主机内存中做数据处理再从主机内存拷贝到设备内存中最后设备才能合法地访问数据。算子全部计算完成后用户要获取训练结果又需要把数据从设备内存拷贝到主机内存中。
![主机内存和设备内存](../img/ch05/host-device-memory.png)
:width:`800px`

View File

@@ -12,11 +12,11 @@ DSL实现的代码。而所谓编译通俗来说起到的是针对目标
如果不考虑优化和实际中芯片的体系结构特点,只需要按照算子表达式的**计算逻辑**,把输入进来的张量全部加载进计算核心里完成计算,之后再把计算结果从计算核心里面取出并保存下来即可。这里的**计算逻辑**指的就是基本数学运算(如加、减、乘、除)以及其他函数表达式(如卷积、转置、损失函数)等。
但是 :numref:`fig:ch05/ch05-memory_architecture`向我们展示的现代计算机存储结构表明:越靠近金字塔顶尖的存储器造价越高但是访问速度越快。
但是 :numref:`ch05-memory_architecture`向我们展示的现代计算机存储结构表明:越靠近金字塔顶尖的存储器造价越高但是访问速度越快。
![代计算机存储层次图](../img/ch05/memory_architecture.png)
:width:`800px`
:label:`fig:ch05/ch05-memory_architecture`
:label:`ch05-memory_architecture`
基于这一硬件设计的事实有局部性Locality概念
@@ -27,11 +27,11 @@ DSL实现的代码。而所谓编译通俗来说起到的是针对目标
满足这两者任一都会有较好的性能提升。基于局部性概念,希望尽量把需要重复处理的数据放在固定的内存位置,且这一内存位置离处理器越近越好,以通过提升访存速度而进行性能提升。
另外,把传统的串行计算任务按逻辑和数据依赖关系进行分割后,有机会得到多组互不相关的数据,并把他们同时计算,如 :numref:`fig:ch05/ch05-parallel_computing`所示。
另外,把传统的串行计算任务按逻辑和数据依赖关系进行分割后,有机会得到多组互不相关的数据,并把他们同时计算,如 :numref:`ch05-parallel_computing`所示。
![串行计算和并行计算区别图](../img/ch05/parallel_computing.png)
:width:`800px`
:label:`fig:ch05/ch05-parallel_computing`
:label:`ch05-parallel_computing`
以上种种在程序实际运行的时候针对数据做出的特殊操作,统称为**调度Schedule**。调度定义了:
@@ -47,8 +47,7 @@ DSL实现的代码。而所谓编译通俗来说起到的是针对目标
算子编译器的一种优化思路是将抽象出来的调度策略进行组合拼接排布出一个复杂而高效的调度集合。子策略组合优化本质上还是基于人工手动模板匹配的优化方式依赖于开发人员对于硬件架构有较深的理解。这种方式较为直接但组合出的优化策略无法调优同时对各类算子精细化的优化也带来较多的人力耗费。本文以TVM为例通过在CPU上加速优化一段实际代码简要介绍其中几种基本调度策略组成的优化算法。
我们以形式为乘累加计算的代码[\[lst:before_tvm\]](#lst:before_tvm){reference-type="ref"
reference="lst:before_tvm"}为例简要分析描述这一算法。该代码的核心计算逻辑为首先对张量C进行初始化之后将张量A与张量B相乘后结果累加到张量C中。
我们以形式为乘累加计算的代码为例简要分析描述这一算法。该代码的核心计算逻辑为首先对张量C进行初始化之后将张量A与张量B相乘后结果累加到张量C中。
``` {#lst:before_tvm caption="乘累加计算代码" label="lst:before_tvm"}
for (m: int32, 0, 1024) {
@@ -72,8 +71,7 @@ Cache为32KB。因此按照此代码形式要将整块张量A、B、C一
$\times$
n.inner构成的小块Block即可而其他的外层循环不会影响最内层小块的访存。其占用内存大小为32
$\times$ 32 $\times$ 3 $\times$ sizeof(float) =
12KB足够放入缓存中。代码[\[lst:after_tvm\]](#lst:after_tvm){reference-type="ref"
reference="lst:after_tvm"}展示了经过该策略优化优化后的变化。
12KB足够放入缓存中。如下代码展示了经过该策略优化优化后的变化。
``` {#lst:after_tvm caption="子策略组合优化后的代码" label="lst:after_tvm"}
// 由for (m: int32, 0, 1024)以32为因子平铺得到外层循环
@@ -120,8 +118,7 @@ for (m.outer: int32, 0, 32) {
Compilation基于约束对调度空间求解和Ansor调度空间搜索等。这类方法的好处是提升了算子编译的泛化能力缺点是搜索空间过程会导致编译时间过长。
以多面体模型编译技术将代码的多层循环抽象为多维空间,将每个计算实例抽象为空间中的点,实例间的依赖关系抽象为空间中的线,主要对循环进行优化。该算法的主要思想是针对输入代码的访存特点进行建模,调整循环语句中的每一个实例的执行顺序,使得新调度下的循环代码有更好的局部性和并行性。
我们以代码[\[lst:before_poly\]](#lst:before_poly){reference-type="ref"
reference="lst:before_poly"}为例介绍该算法。
我们以如下代码为例介绍该算法。
``` {#lst:before_poly caption="待优化代码" label="lst:before_poly"}
for (int i = 0; i < N; i++)
@@ -129,15 +126,14 @@ for (int i = 0; i < N; i++)
a[i+1][j] = a[i][j+1] - a[i][j] + a[i][j-1];
```
如 :numref:`fig:ch05/ch05-poly_test`所示,通过多面体模型算法先对此代码的访存结构进行建模,然后分析实例(即 :numref:`fig:ch05/ch05-poly_test`中节点)间的依赖关系(即 :numref:`fig:ch05/ch05-poly_test`中箭头)。
如 :numref:`ch05-poly_test`所示,通过多面体模型算法先对此代码的访存结构进行建模,然后分析实例(即 :numref:`ch05-poly_test`中节点)间的依赖关系(即 :numref:`ch05-poly_test`中箭头)。
![示例代码的多面体模型](../img/ch05/poly_test.png)
:width:`800px`
:label:`fig:ch05/ch05-poly_test`
:label:`ch05-poly_test`
再进行复杂的依赖分析和调度变换之后得到一个符合内存模型的最优解。代码[\[lst:after_poly\]](#lst:after_poly){reference-type="ref"
reference="lst:after_poly"}显示了经过多面体模型优化后得到的结果。
再进行复杂的依赖分析和调度变换之后得到一个符合内存模型的最优解。如下代码显示了经过多面体模型优化后得到的结果。
``` {#lst:after_poly caption="多面体模型算法优化后的代码" label="lst:after_poly"}
for (int i_new = 0; i_new < N; i_new++)
@@ -145,13 +141,13 @@ for (int i_new = 0; i_new < N; i_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:`fig:ch05/ch05-poly`所示:经过算法优化后解除了原代码中的循环间的依赖关系,从而提高了并行计算的机会。即沿着 :numref:`fig:ch05/ch05-poly`中虚线方向分割并以绿色块划分后,可以实现并行计算。
观察得到的代码,发现优化后的代码较为复杂。但是仅凭肉眼很难发现其性能优势之处。仍需对此优化后的代码进行如算法描述那样建模,并分析依赖关系后得出结论,如 :numref:`ch05-poly`所示:经过算法优化后解除了原代码中的循环间的依赖关系,从而提高了并行计算的机会。即沿着 :numref:`ch05-poly`中虚线方向分割并以绿色块划分后,可以实现并行计算。
该算法较为复杂,限于篇幅,在这里不再详细展开。读者可移步到笔者专门为此例写的文章-《深度学习编译之多面体模型编译------以优化简单的两层循环代码为例》详读。
![多面体模型优化结果](../img/ch05/poly.png)
:width:`800px`
:label:`fig:ch05/ch05-poly`
:label:`ch05-poly`
### 芯片指令集适配
@@ -160,18 +156,18 @@ for (int i_new = 0; i_new < N; i_new++)
当下的AI芯片中常见的编程模型分为单指令多数据Single Instruction,
Multiple Data,
SIMD即单条指令一次性处理大量数据如 :numref:`fig:ch05/ch05-SIMD`所示单指令多线程Single Instruction,
SIMD即单条指令一次性处理大量数据如 :numref:`ch05-SIMD`所示单指令多线程Single Instruction,
Multiple Threads,
SIMT即单条指令一次性处理多个线程的数据如 :numref:`fig:ch05/ch05-SIMT`所示。前者对应的是带有向量计算指令的芯片;后者对应的是带有明显的线程分级的芯片。另外,也有一些芯片开始结合这两种编程模型的特点,既有类似线程并行计算的概念,又有向量指令的支持。针对不同的编程模型,算子编译器在进行优化(如向量化等)时的策略也会有所不同。
SIMT即单条指令一次性处理多个线程的数据如 :numref:`ch05-SIMT`所示。前者对应的是带有向量计算指令的芯片;后者对应的是带有明显的线程分级的芯片。另外,也有一些芯片开始结合这两种编程模型的特点,既有类似线程并行计算的概念,又有向量指令的支持。针对不同的编程模型,算子编译器在进行优化(如向量化等)时的策略也会有所不同。
![单指令多数据流示意图](../img/ch05/SIMD.png)
:width:`800px`
:label:`fig:ch05/ch05-SIMD`
:label:`ch05-SIMD`
![单指令多线程示意图](../img/ch05/SIMT.png)
:width:`800px`
:label:`fig:ch05/ch05-SIMT`
:label:`ch05-SIMT`
一般来说算子编译器在具体的代码中会按照前端、中端、后端逐渐差异化的思路进行实现。即在前端设计中兼容多种不同芯片后端的指令集以帮助编译器用户即AI程序员不需要在乎芯片差异而只需要专注在AI算法逻辑上即可在中间表示IR设计中对不同芯片的体系结构进行区分从而可以实现对不同芯片进行不同的优化方法在后端的目标代码生成部分对各个芯片的不同指令集详细区分以保证生成出的目标代码能够顺利运行在目标芯片上。