4、硬件架构基础:GPU架构概览、内存层次结构、计算单元与调度

做算子融合,说白了就是在跟硬件打交道。你如果不了解GPU的脾气,写出来的融合策略大概率是纸上谈兵。我刚开始接触这行的时候,也踩过不少坑——明明数学上算得清清楚楚,跑起来就是慢。后来才明白,问题出在没摸透硬件的底细。

这一章,我们就来聊聊GPU的骨架。嗯,不聊那些花里胡哨的架构代号,只讲你写融合算子时真正用得上的东西。

4.1 GPU架构概览:一个并行的“小工厂”

GPU和CPU最大的区别是什么?CPU是“全能冠军”,什么活都能干,但一次只能干几件。GPU是“流水线工人”,每个工人只会简单的加减乘除,但胜在人多——成千上万个工人一起上。

我个人习惯把GPU想象成一个巨大的工厂。这个工厂里有多个“车间”(GPC,Graphics Processing Cluster),每个车间里有多个“生产线”(TPC,Texture Processing Cluster),每条生产线上又有多个“工位”(SM,Streaming Multiprocessor)。

你写的每一个算子,最终都会被拆解成无数个小任务,分配到这些工位上并行执行。

核心要点:算子融合的本质,就是减少这些“工位”之间的空闲等待时间,让它们尽可能满负荷运转。

我在项目中遇到过一个问题:两个简单的逐元素操作,分开写跑得挺快,合在一起反而慢了。后来一查,发现是融合后的算子占用了太多寄存器,导致每个SM上能同时跑的线程数大幅下降。这就是典型的“好心办坏事”。

4.2 内存层次结构:速度与容量的博弈

GPU的内存,就像工厂里的物料仓库。离工人越近的仓库,取东西越快,但容量也越小。

这张图展示了GPU的内存层次结构,你可以直观地看到数据流动的路径:

GPU 内存层次结构 全局内存 (Global Memory) 容量大 (GB级) | 速度慢 (~200-400 cycles) | 所有线程可访问 L2 缓存 (L2 Cache) 所有SM共享 | 容量中等 (MB级) | 速度中等 (~100 cycles) L1 缓存 / 共享内存 (Shared Memory) 每个SM独有 | 容量小 (几十KB) | 速度快 (~30 cycles) | 同一Block内线程共享 寄存器 (Registers) 每个线程独有 | 容量极小 (KB级) | 速度最快 (0 cycles) 速度↑ 容量↑

从上往下看,速度越来越慢,但容量越来越大。算子融合要做的,就是尽量把数据留在“上面”这几层,少去碰底下的全局内存。

4.2.1 寄存器:最快的“手边工具”

寄存器是每个线程私有的,速度最快,没有延迟。但数量极其有限——一个SM通常只有几万个寄存器,分给成百上千个线程,每个线程能分到的其实没几个。

我曾经踩过的坑:写融合算子时,为了省事把中间结果都存成局部变量。结果寄存器溢出,数据被自动挪到了L1缓存,性能直接腰斩。所以,控制每个线程的寄存器使用量,是融合优化的第一课。

4.2.2 共享内存:同一Block的“茶水间”

共享内存是同一个Block内所有线程都能访问的。速度比全局内存快一个数量级。我习惯把它当作“手动缓存”——你可以显式控制哪些数据放进去,哪些数据淘汰掉。

举个例子,做矩阵乘法融合时,我会把输入矩阵的小块先搬到共享内存里,让Block内的线程反复使用,而不是每次计算都去全局内存读。这样一改,性能提升往往立竿见影。

4.2.3 全局内存:最后的“大仓库”

全局内存是最大的存储区域,但也是最慢的。所有线程都能读写,但每次访问都要付出几百个时钟周期的代价。

你想想看,如果一个算子需要频繁读写全局内存,那大部分时间都花在等待上了。融合算子的一个核心目标,就是减少这种“长途跋涉”。

4.3 计算单元与调度:工人们怎么干活

GPU的计算核心是SM(Streaming Multiprocessor)。每个SM里包含多个CUDA Core(或者叫流处理器),以及一些特殊功能单元(如Tensor Core)。

调度单位是Warp——通常32个线程为一组。SM以Warp为单位进行调度。也就是说,32个线程要么一起执行,要么一起等待。

一个小技巧:写融合算子时,尽量保证Warp内的32个线程执行相同的指令路径。如果出现分支(if-else),部分线程执行,部分线程等待,这就是所谓的“Warp divergence”。性能损失很大。

4.3.1 指令级并行与线程级并行

GPU擅长的是线程级并行——成千上万个线程同时跑。但每个线程内部的指令级并行能力很弱。所以,不要把希望寄托在单个线程的流水线上,而是要多开线程。

我记得有一次优化一个融合算子,把线程数从128提高到256,性能反而下降了。为什么?因为线程太多,每个线程分到的资源(寄存器、共享内存)太少,导致频繁的上下文切换。嗯,这里要注意:不是线程越多越好,要找到那个平衡点。

4.3.2 Tensor Core:融合算子的“加速神器”

现代GPU(从Volta架构开始)引入了Tensor Core,专门用于矩阵乘法和卷积运算。一个Tensor Core可以在一个时钟周期内完成4x4矩阵的乘加运算。

如果你的融合算子涉及矩阵运算,一定要想办法让Tensor Core参与进来。我见过一个案例,把两个连续的矩阵乘法融合成一个更大的矩阵乘法,然后交给Tensor Core处理,性能提升了3倍多。

计算单元 适用场景 性能特点 融合建议
CUDA Core 逐元素操作、简单算术 通用性强,但吞吐量有限 适合融合多个小算子
Tensor Core 矩阵乘法、卷积 吞吐量极高,但精度受限 优先融合为矩阵运算
特殊功能单元 超越函数(sin, exp等) 单指令完成复杂运算 避免频繁调用

4.4 调度策略:如何让工人们不闲着

GPU的调度器会尽量让SM保持忙碌。当一个Warp因为访存而等待时,调度器会立刻切换到另一个就绪的Warp。这就是所谓的“零开销上下文切换”。

但这里有个前提:必须有足够的就绪Warp。如果线程数太少,调度器就“无米下锅”,SM就会空闲。

我个人习惯用“占用率”(Occupancy)来衡量这个指标。占用率 = 实际活跃Warp数 / 最大支持Warp数。一般来说,占用率越高,隐藏访存延迟的能力越强。

实战建议:写融合算子时,先用工具(如NVIDIA的Nsight Compute)看一下占用率。如果低于50%,说明资源(寄存器或共享内存)可能成了瓶颈。这时候要么减少每个线程的资源使用,要么调整Block大小。

我曾经接手过一个项目,融合后的算子占用率只有30%。排查后发现,每个线程用了太多寄存器。我把一些中间结果放到共享内存里,占用率提升到了70%,性能直接翻倍。

好了,这一章的内容就到这里。GPU架构的知识点很多,但真正影响算子融合的,说白了就是内存层次和计算单元的调度。记住:数据尽量往上放,线程尽量开满,资源别浪费。做到这三点,你的融合算子基本就成功了一大半。

一句话总结:GPU架构不是用来“理解”的,是用来“伺候”的。摸清它的脾气,你的算子才能跑得又快又稳。

可可精品资料库