pytorch默认不感知numa,多卡训练易因跨节点内存访问导致20%+性能损失;须用numactl将每张gpu对应的训练进程绑定至其物理直连的numa节点,通过lspci和/sys/bus/pci/devices/xx/numa_node确认归属,并为每个torchrun worker单独执行numactl --cpunodebind --membind。

PyTorch 默认不感知 NUMA,直接跑多卡训练大概率在跨节点内存上“反复横跳”,性能损失 20% 起步。 你得手动把每个 GPU 对应的训练进程,绑到它物理直连的 NUMA 节点上——不是靠 PyTorch 自身配置,而是用 numactl 在启动前做进程级隔离。
怎么确认 GPU 和 NUMA 节点的物理归属?
GPU 不是独立存在的,它插在 PCIe 插槽上,而 PCIe 控制器属于某个 CPU 插槽(即某个 NUMA 节点)。不能凭空猜,必须查硬件拓扑:
- 运行
lspci -vv | grep -A 10 "VGA\|3D",找到你的 GPU 设备,记下它的Bus号(比如04:00.0) - 再运行
cat /sys/bus/pci/devices/0000:04:00.0/numa_node(把04:00.0替换成你的真实 Bus 号),输出0就表示它挂在 NUMA 节点 0 上 - 交叉验证:用
numactl --hardware看 node 0 的cpus列表,确保这些核心和 GPU 在同一物理 CPU 插槽上(例如双路服务器中,node 0 的 CPU 通常对应 Socket 0)
单机多卡训练时如何用 numactl 绑定?
PyTorch 的 torch.distributed.launch 或 torchrun 启动的是多个子进程,每个进程控制一张 GPU。你需要为每个进程单独指定 NUMA 节点,而不是只绑主进程:
- 不要写成
numactl --cpunodebind=0 --membind=0 torchrun ...—— 这只会绑定主调度进程,子进程仍会随机调度 - 正确做法是让每个
torchrunworker 显式调用numactl,例如启动 2 卡训练(GPU 0 在 node 0,GPU 1 在 node 1):
torchrun --nproc_per_node=2 \ --nnodes=1 \ --node_rank=0 \ --master_addr="127.0.0.1" \ --master_port=29500 \ -m torch.distributed.run \ --rdzv_backend=c10d \ --rdzv_endpoint="127.0.0.1:29500" \ --rdzv_id=1 \ --role=trainer \ --max_restarts=0 \ --tee=3 \ --log_dir=./logs \ train.py
然后在 train.py 开头插入:
import os
import subprocess
if int(os.environ.get("LOCAL_RANK", 0)) == 0:
# 假设 GPU 0 → NUMA node 0,GPU 1 → NUMA node 1
numa_map = {0: 0, 1: 1}
node_id = numa_map[int(os.environ["LOCAL_RANK"])]
os.environ["CUDA_VISIBLE_DEVICES"] = os.environ["LOCAL_RANK"]
subprocess.run(["numactl", "--cpunodebind", str(node_id), "--membind", str(node_id), "python", __file__] + sys.argv[1:], check=True)
exit(0)
更稳妥的做法是改用 shell 脚本启动,对每个 rank 显式调用 numactl:
numactl --cpunodebind=0 --membind=0 python -m torch.distributed.run --nproc_per_node=1 --nnodes=1 --node_rank=0 --master_addr="127.0.0.1" --master_port=29500 train.py & numactl --cpunodebind=1 --membind=1 python -m torch.distributed.run --nproc_per_node=1 --nnodes=1 --node_rank=0 --master_addr="127.0.0.1" --master_port=29501 train.py &
为什么不能只用 torch.cuda.set_device() 或 CUDA_VISIBLE_DEVICES?
这两个 API 只控制 GPU 设备可见性与默认设备选择,完全不涉及 CPU 核心或内存分配策略:
-
CUDA_VISIBLE_DEVICES=0,1仅让进程看到两张卡,但进程本身可能被 Linux 调度器扔到任意 CPU 核心上,且内存默认从任意 NUMA 节点分配 -
torch.cuda.set_device(0)只影响后续 CUDA 操作的目标设备,不影响 host 内存分配路径,也不约束 CPU 绑核 - 真正起作用的是
numactl的--cpunodebind和--membind,它们在进程 fork 时就锁定了 CPU 和内存的 NUMA 范围
容易忽略的关键细节
NUMA 绑定不是一劳永逸的开关,尤其在容器或云环境中容易失效:
- Docker 默认禁用
numactl权限,需加--cap-add=SYS_ADMIN才能生效;Kubernetes Pod 需要securityContext.privileged: true或显式挂载/dev/cpu_dma_latency - 如果用了
torch.multiprocessing.spawn启动多进程,spawn 出的子进程不会自动继承父进程的 NUMA 绑定,必须在每个子进程中重新调用numactl或使用os.sched_setaffinity() - 某些 BIOS 设置(如 Node Interleaving)会关闭 NUMA 模式,此时
numactl --hardware显示只有一个 node,所有优化都无效——务必先关掉这个选项
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











