图形流水线的GPU架构
本文为的翻译与总结。
GPU是高度并行化的工作分配器
工作(在屏幕上获取 drawcall 的三角形)被分成许多较小的任务,甚至可以并行运行的子任务。 每个任务都被调度到可用的资源上,不限于某种类型的任务(顶点着色与像素着色并行化)。
GPU架构
有一个 Giga Thread Engine 管理所有正在进行的工作。 GPU被划分为多个GPC(Graphics Processing Cluster),每个GPC都有多个SM(Streaming Multiprocessor)和一个Raster Engine。在这个过程中有很多互连,最显着的是允许跨 GPC 或其他功能单元(如 ROP(render output unit)子系统)迁移工作的 Crossbar。
程序员认为的工作(执行着色器程序)是在 SM 上完成的。它包含许多对线程进行数学运算的Core。例如,一个thread 可以是顶点着色器或像素着色器调用。这些核心和其他单元由Warp Scheduler驱动,它们管理一组 32 个线程作为 warp 并将要执行的指令交给Dispatch Unit。代码逻辑由调度程序处理,而不是在内核本身内部,它只是从调度程序中看到类似于“寄存器 4234 与寄存器 4235 的和并存储在 4230 中”之类的内容。与核心非常智能的 CPU 相比,核心本身相当愚蠢。 GPU 将智能提升到更高的水平,它执行整个集成(或多个,如果你愿意的话)的工作。
这些单元中有多少实际上在 GPU 上(每个 GPC 有多少个 SM,多少个 GPC..)取决于芯片配置本身。
逻辑流水线
为简单起见,省略了一些细节。 我们假设 drawcall 引用了一些索引和顶点缓冲区,这些缓冲区已经填充了数据并存在于 GPU 的 DRAM 中,并且仅使用顶点和像素着色器。
[*]该程序在图形 api(DX 或 GL)中进行绘制调用。 这会在某个时候到达驱动程序,该驱动程序会进行一些验证以检查事情是否“合法”,并将命令以 GPU 可读的编码插入到 pushbuffer 中。 在 CPU 方面可能会发生很多瓶颈,这就是为什么程序员使用好 api 以及利用当今 GPU 的强大功能的技术很重要的原因。
[*]在一段时间后或明确的“刷新”调用之后,驱动程序已经在 pushbuffer 中缓冲了足够的工作并将其发送给 GPU 处理(操作系统的一些参与)。 GPU 的Host Interface接收将要通过Front End处理的命令。
[*]我们通过处理 indexbuffe r 中的索引并生成我们发送到多个 GPC 的三角形工作批次,在 Primitive Distributor 中开始我们的工作分配。
[*]在 GPC 中,其中一个 SM 的 Poly Morph Engine 负责从三角形索引中获取顶点数据(Vertex Fetch)。
[*]获取数据后, SM 内的warp 被调度,并将在顶点上工作。
[*]SM 的 warp scheduler按顺序发出整个 warp 的指令。 线程以lock-step方式运行每条指令,如果它们不应该主动执行它,可以单独屏蔽。 需要这种掩蔽可能有多种原因。 例如,当当前指令是“if (true)”分支的一部分并且线程特定数据评估为“false”时,或者当一个线程达到循环的终止标准但另一个线程未达到时。 因此,在着色器中有大量分支发散会显着增加warp 中所有线程所花费的时间。 线程不能单独前进,只能作为一个warp共同前进! 然而,warp之间是相互独立的。
[*]warp 的指令可能会一次完成,也可能需要几个dispatch 轮次。 例如,与执行基本数学运算相比,SM 通常具有更少的加载/存储单元。
[*]由于某些指令比其他指令需要更长的时间来完成,尤其是内存加载,warp scheduler 可能会简单地切换到另一个不等待内存的 warp。 这是 GPU 如何克服内存读取延迟的关键概念,它们只是切换活动线程组。 为了使这种切换非常快,scheduler 管理的所有线程在寄存器文件中都有自己的寄存器。 着色器程序需要的寄存器越多,线程/warp的空间就越少。 我们可以在之间切换的warp越少,在等待指令完成(最重要的内存获取)时我们可以做的有用工作就越少。
[*]一旦warp完成了顶点着色器的所有指令,它的结果将由视口变换处理。 三角形被裁剪空间体积裁剪并准备好进行光栅化。我们对所有这些跨任务通信数据使用 L1 和 L2 缓存。
[*]现在它变得令人兴奋,我们的三角形即将被切碎,并可能离开它目前所在的 GPC。 一个三角形可能被多个Raster Engine处理,这取决于它所覆盖的屏幕四边形。 三角形的边界框用于决定哪些Raster Engine需要对其进行处理,因为每个Raster Engine都覆盖了屏幕的多个图块。 它通过 Work Distribution Crossbar 将三角形发送到一个或多个 GPC。 我们现在有效地将我们的三角形分割成许多较小的工作。
[*]目标 SM 的Attribute Setup将确保插值(例如我们在顶点着色器中生成的输出)采用像素着色器友好格式。
[*]GPC 的Raster Engine处理它接收到的三角形,并为它负责的那些部分生成像素信息(还处理背面剔除和 Z 剔除)。
[*]我们再次批量处理 32 个像素线程,或者更准确地说是 8 次 2x2 像素四边形,这是我们在像素着色器中始终使用的最小单元。这个 2x2 四边形允许我们计算诸如纹理 mipmap过滤之类的导数(四边形内纹理坐标的大变化会导致更高的 mip)。 2x2 四边形中样本位置实际上并未覆盖三角形的那些线程将被屏蔽。本地 SM 的warp scheduler之一将管理像素着色任务。
[*]与我们在顶点着色器逻辑阶段相同的warp scheduler指令游戏现在在像素着色器线程上执行。lock-step处理特别方便,因为我们几乎可以免费访问像素四边形中的值,因为所有线程都保证将其数据计算到相同的指令点 。
[*]我们完成了吗? 几乎,我们的像素着色器已经完成了要写入渲染目标的颜色的计算,并且我们还有一个深度值。 在这一点上,我们必须考虑三角形的原始 api 排序,然后再将该数据交给 ROP(render output unit)子系统之一,该子系统本身具有多个 ROP 单元。 这里执行深度测试,与帧缓冲区混合等。 这些操作需要以原子方式发生(一次设置一种颜色/深度),以确保我们没有一个三角形的颜色和另一个三角形的深度值,当它们都覆盖相同的像素时。 NVIDIA 通常应用内存压缩来降低内存带宽要求,从而增加“有效”带宽。
我们完成了,我们已经将一些像素写入渲染目标。 我希望这些信息有助于理解 GPU 中的一些工作/数据流。 它还可能有助于理解为什么与 CPU 同步确实有害的另一个副作用。 必须等到一切都完成并且没有新工作提交(所有单元都空闲),这意味着在发送新工作时,需要一段时间才能再次完全加载,尤其是在大型 GPU 上。
简单来说:
[*]程序在图形 api(DX 或 GL)中进行绘制调用。这会在某个时候到达驱动程序,该驱动程序会进行一些验证以检查事情是否“合法”,并将命令以 GPU 可读的编码插入到 pushbuffer 中。
[*]在一段时间后或明确的“刷新”调用之后,驱动程序已经在 pushbuffer 中缓冲了足够的工作并将其发送给 GPU 处理。
[*]处理 indexbuffer 中的索引并生成我们发送到多个 GPC 的三角形工作批次,在 Primitive Distributor 中开始我们的工作分配。
[*]在 GPC 中,其中一个 SM 的 Poly Morph Engine 负责从三角形索引中获取顶点数据(Vertex Fetch)。
[*]SM 内的warp 被调度,并将在顶点上工作。
[*]SM 的 warp scheduler按顺序发出整个 warp 的指令。
[*]warp 的指令可能会一次完成,也可能需要几个dispatch 轮次。warp scheduler 可能会简单地切换到另一个不等待的 warp。
[*]一旦warp完成了顶点着色器的所有指令,它的结果将由视口变换(在Poly Morph Engine中)处理。 三角形被裁剪空间体积裁剪并准备好进行光栅化。
[*]三角形即将被切碎,并可能离开它目前所在的 GPC。 它通过 Work Distribution Crossbar 将三角形发送到一个或多个 GPC。
[*]目标 SM 的Attribute Setup将确保插值(例如我们在顶点着色器中生成的输出)采用像素着色器友好格式。
[*]GPC 的Raster Engine处理它接收到的三角形,并为它负责的那些部分生成像素信息(还处理背面剔除和 Z 剔除)。
[*]再次批量处理 32 个像素线程,或者更准确地说是 8 次 2x2 像素四边形。本地 SM 的warp scheduler之一将管理像素着色任务。
[*]必须考虑三角形的原始 api 排序,然后再将该数据交给 ROP(render output unit)子系统之一,该子系统本身具有多个 ROP 单元。 这里执行深度测试,与帧缓冲区混合等。
页:
[1]