硬件基础与性能模型: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模型示意图 计算强度 (Ops/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这些指标,再对照着硬件架构去理解,慢慢就熟了。