Base-GPU硬件架构基础
记录一下GPU硬件里ALU、SIMD、Warp/Wave和half精度这些概念的关系。
平时看GPU参数会看到很多名词:多少个ALU、多少个Shader Core、SIMD宽度、Warp、Wave32、FP16双倍性能。这些东西属于不同层级,如果混在一起就很容易得到错误的结论。
先用一句话理解:
ALU:真正执行数学指令的电路。
SIMD/SIMT:让一条指令同时驱动多份数据的执行方式。
Warp/Wave:GPU调度和执行的一组Shader线程。
SM/CU/Shader Core:包含ALU、寄存器、调度器、缓存等资源的更大计算单元。
1.GPU为什么需要大量并行?
渲染一帧时,同一段Shader会处理大量顶点和像素。
比如一个Fragment Shader需要对200万个像素做相似的计算:
采样纹理
计算法线
计算光照
输出颜色
GPU不会像CPU一样重点优化单个线程的复杂控制流,而是安排很多线程同时处理不同数据。只要工作足够多,就可以用高吞吐量隐藏单条指令和内存访问的延迟。
2.ALU是什么?
ALU(Arithmetic Logic Unit,算术逻辑单元)是物理上执行运算的电路,可以理解为GPU里的最小计算执行单元之一。
常见运算包括:
加法、减法、乘法、乘加。
与、或、异或、移位、比较。
Min、Max、Abs等简单数学操作。
但GPU里并不是只有一种万能ALU。不同架构可能还有:
FP32/FP16浮点单元。
INT32整数单元。
SFU特殊函数单元,处理倒数、平方根、三角函数等。
Tensor/Matrix单元,处理矩阵运算。
Load/Store单元,负责内存读写。
Texture Unit,负责纹理寻址、过滤和采样。
所以Shader里的每条指令不一定使用同一种硬件单元。
ALU数量是否直接决定GPU性能?
ALU数量会影响理论计算能力,但不能单独决定GPU性能。
理论计算吞吐大致还要考虑:
ALU数量
每周期可以完成的运算数
GPU频率
指令类型和精度
实际性能还会受到:
ALU利用率
寄存器数量和Occupancy
Cache与显存带宽
纹理采样能力
分支发散
指令依赖和内存延迟
调度器能否持续提供可执行线程
两个GPU即使ALU数量相同,也可能因为架构、频率、缓存和带宽不同而性能差很多。
更准确的说法是:ALU数量决定理论计算能力的一部分,能不能把这些ALU喂满才决定实际吞吐量。
3.SIMD是什么?
SIMD(Single Instruction Multiple Data,单指令多数据)表示一条指令同时处理多份数据。
例如有4个像素需要执行相同加法:
1 | |
标量执行可以理解为一条一条计算。4-Lane SIMD可以用一条指令同时驱动4条Lane:
1 | |
SIMD并不是某个独立零件的固定名字,更像ALU的组织和执行方式。一个SIMD执行单元内部有多条Lane,每条Lane处理一个线程当前的数据。
SIMD宽度
SIMD宽度通常表示一条指令能同时覆盖多少条数据Lane。
为了方便理解,可以假设有一个128bit的执行数据通路:
4 x 32bit float
8 x 16bit half
这个模型可以帮助理解为什么原生FP16硬件有机会在同样宽度里处理更多数据。
但这不是所有GPU都真实采用的结构。GPU可能使用分周期执行、双发射、Packed Math、独立FP16管线等不同实现,不能看到“128bit”就直接推导所有指令的吞吐量。
4.SIMD和SIMT的区别
SIMD通常从硬件指令角度描述“一条指令处理多个数据”。
GPU编程更常用SIMT(Single Instruction Multiple Threads,单指令多线程)理解:
每个Shader线程有自己的寄存器和数据。
多个线程组成一个执行组。
执行组内线程共享当前指令流。
硬件再用SIMD Lane执行这些线程。
对Shader开发者来说,代码看起来是每个线程独立执行:
1 | |
实际硬件会把多个线程的这条乘法组织起来并行执行。
可以简单理解为:
SIMT是开发者看到的线程模型。
SIMD是底层硬件执行这些线程的一种方式。
5.标量ALU和矢量ALU
一些GPU架构会区分Scalar和Vector执行资源。
Vector ALU处理每个Lane不同的数据:
1 | |
每个像素的input不同,需要每条Lane分别计算。
Scalar ALU适合执行整个Wave都相同的值:
1 | |
如果两个变量在一次DrawCall里不变,没必要让Wave里每个线程重复计算相同结果。支持Scalar执行的架构可以计算一次,再广播给整个Wave。
“一个SIMD里有几个Vector ALU、几个Scalar ALU”是具体架构的设计,不能当作所有GPU的通用结构。写Shader时更重要的是区分:
Uniform数据:整个DrawCall或Wave一致。
Per-Lane数据:每个顶点、像素或线程不同。
6.Warp和Wave是什么?
Warp/Wave是GPU调度和执行的一组线程。
Nvidia称为Warp,现代Nvidia GPU一个Warp通常是32个线程。
AMD称为Wavefront或Wave,GCN常见Wave64,RDNA主要使用Wave32,也可以存在Wave64模式。
Vulkan和跨平台API通常使用Subgroup描述类似概念。
Mali、Adreno、Apple GPU也有对应的线程执行组,但宽度会随厂商、架构代际、Shader阶段和编译结果变化。不要在通用Shader代码里硬编码“Mali一定16线程”或“Adreno一定8宽”。需要Subgroup算法时,应该使用API能力查询和目标设备实测。
Warp大小和SIMD宽度是一个东西吗?
不一定。
Warp/Wave大小是逻辑执行组的线程数,SIMD宽度是物理执行管线单次覆盖的Lane数量。
假设:
一个Wave有16个线程。
物理SIMD宽度为4。
最简单的理解是,同一条Wave指令需要分4组覆盖所有线程,也就是至少经过4次Lane执行。
但是现代GPU有流水线、多组执行单元、双发射和不同指令延迟,不能简单认为所有指令都严格等于4个完整时钟周期。这个除法只适合理解“逻辑线程组可能大于物理执行宽度”。
7.SM、CU和Shader Core是什么?
比SIMD更高一层,是厂商定义的GPU计算核心:
Nvidia:SM(Streaming Multiprocessor)。
AMD:CU/WGP(Compute Unit/Work Group Processor)。
Arm:Shader Core。
Qualcomm、Apple也有自己的核心组织方式。
一个计算核心通常不只有ALU,还包含:
Warp/Wave调度器
SIMD执行单元
Scalar/Vector寄存器
Load/Store单元
Texture相关单元
Local/Shared Memory
L1 Cache
特殊函数单元
调度器每个周期从当前驻留的Warp/Wave里选择已经准备好的指令,送到对应执行单元。
当一个Wave在等待纹理或内存时,调度器可以切换到另一个Wave执行。这也是GPU用大量线程隐藏延迟的核心方式。
8.为什么ALU会填不满?
GPU优化目标之一就是让执行单元持续有有效工作,而不是让大量Lane或整个管线空闲。
1.工作量不足
如果只有很少的顶点、像素或Compute线程,无法生成足够多的Warp/Wave,GPU很多核心没有工作。
优化思路:
合并过小的Dispatch。
避免大量只有几个线程的小任务。
Compute Shader的Thread Group数量要足够覆盖GPU。
注意:最后一个Wave没有填满只是局部浪费。真正更常见的问题是整个Dispatch规模太小,或者有太多小Draw/Dispatch带来调度成本。
2.分支发散
同一个Warp/Wave里的线程进入不同分支,硬件需要使用Active Mask分别执行不同路径。
执行A分支时,B分支线程对应的Lane被屏蔽;执行B分支时相反。ALU虽然在发指令,但部分Lane没有产生有效结果。
详细原理见上一篇Shader分支文章。
3.指令依赖
1 | |
后面的指令依赖前面结果,单个线程无法同时执行这些指令。如果没有其他独立指令或其他Wave可以切换,执行管线就会等待。
GPU会通过以下方式隐藏延迟:
同一线程内寻找互不依赖的指令,也就是ILP。
切换到其他已经准备好的Warp/Wave,也就是TLP。
4.内存和纹理等待
ALU需要的数据还没从Cache、显存或Texture Unit返回,只能等待。
这类Shader不是ALU瓶颈,而是Memory/Texture Bound。继续减少几条乘加指令通常没有意义,应该优化数据格式、采样数量、访问连续性和Cache命中。
5.寄存器压力过高
每个Shader线程使用的临时变量越多,消耗的寄存器越多。同一个核心能同时驻留的Warp/Wave数量就可能下降,也就是Occupancy降低。
可切换的Wave少了,纹理和指令延迟更难隐藏。
所以复杂Shader即使ALU指令不算多,也可能因为寄存器压力而变慢。
9.吞吐量和延迟的区别
延迟(Latency):一条指令从发出到结果可用需要多久。
吞吐量(Throughput):单位时间内整个GPU能完成多少工作。
GPU并不要求每条指令延迟特别低,而是依靠大量线程和流水线保持高吞吐量。
例如纹理采样可能要等待很多周期,但只要有其他Wave可以执行,ALU仍然可以保持忙碌。只有当所有驻留Wave都在等待时,硬件才真正停顿。
10.half和float是什么?
half/FP16:通常是16bit浮点数。
float/FP32:通常是32bit浮点数。
FP16占用位数更少,但表示范围和精度也更低。
粗略理解:
FP16有效精度大约3位十进制小数,最大有限值约65504。
FP32有效精度大约7位十进制小数,范围远大于FP16。
具体误差还和数值大小有关。浮点数越大,相邻可表示数之间的距离越大。
为什么half可能更快?
在原生支持FP16的GPU上,half可能有这些收益:
1.同样执行资源每周期处理更多FP16运算,某些架构可达到FP32的2倍算术吞吐。
2.临时变量占用更少寄存器,可能提高Occupancy。
3.使用真实16bit存储格式时,内存和插值带宽更低。
4.Cache一次可以容纳更多数据。
但是这些收益不是任何GPU、任何指令都有。
half一定比float快2倍或4倍吗?
不一定。
“128bit一次处理8个half、4个float,所以算术2倍;half带宽又减半,所以整体4倍”这个推导不能直接成立。
原因是:
计算吞吐和带宽瓶颈通常不会简单相乘。
Shader可能只受其中一个瓶颈限制。
有些GPU会把half提升到FP32执行。
有些指令没有FP16双速率版本。
half变量不一定按16bit存进Buffer。
数据转换和Pack/Unpack本身也可能产生指令。
纹理格式、插值器和寄存器是否真正使用16bit取决于平台和编译结果。
比较合理的结论是:在原生FP16友好的移动GPU上,half可能同时改善算术吞吐、寄存器压力和带宽,但最终收益需要看具体瓶颈。
11.Unity里的half需要注意什么?
1 | |
Unity文档说明,half在部分高性能平台默认可能被编译成float。Unity 6可以通过Shader Precision Model影响跨平台精度映射。
另外,Shader里的half变量和内存里的16bit存储不是一回事。
例如:
Constant Buffer里的half可能仍按32bit大小和对齐存储。
Texture是否省带宽取决于R16F、RGBA16F、RGBA8、压缩纹理等真实格式。
StructuredBuffer是否省带宽取决于数据结构、API和真实16bit类型支持。
min16float表示“至少16bit精度”,驱动也允许使用32bit执行。
所以只把源码里的float改成half,不保证Buffer大小和纹理带宽自动减半。
12.哪些数据适合half?
通常比较适合:
0到1范围的颜色、Mask、粗糙度、金属度。
范围受控的UV和局部坐标。
归一化方向、切线空间法线。
范围明确的材质参数。
允许小误差的光照中间结果。
粒子和普通特效数据。
需要注意的是,归一化向量本身范围适合half,但高光、反射和多次累积对误差可能敏感,不能看到Normal就一律half。
通常应该保留float:
大范围World Position。
View Space Position和深度相关计算。
矩阵变换及多次累积结果。
对精度敏感的几何、阴影和重建算法。
大参数或大范围UV。
长循环累加、积分和误差会持续放大的计算。
对三角函数、指数函数结果精度敏感的部分。
最容易发现的half精度问题:
颜色Banding。
高光闪烁。
远处顶点抖动。
深度重建错误。
UV跳变和纹理采样不稳定。
13.Mali和Adreno的SIMD宽度怎么理解?
“Mali SIMD宽度为4,Adreno SIMD为8或更宽”可以作为某些具体代际资料里的例子,但不能推广到所有Mali和Adreno。
移动GPU架构变化很快:
执行组大小可能变化。
物理ALU宽度和逻辑Subgroup大小可能不同。
同一GPU对不同Shader阶段和精度可能使用不同执行方式。
编译器可能合并、拆分或重新调度指令。
因此跨平台优化时更有价值的结论不是背一个固定宽度,而是:
保持足够的并行工作量。
减少Wave内发散。
控制寄存器压力。
让内存访问连续。
在支持的平台合理使用FP16。
用厂商工具查看真实ISA和性能计数器。
14.从Shader代码怎么对应到硬件?
1 | |
大致会经过:
1.Texture Unit根据UV做纹理采样。
2.采样结果返回寄存器。
3.ALU执行dot、saturate和乘法。
4.多个Shader线程组成Warp/Wave。
5.调度器把同一条指令发给SIMD执行单元。
6.如果使用原生FP16,编译器和硬件可能使用Packed FP16提高吞吐。
7.如果纹理结果还没返回,调度器切换到其他Wave隐藏延迟。
这就是Shader源码、Warp/Wave、SIMD和ALU之间的完整关系。
15.GPU硬件角度的Shader优化
1.先判断瓶颈
ALU Bound:数学指令和特殊函数多。
Texture Bound:纹理采样和过滤压力大。
Bandwidth Bound:大量读写显存。
Latency Bound:并行度或Occupancy不足,无法隐藏等待。
瓶颈不同,优化方向完全不同。
2.提高ALU有效利用率
保持足够工作量。
减少高频分支发散。
减少长指令依赖链。
控制临时变量和寄存器压力。
3.合理使用half
从颜色、Mask、材质参数等安全数据开始。
World Position、Depth和精确几何保留float。
同时检查真实纹理和Buffer格式,不能只改变量声明。
4.提高内存连续性
相邻线程尽量访问相邻数据。
使用合适纹理格式和压缩。
避免随机Buffer访问。
减少不必要的中间RenderTarget读写。
5.查看编译结果
源码不能完全代表GPU执行结果。编译器可能:
合并乘加指令。
删除无用计算。
把half提升到float。
把分支改成Select。
增加Pack/Unpack和类型转换。
应该结合平台工具查看ISA、寄存器、Occupancy和性能计数器。
常用工具:
Nvidia Nsight Graphics
AMD Radeon GPU Analyzer / Radeon GPU Profiler
Arm Performance Studio / Mali Offline Compiler
Snapdragon Profiler
Xcode GPU Frame Capture
16.核心总结
GPU计算架构可以按这个顺序理解:
Shader产生大量线程。
线程组成Warp/Wave/Subgroup。
调度器选择准备好的Warp/Wave指令。
SIMD执行单元让多条Lane同时工作。
ALU在每条Lane上真正完成数学和逻辑运算。
当纹理和内存产生延迟时,GPU切换其他Warp/Wave继续执行;当工作量、Occupancy或并行性不足时,ALU就会空闲。
half优化的核心也不是“位数减半就必然快4倍”,而是看目标GPU是否真正支持更高FP16吞吐、更低寄存器压力和真实16bit存储。
最后一句话:
ALU决定能做计算,SIMD决定怎么并行做,Warp/Wave决定哪些线程一起做,调度器和内存系统决定ALU能不能一直有活干。
引用参考:
NVIDIA CUDA C++ Programming Guide
AMD GPUOpen - RDNA Performance Guide
Unity Manual - Use 16-bit precision in shaders
Microsoft HLSL - Scalar data types