第2章:GPU架构概览——从CUDA核心到线程调度
说到GPU加速,咱们得先搞清楚它的骨架长什么样。我在做Harmony校正算法的初期,其实踩过不少坑——很多性能问题说白了就是没弄明白GPU内部怎么干活。所以这一章,我想带你从一个工程师的角度,把NVIDIA CUDA架构的关键部件捋一遍。
你想想看,一个GPU里面塞了几千个核心,它们怎么分工?怎么协同?数据怎么搬来搬去?理解了这些,后面的优化才能有的放矢。
2.1 整体架构:GPU是个“协处理器”
GPU不单独工作,它听CPU指挥。CPU下发任务,GPU批量执行。这个模型叫“异构计算”。
我习惯把GPU看作一个大规模并行执行单元。它擅长的事情就一件——同时跑几千个轻量级线程。Harmony校正里的每个像素点,理论上都可以交给一个线程来处理。
接下来,我们看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的资源是个“零和博弈”——你给共享内存多了,能跑的线程就少了。
2.3 内存层次:搬数据比算数据更贵
显卡的“内存”不是一块铁板。它有严格的速度和容量分层。我用一张图来帮你建立整体认知:
让我逐个分解:
全局内存(Global Memory)
这是GPU最大的存储区域,所有线程都能读写。但它的延迟高达数百个时钟周期。我们在Harmony校正里处理的输入输出图像数据,默认就存在这里。
共享内存(Shared Memory)
每个SM内部有一小片(通常16~48KB可配置)。它的速度只比寄存器慢一点。对于Harmony校正算法,这是一个“黄金地段”——你可以把像素窗口的邻域数据缓存到共享内存,让相邻线程复用。
我在项目里遇到过这样的情况:一个3x3滤波核,如果每个线程都去全局内存读9个像素,带宽立刻被吃满。换成共享内存后,同一block内的线程只读一次全局内存,然后从共享内存“串门”拿邻居数据,速度翻了不止一倍。
本地内存(Local Memory)
这是寄存器不够用时,编译器偷偷放到全局内存的一个“备胎”。它虽然叫“本地”,但速度跟全局内存一样慢。
常量内存和纹理内存
这两个属于“特殊用途”的内存:
- 常量内存(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继续干活。这个切换是硬件级的,零开销。这就叫“延迟隐藏”。
分支发散到底多伤性能?
这得看你的分支比例。如果一半的线程走A,一半走B,那整个warp的执行时间就变成了A+B的和,性能减半。我在Harmony校正里遇到过类似问题:一个条件判断用来区分图像边缘像素。乍一看没问题,但边缘比例很小,整体性能还好。但后来我发现,在某些高分辨率图像里,边缘像素恰好凑成一组warp——结果那几个warp的性能就特别差。
怎么办? 我当时的解法是把边缘像素单独提出来做串行处理,主体像素用统一分支。虽然代码变复杂了,但整体性能提升了大约15%。
2.5 总结:给Harmony校正的架构提示
我把这一章的关键点汇总成一个表格,方便你翻回来查阅:
| 概念 | 对Harmony校正的影响 | 建议 |
|---|---|---|
| 全局内存带宽 | 输入/输出图像帧的读写瓶颈 | 尽量合并访问、使用共享内存缓存 |
| 共享内存/寄存器 | 局部窗口计算、查找表缓存 | 用于像素邻域复用、LUT查询加速 |
| 常量内存 | 校正参数(增益、偏移、插值系数) | 所有kernel统一使用const __restrict__修饰 |
| 纹理内存 | 子像素插值、空间变换 | 使用cudaBindTexture绑定图像数据 |
| Warp 分支发散 | 条件判断导致的流水线浪费 | 尽量将统一运算与边界处理分离 |