欢迎光临散文网 会员登陆 & 注册

【知乎】【Shader篇】GPU架构与其逻辑管线

2023-12-05 07:05 作者:失传技术  | 我要投稿


【Shader篇】GPU架构与其逻辑管线




vvyDev


图形程序

关注他

172 人赞同了该文章



目录

收起

1、引言

2、GPU架构(上)

2.1、PCIe总线

2.2、Giga Thread Engine

2.3、GPCs(Graphics Processing Cluster)

2.4、Poly Morph Engine

2.5、SM(Streaming Multiprocessors)

3、GPU逻辑管线

3.1、小结

4、GPU架构(下)

SIMD

SIMT

co-issue

Shader指令执行机制

GPU存储器

总结

前言:本篇文章作为Shader系列的第一篇,我觉得还是从GPU架构开始,重新认识Shader是如何被解析执行的,然后把渲染管线与GPU架构关联起来,做到知其所以然。

1、引言

通过分析GPU架构,我们可以学习到Shader是如何在GPU这个黑盒子里运行处理的,然后运用相关特性来优化Shader代码,甚至使用Debugger工具来定位Shader瓶颈。CPU核心里大部分部件都是分支预测、各级缓存,而计算单元相对GPU来说很少,所以CPU擅长处理各种逻辑复杂的代码。而GPU把各种逻辑处理单元简化了,省下芯片空间来增加几千个核心,拥有极强的并行运算能力。

打个比喻,CPU拥有复杂的行政部门,拥有各式管理层,能够高效调用工具人处理事务;而GPU却是精简管理层,增加大量工具人。GPU只需要一个管理员去发出一条指令,然后指派32个工具人同时执行该指令,从而瞬间完成32批任务。所以GPU擅长处理大量并行的事务。

2、GPU架构(上)

纵观GPU架构发展,从Fermi到现在的Ampere、Hopper,总体的架构并没有改变,所以这里以2018年的NVidia Turing架构为例进行解析。下图先放其总体架构:

对应着后续解释渲染光线流程的顺序,我从最外层往里进行说明。

2.1、PCIe总线

硬件上对应的是显卡插槽,和主板相连,从而可以和CPU通信。我们在调用图形API发起DrawCall时,这些指令数据最终就会经过PCIe总线,到达GPU显存,而这些数据的传输交由GPU上的DMA专门负责的。所以CPU只需要发送相关指令给DMA,剩下的就无需插手,达到解耦。

2.2、Giga Thread Engine

获取到数据之后,该部件将这些线程块合理地分配给某个GPC,让GPC去处理该线程块对应的数据,输出结果。如果我们写ComputeShader,我们会将其分为一组一组(比如模糊一张图片,8x8像素为一组),程序员可以对线程块大小、任务大小进行规划,GPU就会将每个任务块分配给GPC。

2.3、GPCs(Graphics Processing Cluster)

一个GPU有多个GPC,每个GPC各自处理自己的任务。进入到某个GPC内部,每个GPC拥有一个光栅化引擎(Raster Engine)和多个SM。SM可以处理VertexShader和PixelShader以及ComputeShader(还有RT Core和Tensor Core,这里不涉及了),在执行完VertexShader之后,数据交由光栅化引擎进行光栅化,然后再将得到的片元重新分配给GPC让其SM执行PixelShader,渲染出最终结果。

2.4、Poly Morph Engine

每个SM都对应一个Poly Morph Engine,该部件组成:VertexFetch、曲面细分、裁剪空间、Attribute Setup。它负责将分配给某个SM的数据通过三角形索引(triangle indices)取出三角形的数据(vertex data),然后交由SM执行Shader处理顶点。

2.5、SM(Streaming Multiprocessors)

我们先来认识一下SM,流多处理器,它可以一次并行处理多条数据流。GPC负载均衡地将任务块分配给某SM,SM得到该任务块后,将其以32个为一组分成一个个线程束(Warp),不足32则补空,然后SM每次调度某个线程束以32个线程来执行。

