在本系列这一篇中所说的性能,是指运算速度之类的性能。
0 GPU体系结构与计算性能
本节的目的不是介绍GPU的体系结构,而是谈几点必要的概念以便读者能更好的理解本文所讨论的性能分析。NVIDIA的GPU和CUDA编程模型是现在深度学习的主要硬件平台,我们将基于它们进行讨论。
下面的手绘图完整的展示了PC以及GPU内部的硬件结构,图中故意忽略了某些硬件单元以便集中在主要概念上。
GPU的全局内存相当于PC中的主存(也就是我们一般说的内存),显存容量就是指全局内存的大小。GPU程序中任何的全局变量都需要保存在全局内存中,比如在深度学习网络的训练过程中,网络参数,各层的输出,以及梯度值等等都需要保存在GPU的全局内存里。而GPU中的SM就相当于PC里的CPU,每个GPU有多个SM,每个SM内又包含若干个SP,一个SP有点类似于多核CPU中的一个核,因此GPU可以同时执行大量的并行计算,编写GPU程序的主要目标就是尽量充分的使用GPU的计算能力。
在CUDA编程模型中,使用kernel function来定义在GPU上运行的并行计算函数,按照调用时的执行参数,由大量的并行线程来同时执行kernel function,每个线程对应到一个SP上。CPU上的主程序调用kernel function并把计算数据传给它。从上图可以看出CPU通过PCI-E总线把数据传输给GPU,GPU把数据保存在自己的全局内存中后,真正要计算的时候,还要把数据load到寄存器里。可以想象,数据从硬盘/主存跨越物理距离来到SM的寄存器需要耗费多少GPU的时钟周期。为了抵消这种延时,SM会把一组线程的内存访问请求合并起来,当这组线程等待内存访问请求的结果时,调度另一组就绪的线程运行,这样使GPU始终处在高负荷的运算状态中。GPU中每个SP拥有(编译决定/动态分配的)独立的寄存器组,线程切换不会有什么开销,所以能够支持这种计算模式。
同时,在代码编写上,需要:
- 最大化并行执行,以充分使用GPU的多核
- 提升计算密度,每个线程尽量进行更多的算术运算
- 尽量减少数据传输,比如充分使用存储的空间和时间局部性,使得无论是从硬盘/主存到GPU全局内存,还是SM读取GPU的全局内存(可以存储在SM的共享内存/Cache里),一次读取的数据可以得到充分多次使用
- 使内存读取的时间和运算的时间充分交叠,比如在GPU进行运算的时候,把下一步运算所需的数据从主存预读进GPU显存,避免GPU的空闲。访问寄存器和访问共享内存/Cache的时间可以忽略不计,所以后面对计算性能的分析中,就不再考虑这两部分。谈到内存访问,主要是指对性能有较大影响的全局内存的访问,或者从主机到GPU的数据拷贝。
总之,要想充分发挥GPU的计算性能,就需要让GPU所有的的运算单元充分跑起来,同时在它下一个计算前就把所需的数据准备好。GPU线程调度,CUDA编译器以及应用代码一起协调起来实现这个目的。那么最后在理论上限制性能发挥的就是GPU程序的计算强度,也就是计算量除以内存访问量。如果一个深度学习网络需要的访存量相对它的计算量过大,即使使用上述的优化手段,也很难有好的性能。
前面谈到可以通过使用缓存来降低GPU内存访问的时间,因此,内存访问量的理论下限就是内存的使用量或者说占用量,也就是GPU中全局内存每被使用一个字至少需要一次写和一次读访问。因而内存使用量可以作为一个基数来帮助分析访存量与计算性能。同时,内存使用量受到GPU内存容量的约束,本身也是一个必须关注的指标。
DenseNet被广泛的抱怨显存使用量太大,所以,我们就先从这个方面来进行分析。
1 DenseNet内存使用分析
1.1 深度学习中的内存分析基础
前一节谈到过哪些数据需要存储在GPU的内存中,这一小节就让我们更详细的分析一下深度学习训练中,显存都消耗在哪了?
- 一是参数本身。对卷积层来说,每一层卷积的参数数量为 kernel_szie × kernel_szie × Channel_In × Channel_Out,注意卷积层的参数量和输入的尺寸无关,无论输入的Width × Height 有多大,都是用同样的卷积核去做卷积,只不过输入越大,做卷积的运算量也会越大。BatchNorm层的参数量为 激活数 × 4 ,激活数也就是上一层输出的数量,等于 Width × Height × Channel&