Skip to main content
说明

以下示例中均使用nvidia/cuda:12.2.0-base-ubuntu20.04镜像结合nvidia-smi topo -m命令来查看GPU拓扑信息以验证CPU&NUMA亲和性设置的正确性,本文章示例不进行性能对比验证,感兴趣的小伙伴可自行实现。

Docker的亲和性参数

Docker提供了多个参数来控制容器的CPUNUMA亲和性:

参数说明示例
--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任务,设置CPUNUMA亲和性仍然非常重要:

  1. 优化CPU-GPU数据传输(内存维度)GPU0连接在NUMA Node 0,即使进程运行在Node 0CPU上,如果内存分配来自NUMA Node 1,数据从CPU内存传输到GPU时仍需要跨NUMA节点,延迟会增加2-3倍。

  2. 优化CPU-GPU通信(PCIe维度)GPU物理连接在特定NUMA节点的PCIe总线上(如GPU0连接在Node 0PCIe),如果CPU进程运行在其他NUMA节点(如Node 1),CPUGPU的控制命令、状态查询等PCIe通信都需要经过NUMA互联总线(QPI/UPI),增加额外延迟。

  3. 提升数据预处理性能:训练任务通常需要大量CPU进行数据加载和预处理(如图像解码、数据增强),这些操作的内存访问如果跨NUMA会显著降低吞吐量。

  4. 减少延迟抖动:即使CPU绑核,内存分配策略不当或PCIe访问跨NUMA仍可能导致性能不稳定。

  5. 实际性能提升:根据测试,单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 0CPU上运行,并不意味着内存也会自动从NUMA Node 0分配。原因如下:

  1. Linux内核的内存分配策略

    • 如果不指定--cpuset-mems,内核可能使用默认的内存策略(如defaultinterleave
    • 内核会根据内存压力从任何有可用内存的NUMA节点分配
    • NUMA Node 0内存不足或碎片化时,会自动从Node 1分配
  2. PCIe通信与DMA传输的影响

    • GPU通过PCIe连接到特定NUMA节点(GPU0-3连接到Node 0PCIe Root Complex
    • 如果CPUNode 0但使用Node 1GPU,或者CPUNode 1但使用Node 0GPU
    • CPUGPU之间的PCIe事务(寄存器访问、中断处理)都需要跨NUMA互联
    • GPU-内存DMA传输同样受NUMA影响:如果GPUNode 0但内存在Node 1GPU读写内存时需要通过QPI/UPINUMA访问,DMA带宽可能降低50%以上
    • NUMAPCIe访问延迟增加,DMA传输效率降低,GPU利用率下降

如果cpuset-cpuscpuset-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

简要说明4GPU都在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节点)

当需要使用5GPU时(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-3NUMA Node 0GPU4NUMA Node 1,必然存在跨NUMA通信
  • --cpuset-mems="0,1":允许使用两个NUMA节点的内存
  • 性能优化建议:如果可能,优先使用4卡(GPU0-3GPU4-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