第3章 多GPU编程模型:单进程多设备管理、多进程模型与CUDA Context管理
多GPU编程,说白了就是让多个GPU一起干活。但怎么分配任务、怎么管理设备,这里面门道不少。我在实际项目中见过不少同学一上来就写`cudaSetDevice`,结果数据传错了设备,运行结果全是错的。今天我们就来把这三种模型理清楚。
3.1 单进程多设备管理 —— 我最常用的方式
单进程多设备,就是一个进程里控制多块GPU。我个人习惯把这种模式叫作「显式切换」,你需要手动告诉CUDA当前用哪块卡。
核心API只有两个:cudaSetDevice() 和 cudaGetDevice()。
// 典型用法:多GPU并行计算
int deviceCount;
cudaGetDeviceCount(&deviceCount);
for (int i = 0; i < deviceCount; i++) {
cudaSetDevice(i);
kernel<<<grid, block>>>(d_data[i]);
}
cudaDeviceSynchronize();
为什么我说这个模式最常用?因为简单。你不需要操心进程间通信,数据共享方便。我曾经在一个8卡服务器上做模型并行,就是用单进程+流(Stream)来管理不同设备上的计算,性能非常好。
| 操作 | API | 说明 |
|---|---|---|
| 设置当前设备 | cudaSetDevice(devID) | 后续操作的目标GPU |
| 查询当前设备 | cudaGetDevice(&devID) | 返回当前线程绑定的设备 |
| 设备同步 | cudaDeviceSynchronize() | 等待当前设备所有操作完成 |
| 跨设备拷贝 | cudaMemcpyPeer | GPU之间直接传输数据 |
3.2 多进程模型 —— 集群场景的首选
多进程模型,每个进程管一块GPU。你想想看,在大规模训练时,一个节点几十张卡,单进程管理太复杂,进程之间隔离性好,一个崩溃不影响其他。
实现方式有两种主流方案:
- MPI + GPU:每个MPI进程绑定一个GPU,通过MPI通信。
- fork + cudaSetDevice:父进程fork出子进程,子进程各自设置不同的设备。
// 伪代码:MPI多GPU示例
MPI_Init(&argc, &argv);
int world_rank;
MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
// 每个进程绑定自己的GPU
cudaSetDevice(world_rank % deviceCount);
// 分配内存,运行kernel
// ...
MPI_Finalize();
3.3 CUDA Context与Device管理 —— 底层原理
CUDA Context是什么?说白了就是GPU的「工作空间」。每个Context包含地址空间、资源句柄、调度队列。你可以把Device想象成物理GPU,Context就是逻辑上的虚拟机。
通常你不直接管Context,CUDA Runtime API自动帮你创建。但当你用CUDA Driver API时,就要自己管了。
// Driver API显式创建Context
CUcontext ctx;
cuCtxCreate(&ctx, 0, device);
// 压入当前线程栈
cuCtxPushCurrent(ctx);
// 执行一些操作...
// 弹出
cuCtxPopCurrent(&ctx);
cuCtxDestroy(ctx);
为什么需要了解这个?我曾遇到过一个问题:在多线程环境下,主线程创建了Context,子线程想用同一个Context,结果报错CUDA_ERROR_INVALID_CONTEXT。因为Context绑定线程,子线程要显式cuCtxPushCurrent。
- Runtime API自动管理Context,适合90%的开发场景。
- Driver API手动管理Context,适合需要精细控制的高级场景。
- 一个Device可以有多个Context,但每个线程同时只有一个活动的Context。
3.4 三种模型的适用场景对比
| 模型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 单进程多设备 | 单节点小规模 GPU (≤8卡) | 编程简单,数据共享容易 | 管理复杂度随GPU数上升 |
| 多进程模型 | 大规模集群、多节点 | 隔离性好,容错强 | 跨进程通信开销,调试困难 |
| Context管理 | 框架开发、底层优化 | 灵活控制资源 | 代码复杂度高 |
最后留个建议:除非你写深度学习框架底层,否则优先用单进程多设备 + Runtime API。我自己的项目里90%的情况都够用了。遇到需要跨节点通信时,再上MPI多进程。别一开始就搞复杂的Context管理,容易把自己绕进去。