第一章: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图,把整个执行流程串起来了:

GPU(全局) SM 0 CUDA核心 × 128 共享内存 48KB 寄存器文件 Warp调度器 Warp0 Warp1 Warp2 Warp3 SM 1 CUDA核心 × 128 共享内存 48KB 寄存器文件 Warp调度器 Warp0 Warp1 Warp2 Warp3 全局内存(Global Memory) 所有SM共享,延迟高(400-800 cycles) Warp(32线程) SM内部资源 全局内存 GPU执行模型:Grid → SM → Warp → Thread

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做高效通信。

想要更多资料,联系可可