摘要
图形学理论的发展和工程上硬件的发展基本处于相辅相成的阶段,并且硬件的发展大大加快了图形学的应用市场的扩展,从早期的图形加速卡到第一个真正的带有图形流水线的GPU硬件,从离线到实时逐步发展,并且近几年为AI计算发展出了GPGPU分支等等,GPU也一直处于活跃的发展态势中。本文我们主要讨论GPU在图形领域的发展情况。
GPU是什么
GPU全称是Graphics Processing Unit,图形处理单元。它的功能最初与名字一致,是专门用于绘制图像和处理图元数据的特定芯片,后来渐渐加入了其它很多功能。
我们日常讨论GPU和显卡时,经常混为一谈,严格来说是有所区别的。GPU是显卡(Video card、Display card、Graphics card)最核心的部件,但除了GPU,显卡还有扇热器、通讯元件、与主板和显示器连接的各类插槽。
GPU的发展过程
虽然专用图形硬件从 1944 年 MIT 的 Whirlwind 项目就开始出现,并于 20 世纪 80 年代逐渐成形,但图形处理器或 GPU 这个名词直到 1999 年才由 NVIDIA 公司创造,此后逐渐发展成为同时具备高速图形处理能力和通用计算能力的强大硬件。
一些关键节点参照下表所示:
时间 | GPU体系结构发展 | 里程碑 |
1980前 | 只支持帧缓冲,只有固定图形功能 | 图形加速器 |
1982 | Geometry Engine代表专用图形处理芯片诞生 | 专用图形处理器 |
1996 | 3DFX Voodoo芯片实现光栅化、纹理映射到帧缓冲,顶点由CPU处理后通过PCI总线传至GPU,硬件图形流水线诞生 | 第0代GPU(硬件图形流水线) |
1999 | NVIDIA推出GeForce256,首次硬件集成完整图形流水线,首次定义GPU名词 | 第1代GPU(完整硬件图形流水线) |
2001 | NVIDIA的GeForce3与ATI的Radeon7500,可编程的vertex shader与fragment shader | 第2代GPU(可编程硬件图形流水线) |
2004 | NVIDIA GeForce6系列具有强大的vertex shader与fragment shader,峰值计算能力超过同期CPU,通用计算概念诞生 | 第2.5代GPU(通用计算诞生) |
2006 | NVIDIA GeForce8系列全面引入unifiedshader,采用硬件多线程共享流多处理器,同时推出CUDA软件栈,GPGPU的概念成熟 | 第3代GPU(通用计算步入商用) |
2011 | AMD推出融合处理器APU概念,定义HSA异构系统体系结构,实现CPU和GPU访存地址统一化 | 异构体系架构标准 |
2018 | NVIDIA 推出Turing架构,RT Core实现光线追踪硬件加速,GPU硬件进入光追时代 | 硬件支持光线追踪 |
GPU渲染管线
GPU内部结构
GPU的微观结构因不同厂商、不同架构都会有所差异,但核心部件、概念、以及运行机制大同小异。下面将展示Nv的Turning架构细节。
6 GPC(图形处理簇)
36 TPC(纹理处理簇)
72 SM(流多处理器)
每个GPC有6个TPC,每个TPC有2个SM
4,608 CUDA核
72 RT核
576 Tensor核
288 纹理单元
12x32位 GDDR6内存控制器 (共384位)
//图在这儿,8页: https://images.nvidia.cn/aem-dam/en-zz/Solutions/design-visualization/technologies/turing-architecture/NVIDIA-Turing-Architecture-Whitepaper.pdf
SM内部结构( Turning )
每个 SM 包含:
- 1个Warp Schedulers:负责warp调度,一个warp由32个线程组成,warp调度器的指令通过Dispatch Units送到Core执行。
- 64 个 CUDA 内核
- 8 个张量内核:一个指令完成矩阵乘法
- 一个 256 KB 的寄存器文件
- 16个LD/ST(load/store)模块来加载和存储数据
- 4个SFU(Special function units)执行特殊数学运算(sin、cos、log等)
- 4 个纹理单元
- 96 KB 的 L1 /共享内存,这些内存可根据计算或图形工作负载配置为各种容量
- 1个光线追踪(RT)核
SM被划分为四个处理模块
- 每个模块有16个FP32内核,16个INT32内核核心,两个张量核,一个warp调度器,和一个调度单元。
- 每个模块包含一个L0指令缓存和一个64 KB的寄存器文件
GPU PipeLine运行机制
//大图在这儿: https://developer.nvidia.com/content/life-triangle-nvidias-logical-pipeline 从Fermi开始NVIDIA使用类似的原理架构,使用一个Giga Thread Engine来管理所有正在进行的工作,GPU被划分成多个GPCs(Graphics Processing Cluster),每个GPC拥有多个SM(SMX、SMM)和一个光栅化引擎(Raster Engine),它们其中有很多的连接,最显著的是Crossbar,它可以连接GPCs和其它功能性模块(例如ROP或其他子系统)。
程序员编写的shader是在SM上完成的。每个SM包含许多为线程执行数学运算的Core(核心)。例如,一个线程可以是顶点或像素着色器调用。这些Core和其它单元由Warp Scheduler驱动,Warp Scheduler管理一组32个线程作为Warp(线程束)并将要执行的指令移交给Dispatch Units。
GPU中实际有多少这些单元(每个GPC有多少个SM,多少个GPC ......)取决于芯片配置本身。例如,GM204有4个GPC,每个GPC有4个SM,但Tegra X1有1个GPC和2个SM,它们均采用Maxwell设计。SM设计本身(内核数量,指令单位,调度程序......)也随着时间的推移而发生变化,并帮助使芯片变得如此高效,可以从高端台式机扩展到笔记本电脑移动。
GPU pipeline流程
- 程序通过图形API(DX、GL)发出drawcall指令,指令会被推送到驱动程序,驱动会检查指令的合法性,然后会把指令放到GPU可以读取的Pushbuffer中。
- 经过一段时间或者显式调用flush指令后,驱动程序把Pushbuffer的内容发送给GPU,GPU通过主机接口(Host Interface)接受这些命令,并通过前端(Front End)处理这些命令。
- 在图元分配器(Primitive Distributor)中开始工作分配,处理indexbuffer中的顶点产生三角形分成批次(batches),然后发送给多个GPCs。这一步的理解就是提交上来n个三角形,分配给这几个PGC同时处理。
- //大图中间的图
- 在GPC中,每个SM中的Poly Morph Engine负责通过三角形索引(triangle indices)取出三角形的数据(vertex data),即图中的Vertex Fetch模块。
- 在获取数据之后,在SM中以32个线程为一组的线程束(Warp)来调度,处理顶点数据。Warp是典型的单指令多线程(SIMT,SIMD单指令多数据的升级)的实现,也就是32个线程同时执行的指令是一模一样的,只是线程数据不一样,这样的好处就是一个warp只需要一个套逻辑对指令进行解码和执行就可以了,芯片可以做的更小更快。
- SM的warp调度器会按照顺序分发指令(shader)给整个warp,单个warp中的线程会锁步(lock-step)执行各自的指令,如果线程碰到不激活执行的情况也会被遮掩(be masked out)。被遮掩的原因有很多,例如当前的指令是if(true)的分支,但是当前线程的数据的条件是false,或者循环的次数不一样(比如for循环次数n不是常量,或被break提前终止了但是别的还在走),因此在shader中的分支会显著增加时间消耗。在一个warp中的分支除非32个线程都走到if或者else里面,否则相当于所有的分支都走了一遍,线程不能独立执行指令而是以warp为单位,而这些warp之间才是独立的。
- warp中的指令可以被一次完成,也可能经过多次调度,例如通常SM中的LD/ST(加载存取)单元数量明显少于基础数学操作单元。
- 由于某些指令比其他指令需要更长的时间才能完成,特别是内存加载,warp调度器可能会简单地切换到另一个没有内存等待的warp,这是GPU如何克服内存读取延迟的关键,只是简单地切换活动线程组。为了使这种切换非常快,调度器管理的所有warp在寄存器文件中都有自己的寄存器。这里就会有个矛盾产生,shader需要越多的寄存器,就会给warp留下越少的空间,就会产生越少的warp,这时候在碰到内存延迟的时候就会只是等待,而没有可以运行的warp可以切换。
//大图上方右侧图片过程 - 一旦warp完成了vertex-shader的所有指令,运算结果会被Viewport Transform模块处理,三角形会被裁剪然后准备栅格化,GPU会使用L1和L2缓存来进行vertex-shader和pixel-shader的数据通信。
- 接下来这些三角形将被分割,再分配给多个GPC,三角形的范围决定着它将被分配到哪个光栅引擎(raster engines),每个raster engines覆盖了多个屏幕上的tile,这等于把三角形的渲染分配到多个tile上面。也就是像素阶段就把按三角形划分变成了按显示的像素划分了。
- SM上的Attribute Setup保证了从vertex-shader来的数据是pixel-shade是可读的。
- 光栅引擎(raster engines)在它接收到的三角形上工作,来负责这些这些三角形的像素信息的生成(同时会处理裁剪Clipping、背面剔除和Early-Z剔除)。
- 32个像素或者说8个2x2的像素块处理线程,被分成一组,这是在像素着色器上面的最小工作单元。这个2x2的四边形允许我们做诸如纹理映射之类的过滤。如果分配的像素没有被三角形包围,则会被丢弃。这些线程同样都会被一个warp所管理。
- 接下来的阶段就和vertex-shader中的逻辑步骤完全一样,但是变成了在像素着色器线程中执行。 由于不耗费任何性能就可以获取像素块内任意像素的值,导致锁步执行非常便利。
- 最后一步,现在像素着色器已经完成了要写入渲染目标的颜色和深度信息的计算,在此时,我们必须考虑三角形的原始api的顺序,然后才将数据移交给ROP(render output unit,渲染输入单元)。一个ROP内部有很多ROP单元,在ROP单元中处理深度测试,和framebuffer的混合,深度和颜色的设置必须是原子操作,否则两个不同的三角形在同一个像素点就会有冲突和错误。