课程链接:深入浅出计算机组成原理_组成原理_计算机基础-极客时间
GPU 是随着计算机里面需要渲染三维图形的出现,而发展起来的设备。
一、GPU和图形渲染
完整的5 个步骤的渲染流程一般也被称为图形流水线(Graphic Pipeline)
1 、顶点处理(Vertex Processing)
构成多边形建模的每一个多边形都有多个顶点(Vertex)。这些顶点都有一个在三维空间里的坐标。在确定当前视角的之后,需要把这些顶点在三维空间里面的位置,转化到屏幕这个二维空间里面。这个转换的操作,就被叫作顶点处理。这种转化都是通过线性代数计算的。建模越精细,需要转换的顶点数量就越多,计算量就越大。而且,这里面每一个顶点位置的转换,互相之间没有依赖,是可以并行独立计算的。
2、图元处理(Primitive Processing)
图元处理就是要把顶点处理完成之后的各个顶点连起来,变成多边形。转化后的顶点,仍然是在一个三维空间里,只是第三维的 Z 轴,是正对屏幕的“深度”。针对这些多边形,需要做一个操作,叫剔除和裁剪(Cull and Clip),也就是把不在屏幕里面,或者一部分不在屏幕里面的内容给去掉,减少接下来流程的工作量。
3、栅格化(Rasterization)
屏幕分辨率是有限的,一般是通过一个个“像素(Pixel)”来显示出内容。做完图元处理的多边形,需要转换成屏幕里面的一个个像素点。这个操作就叫作栅格化。每一个图元都可以并行独立地栅格化。
4、片段处理(Fragment Processing)
计算每一个像素的颜色、透明度等信息,给像素点上色,同样可以每个片段并行、独立进行。
5、像素操作(Pixel Operations)
把不同的多边形的像素点“混合(Blending)”到一起。
既然图形渲染的流程是固定的,那直接用硬件来处理这部分过程,会比制造有同样计算性能的 CPU 要便宜得多。因为整个计算流程是完全固定的,不需要流水线停顿、乱序执行等等的各类导致 CPU 计算变得复杂的问题。
二、GPU和深度学习
因为要实时计算渲染的像素特别地多,图形加速卡登上了历史的舞台。通过 3dFx 的 Voodoo 或者 NVidia 的 TNT 这样的图形加速卡,CPU 就不需要再去处理一个个像素点的图元处理、栅格化和片段处理这些操作。3D 游戏就是从这个时代发展起来的。
(一)Shader 的诞生和可编程图形处理器
在 Voodoo 和 TNT 显卡的渲染管线里面,没有“顶点处理“这个步骤。在当时,把多边形的顶点转化到屏幕的坐标系是由 CPU 完成的。CPU 的性能越好,能够支持的多边形也就越多,对应的多边形建模的效果自然也就越像真人。而 3D 游戏的多边形性能也受限于CPU 的性能。
1999 年 NVidia 推出的 GeForce 256 显卡,把顶点处理的计算能力,也从 CPU 里挪到了显卡里。整个图形渲染过程都在硬件里面固定的管线完成,局限是只能通过更改配置实现不同的图形渲染效果,如果通过改配置做不到,就没有办法了。
2001 年的 Direct3D 8.0 开始,微软第一次引入了可编程管线(Programable Function Pipeline)的概念,使GPU 具有一定的可编程能力,允许程序员在渲染管线(Graphics Pipeline)的一些特别步骤,能够自己去定义处理数据的算法或者操作。
一开始的可编程管线仅限于顶点处理(Vertex Processing)和片段处理(Fragment Processing)部分。
这些可以编程的接口称为 Shader,中文名称就是着色器。之所以叫“着色器”,是因为一开始这些“可编程”的接口,只能修改顶点处理和片段处理部分的程序逻辑,也就是处理光照、亮度、颜色等。
这个时候的 GPU,有两类 Shader: Vertex Shader 和 Fragment Shader。在进行顶点处理的时候,操作的是多边形的顶点;在片段操作的时候,操作的是屏幕上的像素点。对于顶点的操作,通常比片段要复杂一些。所以一开始,这两类 Shader 都是独立的硬件电路,也各自有独立的编程接口。因为这么做,硬件设计起来更加简单,一块 GPU 上也能容纳下更多的 Shader。
虽然顶点处理和片段处理的具体逻辑不太一样,但里面涉及的指令集可以用同一套。把 Vertex Shader 和 Fragment Shader 分开,虽然可以减少硬件设计的复杂程度,但有一半 Shader 始终没有被使用,也带来了浪费,。在整个渲染管线里,Vertext Shader 运行的时候,Fragment Shader 停在那里什么也没干。Fragment Shader 在运行的时候,Vertext Shader 也停在那里发呆。统一着色器架构(Unified Shader Architecture)就应运而生了。
统一着色器架构在GPU 里面放很多个一样的 Shader 硬件电路,然后通过统一调度,把顶点处理、图元处理、片段处理这些任务,都交给这些 Shader 去处理,让整个 GPU 尽可能地忙起来。现代 GPU 的设计,就是统一着色器架构。
因为 Shader 变成一个“通用”的模块,才有了把 GPU 拿来做各种通用计算的用法,即 GPGPU(General-Purpose Computing on Graphics Processing Units,通用图形处理器)。而正是因为 GPU 可以拿来做各种通用的计算,才有了过去 10 年深度学习的火热。
(二)现代GPU的三个核心创意
为什么现代的 GPU 在图形渲染、深度学习上能那么快?
1、芯片瘦身
现代 CPU 里的晶体管变得越来越多,越来越复杂,其实已经不是用来实现“计算”这个核心功能,而是拿来实现处理乱序执行、进行分支预测,以及我们之后要在存储器讲的高速缓存部分。
而在 GPU 里,这些电路就显得有点多余了,GPU 的整个处理过程是一个流式处理(Stream Processing)的过程。因为没有那么多分支条件,或者复杂的依赖关系,所有可以把这些对应的电路都去掉,只留下取指令、指令译码、ALU 以及执行这些计算需要的寄存器和缓存就好了。
2、多核并行和SIMT
无论是对多边形里的顶点进行处理,还是屏幕里面的每一个像素进行处理,每个点的计算都是独立的,所以,GPU 的运算是天然并行的,简单地添加多核的 GPU,就能做到并行加速。
CPU 里有一种叫作 SIMD 的处理技术,是说,在做向量计算的时候,要执行的指令是一样的,只是同一个指令的数据有所不同而已。
GPU 的渲染管线里,无论是顶点去进行线性变换,还是屏幕上临近像素点的光照和上色,都是在用相同的指令流程进行计算。于是就借鉴了 CPU 里面的 SIMD,用了一种叫作SIMT(Single Instruction,Multiple Threads)的技术。SIMT 比 SIMD 更加灵活。在 SIMD 里面,CPU 一次性取出了固定长度的多个数据,放到寄存器里面,用一个指令去执行。而 SIMT,可以把多条数据,交给不同的线程去处理。
各个线程里面执行的指令流程是一样的,但是可能根据数据的不同,走到不同的条件分支。这样,相同的代码和相同的流程,可能执行不同的具体的指令。GPU 设计就可以进一步进化,也就是在取指令和指令译码的阶段,取出的指令可以给到后面多个不同的 ALU 并行进行运算。这样,一个 GPU 的核里,就可以放下更多的 ALU,同时进行更多的并行运算了。
3、GPU里的“超线程”
GPU里没有分支预测的电路。GPU 里的指令,可能会遇到和 CPU 类似的“流水线停顿”(超线程)问题。在 GPU 上,遇到停顿的时候,可以调度一些别的计算任务给当前的 ALU。和超线程一样,既然要调度一个不同的任务过来,就需要针对这个任务,提供更多的执行上下文。所以,一个 Core 里面的执行上下文的数量,需要比 ALU 多。
【关于现代 GPU 的工作原理,可以仔细阅读一下 haifux.org 上的这个PPT,里面图文并茂地解释了现代 GPU 的架构设计的思路。】
课程链接:深入浅出计算机组成原理_组成原理_计算机基础-极客时间