一、多GPU编程
硬件的单机性能天花板短期内已很难大幅突破,但“单线程不够就上多线程”的思路依然有效。一个 GPU 搞不定,那就两个、三个……直到集群能承载的极限。本质上,这就是从单机到集群的大规模并行处理,而且是多维度的并行。
二、CUDA的支持
对 CUDA 来说,作为 GPU 上层的抽象框架,天然就应该支持多 GPU 场景。毕竟 GPU 是自己造的,也只有自己最清楚底层如何管理和通信才能更高效。
CUDA 通过主机 API、驱动程序框架以及 GPU 硬件的底层能力来支撑多 GPU 编程,主要技术点包括:
- 支持主机线程的 CUDA 上下文管理
- 支持所有 GPU 的统一内存寻址
- GPU 间的 P2P 大内存传送
- 更低粒度的 P2P GPU 内存加载和存储访问
- 通过进程间通信、NCCL 并行归约、MPI 及 GPU-Direct RDMA 等方式,支持更高层抽象及系统软件
并行编程的难度大家都清楚,关键在于如何高效管理多个 GPU 的资源和任务匹配。重点就是不同物理空间下 CUDA 上下文的管理,涉及任务的协调、分配,以及不同硬件间的通信和任务资源的同步。
三、多GPU下的设备和内存管理
在多 GPU 编程场景中,需要重点关注两个方面:
-
设备的管理
多个 GPU 如何获取、如何发现?CUDA 提供了枚举接口 cudaGetDeviceCount 和 cudaGetDeviceProperties 等。同时,多个 GPU 间的设备切换可用 cudaSetDevice。每个设备也拥有自己的流、事件及相关控制接口,常见的有 cudaEventRecord、cudaEventSynchronize 以及 cudaStreamWaitEvent 等。
-
内存的管理
无论在哪种设备上编程,CPU、GPU、ARM 还是 DSP,内存管理都是最重要的环节之一。在多 GPU 环境下,更接近于分布式编程,如何在指定的 GPU 间进行通信成为重中之重,也就是 P2P 内存传输、访问以及数据一致性。相关接口如 cudaMemcpy、cudaMemcpyDeviceToDevice、cudaMemcpyPeer、cudaMemcpyPeerAsync 及 cudaMemcpy3DPeerAsync 等。
在多个 GPU 间如何进行内存管理(包括分配机制、内存类型等),如何在 Windows 或 Linux 下处理 IOMMU 设备、PCI 访问控制服务及虚拟化机制,这些在 CUDA 的官方文档中都有专门的细节说明,并非简单几个接口就能说清。有兴趣的可以直接翻阅最新文档。
四、开发方式
多 GPU 的开发方式,本质上和分布式编程、多核编程相似。只不过 CUDA 编程分成了主机端和设备端,于是形成了 CPU 端的 N 与 GPU 间的 N 如何协调的问题。
-
一个主机线程操作多个 GPU
最容易理解的形式,一个大“总管”来管理多个 GPU,完成设备、内存、任务的分配和处理。
-
多个主机线程管理多个 GPU,线程与 GPU 一一映射
这是一种相对简单的并行方式:任务并行,但任务间交互很少。抽象来看,就是一个 GPU 编程的模型,而且同一线程的内存地址空间是共享的,实现起来比较直观。
-
多个主机进程,进程与 GPU 一一映射
这个与上面类似,只是用进程作为映射单元。进程的地址空间是独立的,因此卡间通信会更复杂,更适合某些特定场景,例如 MPS。
-
多进程多线程管理多 GPU
最复杂的情况之一:主机通过多个进程,每个进程又包含多个线程,每个线程与 GPU 一一映射操作。资源调度和通信的复杂度大幅增加。
-
多节点的 NVLink 连接集群,在系统层面管理线程、进程与 GPU 的驱动映射
这已经可以类比为分布式编程,也是多 GPU 最复杂的形态。一般开发者很少会直接面对这种场景。
五、总结
多 GPU 编程的实战环境暂时无法完全复现,所以这里就不提供例程了。本文算是对 CUDA 多 GPU 编程的一种前瞻性开发说明,大家有机会可以自行实践。一般来说,多数普通开发者遇到这种场景的概率确实不大。如果想进一步交流 GPU 并行编程的思考与技巧,欢迎来云栈社区一起探讨。
|