三角形的生命周期 - NVIDIA的逻辑管线

自从五年前发布突破性的Fermi架构以来,可能是时候重新审视其背后的基本图形架构了。Fermi是第一款实现完全可扩展图形引擎的NVIDIA GPU,其核心架构在Kepler和Maxwell中也得以体现。以下文章,特别是下面的“压缩管线知识”图像,旨在基于各种公开材料(如白皮书或GTC教程)为读者提供一个入门指南,介绍GPU架构的不同方面。本文重点关注GPU如何工作的图形视角,尽管一些原则(例如着色器程序代码的执行方式)在计算方面是相同的。

GPU是超级并行的工作分配器
为什么会有这么多复杂性?在图形处理中,我们必须处理数据放大,这会产生大量可变的工作负载。每个绘制调用可能生成不同数量的三角形。裁剪后的顶点数量与我们最初构建三角形时的数量不同。经过背面剔除和深度剔除后,并非所有三角形都需要在屏幕上显示像素。三角形在屏幕上的大小可能意味着它需要数百万个像素,或者根本不需要。

因此,现代GPU让它们的原始图形(三角形、线条、点)遵循逻辑管线,而不是物理管线。在G80统一架构之前的旧时代(想想DX9硬件、PS3、Xbox360),管线在芯片上通过不同的阶段表示,工作依次通过这些阶段。G80本质上根据负载重用了一些单元用于顶点和片段着色器计算,但它仍然对原始图形/光栅化等过程采用串行处理。随着Fermi的推出,管线变得完全并行,这意味着芯片通过重用多个引擎实现逻辑管线(一个三角形经过的步骤)。

假设我们有两个三角形A和B。它们的工作部分可能处于不同的逻辑管线步骤中。A已经被变换并需要进行光栅化。它的一些像素可能已经在运行像素着色器指令,而其他像素则被深度缓冲(Z剔除)拒绝,其他像素可能已经写入帧缓冲,而一些可能实际上在等待。与此同时,我们可能正在获取三角形B的顶点。因此,尽管每个三角形必须经过逻辑步骤,但许多三角形可以在其生命周期的不同步骤中被积极处理。工作(将绘制调用的三角形显示在屏幕上)被分解为许多更小的任务,甚至是可以并行运行的子任务。每个任务被调度到可用的资源上,这并不局限于某种类型的任务(顶点着色与像素着色并行)。

想象一条分流的河流。并行管线流是相互独立的,每个都有自己的时间线,有些可能分支得比其他的更多。如果我们根据GPU当前处理的三角形或绘制调用对GPU的单元进行颜色编码,它将是多彩的闪烁灯 😃

GPU架构
自Fermi以来,NVIDIA采用了类似的原则架构。GPU被划分为多个GPC(图形处理集群),每个GPC有多个SM(流式多处理器)和一个光栅引擎。在这个过程中有很多互连,最显著的是交叉开关,它允许工作在GPC或其他功能单元(如ROP(渲染输出单元)子系统)之间迁移。

程序员所考虑的工作(着色器程序执行)是在SM上完成的。它包含许多核心,这些核心为线程执行数学运算。例如,一个线程可以是顶点或像素着色器的调用。这些核心和其他单元由Warp调度器驱动,Warp调度器管理32个线程的组作为warp,并将要执行的指令交给调度单元。代码逻辑由调度器处理,而不是在核心内部,核心只看到来自调度器的类似“将寄存器4234与寄存器4235相加并存储在4230”的指令。与CPU相比,核心本身相对简单。GPU将智能放在更高的层次,它管理整个集合的工作(或者说多个集合)。

这些单元在GPU上的实际数量(每个GPC有多少SM,多少GPC等)取决于芯片的配置。如上所示,GM204有4个GPC,每个GPC有4个SM,但例如Tegra X1只有1个GPC和2个SM,都是Maxwell设计。SM设计本身(核心数量、指令单元、调度器等)也随着代际的变化而变化(见第一张图),并帮助使芯片高效地从高端桌面扩展到笔记本和移动设备。

逻辑管线
为了简化起见,省略了一些细节。我们假设绘制调用引用了一些已经填充数据的索引和顶点缓冲区,并且这些数据存储在GPU的DRAM中,仅使用顶点和像素着色器(GL:片段着色器)。程序在图形API(DX或GL)中发出绘制调用。这在某个时刻到达驱动程序,驱动程序进行一些验证以检查事情是否“合法”,并将命令以GPU可读的编码插入到推送缓冲区中。在CPU端,这里可能会发生许多瓶颈,这就是为什么程序员使用API和利用当今GPU的强大功能的技术非常重要。

