GPU计算基础:CPU与GPU架构对比、CUDA编程模型简介、GPU内存层次结构、并行计算思维
各位同学好,我是老张。今天咱们聊聊GPU计算的基础知识。说实话,我刚开始接触GPU编程时,也踩过不少坑。记得第一次用CUDA写一个简单的向量加法,结果跑出来比CPU还慢——当时我整个人都懵了。后来才明白,不理解硬件架构就去写代码,就像不懂交通规则就上路开车,迟早要出事。
所以这一章,咱们先把地基打牢。我会从CPU和GPU的架构差异讲起,然后带你看看CUDA的编程模型,再深入GPU的内存层次,最后聊聊并行计算的思维方式。嗯,内容不少,但都是干货。
1. CPU与GPU架构对比:为什么GPU更适合并行计算?
先问大家一个问题:为什么CPU核心数那么少,GPU核心数却成百上千?
说白了,这是设计目标不同导致的。CPU的设计哲学是「单线程性能最大化」。它要处理各种复杂的逻辑分支、乱序执行、分支预测……这些都需要大量的晶体管来支撑。你想想看,一个CPU核心可能只有几个ALU(算术逻辑单元),但控制单元和缓存却占了芯片面积的大半。
GPU就不一样了。它的设计目标是「吞吐量最大化」。GPU把大部分晶体管都用来做计算单元,控制单元和缓存被压缩到极致。我打个比方:CPU就像一辆法拉利,能跑得飞快,但一次只能拉两个人;GPU就像一辆大巴车,速度没那么快,但一次能拉几十个人。
具体差异我整理了一张表:
| 对比维度 | CPU | GPU |
|---|---|---|
| 核心数量 | 4-32个(高性能) | 数千个 |
| 控制单元占比 | 约40% | 约5% |
| 缓存大小 | 大(MB级别) | 小(KB级别) |
| 内存带宽 | 50-100 GB/s | 500-2000 GB/s |
| 适用场景 | 串行任务、复杂逻辑 | 大规模并行计算 |
我在做高频因子计算时,经常遇到这样的场景:需要同时计算上千只股票的因子值。如果用CPU,只能一个一个算;用GPU,可以同时算。这就是GPU的优势所在。
核心要点:CPU擅长「延迟优化」,GPU擅长「吞吐量优化」。在金融计算中,我们追求的是单位时间内处理的数据量,所以GPU天然适合。
2. CUDA编程模型简介:从Hello World开始
CUDA是NVIDIA推出的并行计算平台。说白了,它让我们能用C++风格的语法,直接操作GPU进行计算。
先看一个最简单的例子:
// 核函数:在GPU上执行的函数
__global__ void add_vectors(float *a, float *b, float *c, int n) {
int idx = threadIdx.x + blockIdx.x * blockDim.x;
if (idx < n) {
c[idx] = a[idx] + b[idx];
}
}
int main() {
int n = 1000000;
float *d_a, *d_b, *d_c;
// 在GPU上分配内存
cudaMalloc(&d_a, n * sizeof(float));
cudaMalloc(&d_b, n * sizeof(float));
cudaMalloc(&d_c, n * sizeof(float));
// 启动核函数:1个block,256个线程
add_vectors<<<1, 256>>>(d_a, d_b, d_c, n);
// 同步并释放内存
cudaDeviceSynchronize();
cudaFree(d_a);
cudaFree(d_b);
cudaFree(d_c);
return 0;
}
这里有几个关键概念:
- 核函数(Kernel):用
__global__修饰的函数,在GPU上执行 - 线程(Thread):最小的执行单元
- 线程块(Block):一组线程,可以协作
- 网格(Grid):一组线程块
我个人习惯把线程块想象成一个「工作组」。组内的线程可以共享数据,组之间则相对独立。这个设计很重要——它决定了我们如何组织计算任务。
小技巧:启动核函数时,<<<gridDim, blockDim>>>中的两个参数分别指定网格和线程块的维度。我一般先确定block大小(通常是128或256的倍数),再根据数据量计算grid大小。
3. GPU内存层次结构:速度与容量的博弈
GPU的内存有好几种,速度差异巨大。我曾经因为用错了内存类型,导致一个因子计算程序慢了10倍。所以这块一定要搞清楚。
先看这张图:
这张图展示了GPU内存的层次结构。从上到下,速度越来越慢,容量越来越大。
- 寄存器:每个线程私有,速度最快。但数量有限,一个线程最多能用几十个。我写因子计算时,会把频繁使用的中间变量放在寄存器里。
- 共享内存:同一个Block内的线程可以共享。速度比全局内存快一个数量级。我在做因子聚合计算时,经常用共享内存来缓存数据,避免反复访问全局内存。
- 全局内存:所有线程都能访问,但速度最慢。数据从CPU传到GPU,默认就存在这里。
避坑指南:我曾经犯过一个错误——在核函数里频繁访问全局内存,导致性能瓶颈。后来把热点数据搬到共享内存里,速度提升了5倍。记住:尽量减少全局内存访问,能用共享内存就用共享内存。
4. 并行计算思维:从串行到并行的转变
很多做金融的同学,习惯了串行思维。写代码时,脑子里想的是「先做A,再做B,最后做C」。但GPU编程要求我们换一种思维方式。
并行计算的核心思想是:把一个大任务拆分成无数个小任务,同时执行。
举个例子。假设我们要计算1000只股票的5日均线。串行思维是:
for each stock in stocks:
for each day in days:
calculate_ma5(stock, day)
并行思维是:
// 每个线程负责一只股票的一天计算
__global__ void calculate_ma5_kernel(float *prices, float *ma5, int n_stocks, int n_days) {
int stock_id = blockIdx.x;
int day_id = threadIdx.x;
if (stock_id < n_stocks && day_id < n_days) {
// 计算该股票该日的5日均线
ma5[stock_id * n_days + day_id] = ...;
}
}
你看,我们把1000只股票×1000天 = 100万个任务,同时扔给GPU去算。这就是并行计算的威力。
我个人总结了几条并行计算的心得:
- 数据并行:同样的操作,应用到不同的数据上。这是最常见的模式。
- 任务并行:不同的操作,同时执行。在因子计算中用得较少。
- 流水线并行:把任务分成多个阶段,每个阶段用不同的硬件资源。适合复杂的计算流程。
核心思维转变:从「我该怎么一步步算」变成「我该怎么把任务拆成独立的小块」。每个小块之间不能有数据依赖,否则就无法并行。
嗯,这一章的内容就到这里。记住:理解硬件是写好GPU代码的前提。下一章我们会深入CUDA编程的实战技巧,包括内存优化、线程调度等。到时候我会分享更多我在高频因子计算中踩过的坑和总结的经验。
课后练习:试着用串行思维和并行思维,分别描述「计算1000只股票的日收益率标准差」这个任务。看看你能不能写出对应的伪代码。