GPU架构基础:理解SM、Warp、Thread层级关系,以及内存层次结构

各位同学,今天我们来聊聊GPU架构最核心的东西。说实话,很多人写了很久的CUDA代码,却连SM和Warp的关系都没搞明白。我当年刚入行时也是这样,直到有一次在项目里遇到了严重的性能瓶颈,才逼着自己把底层架构啃了一遍。嗯,今天我就把这些经验分享给你。

一、从宏观到微观:SM、Warp、Thread

先看一张图,这是我手绘的GPU架构层级关系。你仔细看,就能明白它们是怎么嵌套的。

GPU(图形处理器) SM(流多处理器) Warp 0(32线程) Warp 1(32线程) Warp 2(32线程) Warp 3(32线程) SM(流多处理器) Warp 0(32线程) Warp 1(32线程) Warp 2(32线程) Warp 3(32线程) Thread(线程)—— 每个Warp包含32个线程 T0 T1 T2 ...... T30 T31 一个Warp内的32个线程以SIMT方式执行同一指令

1. Thread(线程)—— 最小的执行单元

每个Thread就是一条独立的执行流。你写的每个CUDA核函数,最终都会被拆成成千上万个Thread来跑。我个人习惯把Thread想象成工厂里的工人,每个工人干同样的活,但处理不同的数据。

关键点:Thread是程序员能直接控制的最小单位。但GPU实际调度时,从来不会单独调度一个Thread。

2. Warp(线程束)—— 真正的调度单元

Warp由32个连续的Thread组成。这是GPU硬件调度的基本单位。说白了,SM每次取指令、执行指令,都是以Warp为单位进行的。

为什么会这样?因为GPU的设计哲学是「以空间换时间」。32个Thread共享一套指令单元,同时执行相同的指令,只是处理的数据不同。这种模式叫SIMT(单指令多线程)。

实战经验:我在优化矩阵乘法时发现,如果Warp内出现分支分歧(比如if-else),性能会急剧下降。因为GPU会串行执行两个分支,相当于浪费了一半的线程。所以写代码时尽量让Warp内的32个线程走相同的路径。

3. SM(流多处理器)—— 真正的计算核心

SM是GPU内部最核心的计算单元。一个GPU通常有几十到上百个SM。每个SM包含:

  • 多个Warp调度器
  • 大量的CUDA核心(也叫流处理器)
  • 共享内存(Shared Memory)
  • 寄存器文件(Register File)
  • 一级缓存(L1 Cache)

每个SM可以同时驻留多个Warp。比如一个SM有64个CUDA核心,它可以同时调度2个Warp(64个线程)执行。但实际上,SM会维护一个Warp池,里面可能有几十个Warp在排队。当一个Warp因为访存而等待时,SM会立刻切换到另一个Warp执行——这就是GPU的零开销上下文切换。

注意:SM能同时容纳的Warp数量是有限的,这个上限由寄存器数量和共享内存大小决定。如果你每个Thread用了太多寄存器,SM能容纳的Warp就会减少,导致Occupancy(占用率)下降。我曾经在一个项目里就是因为寄存器使用过多,导致Occupancy从100%掉到了25%,性能直接腰斩。

二、内存层次结构:从快到慢,从大到小

GPU的内存层次,说白了就是「越快的越贵,越贵的越小」。我们来看看这张内存层次图:

寄存器(Register) 每个Thread私有,约256个/线程 局部内存(Local Memory) 寄存器溢出时使用,实际在Global Memory中 共享内存(Shared Memory) 同一个Block内所有Thread共享,约48KB~164KB/SM 全局内存(Global Memory) 所有Thread可访问,容量最大(GB级),延迟最高

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

寄存器是每个Thread私有的,速度跟CPU的寄存器一样快——一个时钟周期就能访问。但数量非常有限。以NVIDIA A100为例,每个SM有65536个寄存器,如果每个Thread用32个寄存器,那一个SM最多能驻留65536/32=2048个线程。

核心原则:能用寄存器就别用Local Memory。编译器有时会把寄存器溢出的变量放到Local Memory里,这会导致性能暴跌。我建议你用--ptxas-options=-v编译选项查看寄存器使用情况,如果发现spill(溢出),就要优化代码了。

2. 局部内存(Local Memory)—— 寄存器的备胎

当寄存器不够用时,编译器会自动把一些变量放到Local Memory里。但Local Memory实际上是在Global Memory里划了一块区域,所以延迟很高(几百个时钟周期)。

你想想看,本来一个时钟周期能搞定的事,现在要等几百个周期,这性能损失有多大。所以写代码时要尽量避免寄存器溢出。

3. 共享内存(Shared Memory)—— 程序员手中的利器

共享内存是同一个Block内所有Thread共享的,速度比Global Memory快很多(大约20-30倍)。它是我们做性能优化时最常用的手段。

我在项目中遇到过这样一个场景:做矩阵转置时,如果直接读写Global Memory,因为访问模式不连续,带宽利用率只有10%左右。后来我把数据先读到Shared Memory里,再转置写回Global Memory,带宽利用率直接飙到了80%以上。

使用技巧:Shared Memory分为32个Bank(存储体)。如果多个Thread同时访问同一个Bank,就会发生Bank Conflict,导致串行化。我建议你尽量让连续的Thread访问连续的Bank,避免冲突。

4. 全局内存(Global Memory)—— 容量最大,延迟最高

Global Memory就是显存,容量最大(几GB到几十GB),但延迟也最高(大约400-800个时钟周期)。所有Thread都能访问,但访问速度取决于访问模式。

这里有个关键概念叫「合并访问」(Coalesced Access)。说白了,就是让连续的Thread访问连续的内存地址。比如Thread 0访问地址0,Thread 1访问地址4,Thread 2访问地址8...这样GPU可以把这些访问合并成一次大的内存事务,效率极高。

避坑指南:我曾经犯过一个低级错误——在Global Memory上做随机访问。每个Thread访问一个随机地址,结果带宽利用率不到5%。后来改成连续访问,性能提升了20倍。记住:Global Memory最怕随机访问,最爱连续访问。

三、内存层次总结

内存类型 作用域 延迟 容量 典型用途
寄存器 单个Thread 1个时钟周期 约256个/线程 局部变量、中间计算结果
局部内存 单个Thread 数百个时钟周期 取决于显存 寄存器溢出时的备胎
共享内存 单个Block 约20-30个时钟周期 48KB~164KB/SM Block内数据共享、数据重用
全局内存 所有Thread 400-800个时钟周期 GB级别 主要数据存储、主机与设备通信

嗯,到这里GPU架构的基础就讲完了。你可能会问:这些知识在实际优化中怎么用?别急,后面我们会一步步深入。记住一句话:理解硬件,才能写出高效的代码。

可可精品资料库