<audio id="audio" title="31 | GPU（下）：为什么深度学习需要使用GPU？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/63/7d/6344f07eca4814953ae3a9d8866dd47d.mp3"></audio>

上一讲，我带你一起看了三维图形在计算机里的渲染过程。这个渲染过程，分成了顶点处理、图元处理、 栅格化、片段处理，以及最后的像素操作。这一连串的过程，也被称之为图形流水线或者渲染管线。

因为要实时计算渲染的像素特别地多，图形加速卡登上了历史的舞台。通过3dFx的Voodoo或者NVidia的TNT这样的图形加速卡，CPU就不需要再去处理一个个像素点的图元处理、栅格化和片段处理这些操作。而3D游戏也是从这个时代发展起来的。

你可以看这张图，这是“古墓丽影”游戏的多边形建模的变化。这个变化，则是从1996年到2016年，这20年来显卡的进步带来的。

<img src="https://static001.geekbang.org/resource/image/1d/c3/1d098ce5b2c779392c8d3a33636673c3.png" alt="">

## Shader的诞生和可编程图形处理器

不知道你有没有发现，在Voodoo和TNT显卡的渲染管线里面，没有“顶点处理“这个步骤。在当时，把多边形的顶点进行线性变化，转化到我们的屏幕的坐标系的工作还是由CPU完成的。所以，CPU的性能越好，能够支持的多边形也就越多，对应的多边形建模的效果自然也就越像真人。而3D游戏的多边形性能也受限于我们CPU的性能。无论你的显卡有多快，如果CPU不行，3D画面一样还是不行。

所以，1999年NVidia推出的GeForce 256显卡，就把顶点处理的计算能力，也从CPU里挪到了显卡里。不过，这对于想要做好3D游戏的程序员们还不够，即使到了GeForce 256。整个图形渲染过程都是在硬件里面固定的管线来完成的。程序员们在加速卡上能做的事情呢，只有改配置来实现不同的图形渲染效果。如果通过改配置做不到，我们就没有什么办法了。

这个时候，程序员希望我们的GPU也能有一定的可编程能力。这个编程能力不是像CPU那样，有非常通用的指令，可以进行任何你希望的操作，而是在整个的**渲染管线**（Graphics Pipeline）的一些特别步骤，能够自己去定义处理数据的算法或者操作。于是，从2001年的Direct3D 8.0开始，微软第一次引入了**可编程管线**（Programable Function Pipeline）的概念。

<img src="https://static001.geekbang.org/resource/image/27/6d/2724f76ffa4222eae01521cd2dffd16d.jpeg" alt="">

一开始的可编程管线呢，仅限于顶点处理（Vertex Processing）和片段处理（Fragment Processing）部分。比起原来只能通过显卡和Direct3D这样的图形接口提供的固定配置，程序员们终于也可以开始在图形效果上开始大显身手了。

这些可以编程的接口，我们称之为**Shader**，中文名称就是**着色器**。之所以叫“着色器”，是因为一开始这些“可编程”的接口，只能修改顶点处理和片段处理部分的程序逻辑。我们用这些接口来做的，也主要是光照、亮度、颜色等等的处理，所以叫着色器。

这个时候的GPU，有两类Shader，也就是Vertex Shader和Fragment Shader。我们在上一讲看到，在进行顶点处理的时候，我们操作的是多边形的顶点；在片段操作的时候，我们操作的是屏幕上的像素点。对于顶点的操作，通常比片段要复杂一些。所以一开始，这两类Shader都是独立的硬件电路，也各自有独立的编程接口。因为这么做，硬件设计起来更加简单，一块GPU上也能容纳下更多的Shader。

不过呢，大家很快发现，虽然我们在顶点处理和片段处理上的具体逻辑不太一样，但是里面用到的指令集可以用同一套。而且，虽然把Vertex Shader和Fragment Shader分开，可以减少硬件设计的复杂程度，但是也带来了一种浪费，有一半Shader始终没有被使用。在整个渲染管线里，Vertext Shader运行的时候，Fragment Shader停在那里什么也没干。Fragment Shader在运行的时候，Vertext Shader也停在那里发呆。

本来GPU就不便宜，结果设计的电路有一半时间是闲着的。喜欢精打细算抠出每一分性能的硬件工程师当然受不了了。于是，**统一着色器架构**（Unified Shader Architecture）就应运而生了。

既然大家用的指令集是一样的，那不如就在GPU里面放很多个一样的Shader硬件电路，然后通过统一调度，把顶点处理、图元处理、片段处理这些任务，都交给这些Shader去处理，让整个GPU尽可能地忙起来。这样的设计，就是我们现代GPU的设计，就是统一着色器架构。