SM里面有个线程束调度器(Warp Scheduler),负责安排哪个Warp进行入32个核心进行执行。

到这里为止,我先结合以上内容,总结GPU的逻辑管线流程,然后在 GPU架构(下) 继续分析SM内容详细(硬核)的架构。

3、GPU逻辑管线

参考NVIDIA官网资料:Life of a triangle - NVIDIA's logical pipeline

  • 1. 程序通过图形API发出DrawCall指令,指令会被推送到驱动程序,驱动会检查指令的合法性,然后会把指令放到GPU可以读取的Pushbuffer中,显然这个Pushbuffer在系统内存,GPU可以通过PCIe共享访问的;

  • 2. 经过一段时间或者显示调用flush之后,这些Command就会发送到GPU,GPU通过PCIe Host Interface接收,然后交由Front End处理;

  • 3. 图元分配器处理IBO,将组好的三角形批次分配给GPCs(之所以这里需要组好三角形,是因为VS执行完后需要给GPC里的光栅化引擎处理,下文提及);

  • 4. 在GPC中,每个SM中的Poly Morph Engine负责通过三角形索引(Vertex Fetch)取出三角形的数据(vertex data);

  • 5. 获取到顶点数据后,将其划分为32线程为一组的Warps,至此,SM得到了一组Warp(每个Warp对应32个顶点,交由SM里32个线程并行执行VS处理);

  • 6. SM里的Warp Scheduler负责调度Warp以lock-step运行该Warp里绑定的Shader的每条指令;

  • 7. 这些指令的执行有些是立马完成的,有些不是,比如读取纹理;

  • 8. 为了解决7的延迟问题,通过切换Warp来提高core利用率。比如当前指令在读取纹理,那么SM就切换另外一个Warp来执行,只要有足够多的Warp来填满该延迟,那么该SM的利用率就高;

  • 9. 完成Vertex-Shader后,通过L1、L2缓存传递数据,将结果交由Viewport Transform处理,进行齐次裁剪空间下对超出范围的三角形裁剪,等待下一步光栅化;

  • 10. 此时得到的结果将离开当前GPC,然后根据三角形的包围盒所占屏幕Tile,通过Work Distribution Crossbar重新分配给1个或多个GPC;

  • 11. SM里的PolyMorph Engine获取到数据,Attribute Setup将处理需要插值的数值(比如在VS中输出的各种结果),以提供给后续PS使用;

  • 12. Raster Engine光栅化,进行背面剔除,z-cull,early-z;

  • 13. 再次组成一个个Warp,此时的Warp是以8个2x2的像素块,共32像素对应32线程的形式划分。2x2的像素块的形式有助于计算相邻像素之间的求导,比如通过变化率得知mip级数;

  • 14. 和VS阶段一样,通过Warp Scheduler调度Warps,以lock-step方式执行每个Warp,直到完成PixelShader;

  • 15. ROP (render output unit)渲染输出单元将进行深度测试,将结果blend到RT/framebuffer;这些取值测试、写入都是原子操作,结果的输出也会压缩以优化带宽消耗。

3.1、小结

从图形API调用到GPU端,从GPC分配到拉取顶点数据,进行SM的Warp调度进行VertexShader并行执行。然后继续由Viewport Transform到Raster Engine,再次进行GPC分配,并行执行PixelShader。其中最大的特性是SM通过Warp调度器对SM的Warps列表,每次派发一个Warp。每个Warp的大小为32,刚好对应SM 32个线程。所以该SM内如果是高效执行的话,应该每时每刻都会有一个Warp里32任务,正在被SM的32个线程并发执行。SM的架构细节在下小节会进行更新详细的分析。

4、GPU架构(下)

