三角形的生命-NVIDIA的逻辑管道

发布时间 2023-11-11 04:23:28作者: 吴建明wujianming

三角形的生命-NVIDIA的逻辑管道

自从突破性的费米架构发布近5年以来,也许是时候刷新其下的主要图形架构了。费米是第一个实现完全可扩展图形引擎的NVIDIA GPU,其核心架构可以在开普勒和麦克斯韦中找到。

本文关注GPU如何工作的图形,尽管一些原理(如着色器程序代码的执行方式)对于计算是相同的。

 GPU是超级并行工作分配器

为什么这么复杂?在图形中必须处理数据放大,这会产生大量可变的工作负载。每个drawcall可以生成不同数量的三角形。剪裁后的顶点数量与三角形最初的组成部分不同。经过背面和深度剔除后,并非所有三角形都需要屏幕上的像素。三角形的屏幕大小可能意味着它需要数百万像素,或者根本不需要。

因此,现代GPU让它们的基元(三角形、直线、点)遵循逻辑管道,而不是物理管道。在G80统一架构(想想DX9硬件、ps3、xbox360)之前的旧日子里,流水线在芯片上用不同的阶段表示,工作会一个接一个地贯穿其中。

根据负载的不同,G80基本上为顶点和碎片着色器计算重复使用了一些单元,但它仍然有一个用于基元/光栅化等的串行过程。有了费米,管道变得完全平行,这意味着芯片通过重复使用芯片上的多个引擎来实现逻辑管道(三角形经过的步骤)。

假设有两个三角形A和B。它们的部分工作可能在不同的逻辑管道步骤中。A已被转换,需要进行光栅化。它的一些像素可能已经在运行像素着色器指令,而其他像素则被深度缓冲区(Z-cull)拒绝,其他像素可能已经被写入帧缓冲区,还有一些可能正在等待。除此之外,还可以获取三角形B的顶点。

除此之外还可以获取三角形B的顶点。因此,虽然每个三角形都必须经过逻辑步骤,但其中许多可以在其生命周期的不同步骤中进行主动处理。作业(在屏幕上获取drawcall的三角形)被拆分为许多较小的任务,甚至可以并行运行的子任务。每个任务都按可用资源进行调度,但不限于特定类型的任务(顶点着色与像素着色平行)。

想想一条呈扇形的溪流。相互独立的并行管道流,每个人都在自己的时间线上,有些可能比其他人分支更多。如果根据三角形对GPU的单元进行颜色编码,或者绘制当前正在使用的图形,那么它将是多色闪烁灯。

GPU 架构

 由于费米NVIDIA具有类似的原理架构。有一个Giga线程引擎来管理正在进行的所有工作。GPU被划分为多个GPC(图形处理集群),每个GPC都有多个SM(流式多处理器)和一个光栅引擎。这个过程中有很多互连,最引人注目的是一个Crossbar,它允许在GPC或其他功能单元(如ROP(渲染输出单元)子系统)之间进行工作迁移。

程序员想到的工作(着色器程序执行)是在SM上完成的。它包含许多为线程进行数学运算的核心。例如,一个线程可以是顶点或像素着色器调用。这些核心和其他单元由Warp调度驱动,Warp 调度管理一组32个线程作为Warp,并将要执行的指令移交给发布单元。

代码逻辑由调度器处理,而不是在内核内部,内核本身只是从调度器看到类似于“和寄存器4234与寄存器4235并存储在4230中”的东西。与CPU相比,核心本身相当愚蠢,而CPU的核心相当聪明。GPU将智能性提升到更高的级别,它可以进行整个集成的工作(如果愿意的话,可以进行多个集成)。

这些单元中有多少实际上在GPU上(每个GPC有多少SM,有多少GPC…)取决于芯片配置本身。如上所述,GM204具有4个GPC,每个GPC具有4个SM,但例如Tegra X1具有1个GPC和2个SM,两者都采用Maxwell设计。SM设计本身(内核、指令单元、调度器……的数量)也随着时间的推移而一代又一代地发生了变化(见第一张图),这有助于提高芯片的效率,使其可以从高端台式机扩展到笔记本电脑,再扩展到移动设备。

逻辑管道

为了简单起见,省略了几个细节。假设drawcall引用了一些索引和顶点缓冲区,该缓冲区已经填充了数据,并且存在于GPU的DRAM中,并且只使用顶点和像素着色器(GL:fragmentshader)。

 该程序在图形api(DX或GL)中进行drawcall。这会在某个时候到达驱动程序,该驱动程序会进行一些验证,以检查事情是否“合法”,并将命令插入到推送缓冲区内的GPU可读编码中。在CPU方面可能会发生很多瓶颈,这就是为什么程序员很好地使用api以及利用当今GPU的能力的技术很重要。

