gitbook/深入浅出计算机组成原理/docs/105401.md
2022-09-03 22:05:03 +08:00

130 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 31 | GPU为什么深度学习需要使用GPU
上一讲,我带你一起看了三维图形在计算机里的渲染过程。这个渲染过程,分成了顶点处理、图元处理、 栅格化、片段处理,以及最后的像素操作。这一连串的过程,也被称之为图形流水线或者渲染管线。
因为要实时计算渲染的像素特别地多图形加速卡登上了历史的舞台。通过3dFx的Voodoo或者NVidia的TNT这样的图形加速卡CPU就不需要再去处理一个个像素点的图元处理、栅格化和片段处理这些操作。而3D游戏也是从这个时代发展起来的。
你可以看这张图这是“古墓丽影”游戏的多边形建模的变化。这个变化则是从1996年到2016年这20年来显卡的进步带来的。
![](https://static001.geekbang.org/resource/image/1d/c3/1d098ce5b2c779392c8d3a33636673c3.png)
[图片来源](http://www.gamesgrabr.com/blog/2016/01/07/the-evolution-of-lara-croft/)
## 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的概念。
![](https://static001.geekbang.org/resource/image/27/6d/2724f76ffa4222eae01521cd2dffd16d.jpeg)
早期的可编程管线的GPU提供了单独的顶点处理和片段处理像素处理的着色器
一开始的可编程管线呢仅限于顶点处理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年深度学习的火热。
![](https://static001.geekbang.org/resource/image/da/93/dab4ed01f50995d82e6e5d970b54c693.jpeg)
## 现代GPU的三个核心创意
讲完了现代GPU的进化史那么接下来我们就来看看为什么现代的GPU在图形渲染、深度学习上能那么快。
### 芯片瘦身
我们先来回顾一下之前花了很多讲仔细讲解的现代CPU。现代CPU里的晶体管变得越来越多越来越复杂其实已经不是用来实现“计算”这个核心功能而是拿来实现处理乱序执行、进行分支预测以及我们之后要在存储器讲的高速缓存部分。
而在GPU里这些电路就显得有点多余了GPU的整个处理过程是一个[流式处理](https://en.wikipedia.org/wiki/Stream_processing)Stream Processing的过程。因为没有那么多分支条件或者复杂的依赖关系我们可以把GPU里这些对应的电路都可以去掉做一次小小的瘦身只留下取指令、指令译码、ALU以及执行这些计算需要的寄存器和缓存就好了。一般来说我们会把这些电路抽象成三个部分就是下面图里的取指令和指令译码、ALU和执行上下文。
![](https://static001.geekbang.org/resource/image/4c/9d/4c153ac45915fbf3985d24b092894b9d.jpeg)
### 多核并行和SIMT
这样一来我们的GPU电路就比CPU简单很多了。于是我们就可以在一个GPU里面塞很多个这样并行的GPU电路来实现计算就好像CPU里面的多核CPU一样。和CPU不同的是我们不需要单独去实现什么多线程的计算。因为GPU的运算是天然并行的。
![](https://static001.geekbang.org/resource/image/3d/ac/3d0859652adf9e3c0305e8e8517b47ac.jpeg)
我们在上一讲里面其实已经看到无论是对多边形里的顶点进行处理还是屏幕里面的每一个像素进行处理每个点的计算都是独立的。所以简单地添加多核的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 InstructionMultiple Threads的技术。SIMT呢比SIMD更加灵活。在SIMD里面CPU一次性取出了固定长度的多个数据放到寄存器里面用一个指令去执行。而SIMT可以把多条数据交给不同的线程去处理。
各个线程里面执行的指令流程是一样的但是可能根据数据的不同走到不同的条件分支。这样相同的代码和相同的流程可能执行不同的具体的指令。这个线程走到的是if的条件分支另外一个线程走到的就是else的条件分支了。
于是我们的GPU设计就可以进一步进化也就是在取指令和指令译码的阶段取出的指令可以给到后面多个不同的ALU并行进行运算。这样我们的一个GPU的核里就可以放下更多的ALU同时进行更多的并行运算了。
![](https://static001.geekbang.org/resource/image/3d/28/3d7ce9c053815f6a32a6fbf6f7fb9628.jpeg)
### GPU里的“超线程”
虽然GPU里面的主要以数值计算为主。不过既然已经是一个“通用计算”的架构了GPU里面也避免不了会有if…else这样的条件分支。但是在GPU里我们可没有CPU这样的分支预测的电路。这些电路在上面“芯片瘦身”的时候就已经被我们砍掉了。
所以GPU里的指令可能会遇到和CPU类似的“流水线停顿”问题。想到流水线停顿你应该就能记起我们之前在CPU里面讲过超线程技术。在GPU上我们一样可以做类似的事情也就是遇到停顿的时候调度一些别的计算任务给当前的ALU。
和超线程一样,既然要调度一个不同的任务过来,我们就需要针对这个任务,提供更多的**执行上下文**。所以一个Core里面的**执行上下文**的数量需要比ALU多。
![](https://static001.geekbang.org/resource/image/c9/b8/c971c34e0456dea9e4a87857880bb5b8.jpeg)
## GPU在深度学习上的性能差异
在通过芯片瘦身、SIMT以及更多的执行上下文我们就有了一个更擅长并行进行暴力运算的GPU。这样的芯片也正适合我们今天的深度学习的使用场景。
一方面GPU是一个可以进行“通用计算”的框架我们可以通过编程在GPU上实现不同的算法。另一方面现在的深度学习计算都是超大的向量和矩阵海量的训练样本的计算。整个计算过程中没有复杂的逻辑和分支非常适合GPU这样并行、计算能力强的架构。
我们去看NVidia 2080显卡的[技术规格](https://www.techpowerup.com/gpu-specs/geforce-rtx-2080.c3224),就可以算出,它到底有多大的计算能力。
2080一共有46个SMStreaming Multiprocessor流式处理器这个SM相当于GPU里面的GPU Core所以你可以认为这是一个46核的GPU有46个取指令指令译码的渲染管线。每个SM里面有64个Cuda Core。你可以认为这里的Cuda Core就是我们上面说的ALU的数量或者Pixel Shader的数量46x64呢一共就有2944个Shader。然后还有184个TMUTMU就是Texture Mapping Unit也就是用来做纹理映射的计算单元它也可以认为是另一种类型的Shader。
![](https://static001.geekbang.org/resource/image/14/e2/14d05a43f559cecff2b0813e8d5bdde2.png)
[图片来源](https://www.anandtech.com/show/13282/nvidia-turing-architecture-deep-dive/7)
2080 Super显卡有48个SM比普通版的2080多2个。每个SMSM也就是GPU Core里有64个Cuda Core也就是Shader
2080的主频是1515MHz如果自动超频Boost的话可以到1700MHz。而NVidia的显卡根据硬件架构的设计每个时钟周期可以执行两条指令。所以能做的浮点数运算的能力就是
2944 + 184× 1700 MHz × 2 = 10.06 TFLOPS
对照一下官方的技术规格正好就是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。
欢迎在留言区写下你的答案,你也可以把今天的内容分享给你的朋友,和他一起学习和进步。