目录

为什么 GPU 能加速深度学习

接触过深度学习的同学都知道在训练和推理中使用 GPU 能加速,但是相对于 CPU 来说为什么 GPU 能在深度学习中提供更快的处理速度?我把自己学习和总结的成果分享在这篇文章中。

CPU 和 GPU

CPU 的结构主要包括运算器(ALU, Arithmetic and Logic Unit)、控制单元(CU, Control Unit)、寄存器(Register)、高速缓存器(Cache)和它们之间通讯的数据、控制及状态的总线。 CPU 是基于低延时的设计,简单来说包括:计算单元、控制单元和存储单元,架构可参考下图:

cpu-arch

CPU 的特点:

  • CPU有强大的ALU(算术运算单元),它可以在很少的时钟周期内完成算术计算 当今的 CPU 可以达到 64bit 双精度。执行双精度浮点源算的加法和乘法只需要1~3个时钟周期(CPU的时钟周期的频率是非常高的,达到1.532~3gigahertz(千兆HZ, 10的9次方))
  • 大的缓存可以降低延时 保存很多的数据放在缓存里面,当需要访问的这些数据,只要在之前访问过的,如今直接在缓存里面取即可。
  • 复杂的逻辑控制单元
    当程序含有多个分支的时候,它通过提供分支预测的能力来降低延时。 数据转发。当一些指令依赖前面的指令结果时,数据转发的逻辑控制单元决定这些指令在 pipeline 中的位置并且尽可能快的转发一个指令的结果给后续的指令。这些动作需要很多的对比电路单元和转发电路单元。

GPU 全称为 Graphics Processing Unit,中文为图形处理器,就如它的名字一样,GPU 最初是用在个人电脑、工作站、游戏机和一些移动设备(如平板电脑、智能手机等)上运行绘图运算工作的微处理器。

GPU 是基于大的吞吐量设计,GPU 简单架构参考下图:

gpu-arch

GPU 的特点:

  • 有很多的 ALU 和很少的 cache
    缓存的目的不是保存后面需要访问的数据的,这点和 CPU 不同,而是为 thread 提高服务的。如果有很多线程需要访问同一个相同的数据,缓存会合并这些访问,然后再去访问 dram(因为需要访问的数据保存在 dram 中而不是 cache 里面),获取数据后 cache 会转发这个数据给对应的线程,这个时候是数据转发的角色。但是由于需要访问 dram,自然会带来延时的问题。
  • GPU 的控制单元(左边黄色区域块)可以把多个的访问合并成少的访问
  • GPU 的虽然有 dram 延时,却有非常多的 ALU 和非常多的 thread
    为了平衡内存延时的问题,GPU 可以充分利用多的 ALU 的特性达到一个非常大的吞吐量的效果。尽可能多的分配 Threads。

CPU 和 GPU 的优势

当代 CPU 的微架构是按照兼顾“指令并行执行”和“数据并行运算”的思路而设计,就是要兼顾程序执行和数据运算的并行性、通用性以及它们的平衡性。
CPU 的微架构偏重于程序执行的效率,不会一味追求某种运算极致速度而牺牲程序执行的效率。
CPU 微架构的设计是面向指令执行高效率而设计的,因而 CP U是计算机中设计最复杂的芯片。和 GPU 相比,CPU 核心的重复设计部分不多,这种复杂性不能仅以晶体管的多寡来衡量,这种复杂性来自于实现:如程序分支预测,推测执行,多重嵌套分支执行,并行执行时候的指令相关性和数据相关性,多核协同处理时候的数据一致性等等复杂逻辑。

GPU 其实是由硬件实现的一组图形函数的集合,这些函数主要用于绘制各种图形所需要的运算。这些和像素,光影处理,3D 坐标变换等相关的运算由 GPU 硬件加速来实现。图形运算的特点是大量同类型数据的密集运算——如图形数据的矩阵运算,GPU 的微架构就是面向适合于矩阵类型的数值计算而设计的,大量重复设计的计算单元,这类计算可以分成众多独立的数值计算——大量数值运算的线程,而且数据之间没有像程序执行的那种逻辑关联性。
GPU 微架构复杂度不高,尽管晶体管的数量不少。从应用的角度看,如何运用好GPU的并行计算能力主要的工作是开发好它的驱动程序。
GPU 驱动程序的优劣很大程度左右了GPU实际性能的发挥。
因此从架构上看

  • CPU 擅长的是像操作系统、系统软件和通用应用程序这类拥有复杂指令调度、循环、分支、逻辑判断以及执行等的程序任务。它的并行优势是程序执行层面的,程序逻辑的复杂度也限定了程序执行的指令并行性,上百个并行程序执行的线程基本看不到。
  • GPU 擅长的是图形类的或者是非图形类的高度并行数值计算,GPU 可以容纳上千个没有逻辑关系的数值计算线程,它的优势是无逻辑关系数据的并行计算。

深度学习的特征