先大概看一下SM包含的模块,熟悉一下:

  1. Instruction Cache:指令缓存,缓存了该SM里Warps的指令;

  2. Warp Scheduler:线程束调度器;

  3. Dispatch Unit:指令分发器,根据Warp Scheduler的调度向核心发送该Warp的指令;

  4. Register File:寄存器,编译好的机器码如ADD r1 r2 r3,这些r开头的就是一个个寄存器,给Core提供计算参数或者存储输出结果,上图的SM中有3万多个32bit的寄存器,Warp中每个任务都会分配私有的寄存器;

  5. Core:计算核心,负责浮点数和整数的计算;

  6. SFU:Special Function Units,执行特殊数学计算(sin、cos、log等);

  7. LD/ST:Load/Store,访存单元,加载和存储数据;

  8. L1 Cache:一级缓存,片上内存,即该内存是位于芯片内部的,速度很快;

  9. Shared Memory:共享内存,片上内存;

  10. Tex与Texture Cache:纹理单元用于采样纹理,纹理缓存;

  11. PolyMorph Engine:多边形引擎,用于处理顶点数据拉取、Viewport Transform等。

前文提到GPU相比于CPU不同的地方,是各种控制器分支预测的精简,这里将体现出来:

SIMD

单指令多数据,该功能对应于Core模块。在Shader里,我们经常处理矩阵、Position、Color,Vector等多维数据。如果以CPU指令执行需要每个维度都执行一次该指令,但是GPU提供了SMID,如SMID_ADD,它只需要执行一次指令就能计算完整个向量。

SIMT

单指令多线程,该功能对应SM的Warp Scheduler、Dispatch Unit模块。SM存在着众多的Warp,而Warp是逻辑层面上的概念。每个Warp里面都含有32个任务,它们对应同一个Shader,并且是同步执行的。这样的好处是Warp Scheduler只需要对Warp进行调度,一条指令的派发就能驱动Warp对应的32个线程运行起来。

co-issue

可以将1维和3维数据、2维和2维数据合并成4维,这样就可以以SIMD形式运算。

Shader指令执行机制

SM的特点:。

SM可以接收多个任务块(也称为线程块Block或者Work Group),每个任务块绑定一个Shader,然后SM会将任务块以32大小划分为Warp。SM管理多个Warp,然后在适当的时候调度某个Warp来执行。此时,SM以SIMT形式驱动计算单元以lock-step方式执行该Warp的指令。

举个例子,渲染一个模型,首先会分成多个任务块,假设其中一个任务块42个三角形,42*3=126个顶点,将这个任务块分配给某个SM之后,这个SM会以32为一组划分为Warp,即126/32=4个Warp,这4个Warp对应相同Shader,Warp内的32个顶点执行是同步的,而Warp与Warp之间虽然Shader是一样的,但执行的进度是独立的。除非在代码中,我们主动调用GroupMemoryBarrierWithGroupSync()来强制任务块内的线程同步。这时候,这4个Warp就会强制同步到同一个代码点,但这种操作对性能的影响比较大。

1、Warp Scheduler的任务就是协调众多的Warp去执行,这样有什么好处呢?

如上图的Warp1,当它在执行时遇到不能马上执行完的指令(比如纹理采样,或者需要到GlobalMemory加载数据等),SM为了不让Core空闲(Stall),会切换Warp2、Warp3、Warp...n来执行。所以只要Warp够多,那么总能找到需要处理的任务,来填补Warp1的空挡时间。这里的术语叫 延迟隐藏(Latency Hiding)。从GlobalMemory的访存有几百个周期延迟,所以要有足够多的Warp才能保证延迟隐藏的效果。Warp的数量受限如下图:

  • SM的CTA(Block) 任务块上限;

  • SM的Warp slots上限;

  • SM总线程数上限;

  • SM寄存器上限;

  • Shared Memory上限;

  • CTA(Block) 的Dimension也会间接影响;


2、32线程同步执行Warp的32个任务,优缺点

优点:管理上分而治之,以32为一组;适合天然需要并行处理的任务,比如图像渲染,存在大量的顶点和像素,而这些顶点和像素的处理过程是一样的,即Shader一样。

