第2章:GPU架构概览——从CUDA核心到线程调度

说到GPU加速,咱们得先搞清楚它的骨架长什么样。我在做Harmony校正算法的初期,其实踩过不少坑——很多性能问题说白了就是没弄明白GPU内部怎么干活。所以这一章,我想带你从一个工程师的角度,把NVIDIA CUDA架构的关键部件捋一遍。

你想想看,一个GPU里面塞了几千个核心,它们怎么分工?怎么协同?数据怎么搬来搬去?理解了这些,后面的优化才能有的放矢。

2.1 整体架构:GPU是个“协处理器”

GPU不单独工作,它听CPU指挥。CPU下发任务,GPU批量执行。这个模型叫“异构计算”。

我习惯把GPU看作一个大规模并行执行单元。它擅长的事情就一件——同时跑几千个轻量级线程。Harmony校正里的每个像素点,理论上都可以交给一个线程来处理。

关键概念: GPU是“吞吐量优先”设计,CPU是“延迟优先”设计。哪怕单个线程慢一点,只要同时跑的线程够多,总吞吐量就能拉满。

接下来,我们看GPU内部的核心计算单元——SM。

2.2 SM(流多处理器)——GPU的“计算心脏”

一个GPU由多个SM组成。每个SM内部又包含:

  • CUDA核心:执行整数和浮点运算的实际单元。SM里通常有几十到上百个。
  • 共享内存(Shared Memory):SM内部的高速缓存,由所有CUDA核心共享。延迟极低。
  • 寄存器文件(Register File):分配给每个线程的私有存储。容量有限,但速度最快。
  • Warp调度器(Warp Scheduler):负责在硬件层面切换和发射线程指令。
  • 特殊功能单元(SFU):处理超越函数(如sin、cos、sqrt)等复杂运算。

我记得第一次做SM资源分析时,我还在困惑为什么我的kernel跑得那么慢。后来发现是共享内存分配太多,导致每个SM上能同时驻留的线程数被腰斩。说白了,SM的资源是个“零和博弈”——你给共享内存多了,能跑的线程就少了。

避坑指南: 我曾经只关注CUDA核心数量,忽略了SM内部的资源限制。后来才明白,一个SM能在硬件上同时容纳的线程数是有限的(比如最大2048个线程)。你的kernel每用一条寄存器,就少一个线程的“位置”。

2.3 内存层次:搬数据比算数据更贵

显卡的“内存”不是一块铁板。它有严格的速度和容量分层。我用一张图来帮你建立整体认知:

GPU 内存层次与访问速度 全局内存 (Global Memory) 容量: 4~80 GB | 延迟: ~400-800 周期 L2 缓存 延迟: ~200 周期 SM 0 共享内存 (Shared Mem) L1 缓存 / 纹理缓存 Warp 0 Warp 1 Warp 2 Warp 3 SM 1 共享内存 (Shared Mem) L1 缓存 / 纹理缓存 SM N 共享内存 (Shared Mem) L1 缓存 / 纹理缓存 说明:每个 SM 拥有独立的共享内存和 L1 缓存;全局内存是所有 SM 共享的“大仓库” 速度比较:寄存器 > 共享内存 >> L1 > L2 >> 全局内存

让我逐个分解:

全局内存(Global Memory)

这是GPU最大的存储区域,所有线程都能读写。但它的延迟高达数百个时钟周期。我们在Harmony校正里处理的输入输出图像数据,默认就存在这里。

⚠ 关键: 如果你的kernel每次都直接从全局内存读写像素,性能大概率会拉胯。我建议至少把频繁访问的数据(比如查找表)搬到更近的内存层次去。

共享内存(Shared Memory)

每个SM内部有一小片(通常16~48KB可配置)。它的速度只比寄存器慢一点。对于Harmony校正算法,这是一个“黄金地段”——你可以把像素窗口的邻域数据缓存到共享内存,让相邻线程复用。

我在项目里遇到过这样的情况:一个3x3滤波核,如果每个线程都去全局内存读9个像素,带宽立刻被吃满。换成共享内存后,同一block内的线程只读一次全局内存,然后从共享内存“串门”拿邻居数据,速度翻了不止一倍。

本地内存(Local Memory)

这是寄存器不够用时,编译器偷偷放到全局内存的一个“备胎”。它虽然叫“本地”,但速度跟全局内存一样慢。

避坑: 我曾经写了很大的局部数组,以为在寄存器里,结果性能崩了。一看PTX代码,编译器全给溢出到本地内存了。这时候要么减小数组,要么改用手动tiling。

常量内存和纹理内存

这两个属于“特殊用途”的内存:

  • 常量内存(Constant Memory):只读,有硬件广播机制。用于存储校正参数(如增益矩阵、偏移向量),所有线程同时读同一个地址时效率极高。
  • 纹理内存(Texture Memory):也是只读,支持硬件插值和边界处理。在Harmony校正中,如果你的像素坐标有子像素移位,纹理内存的线性插值可以“免费”提供。
我个人的经验: 对于查找表这类“一次性全部线程读相同地址”的数据,首选常量内存。对于需要随机访问且涉及插值的,纹理内存是更好的选择。

2.4 线程调度:GPU如何让数千个线程“无缝切换”

GPU的调度机制跟CPU完全不一样。CPU切换线程很“重”,要保存恢复上下文。GPU则轻量得多。

Warp——调度的基本单位

在硬件层面,32个线程被绑定成一个Warp。SM里的warp调度器一次只能给一个warp发射一条指令。为什么是32?这是NVIDIA设计的“最小执行单元”。

你想想看,如果你有1600个线程,SM会把它拆成50个warp。调度器在每个时钟周期,选一个可以执行的warp,发射指令。

为什么需要这样设计? 用“流水线”的观点看:当一个warp等待全局内存返回数据时,调度器可以立刻切到另一个warp继续干活。这个切换是硬件级的,零开销。这就叫“延迟隐藏”。

我的观察: 很多GPU加速的坑,都是由于warp内的线程出现了“分支发散”(branch divergence)。比如你写了一个if-else语句,warp里一部分线程走if,一部分走else,那么所有线程实际上都会执行两个分支,只是不满足条件的线程被“掩蔽”了。

分支发散到底多伤性能?

这得看你的分支比例。如果一半的线程走A,一半走B,那整个warp的执行时间就变成了A+B的和,性能减半。我在Harmony校正里遇到过类似问题:一个条件判断用来区分图像边缘像素。乍一看没问题,但边缘比例很小,整体性能还好。但后来我发现,在某些高分辨率图像里,边缘像素恰好凑成一组warp——结果那几个warp的性能就特别差。

怎么办? 我当时的解法是把边缘像素单独提出来做串行处理,主体像素用统一分支。虽然代码变复杂了,但整体性能提升了大约15%。

2.5 总结:给Harmony校正的架构提示

我把这一章的关键点汇总成一个表格,方便你翻回来查阅:

概念对Harmony校正的影响建议
全局内存带宽输入/输出图像帧的读写瓶颈尽量合并访问、使用共享内存缓存
共享内存/寄存器局部窗口计算、查找表缓存用于像素邻域复用、LUT查询加速
常量内存校正参数(增益、偏移、插值系数)所有kernel统一使用const __restrict__修饰
纹理内存子像素插值、空间变换使用cudaBindTexture绑定图像数据
Warp 分支发散条件判断导致的流水线浪费尽量将统一运算与边界处理分离