30 | GPU(上):为什么玩游戏需要使用 GPU?
图形渲染的流程
现在我们电脑里面显示出来的 3D 的画面,其实是通过多边形组合出来的。
实际这些人物在画面里面的移动、动作,乃至根据光线发 生的变化,都是通过计算机根据图形学的各种计算,实时渲染出来的。
这个对于图像进行实时渲染的过程,可以被分解成下面这样 5 个步骤(图形流水线):
- 顶点处理(Vertex Processing):
构成多边形建模的每一个多 边形呢,都有多个顶点(Vertex)。这些顶点都有一个在三 维空间里的坐标。但是我们的屏幕是二维的,所以在确定当 前视角的时候,我们需要把这些顶点在三维空间里面的位 置,转化到屏幕这个二维空间里面。这个转换的操作,就被叫作顶点处理。
这样的转化都是通过线性代数的计算来进行的。建模越精细,需要转换的顶点数量就越多,计算量就越大。而且, 这里面每一个顶点位置的转换,互相之间没有依赖,是可以并行独立计算的。 - 图元处理(Primitive Processing)
要把顶点处理完成之后的各个顶点连起来,变成多边形。其实转化后的顶点,仍然是在一个三维空间里,只是第三维的 Z 轴,是正对屏幕的“深度”。所以我们针对这些多边形,需要做一个操作,叫剔除和裁剪(Cull and Clip),也就是把不在屏幕里面,或者一部分不在屏幕里面的内容给去掉,减少接下来流程的工作量。 - 栅格化(Rasterization)
屏幕分辨率是有限的。它一般是通过一个个“像素(Pixel)”来显示出内容的。所以,对于做完图元处理的多边形,我们要开始进行第三步操作。这个操作就是把它们转换成屏幕里面的一个个像素点。这个操作呢,就叫作栅格化。这个栅格化操作,有一个特点和上面的顶点处理是一样的,就是每一个图元都可以并行独立地栅格化。 - 片段处理(Fragment Processing) 计算每一个像素的颜色、透明度等信息,给像素点上色。同样也可以每个片段并行、独立进行,和上面的顶点处理和栅格化一样。
- 像素操作(Pixel Operations) 把不同的多边形的像素点“混合 (Blending)”到一起。可能前面的多边形可能是半透明 的,那么前后的颜色就要混合在一起变成一个新的颜色;或者前面的多边形遮挡住后面的多边形,那么只要显示前面多边形的颜色就好了。最终,输出到显示设备。
解放图形渲染的GPU
整个计算流程是完全固定的,不需要流水线停顿、乱序执行等等的各类导致 CPU 计算变得复杂的问题。 我们也不需要有什么可编程能力,只要让硬件按照写好的逻 辑进行运算就好了。
31 | GPU(下):为什么深度学习需要使用 GPU?
Shader 的诞生和可编程图形处理器
程序员希望我们的 GPU 也能有一定的可编程能 力。这个编程能力不是像 CPU 那样,有非常通用的指令, 可以进行任何你希望的操作,而是在整个的渲染管线 (Graphics Pipeline)的一些特别步骤,能够自己去定义处理数据的算法或者操作。于是,从 2001 年的 Direct3D 8.0 开始,微软第一次引入了可编程管线(Programable Function Pipeline)的概念。
早期的可编程管线的 GPU,提供了单独的顶点处理和片段 处理(像素处理)的着色器。
一开始的可编程管线呢,仅限于顶点处理(Vertex Processing)和片段处理(Fragment Processing)部分。 比起原来只能通过显卡和 Direct3D 这样的图形接口提供的 固定配置,程序员们终于也可以开始在图形效果上开始大显 身手了。
这些可以编程的接口,我们称之为Shader,中文名称就是着色器。一开始这些“可编程”的接口,只能修改顶点处理和片段处理部分的程序逻辑。我们用这些接口来做的,也主要是光照、亮度、颜色等等的处理,所以叫着色器。
早期的shader接口分开:
GPU,有两类 Shader,也就是 Vertex Shader 和 Fragment Shader。我们在上一讲看到,在进行顶点处理的时候,我们操作的是多边形的顶点;在片段操作的时候,我们操作的是屏幕上的像素点。对于顶点的操作,通常比片段要复杂一些。所以一开始,这两类 Shader 都是独立的硬件电路,也各自有独立的编程接口。
统一着色器架构(Unified Shader Architecture):
既然大家用的指令集是一样的,那不如就在 GPU 里面放很多个一样的Shader硬件电路,然后通过统一调度,把顶点 处理、图元处理、片段处理这些任务,都交给这些Shader去处理,让整个GPU尽可能地忙起来。
现代GPU的三个核心创意
1. 芯片瘦身
现代 CPU 里的晶体管变得越来越多,越来越复杂, 其实已经不是用来实现“计算”这个核心功能,而是拿来实现处理乱序执行、进行分支预测,以及高速缓存。
GPU 的整个处理过程是一个流式处理(Stream Processing)的过程。因为没有那么多分支条件,或者复杂的依赖关系,我们可以把 GPU 里这些对应的电路都可以去掉,做一次小小的瘦身, 只留下取指令、指令译码、ALU 以及执行这些计算需要的寄存器和缓存就好了。
会把这些电路抽象成三个部分,取指令和指令译码、ALU 和执行上下文。
2. 多核并行和SIMT
GPU 电路就比 CPU 简单很多了。于是,我们就可以在一个 GPU 里面,塞很多个这样并行的 GPU 电路来实现计算,就好像 CPU 里面的多核 CPU 一样。和 CPU 不同的是,我们不需要单独去实现什么多线程的计算。因为 GPU 的运算是天然并行的。
CPU 里有一种叫作 SIMD 的处理技术。这个技术是说,在做向量计算的时候,我们要执行的 指令是一样的,只是同一个指令的数据有所不同而已。
GPU 就借鉴了CPU里面的 SIMD,用了一种叫作SIMT(Single Instruction,Multiple Threads)的技术。
在 SIMD 里面,CPU一次性取出了固定长度的多个数据,放到寄存器里面,用一个指令去执行。而 SIMT,可以把多条数据,交给不同的线程去处理。
各个线程里面执行的指令流程是一样的,但是可能根据数据 的不同,走到不同的条件分支。这样,相同的代码和相同的 流程,可能执行不同的具体的指令。这个线程走到的是 if 的 条件分支,另外一个线程走到的就是 else 的条件分支了。
GPU 设计就可以进一步进化,也就是在取指令和指令译码的阶段,取出的指令可以给到后面多个不同的 ALU 并行进行运算。这样,我们的一个 GPU 的核里,就可以放下更多的 ALU,同时进行更多的并行运算了。
3. GPU 里的“超线程”
虽然 GPU 里面的主要以数值计算为主。不过既然已经是一个“通用计算”的架构了,GPU 里面也避免不了会有 if…else 这样的条件分支。但是,在 GPU 里我们可没有 CPU 这样的分支预测的电路。这些电路在上面“芯片瘦身”的时候,就已经被我们砍掉了。
所以,GPU 里的指令,可能会遇到和 CPU 类似的“流水线停顿”问题。想到流水线停顿,你应该就能记起,我们之前在 CPU 里面讲过超线程技术。在 GPU 上,我们一样可以做类似的事情,也就是遇到停顿的时候,调度一些别的计算任务给当前的 ALU。
和超线程一样,既然要调度一个不同的任务过来,我们就需要针对这个任务,提供更多的执行上下文。所以,一个 Core 里面的执行上下文的数量,需要比 ALU 多。
GPU 在深度学习上的性能差异
32 | FPGA、ASIC和TPU(上):计算机体系结构的黄金时代
FPGA
有没有什么办法,不用单独制造一块专门的芯片来验证硬件设计呢?能不能设计一个硬件,通过不同的程序代码, 来操作这个硬件之前的电路连线,通过“编程”让这个硬件变成我们设计的电路连线的芯片呢?
现场可编程门阵列(Field-Programmable Gate Array)
P 代表 Programmable,这个很容易理解。也就是说这 是一个可以通过编程来控制的硬件。
G 代表 Gate 也很容易理解,它就代表芯片里面的门电 路。我们能够去进行编程组合的就是这样一个一个门电路。
A 代表的 Array,叫作阵列,说的是在一块 FPGA 上,密 密麻麻列了大量 Gate 这样的门电路。
最后一个 F,不太容易理解。它其实是说,一块 FPGA 这 样的板子,可以进行在“现场”多次地进行编程。它不像 PAL(Programmable Array Logic,可编程阵列逻辑) 这样更古老的硬件设备,只能“编程”一次,把预先写好 的程序一次性烧录到硬件里面,之后就不能再修改了
我们可以像软件一样对硬件编程,可以反复烧录,还有海量的门电路,可以组合实现复杂的芯片功能。
FPGA 的解决方案:
1. 用存储换功能实现组合逻辑
在实现 CPU 的功能的 时候,我们需要完成各种各样的电路逻辑。在 FPGA 里,这 些基本的电路逻辑,不是采用布线连接的方式进行的,而是 预先根据我们在软件里面设计的逻辑电路,算出对应的真值 表,然后直接存到一个叫作 LUT(Look-Up Table,查找 表)的电路里面。这个 LUT 呢,其实就是一块存储空间, 里面存储了“特定的输入信号下,对应输出 0 还是 1”。
2. 对于需要实现的时序逻辑电路,我们可以在 FPGA 里面直接放上 D 触发器,作为寄存器。
这个和 CPU 里的触 发器没有什么本质不同。不过,我们会把很多个 LUT 的电 路和寄存器组合在一起,变成一个叫作逻辑簇(Logic Cluster)的东西。在 FPGA 里,这样组合了多个 LUT 和寄 存器的设备,也被叫做 CLB(Configurable Logic Block, 可配置逻辑块)。
3. FPGA 是通过可编程逻辑布线,来连接各个不同的 CLB,最终实现我们想要实现的芯片功能。
这个可编程逻辑 布线,你可以把它当成我们的铁路网。整个铁路系统已经铺 好了,但是整个铁路网里面,设计了很多个道岔。我们可以 通过控制道岔,来确定不同的列车线路。在可编程逻辑布线 里面,“编程”在做的,就是拨动像道岔一样的各个电路开 关,最终实现不同 CLB 之间的连接,完成我们想要的芯片功能。
通过 LUT 和寄存器,我们能够组合出很多 CLB,而 通过连接不同的 CLB,最终有了我们想要的芯片功能。最关 键的是,这个组合过程是可以“编程”控制的。而且这个编 程出来的软件,还可以后续改写,重新写入到硬件里。让同 一个硬件实现不同的芯片功能。从这个角度来说,FPGA 也 是“软件吞噬世界”的一个很好的例子。
ASIC
为这些有专门用途的场景,单独设计一个芯片。这些专门设计的芯片呢,我们称之为 ASIC(Application-Specific Integrated Circuit),也就是专用集成电路。
因为 ASIC 是针对专门用途设计的,所以它的电路更精简,单片的制造成本也比 CPU 更低。而且,因为电路精 简,所以通常能耗要比用来做通用计算的 CPU 更低。
FPGA 的硬件上有点儿“浪费”。每一个 LUT 电路,其实都是一个小小的“浪费”。一个 LUT 电路设计出来之后,既可以实现与门,又可以实现或 门,自然用到的晶体管数量,比单纯连死的与门或者或门的要多得多。同时,因为用的晶体管多,它的能耗也比单纯连死的电路要大,单片 FPGA 的生产制造的成本也比 ASIC 要高不少。
FPGA 的优点在于,它没有硬件研发成本。ASIC的电路设计,需要仿真、验证,还需要经过流片(Tape out),变成一个印刷的电路板,最终变成芯片
单个 ASIC 的生产制造成本比 FPGA 低,ASIC 的能耗也比 能实现同样功能的 FPGA 要低。能耗低,意味着长时间运行 这些芯片,所用的电力成本也更低。 但是,ASIC 有一笔很高的 NRE(Non-Recuring Engineering Cost,一次性工程费用)成本。这个成本,就是 ASIC 实际“研发”的成本。只有需要大量生产 ASIC 芯 片的时候,我们才能摊薄这份研发成本。
33 | 解读TPU:设计和拆解一块ASIC芯片
最知名、最具有实用价值的 ASIC 就是 TPU 了。
TPU V1 想要解决什么问题?
在深度学习热起来之后,计算量最大的是什么呢?并不是进行深度学习的训练,而是深度学习的推断部分。
第一代的 TPU,首先优化的并不是深度学习的模型训练,而是深度学习的模型推断。
模型的训练和推断有什么不同呢?主要有三个点。
- 深度学习的推断工作更简单,对灵活性的要求也就更低。模型推断的过程,我们只需要去计算一些矩阵的乘法、加法,调用一些 Sigmoid 或者 RELU 这样的激活函数。这样 的过程可能需要反复进行很多层,但是也只是这些计算过程的简单组合。
- 深度学习的推断的性能,首先要保障响应时间的指标。
计算 机关注的性能指标,有响应时间(Response Time)和吞吐率(Throughput)。我们在模 型训练的时候,只需要考虑吞吐率问题就行了。因为一个模型训练少则好几分钟,多的话要 几个月。而推断过程,像互联网广告的点击预测,我们往往希望能在几十毫秒乃至几毫秒之 内就完成,而人脸识别也不希望会超过几秒钟。很显然,模型训练和推断对于性能的要求是 截然不同的。 - 深度学习的推断工作,希望在功耗上尽可能少一些。
第一代 TPU 的设计目标。在保障响应时间的情况下, 能够尽可能地提高能效比这个指标,也就是进行同样多数量的推断工作,花费的整体能源要 显著低于 CPU 和 GPU。
深入理解 TPU V1
快速上线和向前兼容,一个 FPU 的设计
向前兼容:TPU 并没有设计成一个独立 的“CPU“,而是设计成一块像显卡一样,插在主板 PCI-E 接口上的板卡。更进一步地, TPU 甚至没有像我们之前说的现代 GPU 一样,设计成自己有对应的取指令的电路,而是通 过 CPU,向 TPU 发送需要执行的指令。
快速上线:一个像 FPU(浮点数处理器)的协处理器(Coprocessor),而不是像 CPU 和 GPU 这样可以独 立工作的 Processor Unit。
专用电路和大量缓存,适应推断的工作流程
整个 TPU 的硬件,完全是按照深度学习一个层(Layer)的计算流程来设计的
控制电路(Control)只占了 2%。这是因为, TPU 的计算过程基本上是一个固定的流程。不像我们之前讲的 CPU 那样,有各种复杂的控 制功能,比如冒险、分支预测等等。
超过一半的 TPU 的面积,都被用来作为 Local Unified Buffer(本地统一缓 冲区)(29%)和矩阵乘法单元(Matrix Mutliply Unit)了。
统一缓冲区(Unified Buffer),则由 SRAM 这样高速的存储设备组成。SRAM 比起内存使用的 DRAM 速度要快上很多,但是因为电路密度小,所以占用的空间要大很多。 统一缓冲区之所以使用 SRAM,是因为在整个的推断过程中,它会高频反复地被矩阵乘法单元读写,来完成计算。
整个 TPU 里面,每一个组件的设计,完全是为了深度学习的推断过程设计出来 的。这也是我们设计开发 ASIC 的核心原因:用特制的硬件,最大化特定任务的运行效率。
细节优化,使用 8 Bits 数据
矩阵乘法单元,没有用 32 Bits 来存放一个浮点数,而 是只用了一个 8 Bits 来存放浮点数。这是因为,在实践的机器学习应用中,会对数据做归 一化(Normalization)和正则化(Regularization)的处理,会使得 我们在深度学习里面操作的数据都不会变得太大。通常来说呢,都能控制在 -3 到 3 这样一定的范围之内。 因为这个数值上的特征,我们需要的浮点数的精度也不需要太高了。
在深度学习里,常常够用了。特别是在模型推断的时候,要求的计算精度,往往可以比模型训练低。所以,8 Bits 的矩阵乘法器,就可以放下更多的计算量,使得 TPU 的推断速度更快。
34 | 理解虚拟机:你在云上拿到的计算机是什么样的?
上世纪 60 年代,计算机还是异常昂贵的设备,实际的计算机使用需求要面临两个挑战。第一,计算机特别昂贵,我们要尽可能地让计算机忙起来,一直不断地去处理一些计算任务。 第二,很多工程师想要用上计算机,但是没有能力自己花钱买一台,所以呢,我们要让很多人可以共用一台计算机。
缘起分时系统
计算机,会自动给程序或任务分配计算时间。你只需要为你花费的“计算时间”和使用的电话线路付费就可以了。
从“黑色星期五”到公有云
直接出租物理服务器,意味着亚马逊只能进行服务器的“整租”,这样大部分中小客户就不愿意了。
这个“整租”的问题,还发生在“时间”层面。物理服务器里面装好的系统和应用,不租了 而要再给其他人使用,就必须清空里面已经装好的程序和数据,得做一次“重装”。
对于想要租用服务器的用户来说,最好的体验不是租房子,而是住酒店。我住一天, 我就付一天的钱。这次是全家出门,一次多定几间酒店房间就好啦。
虚拟机技术,使得我们可以在一台物理 服务器上,同时运行多个虚拟服务器,并且可以动态去分配,每个虚拟服务器占用的资源。 对于不运行的虚拟服务器,我们也可以把这个虚拟服务器“关闭”。这个“关闭”了的服务 器,就和一个被关掉的物理服务器一样,它不会再占用实际的服务器资源。但是,当我们重 新打开这个虚拟服务器的时候,里面的数据和应用都在,不需要再重新安装一次。
虚拟机的技术变迁
虚拟机(Virtual Machine)技术,其实就是指在现有硬件的操作系统上,能够模拟一个计算机系统的技术。而模拟一个计算机系统,最简单的办法,其实不能算是虚拟机技术,而是一个模拟器(Emulator)。
解释型虚拟机
要模拟一个计算机系统,最简单的办法,就是兼容这个计算机系统的指令集。我们可以开发 一个应用程序,跑在我们的操作系统上。这个应用程序呢,可以识别我们想要模拟的、计算 机系统的程序格式和指令,然后一条条去解释执行。
在这个过程中,我们把原先的操作系统叫作宿主机(Host),把能够有能力去模拟指令执 行的软件,叫作模拟器(Emulator),而实际运行在模拟器上被“虚拟”出来的系统呢, 我们叫客户机(Guest VM)。
这个方式,其实和运行 Java 程序的 Java 虚拟机很像。只不过,Java 虚拟机运行的是 Java 自己定义发明的中间代码,而不是一个特定的计算机系统的指令。
这种解释执行另一个系统的方式,有没有真实的应用案例呢?当然是有的,如果你是一个 Android 开发人员,你在开发机上跑的 Android 模拟器,其实就是这种方式。如果你喜欢 玩一些老游戏,可以注意研究一下,很多能在 Windows 下运行的游戏机模拟器,用的也 是类似的方式。
这种解释执行方式的最大的优势就是,模拟的系统可以跨硬件。比如,Android 手机用的 CPU 是 ARM 的,而我们的开发机用的是 Intel X86 的,两边的 CPU 指令集都不一样,但 是一样可以正常运行。如果你想玩的街机游戏,里面的硬件早就已经停产了,那你自然只能 选择 MAME 这样的模拟器。
不过这个方式也有两个明显的缺陷。第一个是,我们做不到精确的“模拟”。很多的老旧的 硬件的程序运行,要依赖特定的电路乃至电路特有的时钟频率,想要通过软件达到 100% 模拟是很难做到的。第二个缺陷就更麻烦了,那就是这种解释执行的方式,性能实在太差 了。因为我们并不是直接把指令交给 CPU 去执行的,而是要经过各种解释和翻译工作。
Type-1 和 Type-2:虚拟机的性能提升
我们希望我们的虚拟化技术,能够克服上面的模拟器方式的两个缺陷。同时,我们可以放弃掉模拟器方式能做到的跨硬件平台的这个能力。
首先我们需要一个“全虚拟化”的技术,也就是说,我们可以在现有的物理服务器的 硬件和操作系统上,去跑一个完整的、不需要做任何修改的客户机操作系统(Guest OS)。那么,我们怎么在一个操作系统上,再去跑多个完整的操作系统呢?答案就是,我 们自己做软件开发中很常用的一个解决方案,就是加入一个中间层。在虚拟机技术里面,这 个中间层就叫作虚拟机监视器,英文叫 VMM(Virtual Machine Manager)或者 Hypervisor。
如果说我们宿主机的 OS 是房东的话,这个虚拟机监视器呢,就好像一个二房东。我们运行 的虚拟机,都不是直接和房东打交道,而是要和这个二房东打交道。我们跑在上面的虚拟机呢,会把整个的硬件特征都映射到虚拟机环境里,这包括整个完整的 CPU 指令集、I/O 操 作、中断等等。
既然要通过虚拟机监视器这个二房东,我们实际的指令是怎么落到硬件上去实际执行的呢? 这里有两种办法,也就是 Type-1 和 Type-2 这两种类型的虚拟机。
在 Type-2 虚拟机里,我们上面说的虚拟机监视器好像一个运行在操作系统上的软件。你的客户机的操作系统呢,把最终到硬件的所有指令,都发送给虚拟机监视器。而虚拟机监视器,又会把这些指令再交给宿主机的操作系统去执行。把在模拟器里的指令翻译工作,挪到了虚拟机监视器里。没错,Type-2 型的虚拟机,更多是用 在我们日常的个人电脑里,而不是用在数据中心里。
在数据中心里面用的虚拟机,我们通常叫作 Type-1 型的虚拟机。这个时候,客户机的指令交给虚拟机监视器之后呢,不再需要通过宿主机的操作系统,才能调用硬件,而是可以直接由虚拟机监视器去调用硬件。在数据中心里面,我们并不需要在 Intel x86 上面去跑一个 ARM 的程序,而是直接 在 x86 上虚拟一个 x86 硬件的计算机和操作系统。所以,我们的指令不需要做什么翻译工 作,可以直接往下传递执行就好了,所以指令的执行效率也会很高。在 Type-1 型的虚拟机里,我们的虚拟机监视器其实并不是一个操作系统之上的应用 层程序,而是一个嵌入在操作系统内核里面的一部分。因为虚拟机监视器需要直接和硬件打交道,所以它也需要包含能够直接操作硬件的驱动程 序。所以 Type-1 的虚拟机监视器更大一些,同时兼容性也不能像 Type-2 型那么好。不 过,因为它一般都是部署在我们的数据中心里面,硬件完全是统一可控的,这倒不是一个问题了。
Docker:新时代的最佳选择?
Type-1 型的虚拟机看起来已经没有什么硬件损耗。但是,这里面还是有一个浪费的 资源。在我们实际的物理机上,我们可能同时运行了多个的虚拟机,而这每一个虚拟机,都运行了一个属于自己的单独的操作系统。
我们想要的未必是一个完整的、独立的、全虚拟化的虚拟机。我们很多 时候想要租用的不是“独立服务器”,而是独立的计算资源。在服务器领域,我们开发的程 序都是跑在 Linux 上的。其实我们并不需要一个独立的操作系统,只要一个能够进行资源 和环境隔离的“独立空间”就好了。那么,能够满足这个需求的解决方案,就是过去几年特 别火热的 Docker 技术。
在实践的服务器端的开发中,虽然我们的应用环境需要各种各样不同的依赖,可能是不同的 PHP 或者 Python 的版本,可能是操作系统里面不同的系统库,但是通常来说,我们其实都是跑在 Linux 内核上的。通过 Docker,我们不再需要在操作系统上再跑一个操作系统, 而只需要通过容器编排工具,比如 Kubernetes 或者 Docker Swarm,能够进行各个应用 之间的环境和资源隔离就好了。
这种隔离资源的方式呢,也有人称之为“操作系统级虚拟机”,好和上面的全虚拟化虚拟机 对应起来。不过严格来说,Docker 并不能算是一种虚拟机技术,而只能算是一种资源隔离 的技术而已。
总结
图形渲染流程、GPU架构、FPGA和ASIC(通用与专用)、TPU、虚拟机和Docker