深入解析GPU图形流水线:从最小计算单位到一帧画面的诞生

发布时间:2026/9/7 20:35:30
深入解析GPU图形流水线:从最小计算单位到一帧画面的诞生 说实话我接触GPU也有不少年头了。最早是打游戏追求高帧率后来做实时渲染研究再后来身边搞AI的朋友天天在服务器上把GPU Load拉到百分之百可你问他显卡里那一大坨芯片到底是怎么干活的它凭什么一秒钟能算出几千万个像素的颜色大部分人能答上来的也就是并行嘛很多核嘛这六个字。这也正常。GPU对绝大多数人来说就是个黑盒子你喂它顶点数据、纹理参数它吐给你一帧画面过程快到你根本来不及想。但如果你真的钻进去会发现这里面的设计思路和CPU完全是两个物种。这篇文章我打算用系列的形式把GPU芯片内部的图形计算这件事掰开揉碎讲清楚。第一篇我们先解决两个最基础的问题GPU里最小的计算单位到底是什么以及一帧画面从CPU下发指令到最终显示到屏幕上中间到底走了怎样一条流水线。1. 渲染一帧画面有多重GPU为什么被设计成现在这样1.1 一次画面渲染到底要算多少东西很多人低估了实时图形渲染的计算量。咱们先算一笔账一块普通的1080p屏幕分辨率是1920乘1080大约207万个像素。每个像素在屏幕上显示出来之前要经过至少一次像素着色器计算而像素着色器内部要做纹理采样、光照计算、颜色混合有些复杂的PBR材质甚至要做多次采样和几十次浮点运算。207万乘以几十次这是单帧的像素阶段计算量。再加上场景里的模型顶点一个中等规模的角色模型可能有10万到几十万个三角形每个三角形都有顶点顶点要经过变换、裁剪、光照计算。一个战斗大场景几百个模型下来上千万个三角形一点都不夸张。这还只是一帧。显示器一秒要刷新60次也就是一秒钟要重复上面这个流程60遍。你想想这个数字有多吓人每秒需要完成几亿到几十亿次图形相关的浮点运算。CPU那颗核只有几个到几十个核心主频再高也扛不住这种吞吐量。这决定了GPU必须走一条和CPU截然不同的路用巨量的小核心并行堆算力而不是用少数几个大核心硬卷单线程性能。我用一个生活化的类比CPU像是一个做饭特别快的厨师一个人能同时炒三个菜还能兼顾摆盘GPU则像是一整个快餐连锁后厨几百个厨子分工明确每人只负责自己那一小份但单位时间内出餐的总量能吊打任何单兵。图形计算恰好就是这种可以被切成无数独立小任务的工作天生适合后者。1.2 图形专用架构的三块基石聊到GPU芯片设计绕不开三个基础模块执行浮点运算的ALU阵列、海量显存带宽、以及一堆固定功能单元。ALU阵列就是那一大排做加法和乘法的算术单元。CPU里有算术逻辑单元但GPU里这种单元的密度要高得多。芯片面积就这么大晶体管数量也就这么些NVIDIA和AMD的设计哲学是明确偏科把绝大部分晶体管花在ALU上而不是花在分支预测、乱序执行这些让单个线程跑得更快的东西上。因为在图形渲染里你不需要单个任务跑得飞快你需要的是总吞吐量足够大。并且图形渲染每个像素的计算彼此独立像素A算成什么样完全不影响像素B这就是传说中的数据并行。ALU阵列正好批量处理这种任务每个ALU负责一个像素或一个顶点的计算一颗GPU里几千上万个ALU同时开工画面就唰地一下出来了。第二个基石是显存带宽。GPU的算力非常恐怖但算力再高数据喂不进去也是白搭。像素着色器每算一个像素都要去采样纹理纹理数据存在显存里显存和计算单元之间的通路越宽数据搬运就越快。所以你看现在游戏显卡的显存位宽动辄256bit、384bit显存频率也越来越高都是为了确保纹理数据能大量、快速地送到计算单元嘴边。用大白话说ALU是灶台显存带宽是送菜通道通道窄了再多的灶台也得空等。第三个基石固定功能单元。GPU芯片里不全是能执行任意指令的通用计算单元还有一大块专用电路。比如光栅化阶段要判断一个三角形覆盖了哪些像素这种操作几乎是每帧每帧重复的没必要用通用ALU慢慢算直接做成专用硬件电路输入三角形顶点数据输出覆盖的像素列表速度和功耗都更优。纹理采样也有专门的纹理单元深度测试有专门的深度测试硬件。这些固定功能单元加上可编程的着色器阵列一起构成了我们常说的图形流水线。这三个模块各司其职就是一个GPU做图形计算的基本盘。接下来我们钻到芯片里面看看那个最小的计算单位到底长什么样。2. 芯片里的最小计算单位从线程到SM的层层拆解2.1 线程不是你想的那个线程是硬件线程你在GPU上写一个像素着色器每个像素执行一次一个1080p的画面就有207万个像素任务。这些任务在GPU眼里就是一个个线程——注意这个线程和你在操作系统里听到的CPU线程完全是两码事。操作系统线程有栈、有上下文、切换起来要保存恢复一大堆状态开销很大。GPU线程则极其轻量基本上就是一组寄存器和一条指令指针芯片里同时驻留着几万个这样的线程都毫无压力。GPU并不是每来一个线程就分配给一个ALU去执行。真正的执行粒度是线程束NVIDIA管它叫warp一个warp固定包含32个线程。AMD那边也有类似概念叫wave32或wave64意思是32或64个线程捆成一束。这里有个关键设计GPU执行指令时一个warp里的32个线程是一起执行同一条机器指令的。什么意思呢比如像素着色器里有一行采样纹理那么这32个线程会同时去执行采样纹理这条指令只是每个线程操作自己那份数据。32个人拿着同一本菜谱做菜只是每人手里的食材不同。这种模式叫SIMT单指令多线程。为什么偏偏是32而不是8或者64这是NVIDIA在指令宽度、调度延迟、寄存器压力之间权衡很久的结果。32个线程执行一条指令硬件正好把它做成一个SIMD宽度为32的指令来处理效率高同时32个线程需要的寄存器数量也在合理范围内。你不需要记住每个细节但需要理解一点GPU调度和派发工作的最小单位是warp而warp里的单个线程才是逻辑上最小的执行单位。2.2 SMGPU里的小芯片真正的计算工厂如果把线程比作零件那么真正组装零件的车间就是流式多处理器SMStreaming Multiprocessor。NVIDIA的GPU核心从图灵到Ada Lovelace架构一颗芯片里集成着几十到上百个SM。每个SM内部都有自己的ALU阵列、寄存器文件、共享内存、纹理单元、光栅处理单元、warp调度器。用一个具体的芯片举例RTX 4090用的是AD102核心里面有128个SM每个SM包含128个FP32浮点计算单元加起来就是16384个核心。市面上宣传的多少多少CUDA核心就是这么来的本质上是SM内的浮点ALU总数。SM的内部结构很有意思。每个SM分成几个子分区最常见是4个每个子分区都有自己独立的调度器。调度器负责给warp发指令这个warp的指令准备好了就给ALU阵列发出去执行下一个warp再跟上。因为ALU数量远少于驻留的线程数所以每个warp都是排队等着被调度执行的。SM内还配了不小的寄存器文件和共享内存前者负责给线程暂存计算结果后者是同一SM内线程协作时快速交换数据的地方。你可能会问这么多SM拼在一起就是一颗GPU那它们的任务怎么分配这就靠前面提到的那套固定功能单元和前端的任务分发硬件。图形渲染时GPU前端Graphics Frontend接收到CPU下发的绘制命令把顶点和像素的分块任务分配给各个SM去处理。所以SM的个数、每个SM的ALU数量直接决定了这颗GPU吞吐量的上限。2.3 为什么要塞那么多线程在芯片里隐藏延迟是核心有个问题不知道你注意到没有计算单元的数量是几千个但芯片里驻留的线程通常有几万甚至几十万个为什么非要多出来这么多排队的答案都在延迟隐藏这四个字上。显存的延迟是很高的一次内存访问可能要几百纳秒。在这几百纳秒里ALU如果干等着数据回来那就纯属浪费。而GPU的调度器有一个绝活一个warp在等显存数据时调度器立刻切换到另一个指令已经准备好的warp去执行切换开销接近于零因为无非就是换一组寄存器读取。你可以把这个过程想象成点菜。一个顾客warp点了菜在等菜的时候服务员调度器不会干站着而是立刻去服务另一个已经想好吃什么的顾客。只要餐厅里同时坐着足够多的顾客服务员和厨师就能一直保持忙碌。GPU中同时坐在餐厅里的顾客的数量就是所谓的occupancy占用率。占用率越高ALU越不容易因为等数据而空转。这也是为什么GPU的线程数一定要远超核心数多出来的部分不是为了并发而是为了填坑。理解了SM、warp和延迟隐藏你就知道一颗GPU的计算核心部分大概是怎么回事了。但是光有这些计算单元还不足以画出画面它还需要一整条配合默契的生产线来把顶点一步步变成像素。这就是下一节要说的完整图形流水线。3. 完整图形流水线一帧画面从指令到像素的旅程3.1 第一步CPU怎么把画什么交给GPU图形渲染并不是GPU自己拍脑袋决定的一切源于CPU上跑的游戏或应用。程序通过图形API——DirectX 12也好Vulkan也好往驱动里提交一组绘图命令每条命令就是我要用一个什么样的着色器、绑定哪些资源、画哪些顶点。驱动会把这些命令翻译成GPU能懂的格式放进一块命令缓冲区最后通过一个提交指令推送给GPU前端的命令处理器。命令处理器Command Processor / Frontend收到命令后会把不同阶段的子任务喂给不同的硬件单元。这里有个点值得多说一句CPU和GPU之间是异步的CPU提交命令不等GPU执行完就继续往下准备下一帧了这也是帧延迟的产生原因之一。你在游戏里调低画质帧数变高但操作手感总觉得有点肉就跟这条命令提交队列的流水长度有关系。对GPU来说接下来真正干活的起点是输入装配器Input Assembler。它根据绘图命令里的描述从显存中取出顶点缓冲、索引缓冲的数据把它们装配成一个一个的三角形准备交给下一个阶段。3.2 顶点阶段让每个三角形站到该站的位置三角形装配完顶点就要进可编程的顶点着色器Vertex Shader了。这是图形流水线里第一个可编程阶段也是最近接几何层面的一步。顶点着色器干的事情非常明确把模型的本地坐标变换到世界坐标、相机坐标最后投影到屏幕坐标。这个过程中还会做顶点颜色计算、法线变换如果是骨骼动画还会根据骨骼权重把顶点蒙皮到骨骼上。每个顶点都被当作一个独立线程来处理所以大量顶点的变换完全是并行的。我曾经做过一个场景里面有一万多棵树每棵树几千个三角形如果没有GPU把顶点变换并行掉CPU算一帧能卡到怀疑人生。顶点着色器输出的是经过变换的顶点坐标和一系列附加属性颜色、法线、纹理坐标。紧接着固定功能的图元装配和裁剪阶段会接手把顶点按拓扑结构连成三角形再判断哪些三角形完全在屏幕可见范围之外直接裁掉省得后面白算。这里有个值得一提的可选阶段曲面细分和几何着色器。曲面细分用于把一个粗糙的低模网格细分成高密度网格让轮廓更圆润几何着色器则可以对整个三角形做增删改比如把线段扩展成草地叶片。这两个阶段在现代引擎里用得越来越少了主要原因在于它们和后续的光栅化耦合太紧灵活性差调度开销也不小。现在大家更愿意在计算着色器里自己搞一套或者直接改用后面的Mesh Shader那一套任务网格方案。但理解基础流水线时知道这些阶段存在即可。3.3 光栅化三角形是怎么碎成像素的顶点阶段结束时一个三角形的三个顶点已经都在屏幕坐标空间里了。但屏幕上显示的是一块块像素不是一个漂亮的线框三角形。这中间最关键的一步就是光栅化Rasterization把连续几何的三角形离散成屏幕上一颗颗被它覆盖的像素。很多初学者会以为光栅化就是找到三角形内的像素填个色但工程实现上这步极其讲究效率。传统做法是扫描线算法一行一行地计算三角形覆盖的边界现代GPU则偏好用边函数Edge Function做覆盖测试。简单理解就是对每一颗像素用三角形三条边的方向判断它在三角形内部还是外部三边内外判断都通过这颗像素就算被覆盖了。硬件里专门有一套三角形设置Triangle Setup和三角形遍历Triangle Traversal电路来做这件事效率极高。光栅化阶段还会顺带做属性的重心坐标插值——顶点有着色需要的纹理坐标、法线、深度值但三角形内部的像素并没有直接对应的数值怎么办用重心坐标把三个顶点的属性按比例插值出来。这就是为什么一个三角形的颜色可以是渐变过渡的。而这些插值出来的数值会作为输入交给下一步的像素着色器。3.4 像素阶段每一颗像素的上色时刻像素着色器Pixel Shader / Fragment Shader是整个流水线里工作量最大的阶段。每个被光栅化的像素都要执行一遍像素着色器里的代码从纹理里采样颜色结合光照方向算漫反射、高光再做阴影、环境遮挡、反射折射等各种效果。算到这一步像素才真正拥有最终显示在屏幕上的颜色。像素着色器的计算量和着色器里纹理采样、ALU指令的数量直接相关。我之前做过一个水面渲染效果每个像素要做三次反射采样、一次折射采样、四次深度相关的噪声采样再加上一堆PBR计算同分辨率下帧率和简单光照场景能差出一倍还多。但像素着色器有个很关键的优化你没注意到它未必会在每一个像素上都执行一次。硬件在做像素着色前会先做一次提前深度测试Early-Z。如果当前像素的深度值比深度缓冲里已有的值更靠后说明它已经被前面的物体挡住了那后面的像素着色器根本不需要执行直接丢弃。这一步能省下巨大的计算量尤其是渲染复杂场景时很多时候场景里大部分像素是被遮挡的。早期显卡没有这个机制像素着色器哪怕算完也会被覆盖白白浪费性能。你现在看一些渲染性能分析报告里讲overdraw过绘制指的就是这个阶段没能提前剔除掉的无用着色计算。3.5 输出合并与帧缓冲像素的出厂质检和装车像素着色器算出的颜色并不是直接写到屏幕上的。GPU会把计算结果送到输出合并单元ROPRender Output Unit在这里做最后一轮固定功能处理深度/模板测试、透明度混合、多重采样抗锯齿的解析等。这些操作没有可编程的灵活性但其专用电路的处理速度非常快。在这个阶段总免不了两个概念缓冲和同步。画面渲染不会直接画到显示器正在扫描的帧缓冲上而是画到一个后台缓冲画完后再把前后台缓冲交换一下双缓冲。如果开了垂直同步交换动作要等显示器刷新信号对齐避免画面撕裂。如果帧率不够垂直同步会把显示帧率钉在30FPS——这个很多人应该都遇到过画面不撕裂了但流畅度掉一半就是这几块缓冲之间同步关系决定的。到这一步一帧画面的像素数据经过顶点变换、光栅化、像素着色、输出合并终于写进了可以提交给显示器的帧缓冲里。显示器每隔一个刷新周期把这块缓冲读出来就是你屏幕上看到的那一帧流畅画面。4. 光栅化到底是怎么扫出像素的一个简化示例4.1 边函数判断三角形覆盖的数学原理上面说了光栅化的概念可能还有人觉得不过瘾。我就拿像素覆盖检测这一个细节展开讲讲这也是很多引擎开发新手最容易忽视的数学细节。想象屏幕上有一个三角形三个顶点分别是v0、v1、v2。我们要判断某个像素点p是否落在三角形内部。数学上有个很优雅的办法计算p相对于三角形三条边的有向距离也就是用叉积判断点在边的哪一侧。对边v0到v1来说p的边函数值可以简化成edge(p) (p.x - v0.x) * (v1.y - v0.y) - (p.y - v0.y) * (v1.x - v0.x)如果edge(p) 0说明p在这条边的某一侧具体哪一侧取决于三角形顶点是顺时针还是逆时针排列以及约定。对三条边都做一次判断如果三次结果都符合约定p就在三角形内部。实际硬件做这事的时候会聪明得多。光栅化器会先计算出三角形外接矩形只在这个小范围内遍历像素再对每个像素做三边判断。而且因为边函数是线性函数它还能用增量算法加速检查完左边像素再检查右边相邻像素时只需要做一次加法而不是重新做一遍完整的叉积计算。这些细节听着琐碎但GPU每秒钟要处理几千万个三角形任何一步省下来的功耗和晶体管在几十亿次重复下都是惊人的收益。4.2 穷举判断看起来笨为什么反而是最优解你可能想问为什么要用这种遍历包围盒内每个像素做判断的笨办法而不是用扫描线那种几何计算更少的方式早期显卡确实用扫描线但后来硬件光栅化主流转到了基于edge function的遍历法。原因很实际edge function遍历法的硬件实现非常规整、方便并行。处理一个bbox里的所有像素完全不需要考虑三角形的几何形状只需要做一个又一个独立的、互不依赖的判断。这让光栅化器可以被拆成无数个小处理单元并行跑32颗像素并行判断和1颗像素顺序判断的吞吐量天差地别。扫描线算法则需要大量串行的边界计算并行度上不去。所以在图形渲染这条路上你会发现一个贯穿始终的思想很多看似浪费算力的做法恰恰因为简单、规整、可并行而成为硬件的最优解。宁可多算几颗不在三角形里的像素也要保证ALU始终在满负荷工作。这个思路从光栅化一路延伸到整颗GPU的设计哲学理解了它你再看渲染器为什么喜欢把任务切成一大块一大块均匀分配给所有SM就会觉得很自然。5. 实操者视角怎么判断GPU在流水线哪一步卡住了5.1 看远不止GPU Load百分之多少这么简单写完了原理说点实操层面的东西。很多人拿到一台机器看性能第一反应是开任务管理器看GPU占用率但那个数字很骗人。GPU Load高只说明某个引擎在忙不代表它忙对了地方GPU Load低也不代表显卡不行有可能是整个图形流水线在某个更早的环节被卡住了。我自己做性能排查时常用的工具组合是这些GPU-Z看传感器里的GPU Load、显存占用、显存带宽占用比率、温度功耗。重点是显存带宽占用这个指标如果带宽占用很高而GPU Load没那么高基本能断定是带宽受限而不是计算受限。nvidia-smi被动看时间的工具适合在AI推理训练场景里快速确认显存、算力占用但对图形性能分析帮助有限它看不到每个渲染阶段的具体时间。RenderDoc / Nsight Graphics这个级别是真正做图形调试的工具。RenderDoc可以抓取一帧画面逐Draw Call查看每个阶段的顶点输入、着色器执行、输出颜色能直观看到哪些像素被Early-Z剔除了哪个Draw Call的像素着色器耗时最长。Nsight Graphics则有更详细的硬件计数器能看每一渲染阶段的cycle级时间分布。5.2 降低分辨率帧率没变——这是经典瓶颈信号性能分析里最经典的定位法就是控制变量法。你怀疑像素着色器是瓶颈那就把分辨率从4K降到1080p。如果帧率几乎不变说明你的瓶颈根本不在像素阶段——大概率是顶点阶段或者CPU侧的Draw Call提交卡住了。如果帧率大幅上涨那就是像素阶段或者填充率、带宽受限因为分辨率变化直接影响的是像素数量和纹理采样量。另一个我常用的手段是单独关掉某个后处理效果。比如关掉动态模糊或环境光遮蔽帧率回升巨大那就锁定了这个效果对应的shader是整个管线的大头。再去看这个shader对应的纹理采样次数和ALU指令数就能判断是该优化算法还是该减少采样量。很多人一碰到GPU性能问题就想着换显卡其实很多时候你只要把某个shader里的半精度浮点换回全精度或者把几个纹理采样合并一下帧率就能救回来。5.3 别被GPU Load 99%骗了帧生成时间才靠谱还有一个常见的坑就是只看平均帧率不看帧生成时间曲线。你可以在游戏里打开帧时间显示或者用FrameView、PresentMon这类工具记录每一帧的实际生成时间。如果帧生成时间曲线是锯齿状尖峰忽高忽低说明周期性卡顿往往是着色器编译、资源加载、或者GPU任务队列里某一步出现了瞬间过载。这种情况平均帧率甚至能到100帧但玩起来一卡一卡的非常劝退。遇到这种问题我一般先看一眼是不是shader编译卡顿尤其是用DirectX 12和Vulkan的游戏驱动会在首次遇到某类着色器时才去编译导致场景切换瞬间掉帧。很多新关卡一进去卡一下的体验就是这个问题。解决方案要么是让驱动提前做管线缓存预编译要么在加载界面里强制引导编译。这些细节比盲目换硬件更值得优先排查。6. 常见问题速查图形流水线高频坑一览下面这个表是我在实际项目里比较高频遇到的症状和排查方向列出来给各位抄作业用症状最可能的瓶颈阶段优先排查的方向分辨率调高后帧率明显掉像素着色器/填充率/带宽看显存带宽占用是否接近上限降低抗锯齿等级分辨率怎么改帧率都没太大变化顶点阶段或CPU提交环节监控CPU单核占用减少Draw Call考虑合批GPU Load显示99%但画面还是卡流水线内某阶段长期饱和用Nsight Graphics按阶段看耗时占比定位具体环节帧生成时间曲线锯齿严重着色器编译/资源流加载开启管线缓存预编译检查是否有异步资源加载缺失显存占用飙高但频率下降显存溢出与数据换页降低纹理压缩格式检查是否有资源重复绑定在显存里开了垂直同步依然画面撕裂缓冲交换与刷新率未对齐确认是否开启了混合帧率缓冲模式尝试三缓冲配合G-Sync/FreeSync再补充一个我用得比较多的经验如果你发现某个场景帧率奇低但GPU Load反而正常多半是场景里某个后期特效触发了深度缓冲的额外读写路径。深度缓冲是整条流水线里被多个阶段反复读写的公共资源一旦某个特效需要把深度缓冲当纹理来用它往往要先把整块深度复制一份这个拷贝开销经常被低估。这类问题在RenderDoc里非常好观察打开资源视图看看深度缓冲有没有被额外绑定到着色器就能确认。还有一点PC端各种奇葩环境带来的问题也很多。比如我遇到过一台机器显卡驱动更新到最新版之后反而出现周期性掉帧最后回退到两个版本之前的驱动才恢复正常。做图形相关的工作或调试驱动版本的选择是个很玄学的事情并不总是越新越好。我的习惯是锁定一个经过验证的稳定版本除非新驱动有明确的功能性更新针对特定游戏的优化、修复某个Bug否则不轻易动它。7. 结尾这还只是第一层把最小计算单位和图形流水线串起来看你会发现GPU的图形计算其实是一套非常讲究的分工流水线前端接收命令SM里的warp负责顶点和像素的可编程计算固定功能单元负责光栅化、深度测试、输出合并最后把像素数据写进帧缓冲。所有环节各司其职环环相扣哪一环慢了整条流水线就会在那里等着而整颗芯片的每一个SM又被设计了极高的占用率来尽量填满每一刻的等待空隙。从我个人做性能分析的经验来看理解这套流水线最大的价值是它给了你一个排查问题的坐标系当帧率出问题时你能大致判断问题发生在哪个环节再对症下药。这一篇把大框架铺开了但GPU内部还有很多非常值得钻的细节比如warp调度是如何影响执行效率的、显存带宽到底怎么计算和优化、现代GPU里的Mesh Shader又怎么改变了这条传统流水线。这些内容篇幅都不短下一篇我会继续挑几个方向往里挖如果你在实操中有遇到过跟这篇文章相关的问题也欢迎一起交流。