硬件基础与性能模型:GPU架构简介
做算子优化,不懂硬件就是瞎蒙。我见过太多人,代码写得花里胡哨,一跑性能惨不忍睹。说白了,你得知道你的代码在硬件上是怎么执行的,才能写出高效的算子。
这一章,我们聊聊GPU的硬件架构和性能模型。这些东西看起来枯燥,但实际干活的时候,你会发现它们就是你的"内功心法"。
GPU架构核心:SM、Warp、Shared Memory
先说说GPU最核心的执行单元——SM(Streaming Multiprocessor,流式多处理器)。你可以把SM想象成一个"迷你CPU",但它跟CPU完全不同。一个GPU里有几十到上百个SM,每个SM里又有大量的CUDA核心。
我习惯把SM比作一个工厂车间,CUDA核心就是车间里的工人。工人虽然多,但干活的时候得听指挥。
Warp:真正的执行单位
这里有个关键概念——Warp。GPU执行指令的最小单位不是单个线程,而是32个线程组成的一个Warp。这32个线程执行同一条指令,只是处理不同的数据。
为什么会这样?说白了,这是GPU为了省面积、省功耗的设计。32个线程共享一个指令单元,取指、译码的开销就摊薄了。
关键点:一个Warp里的32个线程必须执行同一条指令。如果遇到分支,比如if-else,那部分线程就得"等待",这就是所谓的Warp divergence(分支发散)。
我在项目中遇到过一个问题:一个简单的softmax算子,因为对每个元素做了条件判断,导致Warp内部分线程执行if分支,部分执行else分支。性能直接掉了40%。后来改成用mask操作,避免了分支,速度就上来了。
Shared Memory:程序员可控的缓存
Shared Memory是GPU上的一块特殊存储,位于SM内部。它的特点是:快,比全局内存快两个数量级;小,每个SM通常只有几十到一百多KB;程序员可控,你得手动管理数据搬入搬出。
我刚开始做算子优化时,总觉得Shared Memory太小,不够用。后来发现,关键不在于它有多大,而在于你怎么用。比如矩阵乘法,把子矩阵分块加载到Shared Memory里,复用次数多了,全局内存访问就少了,性能自然就上去了。
| 存储类型 | 延迟 | 大小 | 作用域 |
|---|---|---|---|
| 全局内存 | ~400-800 cycles | GB级别 | 所有线程 |
| Shared Memory | ~20-30 cycles | 几十KB | 单个Block |
| 寄存器 | ~1 cycle | 每个线程几十个 | 单个线程 |
CPU与GPU的协同
CPU和GPU怎么配合?简单说:CPU负责"指挥",GPU负责"干活"。
CPU擅长处理复杂的控制逻辑、分支判断、串行任务。GPU擅长做大量相同运算的并行任务。所以典型的模式是:CPU把数据准备好,调用GPU kernel,GPU算完,CPU再把结果拿回来。
这里有个坑——数据传输。CPU和GPU之间通过PCIe总线通信,带宽有限,延迟还高。我曾经做过一个项目,数据量不大但频繁传输,结果大部分时间都花在数据搬运上,GPU反而在"摸鱼"。
避坑指南:尽量减少CPU和GPU之间的数据传输。能一次传完就别分多次,能用异步传输就别用同步。我曾经因为没注意这个,一个简单的向量加法,GPU计算只用了0.1ms,数据传输却花了5ms,你说亏不亏?
Roofline模型
Roofline模型是个好东西。它帮你回答一个问题:我的算子到底是受计算能力限制,还是受内存带宽限制?
说白了,Roofline模型就是一张图。横轴是"计算强度"(Operations per Byte,每字节数据做多少次运算),纵轴是"性能"(FLOPS)。
图上有两条线:一条是内存带宽线(斜线),一条是计算能力线(水平线)。两条线相交的地方,就是"拐点"。
你看,向量加法在图的左边,属于"访存密集"——它受带宽限制。矩阵乘法在右边,属于"计算密集"——它受计算能力限制。
我个人的习惯是:拿到一个算子,先算它的计算强度,然后看它在Roofline模型上的位置。如果在左边,我就想办法优化访存(比如用Shared Memory、合并访问)。如果在右边,我就想办法提高计算效率(比如用更快的数学函数、减少Warp divergence)。
计算密集与访存密集算子
这两个概念其实很好理解:
- 计算密集算子:每个数据元素要做很多次运算。比如矩阵乘法、卷积、FFT。这类算子的瓶颈在计算单元,优化方向是提高计算利用率。
- 访存密集算子:每个数据元素只做少量运算。比如向量加法、元素级操作、Reduce。这类算子的瓶颈在内存带宽,优化方向是减少访存次数、提高访存效率。
小技巧:判断一个算子是计算密集还是访存密集,可以算一下"计算强度"(FLOPs / Bytes)。如果计算强度大于拐点值,就是计算密集;反之就是访存密集。不同GPU的拐点值不同,A100大概在100左右,V100在50左右。
我记得有一次优化一个LayerNorm算子。一开始我拼命优化计算逻辑,折腾了半天,性能提升不到5%。后来用Roofline模型一分析,发现它其实是访存密集的——每个元素只做了几次乘加运算,但数据量很大。于是我把优化方向转向访存:用向量化加载、合并访问、减少全局内存读写。结果性能提升了3倍。
嗯,这就是Roofline模型的魅力。它让你知道"力气该往哪里使"。
最后说一句:硬件知识不是背出来的,是用出来的。你写一个算子,跑一下profiling,看看SM利用率、带宽利用率、Warp occupancy这些指标,再对照着硬件架构去理解,慢慢就熟了。