经过一段时间或显式的“刷新”调用后,驱动程序在推送缓冲区中缓冲了足够的工作,并将其发送到GPU进行处理(在操作系统的某种参与下)。GPU的主机接口接收命令,这些命令通过前端进行处理。
我们在原始分发器中开始工作分配,通过处理索引缓冲区中的索引并生成三角形工作批次,将其发送到多个GPC。在一个GPC内,某个SM的多态引擎负责从三角形索引中获取顶点数据(顶点获取)。
数据获取后,32个线程的warp在SM内被调度并将处理这些顶点。
SM的warp调度器按顺序发出整个warp的指令。线程以锁步方式运行每条指令,如果它们不应主动执行,则可以单独屏蔽。需要这种屏蔽的原因有很多。例如,当当前指令是“if (true)”分支的一部分,而线程特定的数据评估为“false”时,或者当一个线程的循环终止条件已达到而另一个线程尚未达到时。因此,在着色器中有大量分支分歧会显著增加warp中所有线程的执行时间。线程不能单独推进,只能作为一个warp!然而,warps是相互独立的。
warp的指令可以一次完成,也可以需要多个调度轮次。例如,SM通常在负载/存储方面的单元少于进行基本数学运算的单元。
由于某些指令完成所需的时间比其他指令长,尤其是内存加载,warp调度器可能会简单地切换到另一个不等待内存的warp。这是GPU克服内存读取延迟的关键概念,它们简单地切换出一组活动线程。为了使这种切换非常快速,所有由调度器管理的线程在寄存器文件中都有自己的寄存器。着色器程序需要的寄存器越多,线程/warp可用的空间就越少。我们可以切换的warp越少,在等待指令完成(主要是内存获取)时,我们可以做的有用工作就越少。一旦warp完成了顶点着色器的所有指令,其结果将被视口变换处理。三角形被裁剪到裁剪空间体积,并准备进行光栅化。我们使用L1和L2缓存进行所有这些跨任务通信数据。现在事情变得激动人心,我们的三角形即将被切割,并可能离开它当前所在的GPC。三角形的边界框用于决定哪些光栅引擎需要处理它,因为每个引擎覆盖屏幕的多个瓦片。它通过工作分配交叉开关将三角形发送到一个或多个GPC。我们实际上将三角形分割成许多更小的工作。目标SM的属性设置将确保插值量(例如我们在顶点着色器中生成的输出)以像素着色器友好的格式存在。
GPC的光栅引擎处理接收到的三角形,并为其负责的那些部分生成像素信息(还处理背面剔除和Z剔除)。
我们再次批量处理32个像素线程,或者更确切地说是8个2x2像素四边形,这是我们在像素着色器中始终使用的最小单元。这个2x2四边形使我们能够计算诸如纹理mip映射过滤的导数(四边形内纹理坐标的巨大变化会导致更高的mip)。在2x2四边形内,样本位置实际上不覆盖三角形的线程被屏蔽(gl_HelperInvocation)。本地SM的warp调度器将管理像素着色任务。
在顶点着色器逻辑阶段中,我们所拥有的warp调度器指令游戏,现在在像素着色器线程上执行。锁步处理特别方便,因为我们几乎可以免费访问像素四边形内的值,因为所有线程都保证在同一指令点计算其数据(NV_shader_thread_group)。我们到了吗?差不多,我们的像素着色器已经完成了要写入渲染目标的颜色计算,我们也有一个深度值。在这一点上,我们必须考虑三角形的原始API顺序,然后再将数据交给其中一个ROP(渲染输出单元)子系统,该子系统本身有多个ROP单元。在这里执行深度测试、与帧缓冲的混合等操作。这些操作需要原子性地进行(一次设置一个颜色/深度),以确保我们不会在同一像素上有一个三角形的颜色和另一个三角形的深度值。NVIDIA通常应用内存压缩,以减少内存带宽需求,从而提高“有效”带宽(见GTX 980 PDF)。
呼!我们完成了,我们已经将一些像素写入了渲染目标。我希望这些信息有助于理解GPU内部的一些工作/数据流。这也可能有助于理解与CPU同步的另一个副作用,为什么这真的很有害。必须等待所有工作完成且没有新工作提交(所有单元变为空闲状态),这意味着在发送新工作时,需要一段时间才能使所有单元再次完全负载,尤其是在大型GPU上。

在下面的图像中,您可以看到我们如何渲染CAD模型,并根据不同的SM或warp ID对其进行着色,这些ID对图像的生成做出了贡献(NV_shader_thread_group)。结果不会是帧一致的,因为工作分配会在每帧之间变化。该场景使用许多绘制调用进行渲染,其中一些也可能并行处理(使用NSIGHT,您也可以看到一些绘制调用的并行性)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你一身傲骨怎能输

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值