GPU架构与内核执行基础:SM架构、线程层次与内存层次

在进入NVIDIA Nsight Compute(以下简称Ncu)的性能分析之前,必须首先建立对GPU底层执行模型的清晰认知。Ncu的所有采样指标、吞吐率计算、瓶颈判定逻辑,本质上都是围绕“SM(Streaming Multiprocessor,流式多处理器)如何调度线程、线程如何访问内存”这两个核心问题展开的。如果对硬件执行模型理解不透彻,分析报告中的“SM Busy”“Warp Stall”“DRAM Throughput”等指标就只是一堆无意义的数字。本章将系统拆解GPU的物理执行单元(SM)、逻辑线程层次(Grid/Block/Warp)以及完整的内存层次结构,并说明它们如何共同决定内核的实际运行效率。

GPU 内核执行模型总览:从线程到硬件 逻辑线程层次 Grid(内核启动) dimGrid = (bx, by, bz) Block(CTA) dimBlock = (tx, ty, tz) Warp(32线程) SIMT 调度单元 映射 物理硬件资源(SM 内部) Streaming Multiprocessor (SM) SM 子分区 0 32 CUDA 核心 4 个调度器 Warp 槽位 寄存器堆 SM 子分区 1 32 CUDA 核心 4 个调度器 Warp 槽位 寄存器堆 SM 子分区 2 32 CUDA 核心 4 个调度器 Warp 槽位 寄存器堆 SM 子分区 3 32 CUDA 核心 4 个调度器 Warp 槽位 寄存器堆 共享内存 / L1 缓存(统一架构,约 128-256 KB) 内存层次与访问路径 寄存器文件 每个线程私有 ~64K 32-bit 寄存器/SM 共享内存 Block 内共享 ~48-228 KB/SM 局部内存 寄存器溢出时使用 物理位于 DRAM L2 缓存 所有 SM 共享 ~40-50 MB 全局内存 DRAM (HBM/GDDR) 带宽 ~1-3 TB/s 延迟: ~1 周期 延迟: ~20-30 周期 延迟: ~200-400 周期 延迟: ~200-400 周期 延迟: ~400-800 周期 带宽: 极高 带宽: ~10-20 TB/s 带宽: 同 DRAM 带宽: ~5-10 TB/s 带宽: ~1-3 TB/s 关键路径:Warp 指令 → 调度器 → 执行单元 → 寄存器/共享内存/L1 → L2 → DRAM Ncu 的采样点:Warp State、Pipe Utilization、Memory Throttle 均基于此模型

2.1 SM架构:GPU的并行计算核心

SM(Streaming Multiprocessor)是GPU中最基本的物理计算单元。以NVIDIA Ampere架构的A100为例,每个A100 GPU包含108个SM,而每个SM内部又划分为4个处理块(SM子分区)。每个处理块拥有32个FP32 CUDA核心、16个FP64单元、16个INT32单元以及4个Tensor Core(第三代)。这种设计使得一个SM能够同时执行来自不同Warp的指令,实现指令级并行与线程级并行的叠加。

理解SM架构的关键在于“Warp调度”机制。每个SM子分区拥有独立的Warp调度器和指令分发单元,每个周期可以发射一条指令到对应的执行单元。当某个Warp因等待内存访问结果而暂停时,调度器会立即切换到另一个就绪的Warp,这种机制称为“零开销上下文切换”。正是这种设计,使得GPU能够用“大量线程并行”来掩盖“单线程延迟”,从而在高延迟的内存访问场景下依然保持较高的吞吐率。

架构代际SM子分区数每SM最大Warp数每SM寄存器数共享内存/SM代表GPU
Volta (V100)46465,536 (256KB)96KBV100, Titan V
Turing (RTX 20)43265,53664KBRTX 2080 Ti
Ampere (A100)46465,536164KBA100, RTX 30
Hopper (H100)46465,536228KBH100, H800

在Ncu的Source页中,Executed Ipc Active指标反映了SM子分区每个周期的指令发射数。如果该值长期低于理论峰值(如Ampere的4),说明Warp调度器存在空闲周期,可能是由于Warp阻塞(等待内存、同步)或指令依赖导致。此时应结合Warp Stall详情页判断具体阻塞原因。