深度学习的概念源于人工神经网络的研究。含多隐层的多层感知器就是一种深度学习结构。深度学习通过组合低层特征形成更加抽象的高层表示属性类别或特征,以发现数据的分布式特征表示。
深度学习采用的模型为深层神经网络(Deep Neural Networks,DNN)模型,即包含多个隐藏层(Hidden Layer,也称隐含层)的神经网络(Neural Networks,NN)。深度学习利用模型中的隐藏层,通过特征组合的方式,逐层将原始输入转化为浅层特征、中层特征、高层特征直至最终的任务目标。
而在上面的 GPU 的介绍中我们看到 GPU 非常擅长高度并行(embarrassingly parallel)数值计算。在并行计算中,高度并行任务是指将整个任务分割成一组较小的任务以并行计算的任务。 高度并行任务是那些很容易看到一组小任务彼此独立的任务。由于这个原因,神经网络高度并行。我们用神经网络做的许多计算都可以很容易地分解成更小的计算,这样小的计算集就不会相互依赖。这些小的计算我们拿卷积来举例。

卷积

下图的这个示例展示了卷积的计算过程

convolution

底部有一个蓝色的输入通道。在输入通道上滑动的底部有一个阴影的卷积滤波器,还有一个绿色的输出通道。 图上蓝色(底部)表示输入通道; 阴影(覆盖在蓝色上)表示3*3的卷积过滤器; 绿色(顶部)表示输出通道。
对于蓝色输入通道上的每个位置,3x3过滤器进行计算,将蓝色输入通道的阴影部分映射到绿色输出通道的相应阴影部分。在动画中,这些计算一个接一个地依次进行。但是,每个计算都是独立于其他计算的,这意味着任何计算都不依赖于任何其他计算的结果。因此,所有这些独立的计算都可以在GPU上并行进行,从而产生整个输出通道。这让我们看到,卷积运算可以通过使用并行编程方法和 GPU 来加速。

矩阵乘法

https://pics.lxkaka.wang/matrix.jpeg

而在深度学习的卷积过程中少不了矩阵相乘。我们具体看一个矩阵相乘的例子如何在 GPU 中实现。 在C中,一般的实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void matrixMult (int a[N][N], int b[N][N], int c[N][N], int width)
{
	for (int i = 0; i < width; i++) {
		for (int j = 0; j < width; j++) {
			int sum = 0;
			for (int k = 0; k < width; k++) {
				int m = a[i][k];
				int n = b[k][j];
				sum += m * n;
			}
			c[i][j] = sum;
		}
	}
}

其中,矩阵width是矩阵A的列数,显然,上面算法的复杂度是O(N^3)。采用GPU编程只需将上面的方法写成kernel function的形式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
__global__ void matrixMult (int *a, int *b, int *c, int width) {
	int k, sum = 0;
	int col = threadIdx.x + blockDim.x * blockIdx.x;
	int row = threadIdx.y + blockDim.y * blockIdx.y;
	if(col < width && row < width) {
		for (k = 0; k < width; k++) {
			sum += a[row * width + k] * b[k * width + col];
		}
		c[row * width + col] = sum;
	}
}

对比一下C和GPU实现的线程数量和时间复杂度:
| |线程数量| 时间复杂度| |—|— |— | |CPU |1 |N^3 | |GPU |N^2 |N |

Nvidia CUDA

CUDA(Compute Unified Device Architecture)是NVIDIA公司基于其生产的图形处理器 GPU 开发的一个并行计算平台和编程模型。2007年 Nvidia 发布了CUDA编程模型,软件开发人员从此可以使用CUDA在英伟达的GPU上进行并行编程。
继 CUDA之后,Nvidia 不断丰富其软件技术栈,提供了科学计算所必须的 cuBLAS 线性代数库,cuFFT 快速傅里叶变换库等,当深度学习大潮到来时,英伟达提供了cuDNN深度神经网络加速库,目前常用的 TensorFlow、PyTorch 深度学习框架的底层大多基于 cuDNN 库。这些软件工具库使研发人员专注于自己的研发领域,不用再去花大量时间学习 GPU 底层知识。
GPU 编程可以直接使用 CUDA 的 C/C++ 版本进行编程,也可以使用其他语言包装好的库,比如 Python可使用 Numba 库调用 CUDA。
我们此前提到的模型训练和推理不是单靠 GPU 就能完成,而是需要 CPU + GPU 协同工作才能完成。我们在说 GPU 并行计算时,其实是指的基于 CPU+GPU 的异构计算架构。在异构计算架构中,GPU 与CPU 通过 PCIe 总线连接在一起来协同工作,CPU 所在位置称为为主机端(host),而GPU所在位置称为设备端(device),如下图所示

https://pics.lxkaka.wang/cpu-gpu.png

所以 CUDA 的编程模型就是基于这样的异构系统,工作原理如下:

  1. 分配host内存,并进行数据初始化
  2. 分配 device 内存,并从 host 将数据拷贝到 device 上,
  3. 调用 CUDA 的核函数在 device 上完成指定的运算;
  4. 将 device 上的运算结果拷贝到 host 上;

cuda-flow

通过上述的介绍我们明白深度学习高度并行计算的特性与 GPU 大量的计算核心提供并行计算和更大的内存带宽完美契合,所以 GPU 成了加速深度学习的不二选择。