第一章:GPU架构基础——从SM到Warp,理解GPU的并行执行模型
大家好,我是你们的老朋友。今天咱们聊聊GPU最核心的东西——它的并行执行模型。
说实话,我见过太多人一上来就写CUDA代码,结果性能惨不忍睹。为什么?因为根本不了解GPU是怎么工作的。你想想看,连你的代码在硬件上怎么跑的都不清楚,优化从何谈起?
我个人习惯,学任何东西先搞懂底层。GPU编程更是如此。这一章,我们就从SM到Warp,把GPU的并行模型彻底讲透。
1.1 从宏观到微观:GPU的层次结构
GPU的并行架构,说白了就是三层嵌套:GPU → SM → Warp。
- GPU:整张显卡,包含几十到上百个SM
- SM(Streaming Multiprocessor):流式多处理器,是真正的计算单元
- Warp:32个线程组成的最小调度单位
嗯,这里要注意:Warp才是GPU执行的基本单位,而不是单个线程。这一点太重要了,我后面会反复强调。
核心概念:GPU不是同时执行成千上万个线程,而是以Warp为单位,32个线程一起执行同一条指令。这就是所谓的SIMT(单指令多线程)模型。
1.2 SM内部长什么样?
每个SM内部,大概有这些东西:
- CUDA核心:负责整数和浮点运算
- 共享内存:同一Block内的线程可以访问,速度极快
- 寄存器文件:每个线程私有,容量有限
- Warp调度器:负责选择和发射Warp指令
- 加载/存储单元:处理内存访问
我记得第一次看SM的框图时,觉得这东西像个迷你CPU。但后来发现,它比CPU简单粗暴得多——大量计算单元,少量控制逻辑。这就是GPU能堆出几千个核心的原因。
1.3 Warp:GPU调度的灵魂
Warp是32个线程的集合。为什么是32?这是NVIDIA的设计选择,跟硬件实现有关。
Warp的执行特点:
- 同一Warp内的线程执行同一条指令
- 如果线程走不同的分支(if-else),就会发生Warp分化
- Warp分化会导致部分线程被屏蔽,性能下降
避坑指南:我曾经在一个项目中,因为没注意Warp分化,一个简单的条件判断让性能掉了40%。排查了半天才发现,是线程ID的奇偶判断导致了分支分歧。从那以后,我写条件分支时都会刻意检查线程ID的分布。
1.4 线程层次结构:Grid → Block → Thread
CUDA的线程组织方式,跟硬件层次是对应的:
| 软件概念 | 硬件映射 | 说明 |
|---|---|---|
| Grid | 整个GPU | 一个Kernel启动对应一个Grid |
| Block | 单个SM | 一个Block被分配到一个SM上执行 |
| Thread | CUDA核心 | 实际执行计算的单元 |
| Warp | Warp调度器 | 32个线程一组,硬件调度单位 |
这里有个关键点:一个SM可以同时运行多个Block,但Block之间不能通信。我刚开始做优化时,总想着让Block之间共享数据,后来发现这是不可能的——除非你用全局内存,但那又太慢。
1.5 占用率:你真正该关心的指标
占用率(Occupancy)是指每个SM上活跃的Warp数量与最大Warp数量的比值。
为什么重要?因为GPU靠Warp切换来隐藏延迟。当某个Warp在等内存数据时,调度器会切换到另一个Warp执行。活跃Warp越多,隐藏延迟的能力越强。
个人经验:我一般会先用NVIDIA的占用率计算器算一下理论值。但说实话,100%占用率不一定最好。有时候为了给每个线程更多寄存器,降低一点占用率反而能提升性能。这需要实际测试。
1.6 一张图看懂GPU执行模型
下面这张SVG图,把整个执行流程串起来了:
1.7 实战:如何查看你的GPU参数
别光听我讲,动手看看你的显卡:
// 查看GPU基本信息
nvidia-smi
// 查看SM数量、Warp大小等细节
// 在CUDA代码中:
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, 0);
printf("SM数量: %d\n", prop.multiProcessorCount);
printf("每个SM最大Warp数: %d\n", prop.maxThreadsPerMultiProcessor / 32);
printf("Warp大小: %d\n", prop.warpSize);
我建议你跑一下这段代码。不同型号的GPU,参数差异很大。比如我的RTX 3090有82个SM,每个SM最大支持64个Warp。而老一点的GTX 1080只有20个SM。
小技巧:用cudaGetDeviceProperties可以拿到几乎所有硬件参数。我写优化代码前,都会先跑一遍这个,把关键参数打印出来贴在显示器上。
1.8 本章小结
好了,这一章的内容就这些。总结几个必须记住的点:
- Warp是执行单位,不是线程。32个线程一起走,一个指令一个指令地执行
- SM是物理计算单元,一个SM可以同时跑多个Block
- Warp分化是性能杀手,写分支时要小心
- 占用率不是越高越好,要结合具体场景权衡
嗯,这些概念听起来简单,但真正用好它们,需要大量的实践。下一章我们会深入Warp的细节,讲讲怎么避免Warp分化,怎么利用Warp shuffle做高效通信。
想要更多资料,联系可可