第三章 GPU的硬件组成:流式多处理器(SM)、计算单元(CUDA Core/Tensor Core)、内存层次结构

好,咱们今天聊点硬核的。GPU到底长什么样?

很多软件工程师写CUDA代码,写了好几年,却从没想过硬件是怎么执行这些指令的。我个人觉得,这就像开车不懂发动机原理——能开,但永远开不到极致。

这一章,我带你把GPU的“内脏”翻出来看看。

3.1 流式多处理器(SM)——GPU的“核心团队”

先问一个问题:GPU里有几百甚至几千个核心,它们是怎么组织起来的?

答案是——SM(Streaming Multiprocessor)。你可以把SM理解成一个“小分队”。每个SM内部有自己的计算单元、缓存、调度器。GPU就是由多个SM组成的。

举个例子:NVIDIA的A100有108个SM,每个SM内部又有64个FP32 CUDA Core。所以总核心数就是108×64=6912个。

SM的核心职责:

  • 线程调度:SM负责把线程块(Thread Block)分配给内部的CUDA Core。我习惯把SM比作一个项目经理,它决定哪个线程块什么时候跑、跑在哪个核心上。
  • 指令发射:每个SM内部有多个Warp Scheduler(经线调度器),每个调度器每周期可以发射一条指令给一组线程(32个线程为一组,称为一个Warp)。
  • 资源管理:寄存器、共享内存这些资源,都由SM统一分配。

重要概念:Occupancy(占用率)

SM能同时容纳的Warp数量是有限的。如果每个线程块用太多寄存器,SM能同时跑的线程块就少,占用率就低。我曾经在一个项目里,为了追求单线程性能,给每个线程分配了128个寄存器,结果占用率掉到25%,整体性能反而下降了40%。

嗯,这就是典型的“捡了芝麻丢了西瓜”。

3.2 计算单元:CUDA Core 与 Tensor Core

SM内部有两种“打工人”:CUDA Core和Tensor Core。它们分工不同,各司其职。

3.2.1 CUDA Core——通用计算的主力

CUDA Core是GPU里最基础的计算单元。每个CUDA Core可以执行整数、浮点数的加减乘除等基本运算。

你写的每个CUDA线程,最终都会映射到一个CUDA Core上执行。但注意:一个CUDA Core同一时间只能执行一个线程的一条指令。所以,线程越多,越能“喂饱”这些核心。

CUDA Core的规格(以A100为例):

类型 数量/SM 总数量 主要用途
FP32 Core 64 6912 单精度浮点运算
INT32 Core 64 6912 整数运算、地址计算
FP64 Core 4 432 双精度浮点运算(科学计算)

你可能会问:为什么FP64核心这么少?说白了,游戏和AI训练不需要那么高的精度。双精度是给科学计算准备的,所以数量少,但每个FP64 Core性能很强。

3.2.2 Tensor Core——AI时代的“特种兵”

Tensor Core是NVIDIA从Volta架构开始引入的专用计算单元。它专门为矩阵乘法而生。

为什么需要它?

深度学习里,全连接层、卷积层,本质上都是矩阵乘法。如果用CUDA Core做,一次只能算一个乘加(FMA)。但Tensor Core一次可以算一个4×4矩阵的乘法,也就是64个乘加操作。

Tensor Core的工作方式:

  • 输入:两个4×4的矩阵(A和B)
  • 输出:一个4×4的矩阵(C)
  • 一次操作完成:D = A × B + C

我记得第一次用Tensor Core写矩阵乘法时,性能直接翻了4倍。但要注意:Tensor Core对数据精度有要求,通常需要FP16或BF16输入,FP32累加。

我的建议:

如果你的计算是矩阵乘法密集型(比如AI推理、训练),尽量用cuBLAS或cuDNN库,它们会自动调用Tensor Core。手写CUDA代码调用Tensor Core比较麻烦,需要用到WMMA(Warp Matrix Multiply-Accumulate)API。

