1. GPU计算基础:GPU架构概述、CUDA编程模型、GPU与CPU的协同工作模式

各位同学,咱们今天聊聊GPU计算的基础。说实话,我入行那会儿,GPU还只是用来打游戏的。谁能想到,现在量化交易系统里,GPU已经成了标配。我个人习惯把GPU比作「千手观音」——它擅长同时干很多简单的事,而CPU更像一个「全能博士」,单兵作战能力极强。

1.1 GPU架构概述

先看一张图,这是我手绘的GPU核心架构简图。你想想看,CPU里可能就几个核心,每个核心都配了大缓存、复杂控制单元。GPU呢?它把大量晶体管都堆在了计算单元上。

GPU vs CPU 架构对比 CPU 架构 控制单元 ALU 缓存 控制单元 ALU 缓存 大容量 L3 缓存 内存控制器 GPU 架构 SM SM SM SM SM SM SM SM 共享内存 / L1 缓存 全局内存 (GDDR) 少量强大核心 大量简单核心

这张图很直观。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>>> 指定了线程组织方式。我刚开始写的时候,老是把 blockIdxthreadIdx 搞混。后来记住一句话:blockIdx 是块编号,threadIdx 是块内线程编号,再也没错过。

避坑指南: 我曾经在核函数里用了 printf 调试,结果发现GPU上的 printf 是异步的,输出顺序完全乱套。后来改用 cuda-gdb 或者把中间结果写回CPU再打印,才解决问题。

1.3 GPU与CPU的协同工作模式

GPU不是万能的。它擅长的是「数据并行」——就是同样的操作,作用在大量数据上。但遇到复杂的控制逻辑、文件I/O、网络通信,还是得靠CPU。

我习惯把这种协同模式叫做「CPU当老板,GPU当工人」。老板(CPU)负责:

  • 解析行情数据
  • 管理交易策略逻辑
  • 处理网络请求和文件读写
  • 调度GPU任务

工人(GPU)负责:

  • 批量计算指标(如移动平均、波动率)
  • 蒙特卡洛模拟
  • 矩阵运算(如协方差矩阵)
  • 并行回测

典型的协同流程是这样的:

  1. CPU从交易所接收行情数据
  2. CPU将数据拷贝到GPU显存
  3. GPU启动核函数,并行计算
  4. CPU等待GPU完成(或异步处理其他任务)
  5. GPU将结果拷贝回CPU内存
  6. CPU根据结果执行交易决策

注意: 数据在CPU和GPU之间传输是有开销的。我见过一个团队,每次计算都来回拷贝数据,结果90%的时间都花在传输上。正确的做法是:尽量让数据留在GPU端,减少传输次数。比如,把整个历史行情数据一次性拷贝到显存,然后在GPU上完成所有计算。

你想想看,如果每次计算都来回拷贝,那还不如直接用CPU算呢。所以,数据传输是GPU编程的第一性能瓶颈。我一般会用 cudaMemcpyAsync 配合流(Stream)实现异步传输,让计算和传输重叠起来。

好了,这一章的内容就这些。GPU计算基础打牢了,后面讲资源调度和负载均衡时,你才能理解为什么有些调度策略是有效的。记住一句话:理解硬件,才能用好硬件


专注资料整理