第二章:硬件基础与计算模型:GPU架构概览(SM、Warp、Memory Hierarchy)、CPU与GPU的协同
好,咱们正式开始聊硬件。很多做算子优化的朋友,上来就怼代码,调参数,结果性能死活上不去。为什么?说白了,你不了解你的对手——GPU。你连它内部怎么工作的都不知道,怎么让它听你的话?
我个人习惯,在动手写任何一行CUDA代码之前,先花半小时把目标GPU的架构文档翻一遍。这不是浪费时间,这是磨刀不误砍柴工。今天我就带你把这把刀磨快。
2.1 GPU的核心:SM(流多处理器)
GPU不是一个大铁块。它是由很多个SM组成的。你可以把SM想象成一个微型计算机,有自己的处理器、缓存、调度器。一个GPU有多少个SM,基本决定了它的算力上限。
我在项目中遇到过一个问题:一个kernel在A100上跑得飞快,换到V100上就慢了一半。查了半天,发现是SM数量减半了,但线程块数量没调。嗯,这就是没摸清家底的结果。
每个SM内部包含:
- CUDA核心:执行整数和浮点运算。别被名字骗了,它不是独立的CPU核心,更像是一个高度精简的ALU。
- 共享内存:极低延迟,但容量有限。这是算子优化的兵家必争之地。
- 寄存器文件:每个线程私有的最快存储。但总量固定,线程多了,每个线程分到的就少了。
- Warp调度器:负责把指令发给线程束执行。这个后面细说。
核心观点:SM是并行执行的基本单元。你的优化策略,本质上就是在跟SM的硬件资源做博弈。
2.2 Warp:真正的执行单元
你写代码时觉得是线程在并行跑。其实不是。GPU真正的执行单位是Warp——一个由32个线程组成的集合。这32个线程在SM里是同步执行同一条指令的。
为什么会这样?你想想看,如果每个线程都独立取指令、译码、执行,那光指令调度单元就得占多大面积?GPU把32个线程绑在一起,共享一套指令单元,面积省下来了,算力上去了。代价就是——分支发散。
我曾经在一个项目里,写了个if-else判断,32个线程里一半走if,一半走else。结果呢?性能直接腰斩。因为Warp里的线程必须串行执行不同的分支路径。这就是分支发散,是性能杀手。
避坑指南:我曾经在写Reduce算子时,让每个Warp内的线程根据线程ID做不同的操作。结果性能惨不忍睹。后来改成让整个Warp做同一件事,用Shuffle指令内部通信,速度直接翻倍。记住:让Warp内的线程保持统一行动。
2.3 Memory Hierarchy:存储层次与带宽博弈
GPU的存储层次,说白了就是一个金字塔。越往上越快,但越小越贵。越往下越慢,但越大越便宜。优化算子的本质,就是尽量把数据往金字塔顶端塞。
| 存储层级 | 延迟(大约) | 容量 | 作用域 |
|---|---|---|---|
| 寄存器 | ~1 cycle | 每个线程最多255个 | 线程私有 |
| 共享内存 | ~5-30 cycles | 每个SM 48-164 KB | 线程块内共享 |
| L1缓存 | ~30-80 cycles | 每个SM 128-192 KB | SM内共享 |
| L2缓存 | ~200-400 cycles | 芯片级 40-80 MB | 全局共享 |
| 全局内存 | ~400-800 cycles | 显存 16-80 GB | 所有线程 |
看到这个延迟差距了吗?从全局内存读一次数据,够你在寄存器里算400次加法。所以,减少全局内存访问是优化的第一要义。
我个人的经验是:如果一个kernel的瓶颈在访存,那优化共享内存的使用往往立竿见影。但如果瓶颈在计算,那就要考虑指令级并行或者增加Warp数量来隐藏延迟。
2.4 CPU与GPU的协同:异构计算的正确姿势
CPU和GPU不是竞争关系,是合作关系。CPU擅长控制流和复杂逻辑,GPU擅长大规模并行计算。正确的做法是:CPU当指挥官,GPU当苦力。
具体来说:
- 数据传输:CPU负责把数据从内存搬到显存,再从显存搬回来。这个开销很大,要尽量减少传输次数。
- 任务调度:CPU启动kernel,然后可以去做别的事。GPU异步执行。用cudaStream可以实现流水线。
- 错误处理:GPU出错了,CPU要能捕获并处理。别让GPU默默吞掉错误。
注意:我曾经犯过一个低级错误——在CPU和GPU之间来回传输小数据。每次传输都有微秒级的延迟,累积起来就是灾难。后来我改用固定内存(Pinned Memory)和异步传输,性能提升了一个数量级。记住:批量传输,避免小数据频繁搬运。
还有一个容易被忽略的点:CPU和GPU的同步。你启动一个kernel后,CPU默认不会等它执行完。如果你紧接着就要用结果,必须显式同步。但同步会阻塞CPU,浪费计算资源。我建议用cudaEvent来做细粒度的同步,只在真正需要结果的地方等待。
2.5 知识体系总览
下面这张图,是我自己总结的GPU架构知识体系。每次做优化前,我都会对照着看一遍,确认自己没漏掉什么。
这张图把今天讲的内容串起来了。你每次做优化时,都可以对照着看看:我是在优化SM利用率?还是在减少Warp发散?还是在降低访存延迟?还是在改善CPU-GPU通信?
好了,硬件基础就聊到这儿。记住一句话:不懂硬件的优化,都是瞎优化。下一章我们开始动手写代码,但今天这些概念,会一直贯穿整个课程。