3.3 内存层次结构——速度与容量的博弈

GPU的内存系统,说白了就是一个“金字塔”。越往上越快,但容量越小;越往下越慢,但容量越大。

你想想看,如果所有数据都放在最慢的全局内存里,那计算单元大部分时间都在等数据。所以,理解内存层次结构,是优化性能的关键。

3.3.1 寄存器(Register)——最快,但最稀缺

寄存器是每个线程私有的,速度最快(一个周期内读写)。每个SM有固定数量的寄存器,比如A100每个SM有65536个32位寄存器。

关键点:

  • 每个线程能用的寄存器数量是有限的。如果每个线程用太多寄存器,SM能同时跑的线程数就减少。
  • 编译器会自动分配寄存器,但你也可以用__launch_bounds__来限制每个线程的最大寄存器数。

我曾经踩过的坑:

有一次我写了一个复杂的kernel,里面用了很多局部变量。编译后发现每个线程用了96个寄存器,导致SM只能同时跑8个Warp(正常可以跑64个)。性能惨不忍睹。

后来我把一些中间结果放到共享内存里,寄存器用量降到32个,占用率从12.5%提升到50%,性能翻了3倍。

3.3.2 共享内存(Shared Memory)——线程块内的“高速缓存”

共享内存是每个SM内部的,同一个线程块内的所有线程都可以访问。速度比全局内存快很多(大约20-30倍延迟),但容量有限(A100每个SM最多164KB)。

共享内存的典型用途:

  • 数据复用:多个线程需要读取同一份数据时,先加载到共享内存,避免重复访问全局内存。
  • 线程间通信:同一个线程块内的线程可以通过共享内存交换数据,比用全局内存快得多。
  • 合并访问:把非合并的全局内存访问,先整理到共享内存,再合并读取。

举个例子:矩阵乘法中,每个线程需要读取A的一行和B的一列。如果直接读全局内存,每个线程都要访问很多次。但如果我们把A的一行和B的一列先加载到共享内存,所有线程就可以共享这些数据,大大减少全局内存访问。

3.3.3 全局内存(Global Memory)——最大,但最慢

全局内存就是GPU的显存。容量大(几十GB),但延迟高(几百个周期)。所有线程都可以访问,但速度最慢。

优化全局内存访问的关键:

  • 合并访问(Coalesced Access):相邻的线程访问相邻的内存地址。这是最重要的优化手段。
  • 对齐访问:内存地址最好是128字节对齐。
  • 减少访问次数:能用共享内存或寄存器,就别用全局内存。

一个简单的性能对比:

内存类型 延迟(周期) 带宽(GB/s) 容量
寄存器 1 极高 每个SM 256KB
共享内存 ~20 ~10000 每个SM 164KB
L1缓存 ~30 ~5000 每个SM 192KB
L2缓存 ~200 ~2000 40MB
全局内存 ~400 ~2000 40GB+

3.4 总结:如何利用硬件特性写出高性能代码?

好了,硬件结构讲完了。那对我们写代码有什么实际指导?

  1. 控制寄存器用量:每个线程不要用太多局部变量。如果发现占用率低,先检查寄存器使用量。
  2. 善用共享内存:数据复用场景,优先考虑共享内存。但注意不要超过SM的容量限制。
  3. 全局内存访问要合并:这是最基础也最重要的优化。写代码时,确保线程索引和内存地址是线性对应的。
  4. 考虑使用Tensor Core:如果你的计算是矩阵乘法,别自己手写,用cuBLAS。

我个人习惯是:先写一个能跑的版本,然后用NVIDIA的Nsight Compute工具分析性能瓶颈。看看是计算受限还是内存受限,再针对性地优化。

记住一句话:GPU的硬件设计,就是为了让你用并行度来掩盖延迟。所以,尽量让SM“忙起来”,别让任何一个核心闲着。

下一章,我们会深入讨论线程模型和Warp调度。到时候你会发现,硬件和软件的结合,远比想象中有趣。