以下示例中均使用nvidia/cuda:12.2.0-base-ubuntu20.04镜像结合nvidia-smi topo -m命令来查看GPU拓扑信息以验证CPU&NUMA亲和性设置的正确性,本文章示例不进行性能对比验证,感兴趣的小伙伴可自行实现。
Docker的亲和性参数
Docker提供了多个参数来控制容器的CPU和NUMA亲和性:
| 参数 | 说明 | 示例 |
|---|---|---|
--cpuset-cpus | 指定容器可使用的CPU核心 | --cpuset-cpus="0-31,64-95" |
--cpuset-mems | 指定容器可使用的NUMA内存节点 | --cpuset-mems="0" |
--cpus | 限制容器可使用的CPU核心数量 | --cpus="8.0" |
--cpu-shares | 设置CPU权重(相对值) | --cpu-shares=1024 |
示例1:单GPU任务
假设我们要运行一个使用GPU0的任务:
docker run --gpus '"device=0"' \
--cpuset-cpus="0-31,64-95" \
--cpuset-mems="0" \
nvidia/cuda:12.2.0-base-ubuntu22.04 \
nvidia-smi topo -m
参数说明:
--gpus '"device=0"':使用GPU0--cpuset-cpus="0-31,64-95":将容器绑定到GPU0的亲和CPU核心--cpuset-mems="0":将容器的内存分配限制在NUMA Node 0
结果输出:
GPU0 CPU Affinity NUMA Affinity GPU NUMA ID
GPU0 X 0-31,64-95 0 N/A
为什么单GPU也需要设置CPU&NUMA亲和性?
虽然是单GPU任务,设置CPU和NUMA亲和性仍然非常重要:
-
优化CPU-GPU数据传输(内存维度):
GPU0连接在NUMA Node 0,即使进程运行在Node 0的CPU上,如果内存分配来自NUMA Node 1,数据从CPU内存传输到GPU时仍需要跨NUMA节点,延迟会增加2-3倍。 -
优化CPU-GPU通信(PCIe维度):
GPU物理连接在特定NUMA节点的PCIe总线上(如GPU0连接在Node 0的PCIe),如果CPU进程运行在其他NUMA节点(如Node 1),CPU与GPU的控制命令、状态查询等PCIe通信都需要经过NUMA互联总线(QPI/UPI),增加额外延迟。 -
提升数据预处理性能:训练任务通常需要大量
CPU进行数据加载和预处理(如图像解码、数据增强),这些操作的内存访问如果跨NUMA会显著降低吞吐量。 -
减少延迟抖动:即使
CPU绑核,内存分配策略不当或PCIe访问跨NUMA仍可能导致性能不稳定。 -
实际性能提升:根据测试,单
GPU训练任务正确设置亲和性后,通常可获得10%-20%的性能提升。
需要注意:CPU亲和性 ≠ NUMA内存亲和性
这是一个常见误解,需要特别说明:
--cpuset-cpus参数:控制容器进程可以在哪些CPU核心上执行(进程调度层面)。--cpuset-mems参数:控制容器进程的内存分配来自哪些NUMA节点(内存分配层面),同时也会影响GPU-内存之间的PCIe DMA传输效率(因为GPU物理连接在特定NUMA节点的PCIe总线上)。
为什么指定了CPU亲和性还需要指定NUMA亲和性?
即使使用--cpuset-cpus="0-31,64-95"将进程限制在NUMA Node 0的CPU上运行,并不意味着内存也会自动从NUMA Node 0分配。原因如下:
-
Linux内核的内存分配策略:
- 如果不指定
--cpuset-mems,内核可能使用默认的内存策略(如default或interleave) - 内核会根据内存压力从任何有可用内存的
NUMA节点分配 - 当
NUMA Node 0内存不足或碎片化时,会自动从Node 1分配
- 如果不指定
-
PCIe通信与DMA传输的影响:GPU通过PCIe连接到特定NUMA节点(GPU0-3连接到Node 0的PCIe Root Complex)- 如果
CPU在Node 0但使用Node 1的GPU,或者CPU在Node 1但使用Node 0的GPU CPU与GPU之间的PCIe事务(寄存器访问、中断处理)都需要跨NUMA互联GPU-内存DMA传输同样受NUMA影响:如果GPU在Node 0但内存在Node 1,GPU读写内存时需要通过QPI/UPI跨NUMA访问,DMA带宽可能降低50%以上- 跨
NUMA的PCIe访问延迟增加,DMA传输效率降低,GPU利用率下降
如果cpuset-cpus和cpuset-mems不一致的情况
执行指令:
docker run --gpus '"device=0"' \
--cpuset-cpus="0-31,64-95" \
--cpuset-mems="1" \
nvidia/cuda:12.2.0-base-ubuntu22.04 \
nvidia-smi topo -m
理论上cpuset-cpus="0-31,64-95"的CPU亲和性应该对应的NUMA亲和性是0,但我们这里强制使用NUMA节点1的内存,看看结果如何。
结果输出:
GPU0 CPU Affinity NUMA Affinity GPU NUMA ID
GPU0 X 0-31,64-95 N/A N/A
NUMA亲和性变成了N/A,表示内存分配不在NUMA Node 0,而是跨节点了。
示例2:多GPU任务(同NUMA节点)
假如使用GPU0-GPU3进行4卡任务:
docker run --gpus '"device=0,1,2,3"' \
--cpuset-cpus="0-31,64-95" \
--cpuset-mems="0" \
nvidia/cuda:12.2.0-base-ubuntu22.04 \
nvidia-smi topo -m
简要说明:
4张GPU都在NUMA Node 0,使用相同的CPU亲和性配置
结果输出:
GPU0 GPU1 GPU2 GPU3 CPU Affinity NUMA Affinity GPU NUMA ID
GPU0 X PIX NODE NODE 0-31,64-95 0 N/A
GPU1 PIX X NODE NODE 0-31,64-95 0 N/A
GPU2 NODE NODE X PIX 0-31,64-95 0 N/A
GPU3 NODE NODE PIX X 0-31,64-95 0 N/A
示例3:多GPU任务(跨NUMA节点)
当需要使用5张GPU时(GPU0-4,跨越两个NUMA节点),需要考虑跨NUMA节点的配置:
docker run --gpus '"device=0,1,2,3,4"' \
--cpuset-cpus="0-63,64-127" \
--cpuset-mems="0,1" \
nvidia/cuda:12.2.0-base-ubuntu22.04 \
nvidia-smi topo -m
注意事项:
- 跨NUMA场景:
GPU0-3在NUMA Node 0,GPU4在NUMA Node 1,必然存在跨NUMA通信 --cpuset-mems="0,1":允许使用两个NUMA节点的内存- 性能优化建议:如果可能,优先使用
4卡(GPU0-3或GPU4-7,单NUMA节点)而非5卡,可避免跨NUMA开销
结果输出:
GPU0 GPU1 GPU2 GPU3 GPU4 CPU Affinity NUMA Affinity GPU NUMA ID
GPU0 X PIX NODE NODE SYS 0-31,64-95 0 N/A
GPU1 PIX X NODE NODE SYS 0-31,64-95 0 N/A
GPU2 NODE NODE X PIX SYS 0-31,64-95 0 N/A
GPU3 NODE NODE PIX X SYS 0-31,64-95 0 N/A
GPU4 SYS SYS SYS SYS X 32-63,96-127 1 N/A