第二章:GPU架构基础:SM与CUDA Core的关系、Warp调度机制、内存层次结构对负载的影响

好,咱们正式开始聊GPU的硬核内容。这一章是基础中的基础,但也是很多人容易搞混的地方。我见过不少开发者,写了很久的CUDA代码,却连SM和CUDA Core到底啥关系都说不清楚。嗯,这其实挺要命的。

2.1 SM和CUDA Core:别被名字骗了

先说说SM(Streaming Multiprocessor,流多处理器)。你可以把它理解成GPU里的“计算小组”。每个SM里包含了很多CUDA Core,但注意——CUDA Core并不是一个完整的CPU核心。

说白了,CUDA Core只是一个执行单元。它负责做整数运算、浮点运算这些基础操作。我个人的习惯是,把SM看作一个“车间”,CUDA Core就是车间里的“工人”。工人多了,车间干活就快。

关键点:一个SM里通常有几十到几百个CUDA Core。但别以为Core越多就一定越快。你得看它们怎么配合工作。

举个例子。我手头有块NVIDIA A100 GPU,每个SM里有64个CUDA Core。但实际跑起来,并不是所有Core都能同时干活。为什么?因为还有调度、内存访问这些限制。你想想看,如果工人都在等材料,那再多人也没用。

2.2 Warp调度:GPU的“小组作业”机制

接下来聊Warp。Warp是GPU执行任务的最小单位,通常包含32个线程。这32个线程会一起执行同一条指令。这就是所谓的SIMT(单指令多线程)模式。

我在项目中遇到过一个问题:某个核函数里有个if-else分支,结果导致Warp里的线程走了不同的路径。嗯,这就麻烦了。GPU会串行执行这些分支,先走if的线程,再走else的线程。性能直接砍半。

避坑指南:我曾经在一个图像处理项目里,因为没注意Warp内的分支发散,导致性能从预期的2ms掉到了5ms。后来我把分支条件改成了基于Warp ID的判断,才把性能拉回来。

Warp调度器是SM里的一个关键部件。它负责从多个Warp里选一个来执行。为什么要选?因为内存访问有延迟。当某个Warp在等数据时,调度器会切换到另一个Warp去干活。这样就能隐藏延迟。

说白了,GPU就是靠“人多”来掩盖“慢”的。一个Warp等数据,另一个Warp顶上。只要你有足够多的Warp,就能把内存延迟给填满。

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

GPU的内存层次,说白了就是“越近越快,越远越慢”。但很多人只记住了这句话,没理解它对负载的影响。

内存类型 位置 速度 容量 作用
寄存器 SM内部 最快(~1周期) 很小(每个线程有限) 存局部变量
共享内存 SM内部 快(~5-30周期) 小(几十KB) 线程间通信
L1缓存 SM内部 较快 几十KB 缓存数据
L2缓存 芯片共享 中等(~200周期) 几MB 全局缓存
全局内存 显存 慢(~400-800周期) 大(GB级) 主存储

你看这个表,从寄存器到全局内存,速度差了上百倍。我刚开始做GPU优化时,总想着把所有数据都塞进共享内存。结果呢?共享内存太小,放不下,反而导致线程数受限。

注意:共享内存虽然快,但它是SM内的资源。每个SM的共享内存是有限的。如果你用太多,SM能同时运行的Warp数就会减少。这叫“占用率下降”。

我个人的经验是:先算清楚你的数据访问模式。如果是频繁访问的小数据,放共享内存。如果是偶尔访问的大数据,放全局内存。别盲目追求“快”,要平衡。

2.4 内存访问模式:合并访问是关键

说到全局内存,就不得不提“合并访问”。GPU从全局内存读数据时,是以128字节为单位的。如果你让一个Warp里的32个线程,连续访问相邻的地址,那一次就能读完。这就是合并访问。

反过来,如果线程访问的是随机地址,那GPU就得多次读取。性能直接崩掉。

// 好的例子:合并访问
int idx = blockIdx.x * blockDim.x + threadIdx.x;
float val = data[idx];  // 连续访问

// 坏的例子:非合并访问
int idx = threadIdx.x * gridDim.x + blockIdx.x;
float val = data[idx];  // 跨步访问,性能差

我在做矩阵转置时吃过这个亏。一开始没注意访问模式,性能只有理论峰值的10%。后来改成合并访问,直接提升了4倍。

2.5 负载均衡:SM之间的任务分配

最后聊聊负载均衡。GPU有多个SM,每个SM处理不同的Block。如果某个Block的计算量特别大,那它所在的SM就会忙死,其他SM却闲着。

为什么会这样?因为Block是静态分配到SM的。一旦分配完,就不能动态迁移。所以你得保证每个Block的计算量差不多。

技巧:我建议把大任务拆成多个小Block。比如一个1024x1024的矩阵,拆成32x32的Block。这样每个SM都能均匀分配到任务。

另外,Warp级别的负载均衡也很重要。如果一个Warp里有些线程提前退出了,那剩下的线程还得等。嗯,这其实就是前面说的分支发散问题。

总结一下这一章的核心:

  • SM是车间,CUDA Core是工人
  • Warp是32人小组,要避免组内分歧
  • 内存层次要会用,别让工人等材料
  • 访问模式要连续,别让GPU白干活
  • 任务要拆均匀,别让SM闲死忙死

下一章,咱们会深入聊聊如何用这些知识来优化实际代码。到时候我会拿几个真实项目里的例子,手把手带你调优。