经过一段时间或显式的“刷新”调用后,驱动程序在推送缓冲区中缓冲了足够的工作,并将其发送给GPU处理(操作系统也参与其中)。GPU的主机接口接收通过前端处理的命令。

在原始分配器中开始工作分配,方法是处理indexbuffer中的索引,并生成三角形工作批,然后发送给多个GPC。

 在GPC中,一个SM的多边形变形引擎负责从三角形索引中提取顶点数据(顶点提取)。

在数据被提取后,32个线程的扭曲被安排在SM内部,并将在顶点上工作。

SM的warp调度器按顺序发布整个warp的指令。线程在锁定步骤中运行每条指令,如果它们不应该主动执行,则可以单独屏蔽。需要这种屏蔽的原因可能有多种。例如,当当前指令是“if(true)”分支的一部分并且线程特定的数据被评估为“false”时,或者当在一个线程中达到了循环的终止标准而在另一个线程没有达到时。因此,在着色器中具有大量分支发散会显著增加warps中所有线程所花费的时间。线不能单独前进,只能作为一个经线前进!然而,warps是相互独立的。

warp的指令可能一次完成,也可能需要几个调度回合。例如,SM通常具有比进行基本数学运算更少的加载/存储单元。

由于某些指令比其他指令需要更长的时间才能完成,特别是内存加载,因此warp调度器可以简单地切换到另一个不等待内存的warp。这是GPU如何克服内存读取延迟的关键概念,它们只是切换出活动线程组。为了使这种切换非常快,调度程序管理的所有线程在寄存器文件中都有自己的寄存器。着色器程序需要的寄存器越多,线程/warps的空间就越少。可以切换的warps越少,在等待指令完成时所能做的有用工作就越少(最重要的是内存获取)。

 warps完成顶点着色器的所有指令后,其结果将由“视口变换”处理。三角形被裁剪空间体积剪裁,并准备进行光栅化,对所有这些跨任务通信数据使用L1和L2缓存。

 现在令人兴奋的是,三角形即将被分割,可能会离开它目前所在的GPC。三角形的边界框用于决定哪些光栅引擎需要在它上工作,因为每个引擎覆盖屏幕的多个瓦片。它通过工作分配横杆将三角形发送给一个或多个GPC。现在有效地将三角关系分成了许多较小的工作。

 目标SM处的属性设置将确保插值(例如,在顶点着色器中生成的输出)采用像素着色器友好格式。

GPC的光栅引擎处理它接收到的三角形,并为它负责的部分生成像素信息(还处理背面剔除和Z剔除)。

再次批量处理32个像素线程,或者更好地说是8倍的2x2像素四边形,这是将始终使用的像素内着色器的最小单元。这个2x2 quad允许计算纹理mip贴图过滤之类的导数(quad中纹理坐标的大变化会导致mip更高)。2x2四边形中那些采样位置实际上没有覆盖三角形的线程将被屏蔽(gl_HelperInvocation)。本地SM的一个warp调度器将管理像素着色任务。

在顶点着色器逻辑阶段使用的warp调度器指令游戏,现在在像素着色器线程上执行。

锁步处理特别方便,因为几乎可以免费访问像素四边形中的值,因为所有线程都保证在相同的指令点(NV_shader_thread_group)之前计算其数据。

 到了吗?几乎,像素着色器已经完成了要写入渲染目标的颜色的计算,并且还有一个深度值。在这一点上,在将数据移交给ROP(渲染输出单元)子系统之前,必须考虑三角形的原始api排序,该子系统本身具有多个ROP单元。这里执行深度测试、与帧缓冲区的混合等等。这些操作需要原子化地进行(一次设置一个颜色/深度),以确保当两个三角形都覆盖同一像素时,不会有一个三角形的颜色和另一个三角的深度值。NVIDIA通常应用内存压缩来降低内存带宽需求,从而增加“有效”带宽(请参阅GTX 980 pdf)。

Puh!已经完成了,已经将一些像素写入到h绘制目标中。希望这些信息有助于理解GPU中的一些工作/数据流。

这也可能有助于理解为什么与CPU同步真的很伤人的另一个副作用。一个人必须等到所有工作都完成,并且没有提交新的工作(所有单元都变为空闲),这意味着在发送新工作时,需要一段时间,直到所有工作都再次完全加载,尤其是在大型GPU上。

在下图中,可以看到如何渲染CAD模型,并通过对图像有贡献的不同SM或扭曲ID(NV_shader_thread_group)对其进行着色。结果将不是帧一致的,因为工作分布将随帧变化。场景是使用许多drawcall渲染的,其中一些也可以并行处理(使用NSIGHT,也可以看到一些drawcall并行性)。