有意思的是，这样的GPU并不是先在PC里面出现的，而是来自于一台游戏机，就是微软的XBox 360。后来，这个架构才被用到ATI和NVidia的显卡里。这个时候的“着色器”的作用，其实已经和它的名字关系不大了，而是变成了一个通用的抽象计算模块的名字。

正是因为Shader变成一个“通用”的模块，才有了把GPU拿来做各种通用计算的用法，也就是**GPGPU**（General-Purpose Computing on Graphics Processing Units，通用图形处理器）。而正是因为GPU可以拿来做各种通用的计算，才有了过去10年深度学习的火热。

<img src="https://static001.geekbang.org/resource/image/da/93/dab4ed01f50995d82e6e5d970b54c693.jpeg" alt="">

## 现代GPU的三个核心创意

讲完了现代GPU的进化史，那么接下来，我们就来看看，为什么现代的GPU在图形渲染、深度学习上能那么快。

### 芯片瘦身

我们先来回顾一下，之前花了很多讲仔细讲解的现代CPU。现代CPU里的晶体管变得越来越多，越来越复杂，其实已经不是用来实现“计算”这个核心功能，而是拿来实现处理乱序执行、进行分支预测，以及我们之后要在存储器讲的高速缓存部分。

而在GPU里，这些电路就显得有点多余了，GPU的整个处理过程是一个[流式处理](https://en.wikipedia.org/wiki/Stream_processing)（Stream Processing）的过程。因为没有那么多分支条件，或者复杂的依赖关系，我们可以把GPU里这些对应的电路都可以去掉，做一次小小的瘦身，只留下取指令、指令译码、ALU以及执行这些计算需要的寄存器和缓存就好了。一般来说，我们会把这些电路抽象成三个部分，就是下面图里的取指令和指令译码、ALU和执行上下文。

<img src="https://static001.geekbang.org/resource/image/4c/9d/4c153ac45915fbf3985d24b092894b9d.jpeg" alt="">

### 多核并行和SIMT

这样一来，我们的GPU电路就比CPU简单很多了。于是，我们就可以在一个GPU里面，塞很多个这样并行的GPU电路来实现计算，就好像CPU里面的多核CPU一样。和CPU不同的是，我们不需要单独去实现什么多线程的计算。因为GPU的运算是天然并行的。

<img src="https://static001.geekbang.org/resource/image/3d/ac/3d0859652adf9e3c0305e8e8517b47ac.jpeg" alt="">

我们在上一讲里面其实已经看到，无论是对多边形里的顶点进行处理，还是屏幕里面的每一个像素进行处理，每个点的计算都是独立的。所以，简单地添加多核的GPU，就能做到并行加速。不过光这样加速还是不够，工程师们觉得，性能还有进一步被压榨的空间。

我们在[第27讲](https://time.geekbang.org/column/article/103433)里面讲过，CPU里有一种叫作SIMD的处理技术。这个技术是说，在做向量计算的时候，我们要执行的指令是一样的，只是同一个指令的数据有所不同而已。在GPU的渲染管线里，这个技术可就大有用处了。

无论是顶点去进行线性变换，还是屏幕上临近像素点的光照和上色，都是在用相同的指令流程进行计算。所以，GPU就借鉴了CPU里面的SIMD，用了一种叫作[SIMT](https://en.wikipedia.org/wiki/Single_instruction,_multiple_threads)（Single Instruction，Multiple Threads）的技术。SIMT呢，比SIMD更加灵活。在SIMD里面，CPU一次性取出了固定长度的多个数据，放到寄存器里面，用一个指令去执行。而SIMT，可以把多条数据，交给不同的线程去处理。

各个线程里面执行的指令流程是一样的，但是可能根据数据的不同，走到不同的条件分支。这样，相同的代码和相同的流程，可能执行不同的具体的指令。这个线程走到的是if的条件分支，另外一个线程走到的就是else的条件分支了。

于是，我们的GPU设计就可以进一步进化，也就是在取指令和指令译码的阶段，取出的指令可以给到后面多个不同的ALU并行进行运算。这样，我们的一个GPU的核里，就可以放下更多的ALU，同时进行更多的并行运算了。

<img src="https://static001.geekbang.org/resource/image/3d/28/3d7ce9c053815f6a32a6fbf6f7fb9628.jpeg" alt="">

### GPU里的“超线程”

虽然GPU里面的主要以数值计算为主。不过既然已经是一个“通用计算”的架构了，GPU里面也避免不了会有if…else这样的条件分支。但是，在GPU里我们可没有CPU这样的分支预测的电路。这些电路在上面“芯片瘦身”的时候，就已经被我们砍掉了。

所以，GPU里的指令，可能会遇到和CPU类似的“流水线停顿”问题。想到流水线停顿，你应该就能记起，我们之前在CPU里面讲过超线程技术。在GPU上，我们一样可以做类似的事情，也就是遇到停顿的时候，调度一些别的计算任务给当前的ALU。

和超线程一样，既然要调度一个不同的任务过来，我们就需要针对这个任务，提供更多的**执行上下文**。所以，一个Core里面的**执行上下文**的数量，需要比ALU多。

<img src="https://static001.geekbang.org/resource/image/c9/b8/c971c34e0456dea9e4a87857880bb5b8.jpeg" alt="">

## GPU在深度学习上的性能差异

在通过芯片瘦身、SIMT以及更多的执行上下文，我们就有了一个更擅长并行进行暴力运算的GPU。这样的芯片，也正适合我们今天的深度学习的使用场景。

一方面，GPU是一个可以进行“通用计算”的框架，我们可以通过编程，在GPU上实现不同的算法。另一方面，现在的深度学习计算，都是超大的向量和矩阵，海量的训练样本的计算。整个计算过程中，没有复杂的逻辑和分支，非常适合GPU这样并行、计算能力强的架构。

我们去看NVidia 2080显卡的[技术规格](https://www.techpowerup.com/gpu-specs/geforce-rtx-2080.c3224)，就可以算出，它到底有多大的计算能力。

2080一共有46个SM（Streaming Multiprocessor，流式处理器），这个SM相当于GPU里面的GPU Core，所以你可以认为这是一个46核的GPU，有46个取指令指令译码的渲染管线。每个SM里面有64个Cuda Core。你可以认为，这里的Cuda Core就是我们上面说的ALU的数量或者Pixel Shader的数量，46x64呢一共就有2944个Shader。然后，还有184个TMU，TMU就是Texture Mapping Unit，也就是用来做纹理映射的计算单元，它也可以认为是另一种类型的Shader。

<img src="https://static001.geekbang.org/resource/image/14/e2/14d05a43f559cecff2b0813e8d5bdde2.png" alt="">

2080的主频是1515MHz，如果自动超频（Boost）的话，可以到1700MHz。而NVidia的显卡，根据硬件架构的设计，每个时钟周期可以执行两条指令。所以，能做的浮点数运算的能力，就是：

对照一下官方的技术规格，正好就是10.07TFLOPS。

那么，最新的Intel i9 9900K的性能是多少呢？不到1TFLOPS。而2080显卡和9900K的价格却是差不多的。所以，在实际进行深度学习的过程中，用GPU所花费的时间，往往能减少一到两个数量级。而大型的深度学习模型计算，往往又是多卡并行，要花上几天乃至几个月。这个时候，用CPU显然就不合适了。

今天，随着GPGPU的推出，GPU已经不只是一个图形计算设备，更是一个用来做数值计算的好工具了。同样，也是因为GPU的快速发展，带来了过去10年深度学习的繁荣。

## 总结延伸

这一讲里面，我们讲了，GPU一开始是没有“可编程”能力的，程序员们只能够通过配置来设计需要用到的图形渲染效果。随着“可编程管线”的出现，程序员们可以在顶点处理和片段处理去实现自己的算法。为了进一步去提升GPU硬件里面的芯片利用率，微软在XBox 360里面，第一次引入了“统一着色器架构”，使得GPU变成了一个有“通用计算”能力的架构。

接着，我们从一个CPU的硬件电路出发，去掉了对GPU没有什么用的分支预测和乱序执行电路，来进行瘦身。之后，基于渲染管线里面顶点处理和片段处理就是天然可以并行的了。我们在GPU里面可以加上很多个核。

又因为我们的渲染管线里面，整个指令流程是相同的，我们又引入了和CPU里的SIMD类似的SIMT架构。这个改动，进一步增加了GPU里面的ALU的数量。最后，为了能够让GPU不要遭遇流水线停顿，我们又在同一个GPU的计算核里面，加上了更多的执行上下文，让GPU始终保持繁忙。

GPU里面的多核、多ALU，加上多Context，使得它的并行能力极强。同样架构的GPU，如果光是做数值计算的话，算力在同样价格的CPU的十倍以上。而这个强大计算能力，以及“统一着色器架构”，使得GPU非常适合进行深度学习的计算模式，也就是海量计算，容易并行，并且没有太多的控制分支逻辑。

使用GPU进行深度学习，往往能够把深度学习算法的训练时间，缩短一个，乃至两个数量级。而GPU现在也越来越多地用在各种科学计算和机器学习上，而不仅仅是用在图形渲染上了。

## 推荐阅读

关于现代GPU的工作原理，你可以仔细阅读一下 haifux.org 上的这个[PPT](http://haifux.org/lectures/267/Introduction-to-GPUs.pdf)，里面图文并茂地解释了现代GPU的架构设计的思路。

## 课后思考

上面我给你算了NVidia 2080显卡的FLOPS，你可以尝试算一下9900K CPU的FLOPS。

欢迎在留言区写下你的答案，你也可以把今天的内容分享给你的朋友，和他一起学习和进步。


