核弹厂有一篇关于自家GPU架构和逻辑管线的非常好的文章,如果你想要对GPU的结构有一个比较完整系统的认识,请一定不要错过这篇Life of a Triangle,本文主要参考此处进行总结归并。
管线结构总图
并行管线流中每一条都彼此独立,每一条都有自己的时间线,有一些可能会比其他的有更多分支。如果我们将GPU进行处理所基于的三角形部分的单元或者drawcall当前正在处理的单元进行代码着色的话,看起来会像是七彩的闪烁灯。
因此Fermi NVIDIA也有一个类似原理的架构。它有一个管理所有工作进行的Giga线程引擎(Giga Thread Engine)。GPU被分割成许多个GPCs(Graphics Processing Cluster 图形处理簇),每个簇都有许多SMs(Streaming Multiprocessor 流多重处理器)和一个光栅引擎(Raster Engine)。这个结构中有许多连接器,尤其是Crossbar,允许工作的迁移穿过GPCs或者其他像ROP(render output unit)这样的功能性单元。
程序员所考虑的工作(shader程序计算)是在SMs进行处理的。它包含许多为线程进行数学计算的内核(Cores)。举例来说一个线程可能是VS或者PS调用。这些核心和其他单元由Warp Schedulers驱动,Warp Schedulers管理一组32线程的wrap(Wrap:GPU执行程序时的调度单位,目前cuda的warp的大小为32,同在一个warp的线程,以不同数据资源执行相同的指令,这就是所谓 SIMT)并且把要进行处理的指令递交到Dispatch Units(指令调度器)。代码的逻辑由调度器负责处理而不是在核心自身的内部处理,所以从调度程序上看只会看到类似于“计算寄存器4234和寄存器4235的值的和并且储存到寄存器4230中”。相比较CPU那相当聪明的内核来说(GPU的)一个内核本身是挺蠢的。GPU将比较聪明的部件放置到比较高的层级,它会引导整个工作的进行(或者是你想要的多个工作)。
GPU中实际存在多少个这样的核心(每个GPC多少个SMs,每个GPU有多少个GPCs等)基于芯片配置本身。比如像上面说的GM204有4个GPCs,每个GPC有4个SMs,但是同样是基于Maxwell设计的Tegra X1仅有1个GPC和2个SM。SM自身的设计(内核数量,指令单元,调度器等)也随着时间一代代地进化使得芯片如此高效,以至于它们能广泛应用在高端台式、笔记本和移动平台上。
这一部分是基于数据的流向,对GPU的硬件单元进行了大致的划分,通常来说,GPU会有三个比较重要的部分,分别是图中从顶至下的:控制模块、计算模块和输出模块。通常来说,GPU架构的设计需要有可伸缩性,这样通过增加/阉割计算和输出模块,就能够产生性能不同的同架构产品(比如GTX1070和GTX1080的主要区别就在于GPC和FBP的数量),以满足不同消费水平和应用场景的需求。
控制模块负责接收和验证(主要是Host和Front End)来自CPU的经过打包的PushBuffer(经过Driver翻译的Command Buffer),然后读取顶点索引(注意是Vertex Indices不是Vertex Attributes,主要由Primitive Distributor负责)分发到下游管线或者读取Compute Grid的信息(主要由CWD负责,这部分是Compute Pipeline,不作展开)并向下游分发CTA。
本部分具体工作流程如下:
计算模块是GPU中最核心的部件,Shader的计算就发生在这里。早期的硬件设计上,我们会区分VS,PS等Shader类型,并设计专用的硬件单元去执行对应类型的Shader,但这样的方法并不利于计算单元满负荷运转,所以现在所有的GPU设计都是通用计算单元,为所有Shader类型服务。在NV的显卡里这个模块全称是Graphics Processing Cluster,通常一个GPU会有多个GPC,每个GPC包含一个光栅器(Raster)负责执行光栅化操作,若干个核心的计算模块,称之为Texture Process Cluster(TPC)。
关于TPC,我们进一步分解来看这张大图
通常来说,一个TPC拥有:
其中,TPC内最核心的部件就是SM,这里我们再进一步分解SM看这张大图:
一个SM通常拥有一块专用于缓存Shader指令的L1 Cache,若干线程资源调度器,一个寄存器池,一块可被Compute Pipeline访问的共享内存(Shared Memory),一块专用于贴图缓存的L1 Cache,若干浮点数运算核心(Core),若干超越函数的计算单元(SFU),若干读写单元(Load/Store)。
作为核心计算单元,GPU的设计思路和CPU有很大的不同,就我所知的体现在两个方面:
要详细解释这两点,我们就需要理解GPU的执行模型。
GPU的设计是为了满足大规模并行的计算,为此,它使用的是SIMD(Single Instruction Multiple Data) 的执行模式,在内部,若干相同运算的输入会被打包成一组并行执行,这个组就是GPU的最小执行单元,在NV叫做Warp,每32个thread为一组,在AMD叫做Wavefronts,每64个thread为一组。基于不同的shader阶段,被打包执行的对象会有区别,比如VS里,就是32个顶点为一组,PS里,就是8个pixel quad(2*2像素块)为一组。
那么GPU又如何处理分支呢?我们知道,CPU有一种经典的处理分支的方法,叫做分支预测。CPU会根据一组数据之前的分支结果去预测下一次分支的走向,如果错误就会有额外的开销。GPU没有这么复杂的流程控制,它的流程控制基于一种叫做“active mask”的技术,简单来说就是用一个bit mask去判断当前32个thread的branch状态,如果是0,则表示只需要执行false的branch,如果bit mask是2^32-1,则表示只需要执行true的branch,否则就需要对某些thread执行true,同时另一些在执行true的同时等待这些thread,反之亦然,这种情形下一个warp的执行时间就是Max(branch(true))+Max(branch(false))。
Desktop GPU的内存类型和CPU比较相似,也是多级缓存的机制,我们能够接触到的内存类型包括Register,Shared Memory(本质是L1 Cache的一块),Texture L1 Cache(本质是L1 Cache的一块),Instruction Cache(本质是L1 Cache的一块),L2 Cache,DRAM,各类存储器的容量在是依次增大的,相应的它们在芯片上的位置也是离核心单元SM越来越远,同时访存延迟也是逐级增大的。
对于GPU在这几种内存中的访存延迟,我从这篇文章找到了一些数据:
Mobile GPU没有专用的显存,而是和CPU共享同一块系统内存(缓存机制当然也应该是共享的),但它有一块位于GPU上的专用on chip memory。
GPU拥有大量的寄存器(数量远多于CPU),是为了能够快速的在warp之间做切换:当某个warp被某些指令阻塞的时候(比如贴图采样),warp schedular可以让其处于休眠状态,并且把shader core的资源让出来,唤醒那些未被阻塞的warp。对于CPU来说,context switch的开销来源于寄存器的恢复和保存(没那么多寄存器,只能复用),但是对于GPU,每个warp是独占一份自己的register file的,这样就可以几乎无消耗地切换warp。相应的,一个SM里能同时并行多少个warp,就取决于一段shader到底占用了多少register,占用的越多,则能够并行的warp就越少。
相较于上几代的GPU,Turing在SM中增加了专用于光线追踪的RTCore,以及用于张量计算的TensorCore(后者主要是用于深度学习。在Turing之后,你还可以在做Graphics的同时利用TensorCore去做一些DL的工作,比如DLSS[7]?好像没什么x用)。下面两张图简单解释了RTCore前后光线追踪的基本流程:
这个图看起来很复杂,其实很简单:对于非Turing架构来说,光线和BVH的遍历求交、光线和三角形的求交、光线和三角形交点的着色这三件事,都是翻译成了数千条SM的指令给FP Core执行的。而Turing架构则是把前两件事作为固定硬件单元集成在了RTCore里,所以RTCore核心功能有两个:遍历BVH和光线-三角形快速求交。
接控制模块 1.1.2 工作流程:
输出模块(Framebuffer Partition,FBP)比较简单,最核心的部件是一个称之为ROP(Raster Operation) 的单元,ROP又包含了两个子单元,分别是CROP(Color ROP)和ZROP,前者负责Alpha Blend,MSAA Resolve等操作,并把最终的颜色写到color buffer上,后者则负责进行Stencil/Z Test以及把depth/stencil写到z buffer上。
接计算模块 1.2.3 工作流程:
至此,我们已经完成了,我们写入了一些像素值到渲染目标(Render Target)。我希望这些信息对于理解GPU内部的工作流/数据流有所帮助。当然另一方面也可以帮助理解为什么与CPU同步工作是相当痛苦的。有一点就是如果没有新任务提交进来GPU就必须等待所有CPU的处理完成(所有单元都会变成闲置状态),这意味着当有新任务发送进来时需要等待一段时间才能达到完全负载的状态下,尤其是大型GPU更为明显。
说到渲染管线,就必须介绍当前GPU使用的三种不同的渲染管线:Immediate Mode Rendering, Tile Based Rendering和Tile Based Deferred Rendering。我们用三张图来详细描述IMR,TBR和TBDR三种模式的渲染管线:
IMR(Immediate Mode Rendering)就如字面意思一样——提交的每个渲染要求都会立即开始,这是一种简单而又粗暴的思路,优点缺点都非常明显,如果不用为性能担忧,这种方式会很省事,但是IMR的渲染实行的是无差别对待,那些遮蔽处理的部分依然会被渲染处理器,这也导致无意义的读写操作更多,浪费了大量性能和带宽。
IMR模式的第一个阶段是Vertex Processing,这个环节包括从DRAM/System Memory取Vertex Indices(PD的工作),然后根据Vertex Indices去Vertex Buffer取相应的属性(PE中VAF的工作),需要注意的是,取Indices/Vertex Attributes的阶段都会有L2 Cache在工作,表示如果顶点短时间内被share多次,则可以通过cache命中减少加载时间。加载完顶点数据后,Vertex Shader将会被加载到SM的Instruction Cache,紧接着就是VS在SM的执行。
VS执行完毕后,PE内的固定单元会执行顶点剔除来剔除一些视口外的三角形,背面剔除也在这个阶段发生。
接下来,由Raster对三角形进行光栅化,光栅化完毕的像素将会被打包成warp,经过XBAR重新流入SM(可能是同一个SM,也可能是不同的SM)。重新进入SM的每个pixel会根据其重心坐标,使用PE内的固定单元进行属性插值,从而得到depth,varying attributes等信息。
其中对early-Z test的处理是:
对于没有Apha Test的pixel quad,由ZROP对其执行early-Z test。
对于通过early-Z test的像素,在SM内执行pixel shader。
对于开启Alpha Test的像素,由ZROP对其进行late-Z test,并根据结果决定是否更新FrameBuffer相应位置的颜色和深度值。
若需要更新,则ZROP根据depth test的设置更新z buffer,CROP根据blend的设置去更新color buffer。
注意,IMR的整个流程中,三角形是可以以Stream的形式逐步提交给管线的,先提交的三角形也不需要去等待同一个Render Target上的其他三角形。
IMR是所有Desktop GPU的标配,因为Desktop GPU相较于Mobile GPU,有更多的带宽用于读写,有专用供电接口,也不受限于芯片发热的问题。IMR架构的好处是设计上会相对来说比较清晰简明,并且整个管线是连续的,draw call之间不需要互相等待,有利于最大化吞吐量。对于Mobile GPU来说,只有NV的Tegra系列是基于IMR的。
IMR傻大粗的做法不可取,那就来一个聪明点的方式——TBR(Tile Based Rendering,贴图渲染),它将需要渲染的画面分成一个个的区块(tile),每个区块的坐标通过中间缓冲器以列表形式保存在系统内存中。这种渲染方式的好处就是相对IMR减少了不必要的渲染任务,缺点就是遮蔽碎片依然会少量存在,而且需要中间缓冲器。
TBR架构的GPU会把整个逻辑渲染管线打断成两个阶段:
注:TBR的管线会等待同一个framebuffer上所有的三角形的第一阶段都完成后,才会进入到第二阶段,这就表示,你应该尽可能的少切换framebuffer,让同一个framebuffer的所有三角形全部绘制完毕再去切换
注:TBR的优化实际上是利用缓存的局部性原理。
TBR虽然比IMR聪明多了,不过还是存在不少缺陷,TBDR(Tile Based Deferred Rendering,贴图延迟渲染)闪亮登场,它跟TBR原理相似,但是使用的是延迟渲染(Deferred Rendering),合并了完美像素,通过HSR(Hidden Surface Removal,隐藏面消除)等进一步减少了不需要渲染的过程,降低了带宽需求。实际上这些改变和PC上的渲染有些相似。
TBDR和TBR模式基本类似,唯一的区别在于,TBDR模式在执行光栅化之后,不会急着shading,而是会对rasterized sample进行消隐(基于depth buffer和相同位置的其他sample深度去移除被遮挡的sample),这个消隐的过程结束之后,tile上剩下的sample才会被送到PS里面去做shading。
通常TBR/IMR模式的GPU是基于比较简单的early-Z reject去防止overdraw,TBDR在这个方面则走得更远一点。所以对于IMR/TBR模式的GPU来说,对不透明物体的draw call从前到后排序、Pre-Z pass都能够显著减少overdraw并提高性能;但对于TBDR模式的GPU来说,这两个策略都不会提升性能(管线里面做了相同的事),而且还会影响因性能(排序、Pre-Z pass带来的额外开销)。