1. GPU计算基础:GPU架构概述、CUDA编程模型、GPU与CPU的协同工作模式
各位同学,咱们今天聊聊GPU计算的基础。说实话,我入行那会儿,GPU还只是用来打游戏的。谁能想到,现在量化交易系统里,GPU已经成了标配。我个人习惯把GPU比作「千手观音」——它擅长同时干很多简单的事,而CPU更像一个「全能博士」,单兵作战能力极强。
1.1 GPU架构概述
先看一张图,这是我手绘的GPU核心架构简图。你想想看,CPU里可能就几个核心,每个核心都配了大缓存、复杂控制单元。GPU呢?它把大量晶体管都堆在了计算单元上。
这张图很直观。CPU那边,每个核心都自带「豪宅」——大缓存、复杂控制逻辑。GPU这边,每个SM(流多处理器)就像「集体宿舍」,大家共享资源。我刚开始做量化回测时,用CPU跑蒙特卡洛模拟,一个下午才出结果。换成GPU后,十分钟搞定。差距就这么大。
核心差异总结:
- CPU:延迟优先,适合复杂逻辑、分支预测、串行任务
- GPU:吞吐优先,适合数据并行、简单计算、大规模并发
1.2 CUDA编程模型
CUDA是NVIDIA推出的并行计算平台。说白了,它让你能用C++风格的代码,直接指挥GPU干活。我个人觉得,理解CUDA的关键就三个概念:线程层次结构、内存层次结构、核函数。
线程层次结构
GPU的线程组织方式,有点像军队编制:
- 线程 (Thread):最小编程单元,每个线程执行相同的指令
- 线程块 (Block):一组线程,可以协作、共享数据
- 网格 (Grid):一组线程块,构成整个计算任务
我在项目中遇到过一个问题:一开始把线程块设得太大,结果每个块能用的共享内存不够了。后来我学乖了,先算好资源上限,再定块大小。
内存层次结构
GPU的内存体系,速度从快到慢依次是:
| 内存类型 | 作用域 | 速度 | 容量 |
|---|---|---|---|
| 寄存器 | 单个线程 | 最快 | 极小(每个线程几十个) |
| 共享内存 | 线程块内 | 快 | 小(几十KB) |
| 全局内存 | 所有线程 | 慢 | 大(几GB到几十GB) |
| 常量内存 | 所有线程 | 快(只读) | 小(64KB) |
嗯,这里要注意:全局内存虽然容量大,但访问延迟很高。我见过有人把整个数据集都放全局内存里,结果性能还不如CPU。后来改成用共享内存做数据分块,速度直接翻了三倍。
核函数示例
写一个最简单的向量加法,感受一下CUDA代码长什么样:
// 核函数定义
__global__ void vecAdd(float *A, float *B, float *C, int N) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < N) {
C[i] = A[i] + B[i];
}
}
// 调用核函数
int main() {
// ... 分配内存、拷贝数据 ...
vecAdd<<<gridSize, blockSize>>>(d_A, d_B, d_C, N);
// ... 拷贝结果回CPU ...
}
你看,__global__ 告诉编译器这是个核函数,在GPU上跑。<<<gridSize, blockSize>>> 指定了线程组织方式。我刚开始写的时候,老是把 blockIdx 和 threadIdx 搞混。后来记住一句话:blockIdx 是块编号,threadIdx 是块内线程编号,再也没错过。
避坑指南: 我曾经在核函数里用了 printf 调试,结果发现GPU上的 printf 是异步的,输出顺序完全乱套。后来改用 cuda-gdb 或者把中间结果写回CPU再打印,才解决问题。
1.3 GPU与CPU的协同工作模式
GPU不是万能的。它擅长的是「数据并行」——就是同样的操作,作用在大量数据上。但遇到复杂的控制逻辑、文件I/O、网络通信,还是得靠CPU。
我习惯把这种协同模式叫做「CPU当老板,GPU当工人」。老板(CPU)负责:
- 解析行情数据
- 管理交易策略逻辑
- 处理网络请求和文件读写
- 调度GPU任务
工人(GPU)负责:
- 批量计算指标(如移动平均、波动率)
- 蒙特卡洛模拟
- 矩阵运算(如协方差矩阵)
- 并行回测
典型的协同流程是这样的:
- CPU从交易所接收行情数据
- CPU将数据拷贝到GPU显存
- GPU启动核函数,并行计算
- CPU等待GPU完成(或异步处理其他任务)
- GPU将结果拷贝回CPU内存
- CPU根据结果执行交易决策
注意: 数据在CPU和GPU之间传输是有开销的。我见过一个团队,每次计算都来回拷贝数据,结果90%的时间都花在传输上。正确的做法是:尽量让数据留在GPU端,减少传输次数。比如,把整个历史行情数据一次性拷贝到显存,然后在GPU上完成所有计算。
你想想看,如果每次计算都来回拷贝,那还不如直接用CPU算呢。所以,数据传输是GPU编程的第一性能瓶颈。我一般会用 cudaMemcpyAsync 配合流(Stream)实现异步传输,让计算和传输重叠起来。
好了,这一章的内容就这些。GPU计算基础打牢了,后面讲资源调度和负载均衡时,你才能理解为什么有些调度策略是有效的。记住一句话:理解硬件,才能用好硬件。