1. GPU架构基础:从CPU到GPU的思维转变
做GPU优化,首先得理解GPU到底是个什么东西。我见过不少从CPU转过来的同学,上来就按CPU的思路写CUDA代码,结果性能惨不忍睹。说白了,GPU和CPU的设计哲学完全不同——CPU是「全能选手」,GPU是「流水线工人」。
1.1 GPU vs CPU:设计目标的根本差异
CPU的设计目标是「低延迟」——它要快速响应各种不同类型的任务。所以CPU有庞大的缓存、复杂的控制单元、强大的分支预测。而GPU的设计目标是「高吞吐」——它不在乎单个任务慢一点,但同一时间要处理成千上万个任务。
| 对比维度 | CPU | GPU |
|---|---|---|
| 核心数量 | 4-16个高性能核心 | 数千个轻量级核心 |
| 控制单元 | 复杂(分支预测、乱序执行) | 简单(顺序执行为主) |
| 缓存大小 | 大(MB级别) | 小(KB级别) |
| 内存带宽 | 50-100 GB/s | 900-2000 GB/s |
| 适用场景 | 串行、复杂逻辑 | 并行、数据密集型 |
核心观点:CPU擅长「做一件事很快」,GPU擅长「同时做很多事」。如果你的算法是串行的,老老实实用CPU;如果是数据并行的,GPU才是你的菜。
1.2 SM与CUDA Core:GPU的「车间」和「工人」
GPU内部的基本计算单元叫SM(Streaming Multiprocessor,流式多处理器)。你可以把SM想象成一个「车间」,每个车间里有多个CUDA Core——也就是「工人」。
以NVIDIA的Ampere架构为例,每个SM包含128个CUDA Core。一个GPU可能有几十到上百个SM。所以总CUDA Core数量轻松上万。
我的经验:刚开始做CUDA时,我总以为CUDA Core越多越好。后来发现,真正限制性能的往往是「怎么把数据喂给这些Core」。你想想看,一万个工人等着干活,但原材料(数据)送不过来,那工人再多也没用。
SM内部还有几个关键部件:
- Warp Scheduler:负责调度线程。32个线程组成一个warp,这是GPU执行的基本单位。
- Register File:寄存器文件,每个SM有数万个寄存器。
- Shared Memory:共享内存,SM内部的高速存储。
- Tensor Core:专门做矩阵乘法的加速单元(从Volta架构开始引入)。
1.3 显存层次结构:速度与容量的博弈
GPU的存储层次和CPU类似,但带宽和延迟的差异更大。我习惯把显存层次比作「仓库-工作台-手边工具」的关系。
1.3.1 Global Memory(全局显存)
这是GPU最大的存储空间,所有线程都能访问。但它的延迟高达400-800个时钟周期。我刚开始做优化时,经常因为频繁访问Global Memory导致性能瓶颈。后来学乖了——能少访问就少访问。
避坑指南:我曾经写过一个矩阵乘法的kernel,每次计算都从Global Memory读数据,结果性能还不如CPU。后来改成用Shared Memory做分块,速度直接提升了20倍。记住:Global Memory是「万恶之源」,能不用就不用。
1.3.2 Shared Memory(共享内存)
Shared Memory位于SM内部,延迟只有30个时钟周期左右。同一个Block内的线程可以共享这块内存。它就像车间里的「公共工作台」——大家把原材料搬上来,一起加工。
使用Shared Memory的典型模式:
// 从Global Memory加载数据到Shared Memory
__shared__ float s_data[BLOCK_SIZE];
s_data[threadIdx.x] = g_data[blockIdx.x * blockDim.x + threadIdx.x];
__syncthreads(); // 等待所有线程加载完成
// 在Shared Memory上计算
float result = s_data[threadIdx.x] * 2.0f;
1.3.3 Register(寄存器)
寄存器是GPU里最快的存储,访问只需要1个时钟周期。每个SM有数万个寄存器,但每个线程能用的数量有限(通常是255个)。
我个人的习惯是:尽量把频繁使用的变量声明为局部变量,编译器会自动分配到寄存器。但要注意——寄存器用太多会导致「寄存器溢出」,数据被挤到Local Memory(实际在Global Memory里),性能反而下降。
小技巧:用 --ptxas-options=-v 编译选项可以查看每个kernel用了多少寄存器。如果超过32个,就要考虑优化了。
1.4 内存带宽与延迟:性能的「水龙头」和「水管」
带宽决定了单位时间内能传输多少数据,延迟决定了每次访问需要等多久。GPU的带宽很高(HBM2e可达2 TB/s),但延迟也很高(几百个时钟周期)。
为什么会这样?因为GPU的设计思路是「用并行掩盖延迟」——当一个warp在等数据时,立刻切换到另一个warp执行。只要你有足够多的warp,延迟就被「隐藏」了。
| 存储层级 | 典型带宽 | 典型延迟 | 优化策略 |
|---|---|---|---|
| Register | ~20 TB/s | 1 cycle | 多用局部变量 |
| Shared Memory | ~10 TB/s | ~30 cycles | 数据复用、分块 |
| L2 Cache | ~2 TB/s | 200-400 cycles | 合并访问、缓存友好 |
| Global Memory | ~1.5 TB/s | 400-800 cycles | 合并访问、减少访问次数 |
关键认知:GPU优化的本质就是「管理数据流」——让数据尽量留在寄存器或Shared Memory里,减少对Global Memory的访问。你想想看,如果每次计算都要去Global Memory读数据,那再多的CUDA Core也只能干等着。
嗯,这一章的内容就到这里。记住这些基础概念,后面我们会一步步深入——从内存合并到Bank Conflict,从Occupancy到异步拷贝。打好基础,后面的优化技巧才能用得上。