缺点是分支处理,如下图:

上图列举8个线程同步执行的情况,当该Warp存在if-else分支时,执行底层会有个变量exec数组,存储这每个线程当前的mask状态,如果分支为false不执行,那么就会标记该线程masked out,但仍旧需要跟着其他为true的线程同步“执行”,浪费时间。最差的情况就是32个线程中,只有一个为true,但是其他31个都需要同步等待,此时是利用率仅有1/32。同理,for循环的次数如果不一样也存在着相同的问题,当某些线程的循环次数较少,或者提前break,即使已经完成了也需要等待其他线程。

GPU存储器

上文提到SM的Warp数量关系到延迟隐藏的效果,而Warp的数量又受限于寄存器、SharedMemory等因素,所以我们有必要学习一下GPU的存储器。

  • 分离式架构:CPU与GPU都有单独的内存和缓存,通过PCIe通信。

  • 耦合式架构:GPU共享系统内存和缓存,通俗讲就是我们说的集显,常用于手机,所以手游更考虑性能更考虑带宽消耗,所以移动端使用tbdr来渲染,充分利用片上内存来降低带宽消耗。

分离式架构中GPU存储管理:

因为速度快的缓存/寄存器很贵,而显存的速度,相比于核心的运算速度相差了几百个数量级(比如计算只需要1个时钟,加载数据却需要400个时钟,严重拖后腿行为了),所以需要一套分等级的缓存体系。以下是GPU存储器的访问速度:

存储类型寄存器(几万个/SM)共享内存L1缓存L2缓存纹理、常量(显存)全局内存(显存)访问周期11~321~3232~64400~600400~600

1、寄存器是最快的,我们写Shader,定义的变量、数组、结构体就是存放于此。每个SM都有自己一定数量的寄存器,SM为每个Warp分配其需要的私有的寄存器。也正是因为每线程私有寄存器,所以GPU的Warp线程切换是0成本的,不像CPU有额外的线程上下文切换的消耗。假设一共就1000个寄存器,一个Shader使用10个寄存器,那么最多可以分配给1000/(32*10)=3个Warp;但如果一个Shader使用了11个寄存器,那么最多1000/(32*11)=2个Warp。所以说寄存器数量能够影响该SM的Warp数量上限。

但是如果实在不够寄存器分配了,那么还会有LocalMemory,将溢出的寄存器存储到LocalMemory,而其本质为全局内存,延迟很高,使用L2缓存。

2、共享内存:SM内所有Warp共享的内存,一般可用于频繁操作的数据临时存放,处理完成后再写入到GlobalMemory。我们在Shader里定义如:groupshared uint MaxLuminance;

3、纹理与常量都有其对应的纹理、常量缓存,典型的是纹理周围的数据也许不是内存连续的,但会以周围像素进行缓存。


总结

结合GPU架构与其逻辑管线,分析了SM的执行流程。SM是GPU执行器的核心部件,负责执行Shader程序。在SM中,Shader以lock-step方式并行执行,通过大量Warp切换来隐藏指令的延迟和提高GPU的效率。后续文章将使用相关debugger工具进行Shader Profiler,分析Shader指令延迟(Stall)的原因,进一步优化Shader性能。另外,了解CUDA编程非常有助于理解。



参考:

https://www.cnblogs.com/timlly/p/11471507.html

Life of a triangle - NVIDIA's logical pipeline

编辑于 2023-05-12 13:40・IP 属地广东


gpu渲染



渲染



shader


赞同 172


分享

发布一条带图评论吧


2 条评论

默认

最新

cyberlink

讲的不错

05-31 · IP 属地陕西

回复喜欢

link

能否介绍下d3d和opengl到显卡驱动的封装的过程等内容呢?这样好和程序编译串起来

05-20 · IP 属地上海


【知乎】【Shader篇】GPU架构与其逻辑管线的评论 (共 条)

分享到微博请遵守国家法律