diff --git a/chapter_accelerator/accelerator_practise.md b/chapter_accelerator/accelerator_practise.md index 362388f..b7ea306 100644 --- a/chapter_accelerator/accelerator_practise.md +++ b/chapter_accelerator/accelerator_practise.md @@ -1,6 +1,7 @@ ## 加速器实践 在本节中会通过具体的CUDA代码向读者介绍如何编写一个并行计算的广义矩阵乘法程序,通过提高计算强度、使用共享内存、优化内存读取流水线等方法最终取得接近硬件加速器性能峰值的实现。虽然在以上章节介绍了张量计算核心相关的内容,但由于篇幅限制,在本节中不使用此硬件结构。而是通过使用更为基本的CUDA代码实现FP32的广义矩阵乘法,来讲解若干实用优化策略。 + ### 环境 本节的实践有以下的软件环境依赖: @@ -20,7 +21,7 @@ :label:`sec-accelerator-naive` -依照算法:label:`algo-accelerator-gemm`,编写CPU代码如下所示: +依照算法如 :numref:`gemm-algorith`所示,编写CPU代码如下所示: ```c++ float A[M][K]; float B[K][N]; @@ -109,7 +110,7 @@ Average Throughput: 185.313 GFLOPS 具体的实现如下,由于每个 `float` 类型大小为32个比特,可以将4个 `float` 堆叠在一起构成一个128比特的 `float4` 类,对 `float4` 的访存将会是使用宽指令完成。其具体代码实现见[util.cuh](https://github.com/openmlsys/openmlsys-cuda/blob/main/util.cuh)中。 -在实现GPU核函数过程中要注意,每个线程需要从原本各读取矩阵$A$和矩阵$B$中一个 `float` 数据变为各读取4个 `float` 数据,这就要求现在每个线程负责处理矩阵$C$中$4\times 4$的矩阵块,称之为 `thread tile` 。如图:numref:`use_float4`所示,每个线程从左到右、从上到下分别读取矩阵$A$和矩阵$B$的数据并运算,最后写入到矩阵$C$中。 +在实现GPU核函数过程中要注意,每个线程需要从原本各读取矩阵$A$和矩阵$B$中一个 `float` 数据变为各读取4个 `float` 数据,这就要求现在每个线程负责处理矩阵$C$中$4\times 4$的矩阵块,称之为 `thread tile` 。如 :numref:`use_float4`所示,每个线程从左到右、从上到下分别读取矩阵$A$和矩阵$B$的数据并运算,最后写入到矩阵$C$中。 ![提高计算强度](../img/ch06/6.4/use_float4.png) @@ -117,7 +118,7 @@ Average Throughput: 185.313 GFLOPS :label:`use_float4` -完整代码见[gemm_use_128.cu](https://github.com/openmlsys/openmlsys-cuda/blob/main/gemm_use_128.cu)。我们可以进一步让每个线程处理更多的数据,从而进一步提升计算强度,如图:numref:`use_tile`所示。完整代码见[gemm_use_tile.cu](https://github.com/openmlsys/openmlsys-cuda/blob/main/gemm_use_tile.cu)。 +完整代码见[gemm_use_128.cu](https://github.com/openmlsys/openmlsys-cuda/blob/main/gemm_use_128.cu)。我们可以进一步让每个线程处理更多的数据,从而进一步提升计算强度,如 :numref:`use_tile`所示。完整代码见[gemm_use_tile.cu](https://github.com/openmlsys/openmlsys-cuda/blob/main/gemm_use_tile.cu)。 ![通过提高线程所处理矩阵块的数量来进一步提高计算强度](../img/ch06/6.4/use_tile.png) :width:` 800px` @@ -280,9 +281,9 @@ Average Time: 0.613 ms, Throughput: 14002.600 GFLOPS 要实现一个高性能算子需要依照硬件特性适应性进行若干优化。本节优化策略可总结为以下几点: - - 并行资源映射——提高并行性:将多层级的并行资源(`block` 、`warp` 、`thread` )与对应需要计算和搬移的数据建立映射关系,提高程序并行性。将可并行的计算和数据搬移操作映射到并行资源上,对于广义矩阵乘法实例,在节~\ref{sec-accelerator-naive`朴素实现的例子中,令每个`block` 与矩阵$C$中的一个矩阵块建立映射关系,每个`thread` 与矩阵块中的一个元素建立映射关系。 + - 并行资源映射——提高并行性:将多层级的并行资源(`block` 、`warp` 、`thread` )与对应需要计算和搬移的数据建立映射关系,提高程序并行性。将可并行的计算和数据搬移操作映射到并行资源上,对于广义矩阵乘法实例,在 :numref:`sec-accelerator-naive`朴素实现的例子中,令每个`block` 与矩阵$C$中的一个矩阵块建立映射关系,每个`thread` 与矩阵块中的一个元素建立映射关系。 - - 优化内存结构——减小访存延迟:观察计算过程中同一个`block` 中数据复用的情况,将复用的数据被如共享内存、寄存器等高性能体系结构存储下来,以此提高吞吐量。如在节 :numref`sec-accelerator-naive` 中将矩阵$A$与矩阵$B$中会被同一个 `block` 内不同 `thread` 共同访问的数据缓存到共享内存中。 + - 优化内存结构——减小访存延迟:观察计算过程中同一个`block` 中数据复用的情况,将复用的数据被如共享内存、寄存器等高性能体系结构存储下来,以此提高吞吐量。如在 :numref:`sec-accelerator-naive` 中将矩阵$A$与矩阵$B$中会被同一个 `block` 内不同 `thread` 共同访问的数据缓存到共享内存中。 - 优化指令执行——减小指令发射开销:使用 `#pragma unroll` 功能进行循环展开来提升指令级并行,减少逻辑判断;使用向量化加载指令以提高带宽等,对于Ampere架构,最大向量化加载指令为 `LDG.E.128` ,可以采用 `float4` 类型的数据进行读取。 diff --git a/chapter_accelerator/accelerator_programming.md b/chapter_accelerator/accelerator_programming.md index 8d32728..f8a1192 100644 --- a/chapter_accelerator/accelerator_programming.md +++ b/chapter_accelerator/accelerator_programming.md @@ -6,7 +6,7 @@ ### 硬件加速器的可编程性 :label:`accelerator-programable-title` - :numref:`accelerator-design-title`节中列出的硬件加速器均具有一定的可编程性,程序员可以通过软件编程,有效的使能上述加速器进行计算加速。现有硬件加速器常见的两类编程方式主要有编程接口调用以及算子编译器优化。 + :numref:`accelerator-design-title`中列出的硬件加速器均具有一定的可编程性,程序员可以通过软件编程,有效的使能上述加速器进行计算加速。现有硬件加速器常见的两类编程方式主要有编程接口调用以及算子编译器优化。 #### 编程接口使能加速器 @@ -65,7 +65,7 @@ AKG则是MindSpore社区的开源算子编译工具。与上述介绍的算子 ### 硬件加速器的多样化编程方法 :label:`diversified-programming-title` -矩阵乘法运算作为深度学习网络中占比最大的计算,对其进行优化是十分必要的。因此本节将统一以广义矩阵乘法为实例,对比介绍如何通过不同编程方式使能加速器。广义矩阵乘法指GEMM(General Matrix Multiplication),即$\bm{C} = \alpha \bm{A}\times \bm{B} + \beta \bm{C}$,其中$\bm{A}\in\mathbb{R}^{M\times K}, \bm{B}\in\mathbb{R}^{K\times N}, \bm{C}\in\mathbb{R}^{M\times N}$。 +矩阵乘法运算作为深度学习网络中占比最大的计算,对其进行优化是十分必要的。因此本节将统一以广义矩阵乘法为实例,对比介绍如何通过不同编程方式使能加速器。广义矩阵乘法指GEMM(General Matrix Multiplication),即${C} = \alpha {A}\times {B} + \beta {C}$,其中${A}\in\mathbb{R}^{M\times K}, {B}\in\mathbb{R}^{K\times N}, {C}\in\mathbb{R}^{M\times N}$。 ![矩阵乘法GEMM运算](../img/ch06/gemm.svg) :width:`800px` @@ -78,7 +78,7 @@ AKG则是MindSpore社区的开源算子编译工具。与上述介绍的算子 :label:`sec-accelerator-use-cublas` 在上述不同层级的编程方式中,直接调用算子加速库使能加速器无疑是最快捷高效的方式。NVIDIA提供了cuBLAS/cuDNN两类算子计算库,cuBLAS提供了使能张量计算核心的接口,用以加速矩阵乘法(GEMM)运算,cuDNN提供了对应接口加速卷积(CONV)运算等。 -以 :numref:`accelerator-programable-title`小节的GEMM运算为例,与常规CUDA调用cuBLAS算子库相似,通过cuBLAS加速库使能张量计算核心步骤包括: +以 :numref:`accelerator-programable-title`的GEMM运算为例,与常规CUDA调用cuBLAS算子库相似,通过cuBLAS加速库使能张量计算核心步骤包括: 1. 创建cuBLAS对象句柄且设置对应数学计算模式 diff --git a/chapter_backend_and_runtime/compute_schedule_and_execute.md b/chapter_backend_and_runtime/compute_schedule_and_execute.md index 9bdb12b..13133d7 100644 --- a/chapter_backend_and_runtime/compute_schedule_and_execute.md +++ b/chapter_backend_and_runtime/compute_schedule_and_execute.md @@ -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`所示。 diff --git a/chapter_backend_and_runtime/graph_optimizer.md b/chapter_backend_and_runtime/graph_optimizer.md index 68475f7..7ac11da 100644 --- a/chapter_backend_and_runtime/graph_optimizer.md +++ b/chapter_backend_and_runtime/graph_optimizer.md @@ -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` diff --git a/chapter_backend_and_runtime/memory_allocator.md b/chapter_backend_and_runtime/memory_allocator.md index 4e80054..59da24d 100644 --- a/chapter_backend_and_runtime/memory_allocator.md +++ b/chapter_backend_and_runtime/memory_allocator.md @@ -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` diff --git a/chapter_backend_and_runtime/op_compiler.md b/chapter_backend_and_runtime/op_compiler.md index 3c2243f..fc401c8 100644 --- a/chapter_backend_and_runtime/op_compiler.md +++ b/chapter_backend_and_runtime/op_compiler.md @@ -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)设计中对不同芯片的体系结构进行区分,从而可以实现对不同芯片进行不同的优化方法;在后端的目标代码生成部分对各个芯片的不同指令集详细区分,以保证生成出的目标代码能够顺利运行在目标芯片上。 diff --git a/chapter_model_deployment/model_converter_and_optimizer.md b/chapter_model_deployment/model_converter_and_optimizer.md index b0a5399..19cb181 100644 --- a/chapter_model_deployment/model_converter_and_optimizer.md +++ b/chapter_model_deployment/model_converter_and_optimizer.md @@ -42,7 +42,7 @@ $$\pmb{Y_{bn}}=\gamma\frac{\pmb{X_{bn}}-\mu_{\mathcal{B}}}{\sqrt{{\sigma_{\mathc 同样,这里不需要理解Batchnorm中的所有参数的含义,只需要了解式 :eqref:`ch08-equ-bn_equation`是$\pmb{Y_{bn}}$关于$\pmb{X_{bn}}$的,其他符号均表示常量。 -如 :numref:`ch08-fig-conv_bn_fusion`,当Convlution算子的输出作为Batchnorm输入时,最终Batchnorm算子的计算公式也就是要求$\pmb{Y_{bn}}$关于$\pmb{X_{conv}}$的计算公式,我们将$\pmb{Y_{conv}}$代入到$\pmb{X_{bn}}$,然后将常数项合并提取后,可以得到公式 :eqref:`equ:conv-bn-equation-3`。 +如 :numref:`ch08-fig-conv_bn_fusion`,当Convlution算子的输出作为Batchnorm输入时,最终Batchnorm算子的计算公式也就是要求$\pmb{Y_{bn}}$关于$\pmb{X_{conv}}$的计算公式,我们将$\pmb{Y_{conv}}$代入到$\pmb{X_{bn}}$,然后将常数项合并提取后,可以得到公式 :eqref:`ch08-equ-conv_bn_equation_3`。 $$\pmb{Y_{bn}}=\pmb{A}*\pmb{X_{conv}}+\pmb{B}$$ :eqlabel:`ch08-equ-conv_bn_equation_3` diff --git a/chapter_model_deployment/model_inference.md b/chapter_model_deployment/model_inference.md index cd066db..eb120d8 100644 --- a/chapter_model_deployment/model_inference.md +++ b/chapter_model_deployment/model_inference.md @@ -167,6 +167,6 @@ Winograd算法的整个计算过程在逻辑上可以分为4步,如 :numref:`c :width:`500px` :label:`ch08-fig-winograd` -针对任意的输出大小,要使用\textit{\textbf{F}}(2$\times$2,3$\times$3)的Winograd算法,需要将输出切分成2$\times$2的块,找到对应的输入,按照上述的四个步骤,就可以求出对应的输出值。当然,Winograd算法并不局限于求解\textit{\textbf{F}}(2$\times$2,3$\times$3),针对任意的\textit{\textbf{F}}($m$$\times$$m$,$r$$\times$$r$),都可以找到适当的常量矩阵\textit{\textbf{A}}、\textit{\textbf{B}}、\textit{\textbf{G}},通过间接计算的方式减少乘法次数。但是随着$m$、$r$的增大,输入、输出涉及的加法以及常量权重的乘法次数都在增加,那么乘法次数带来的计算量下降会被加法和常量乘法所抵消。因此,在实际使用场景中,还需要根据Winograd的实际收益来选择。 +针对任意的输出大小,要使用$\textit{\textbf{F}}(2\times2,3\times3)$的Winograd算法,需要将输出切分成$2\times2$的块,找到对应的输入,按照上述的四个步骤,就可以求出对应的输出值。当然,Winograd算法并不局限于求解$\textit{\textbf{F}}(2\times2,3\times3)$,针对任意的$\textit{\textbf{F}}(m\times m,r\times r)$,都可以找到适当的常量矩阵$\textit{\textbf{A}}$、$\textit{\textbf{B}}$、$\textit{\textbf{G}}$,通过间接计算的方式减少乘法次数。但是随着$m$、$r$的增大,输入、输出涉及的加法以及常量权重的乘法次数都在增加,那么乘法次数带来的计算量下降会被加法和常量乘法所抵消。因此,在实际使用场景中,还需要根据Winograd的实际收益来选择。 本小节主要介绍了模型推理时的数据处理和性能优化手段。选择合适的数据处理方法,可以更好地提取输入特征,处理输出结果。并行计算以及算子级别的硬件指令与算法优化可以最大限度的发挥硬件的算力。除此之后,内存的占用及访问速率也是影响推理性能的重要因素,因此推理时需要设计合理的内存复用策略,内存复用的策略已经在编译器后端章节已经做了阐述。