第3章 多GPU编程模型:单进程多设备管理、多进程模型与CUDA Context管理

多GPU编程,说白了就是让多个GPU一起干活。但怎么分配任务、怎么管理设备,这里面门道不少。我在实际项目中见过不少同学一上来就写`cudaSetDevice`,结果数据传错了设备,运行结果全是错的。今天我们就来把这三种模型理清楚。

本章核心知识点:单进程多设备管理、多进程模型、CUDA Context与Device生命周期。三种模型适合不同场景,选对模型能省一半力气。
多GPU编程模型框架 单进程多设备管理 多进程模型 CUDA Context管理 cudaSetDevice / cudaGetDevice MPI + GPU / fork + cudaSetDevice cuCtxCreate / cuCtxPushCurrent 💡 核心建议 小规模GPU (<8卡) 用单进程多设备,简单可靠 大规模集群用多进程模型,隔离性好 Context管理是底层控制,框架一般帮你做了

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)来管理不同设备上的计算,性能非常好。

个人经验:单进程模式下,记得在每个设备上分配独立的内存空间。我见过有人只setDevice一次,后面所有分配都在卡0上,结果卡1看不见数据。一定要在分配内存前先setDevice。
操作API说明
设置当前设备cudaSetDevice(devID)后续操作的目标GPU
查询当前设备cudaGetDevice(&devID)返回当前线程绑定的设备
设备同步cudaDeviceSynchronize()等待当前设备所有操作完成
跨设备拷贝cudaMemcpyPeerGPU之间直接传输数据

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();
避坑指南:我曾经在项目里用fork+exec生成子进程管理GPU,结果子进程继承了父进程的CUDA Context,导致设备资源冲突。正确做法:fork之后,子进程手动调用cudaSetDevice重新绑定,或者更安全的做法是使用MPI。

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。

重要结论:
  1. Runtime API自动管理Context,适合90%的开发场景。
  2. Driver API手动管理Context,适合需要精细控制的高级场景。
  3. 一个Device可以有多个Context,但每个线程同时只有一个活动的Context。

3.4 三种模型的适用场景对比

模型适用场景优点缺点
单进程多设备单节点小规模 GPU (≤8卡)编程简单,数据共享容易管理复杂度随GPU数上升
多进程模型大规模集群、多节点隔离性好,容错强跨进程通信开销,调试困难
Context管理框架开发、底层优化灵活控制资源代码复杂度高

最后留个建议:除非你写深度学习框架底层,否则优先用单进程多设备 + Runtime API。我自己的项目里90%的情况都够用了。遇到需要跨节点通信时,再上MPI多进程。别一开始就搞复杂的Context管理,容易把自己绕进去。