最近在看关于GPU架构和渲染优化方面的内容,记录一下,不正确的地方请大神们斧正!本文将主要分为以下四个部分:
- CPU与GPU
- GPU并行结构
- GPU渲染管线
- GPU渲染优化
一、CPU与GPU
1.CPU与GPU结构对比
下图仅仅是CPU和GPU架构的简化版,真实的架构非常复杂,而且每一步操作都需要多个组件协同进行。
CPU与GPU结构对比图
- 黄色的Control为控制器,用于协调控制整个CPU(GPU)的运行,包括取出指令、控制其他模块的运行等;绿色的ALU(Arithmetic Logic Unit)是算术逻辑单元,用于进行数学、逻辑运算;橙色的Cache和DRAM用于存储信息。
- CPU的流水线较长,控制器较为复杂,计算趋于线性执行(可部分并行),善于处理复杂的逻辑运算。CPU内部ALU数量较少,所以不适合做数值计算密集型的数学计算。
- GPU的流水线很短,控制器较为简单,内部集成了大量ALU。GPU中的ALU可以并行执行,且具有较多浮点运算单元,所以适合进行大量可并行执行的数学计算。我们可以把一些比较独立并且可并行执行的数学计算抛给GPU执行(例如,图像处理、视频编解码、物理粒子计算等)。
2.CPU与GPU数据交互
在程序开始运行之前所有的数据都是保存在磁盘上的。程序开始运行,为了CPU能够快速访问数据,需要先把数据载入内存(RAM)。所有的渲染工作都集中在GPU执行,GPU和CPU一样,也有自己的可以快速访问的类内存结构,叫做显存(VRAM)。在GPU开始工作之前,所有计算所需的资源都必须载入显存:
数据由CPU到GPU
GPU进行计算的时候需要从缓存拿取数据
GPU获取显存数据
当然,与CPU一样,GPU在进行计算获取数据的时候,并不是直接从显存中获取数据,而是经过多级缓存后,然后经过对应的寄存器,最后进入ALU中进行计算,如下图:
数据由L2级缓存到L1l级缓存
数据由L1级缓存到寄存器
同理当GPU完成对数据的计算之后,会经过寄存器—L1级缓存—L2级缓存然后保存在显存中,最终显示器获取显存中的数据进行显示输出。
3.CPU与GPU内存速度
- CPU内存速度
CPU各级内存访问时钟数
- GPU内存速度
GPU各级内存访问时钟数
从上面可以看出CPU和GPU在寄存器—缓存—内存(RAM)访问时间依次增加,对RAM的访问时比较耗时的,而CPU和GPU数据交互时会涉及两者内存(RAM)的访问因此:
(1) 提交渲染数据时,尽量批量提交,减少CPU向GPU提交数据次数。
(2) 尽量避免从GPU回读数据,这样可以减少两者数据交互。
二、GPU并行结构
1.Nvdia GPU架构发展
- 2010 Fermi:Fermi是第一个完整的GPU计算架构。首款可支持与共享存储结合纯cache层次的GPU架构,支持ECC的GPU架构。
- 2012 Kepler:Kepler相较于Fermi更快,效率更高,性能更好。
Kepler与Fermi对比
- 2014 Maxwell:其全新的立体像素全局光照 (VXGI) 技术首次让游戏 GPU 能够提供实时的动态全局光照效果。基于 Maxwell 架构的 GTX 980 和 970 GPU 采用了包括多帧采样抗锯齿 (MFAA)、动态超级分辨率 (DSR)、VR Direct 以及超节能设计在内的一系列新技术。
- 2016 Pascal:Pascal 架构将处理器和数据集成在同一个程序包内,以实现更高的计算效率。1080系列、1060系列基于Pascal架构
Kepler/Maxwell/Pascal相关参数对比
- 2017 Volta: Volta 配备640 个Tensor 核心,每秒可提供超过100 兆次浮点运算(TFLOPS) 的深度学习效能,比前一代的Pascal 架构快5 倍以上。
- 2018 Turing: Turing 架构配备了名为 RT Core 的专用光线追踪处理器,能够以高达每秒 10 Giga Rays 的速度对光线和声音在 3D 环境中的传播进行加速计算。Turing 架构将实时光线追踪运算加速至上一代 NVIDIA Pascal™ 架构的 25 倍,并能以高出 CPU 30 多倍的速度进行电影效果的最终帧渲染。2060系列、2080系列显卡也是跳过了Volta直接选择了Turing架构。
2.GPU架构分解
GPU物体架构
NV GPU架构
- Giga Thread Engine来管理所有正在进行的工作
- GPU被划分成多个GPCs(Graphics Processing Cluster)
- 每个GPC拥有多个SMM(Nvdia后来把SM改为SMM)和一个光栅化引擎(Raster Engine)
SMM架构
- 着色器程序的执行都是在SM上完成的
- sm包含32个运算核心 ,16个LD/ST(load/store)模块来加载和存储数据,4个SFU(Special function units)执行特殊数学运算(sin、cos、log等),128KB寄存器,64KB L1缓存,全局内存缓存,Tex纹理读取单元,TextureCache纹理缓存。
- polyMorph Engine多边形引擎负责属性装配(attribute Setup)、顶点拉取(VertexFetch)、曲面细分、栅格化(这个模块可以理解专门处理顶点相关的东西)
- Warp Schedulers这个模块负责warp调度,一个warp由32个线程组成,warp调度器的指令通过Dispatch Units送到Core执行。
三、GPU渲染管线
1.程序通过图形API(DXGLWEBGL)发出drawcall指令,指令会被推送到驱动程序,驱动会检查指令的合法性,然后会把指令放到GPU可以读取的Pushbuffer中。
2.经过一段时间或者显式调用flush指令后,驱动程序把Pushbuffer的内容发送给GPU,GPU通过主机接口(Host Interface)接受这些命令,并通过Front End处理这些命令。
3.在图元分配器(Primitive Distributor)中开始工作分配,处理indexbuffer中的顶点产生三角形分成批次(batches),然后发送给多个PGCs,这一步的理解就是提交上来n个三角形,分配个这几个PGC同时来处理。
SMM架构
4.在GPC中,每个SM中的Poly Morph Engine负责通过三角形索引(triangle indices)取出三角形的数据(vertex data)[图中的Vertex Fetch模块]。
5.在获取数据之后,在sm中以32个线程为一组的线程束(warp)来调度,来开始处理顶点数据。warp是典型的单指令多线程(SIMT,SIMD单指令多数据的升级)的实现,也就是32个线程同时执行的指令是一模一样的,只是线程数据不一样,这样的好处就是一个warp只需要一个套逻辑对指令进行解码和执行就可以了,芯片可以做的更小更快,只所以可以这么做是由于GPU需要处理的任务是天然并行的。
6.SM的warp调度器会按照顺序分发指令给整个warp,单个warp中的线程会锁步(lock-step)执行各自的指令,如果线程碰到不激活执行的情况也会被遮掩(be masked out),被遮掩的原因有很多,例如当前的指令是if(true)的分支,但是当前线程的数据的条件是false,或者比如一个循环被终止了但是别的还在走,因此在shader中的分支会显著增加时间消耗,在一个warp中的分支除非32个线程都走到if或者else里面,否则相当于所有的分支都走了一遍,线程不能独立执行指令而是以warp为单位,而这些warp相互之间是独立的。
7.warp中的指令可以被一次完成,也可能经过多次调度,例如sm中的加载纹理、数据存取明显少于数学运算。
8.由于某些指令比其他指令需要更长的时间才能完成,特别是内存加载,warp调度器可能会简单地切换到另一个没有内存等待的warp,这是gpu如何克服内存读取延迟的关键,只是简单地切换活动线程组。为了使这种切换非常快,调度器管理的所有warp在寄存器文件中都有自己的寄存器。这里就会有个矛盾产生,shader需要越多的寄存器,就会给warp留下越少的空间,就会产生越少的warp,这时候在碰到内存延迟的时候就会只是等待,而没有可以运行的warp可以切换。
9.一旦warp完成了vertex-shader的所有指令,运算结果会被Viewport Transform模块处理,三角形会被裁剪然后准备栅格化,GPU会使用L1和L2缓存来进行vertex-shader和pixel-shader的数据通信
10.接下来这些三角形将被分割,再分配给多个GPC,三角形的范围决定着它将被分配到哪个光栅引擎(raster engines),每个raster engines覆盖了多个屏幕上的tile,这等于把三角形的渲染分配到多个tile上面。也就是像素阶段就把按三角形划分变成了按显示的像素划分了。
11.sm上的Attribute Setup保证了从vertex-shader来的数据经过插值后是pixel-shade是可读的。
12.GPC上的光栅引擎(raster engines)在它接收到的三角形上工作,来负责这些这些三角形的像素信息的生成。
13.32个像素线程将被分成一组,或者说8个2X2的像素块,这是在像素着色器上面的最小工作单元,在这个像素线程内,如果没有被三角形覆盖就会被遮掩,sm中的warp调度器会管理像素着色器的任务。
14.接下来的阶段就和vertex-shader中的逻辑步骤完全一样,但是变成了在像素着色器线程中执行。 由于不耗费任何性能可以获取一个像素内的值,导致锁步执行(SIMD)非常便利,所有的线程可以保证所有的指令可以在同一点。
15.最后一步,现在像素着色器已经完成了颜色的计算还有深度值的计算,在这个点上,我们必须考虑三角形的原始api顺序,然后才将数据移交给渲染输入单元ROP(render output unit),一个ROP内部有很多ROP单元,在ROP单元中处理深度测试,和framebuffer的混合,深度和颜色的设置必须是原子操作,否则的话两个不同的三角形在同一个像素点就会有冲突和错误。
四、GPU渲染优化
- 减少CPU/GPU 数据交互:数据批量提交,避免GPU数据回读。
- 合并drawcall:因为即使渲染一个三角形,在GPU中也要走系列复杂的流程,这系列流程带来的延迟远超过计算一个三角本身,只有同时并行多处理才能发挥GPU的强大并行能力,这也是我们优化的时候要合并渲染的原因,越合并越能最大限度的利用GPU。
- 降低渲染面数:面数越少VS计算使用的线程就越少,顶点计算就越快。
- 避免在shader中使用if else:因为按照SIMD的执行方式,if else可能会完全不生效,导致两个分支都要走一遍。同样循环中的break也会导致这样的问题。
- 降低采样次数:因为纹理的读取速度实在是太慢了,读取跟不上运算会导致极大的延迟。
参考文献
https://www.nvidia.cn/design-visualization/technologies/turing-architecture/www.nvidia.cn
图形渲染及优化-渲染管线基础-腾讯游戏学院gameinstitute.qq.com
刘小刘:渲染优化-从GPU的结构谈起zhuanlan.zhihu.com