经验提醒:在分析SM利用率时,不要只看“SM Busy”百分比。一个SM可能显示95%忙碌,但实际只有2个Warp在交替执行,指令发射率很低。务必同时查看Issued Warp Per Scheduler和Executed Ipc Active,才能判断SM是否真正被高效利用。

2.2 线程层次:Grid、Block与Warp的协作逻辑

CUDA的线程层次是程序员直接控制的逻辑结构,它决定了数据如何被划分、线程如何协作。内核启动时,程序员指定Grid维度(即Block的排列方式)和Block维度(即线程的排列方式)。例如,kernel<<<gridDim, blockDim>>>()中,gridDim和blockDim可以是三维向量,但实际使用中通常只使用x维度。

Block是线程调度的基本单位。GPU的GigaThread引擎会将Block分配到各个SM上,一个SM可以同时驻留多个Block(取决于资源限制)。Block内的线程通过共享内存和__syncthreads()进行协作。Warp是硬件调度的最小单位,一个Warp包含32个连续的线程ID。当Block大小为128时,它会被拆分为4个Warp,每个Warp独立调度执行。

理解线程层次对性能优化至关重要,因为它直接决定了两个关键指标:Occupancy(占用率)和Block调度效率。Occupancy定义为“驻留在SM上的Warp数 / SM最大支持Warp数”。较高的Occupancy有助于隐藏内存延迟,但过高的Occupancy也可能导致寄存器溢出或缓存抖动。

核心原理:Block大小并非越大越好。假设每个线程需要32个寄存器,SM共有65,536个寄存器。若Block大小为256,则每个Block需要8,192个寄存器,SM最多驻留8个Block(65,536 / 8,192 = 8),即2,048个线程,对应64个Warp,达到Ampere架构的Warp上限。若Block大小为512,则每个Block需要16,384个寄存器,SM最多驻留4个Block,同样达到64个Warp上限。但若每个线程需要64个寄存器,Block大小为512时,SM只能驻留2个Block(32个Warp),Occupancy降至50%。

在Ncu的Launch Statistics页面中,可以查看Block Limit(受限于寄存器、共享内存或Warp槽位)和Occupancy。如果发现Block Limit Registers为限制因素,说明每个线程使用的寄存器过多,可以通过__launch_bounds__或maxrregcount编译选项限制寄存器使用量。

// 示例:使用 __launch_bounds__ 限制寄存器使用
__global__ void __launch_bounds__(256, 8) myKernel(float* data) {
    // 最多256线程/Block,至少8个Block/SM
    // 编译器将据此限制寄存器分配,确保Occupancy
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    // ... 内核逻辑
}

2.3 内存层次:从寄存器到全局内存的完整路径

GPU的内存层次设计遵循“容量小、速度快”与“容量大、速度慢”的权衡原则。从线程视角看,内存分为以下几个层级:

  • 寄存器文件(Register File):每个线程私有的最快存储,访问延迟约1个周期。SM的寄存器文件总容量固定(如A100为256KB),被所有驻留线程瓜分。若每个线程使用超过64个寄存器,将显著降低Occupancy。
  • 共享内存(Shared Memory):Block内所有线程共享,访问延迟约20-30周期。共享内存与L1缓存共享物理存储(统一架构),可通过cudaFuncSetAttribute动态调整比例。共享内存支持Bank并行访问,但存在Bank Conflict问题。
  • 局部内存(Local Memory):当寄存器溢出或访问动态索引的局部数组时,数据被放置在局部内存中。局部内存物理上位于DRAM,但会经过L1/L2缓存,延迟远高于寄存器。
  • L2缓存:所有SM共享的片上缓存,容量约40-50MB(A100)。L2是全局内存访问的必经之路,也是跨SM数据共享的枢纽。
  • 全局内存(Global Memory):即显存DRAM(HBM2e或GDDR6),容量大但延迟高达400-800周期。全局内存的访问效率高度依赖“合并访问”(Coalescing)模式。

在Ncu的Memory Workload Analysis页面中,可以查看Memory Throughput、L1 Hit Rate、L2 Hit Rate以及DRAM Throughput。这些指标帮助判断内核是“计算密集型”还是“内存密集型”。如果DRAM吞吐率接近峰值(如A100的2TB/s),说明内核受内存带宽限制;如果L2命中率低且DRAM吞吐率不高,则可能存在访问模式问题。

内存类型作用域延迟容量优化要点
寄存器线程私有~1周期256KB/SM减少每线程寄存器使用,避免溢出
共享内存Block共享~20-30周期228KB/SM避免Bank Conflict,使用Padding
局部内存线程私有~200-400周期同DRAM避免动态索引局部数组
L2缓存全局共享~200-400周期40-50MB提高时间/空间局部性
全局内存所有线程~400-800周期40-80GB保证合并访问,使用向量化类型
风险提示:局部内存是性能陷阱。当你在内核中声明一个较大的局部数组(如float temp[128])且索引为动态变量时,编译器可能将其放置在局部内存中。Ncu的Local Memory统计会显示Local Load/Store次数。如果该值过高,应重构代码,将数据放入共享内存或使用静态索引。

2.4 执行模型:Warp调度与延迟隐藏

GPU的执行模型可以概括为“大量Warp并行 + 零开销切换”。每个SM子分区拥有一个Warp调度器,每个周期从就绪Warp中选择一个发射指令。所谓“就绪”,是指该Warp的指令操作数已准备就绪(寄存器已写入、内存访问已完成)。当Warp因等待内存数据而阻塞时,调度器切换到其他Warp,从而隐藏内存延迟。

延迟隐藏的效率取决于两个因素:Occupancy和指令级并行(ILP)。高Occupancy意味着有更多Warp可供调度,但每个Warp的指令级并行也能减少调度压力。例如,一个Warp内连续多条独立指令可以连续发射,无需等待。Ncu的Warp State页面会显示Warp处于Active、Stalled、Waiting等状态的比例,帮助定位阻塞原因。

常见的Warp阻塞原因包括:Long Scoreboard(等待全局内存)、Short Scoreboard(等待共享内存)、Barrier(等待同步)、MIO Throttle(内存输入输出队列满)、Not Selected(调度器选择了其他Warp)。在Ncu的Warp Stall详情页中,可以按阻塞原因排序,找出最突出的瓶颈。

案例拆解:矩阵乘法内核的延迟隐藏分析
假设我们有一个简单的矩阵乘法内核,每个线程计算输出矩阵的一个元素。初始版本使用全局内存直接读取A和B矩阵,每个线程需要两次全局内存访问(读A和读B)和一次写操作。在Ncu分析中,发现Long Scoreboard占比高达70%,说明Warp大量时间在等待全局内存数据。

优化步骤:
1. 使用共享内存分块(Tiling):将A和B的子块加载到共享内存,Block内线程复用数据。
2. 调整Block大小为16x16(256线程),每个Block加载两个16x16的共享内存块。
3. 重新分析后,Long Scoreboard降至30%,Short Scoreboard(等待共享内存)升至40%,但整体执行时间缩短了3.2倍。

复盘:共享内存虽然延迟低于全局内存,但Bank Conflict可能导致额外的等待。通过Padding(如将共享内存数组声明为__shared__ float As[16][17])消除了Bank Conflict,进一步提升了性能。

在实际优化中,需要根据Ncu的指标动态调整策略。如果MIO Throttle占比高,说明共享内存或L1的访问队列已满,应减少共享内存访问频率或优化访问模式;如果Barrier占比高,说明Block内同步过于频繁,应减少__syncthreads()调用或调整Block大小。

总结复盘:GPU性能优化的本质是“让Warp调度器始终有就绪的Warp可发射”。Ncu的所有分析工具都围绕这一目标展开:
- 若Executed Ipc Active低 → 增加Occupancy或ILP
- 若Long Scoreboard高 → 使用共享内存/改善合并访问
- 若Short Scoreboard高 → 优化共享内存访问模式
- 若Barrier高 → 减少同步或调整Block粒度
掌握这些对应关系,就能将Ncu的原始数据转化为明确的优化行动。