news 2026/9/10 16:28:40

PyTorch NUMA Binding 完全解析:用 torchrun 把分布式 worker 绑定到就近 CPU 核提升性能

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyTorch NUMA Binding 完全解析:用 torchrun 把分布式 worker 绑定到就近 CPU 核提升性能

PyTorch NUMA Binding 完全解析:用 torchrun 把分布式 worker 绑定到就近 CPU 核提升性能

【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch

NUMA(Non-Uniform Memory Access,非一致性内存访问)多路服务器上,进程访问远端内存节点会产生额外延迟。PyTorch 在torch.numa.binding模块中提供了一套 NUMA 绑定工具,配合torchrun--numa-binding参数或LaunchConfig/elastic_launch编程接口,把每个 worker 进程及其线程绑定到其加速卡(GPU 等)所在 NUMA 节点附近的 CPU 上,从而提升内存局部性与整体训练性能。本文以 docs/source/elastic/numa.md 为骨架,结合 torch/numa/binding.py、torch/distributed/run.py 及 elastic 相关源码,系统讲解四种绑定模式、配置项、编程式接入方式与底层 sysfs/sched 实现原理,帮助你判断何时该用、如何配置、以及内部到底做了什么。

一、什么是 NUMA 绑定,为什么需要它

在单颗 CPU 上,所有核心通过统一内存总线访问同一块物理内存,访问延迟一致。而在多路(multi-socket)服务器上,物理内存被划分到多个 NUMA 节点,每个 CPU socket 拥有距离最近的本地内存节点:

  • 进程运行在 CPU socket A 上,访问挂在 socket A 下的内存(本地节点)速度最快;
  • 若进程被调度到 socket B,或访问了挂在 socket B 下的内存,就需要跨 QPI/UPI 总线访问远端节点,产生额外延迟。

PyTorch 的官方说明(见 torch/numa/binding.py 模块 docstring)明确指出:

In NUMA systems, accessing memory on remote NUMA nodes incurs additional latency. PyTorch provides NUMA binding utilities to promote memory locality by binding worker processes to CPUs near their assigned accelerator devices.

也就是说,NUMA 绑定解决的核心问题是:把“干活”的 CPU 与“放数据”的内存尽量凑在一起。分布式训练中,每个 worker 通常使用一个本地加速器(GPU),若该 worker 进程被操作系统调度到了与其 GPU 相距较远的 CPU 上执行,就会频繁访问远端内存。

官方给出的实践参考数值是:NUMA 绑定通常能带来整体 1%–10% 的性能提升,某些工作负载收益显著更大,但也可能完全没有收益binding.pydocstring 原文:“typically results in 1-10% overall performance improvements, but some workloads may obtain much greater benefits or none at all”)。因此它不是一个“开了必然变快”的开关,而是一个在多路服务器 + 多卡场景下值得尝试的优化手段。

二、快速上手:通过 torchrun 开启 NUMA 绑定

最简单的方式是使用torchrun(PyTorch 官方分布式启动器)的--numa-binding参数。官方文档示例为:

torchrun --numa-binding=node --nproc_per_node=8 train.py

该用法同时被记录在两份官方文档中:

  • docs/source/elastic/numa.md 模块 docstring;
  • torch/distributed/run.py 中 torchrun 的 “NUMA Binding” 章节。

命令行参数的取值只能是四种绑定模式之一(与AffinityMode枚举的值一一对应):nodesocketexclusivecore-complex。在 torch/distributed/run.py 中,argparse 定义如下:

parser.add_argument( "--numa-binding", "--numa_binding", type=str, choices=[mode.value for mode in _AffinityMode], # node / socket / exclusive / core-complex default=None, help="Bind worker processes to CPUs near their assigned GPUs for better performance. " "See torch/numa/binding.py for available modes and details.", )

当用户传入该参数后,torch/distributed/run.py 会把字符串解析为NumaOptions,再透传给LaunchConfig

numa_options = ( None if args.numa_binding is None else _NumaOptions(affinity_mode=_AffinityMode(args.numa_binding)) ) config = LaunchConfig( ... numa_options=numa_options, ... )

也就是说,torchrun --numa-binding=node ...与下面“编程式”写法在底层是同一回事。

适用范围提示:从源码看该功能是“加速器无关”的。绑定目标设备通过torch.accelerator.device_count()torch.accelerator.current_accelerator()torch.get_device_module(...).get_device_properties(...)获取(见 binding.py),因此只要加速器实现能通过get_device_properties暴露 PCI 域/总线/设备号,就适用——不仅仅是 CUDA GPU。

三、四种绑定模式 AffinityMode 详解

AffinityMode是一个字符串枚举,定义于 torch/numa/binding.py,包含四种模式。绑定语义均为:“worker 的 local rank == 设备 local index 的那个加速器”决定该 worker 绑到哪些 CPU

模式值枚举成员绑定粒度适用场景
nodeAffinityMode.NODEworker 绑定到“其设备所在 NUMA 节点”的全部 CPU官方建议:不确定时优先用它
socketAffinityMode.SOCKETworker 绑定到“其设备所在 socket 下所有 NUMA 节点”的全部 CPU每个 socket 有多个 NUMA 节点的机器
exclusiveAffinityMode.EXCLUSIVEworker 独占其设备所在 NUMA 节点 CPU 的一个互不重叠子集同节点多设备需核间互不竞争
core-complexAffinityMode.CORE_COMPLEXworker 绑定到其设备所在 NUMA 节点上的某个核簇(共享 L3 的核组)有共享末级缓存核组的现代 CPU

3.1 node(节点模式)

每个 worker 进程及其线程被绑定到“local index 等于 worker local rank 的那个加速设备”所在 NUMA 节点的全部 CPU。源码 docstring 给出的例子:

:若设备 3 位于 NUMA node 1 上,则 local rank 为 3 的 worker 只能运行在 NUMA node 1 的 CPU 上。

当设备与 NUMA 节点一一对应(每节点一块卡)时,这是最直接的“就近绑定”,隔离清晰、无需额外计算,因此官方注释建议“拿不准就用它”(原文:"If in doubt, use this option rather than the others")。

3.2 socket(插槽模式)

每个 worker 绑定到“其设备所在 socket 上所有 NUMA 节点”的全部 CPU。举例:

:若 socket 0 包含设备 3 以及 NUMA node 0–1,则 local rank 为 3 的 worker 会绑定到 NUMA node 0–1 的 CPU。

源码同时说明了一个退化为 node 模式的情形:当每个 socket 本来就只有单个 NUMA 节点时,socket 模式与 node 模式等价

3.3 exclusive(独占/切分模式)

每个 worker 绑定到其设备所在 NUMA 节点的一个互不重叠、按设备数量均分的 CPU 子集,从而保证同一 NUMA 节点上的两个 worker 不共享物理核。docstring 示例:

:若 NUMA node 1 有 16 个物理核,且设备 2 和设备 3 都在该节点上,则 local rank=2 的 worker 绑定核 0–7,local rank=3 的 worker 绑定核 8–15。

从实现看(binding.py),划分以物理核为最小分配单元:先取该 NUMA 节点允许的 CPU 集合,通过 sysfsthread_siblings_list把同一物理核上的逻辑 CPU(含超线程)归组,再按物理核均分给该节点上的所有设备,余数核逐个分给序号靠前的设备,最后把分到的物理核的全部逻辑 CPU(含兄弟超线程)打包给该 worker。若物理核数小于设备数(即每个设备分不到至少一个物理核),会抛出RuntimeError

3.4 core-complex(核簇模式)

每个 worker 绑定到其设备所在 NUMA 节点上的单个 core complex(一组共享同一 L3 缓存的核),在可能的情况下不同 worker 分到不同的核簇。docstring 示例:

:若 NUMA node 1 有两个核簇(核 0–7 共享一个 L3,核 8–15 共享另一个 L3),且设备 2、3 都在该节点,则 local rank=2 的 worker 绑核 0–7,local rank=3 的 worker 绑核 8–15。

实现上(binding.py)先为该 NUMA 节点的每个逻辑 CPU 解析“与其共享同一最大级缓存(max-level cache)的逻辑 CPU 集合”,再按缓存组聚合;排序时优先给可用 CPU 更多的缓存组、其次给索引更小的组,随后按设备在该节点内的相对序号% 缓存组数轮转分配。这可以理解为比 exclusive 更贴近硬件缓存拓扑的“共享末级缓存局部性”优化。

四、编程式配置:NumaOptions 与 LaunchConfig

除命令行外,官方推荐在LaunchConfig+elastic_launch场景直接传NumaOptions。相关使用方式(来源于 torch/distributed/run.py 与 elastic/multiprocessing/api.py):

from torch.distributed.elastic.multiprocessing import Std from torch.distributed.elastic.rendezvous import RendezvousParameters from torch.distributed.elastic.utils import dist_logging from torch.distributed.launcher.api import LaunchConfig, elastic_launch from torch.numa.binding import AffinityMode, NumaOptions config = LaunchConfig( min_nodes=1, max_nodes=1, nproc_per_node=8, rdzv_backend="c10d", rdzv_endpoint="localhost:0", rdzv_configs={"store_type": "agent_only"}, max_restarts=0, monitor_interval=1, start_method="spawn", # 或 "fork" numa_options=NumaOptions( affinity_mode=AffinityMode.NODE, should_fall_back_if_binding_fails=False, ), ) elastic_launch(config=config, entrypoint=train_main)()

NumaOptions是定义在 binding.py 的 frozen dataclass,全部配置项如下:

字段类型必填/默认含义
affinity_modeAffinityMode必填采用哪种绑定模式(见上文四种模式)
should_fall_back_if_binding_failsbool默认FalseTrue时,NUMA 绑定阶段抛出的任何异常会被静默(降级为日志警告),进程继续不带绑定地运行

官方对should_fall_back_if_binding_fails有非常明确的警示(docstring 原文):

There are no expected exceptions, so avoid using this option. Its purpose is simply to mitigate crash risk while conducting mass rollouts of NUMA binding.

即:正常路径下不该有异常,不要主动打开该开关;它只是在大规模灰度上线 NUMA 绑定、需要兜底避免进程崩溃时才用。这与 binding.py 中_handle_exception的行为一致:默认会raise重新抛出异常;只有开关为True时才打印 warning 并继续执行。

五、绑定是如何“落地”的:两条进程包装路径

NUMA 绑定的触发点在 elastic 的 worker 进程启动环节,源码把场景划分为两条路径,分别处理新起子进程(spawn/通过命令行)当前进程内 fork 出的线程/进程

5.1 路径一:包装命令,前缀 numactl

_maybe_wrap_command_args_with_numa_binding(binding.py)会把原始命令包装成由numactl前缀限定的命令,用于Std/命令行形态的子进程启动。子进程处理模块 elastic/multiprocessing/subprocess_handler/subprocess_handler.py 正是调用它来改写启动参数。

底层拼装逻辑在_assemble_numactl_command_args(binding.py):

return ( "numactl", f"--physcpubind={_get_ranges_str_from_ints(logical_cpu_indices)}", *original_command_args, )

即最终效果相当于:

numactl --physcpubind=0-7,16-23 python train.py --local_rank=3 ...

通过numactl启动的新进程天然带上 CPU 亲和性,其后续创建的所有线程默认也继承该亲和性。

5.2 路径二:包装函数,进程内设置亲和性

对于在现有 Python 进程中直接派生 worker(例如start_method="fork")的场景,源码提供了装饰器_maybe_wrap_with_numa_binding(binding.py):在调用被包装的函数(worker 主函数)之前,先对当前进程内所有线程施加 NUMA 绑定。该路径被 elastic/multiprocessing/api.py 用于 fork 型多进程启动。

真正执行绑定的函数是_bind_all_threads_in_current_process_to_logical_cpus(binding.py),其实现值得细读:

  1. 先记录主线程原始亲和性os.sched_getaffinity(0)
  2. 对当前线程(0)调用os.sched_setaffinity(0, logical_cpu_indices)——注释明确说明主线程“应总能绑定成功”,因此放在 try 之外;
  3. 遍历/proc/self/task下所有线程 ID,逐一查询其亲和性;仅当某线程亲和性与主线程原始亲和性一致时才改写它,以此防御性地避免覆盖那些已被其他机制(如线程池)单独设置了亲和性的线程;线程已退出等导致的异常被吞掉。

这种“只动继承默认亲和性的线程”的保守策略,保证了即使进程内有第三方线程池管理线程,NUMA 绑定也不会误伤。

5.3 公共判定流程

两条路径共享同一套“该绑到哪些逻辑 CPU”的判定管线_maybe_apply_numa_binding_to_current_process/_get_validated_logical_cpus_to_bind_to(binding.py):

device_index + affinity_mode │ ▼ _get_logical_cpus_to_bind_to() ← 按四种模式分别计算 │ ▼ _raise_if_binding_invalid() ← 校验 numactl 存在 + CPU 集合非空 │ ▼ numactl 包装(命令行路径) 或 sched_setaffinity(进程内线程绑定路径)

成功或失败都会调用signpost_event(category="numa_binding", name="apply_success"/"apply_exception", ...)上报埋点,便于大规模灰度时观测成功率(binding.py)。

六、底层原理:设备→NUMA 节点→CPU 的映射是怎么算出来的

这是整个实现最“硬核”的部分。NUMA 绑定不依赖任何假设,全部通过读取 Linux sysfs 与内核调度接口完成,核心映射链如下。

6.1 从加速卡反查其所在 NUMA 节点

_get_numa_node_index_for_device_index(binding.py)通过 PCI 拓扑反查设备所属 NUMA 节点:

  1. 通过torch.accelerator拿到当前加速器设备模块,调用get_device_properties(device_index)读取pci_domain_idpci_bus_idpci_device_id
  2. 格式化为 sysfs PCI 地址:f"{domain:04x}:{bus:02x}:{device:02x}.0"(例如0000:dc:00.0);
  3. 读取/sys/bus/pci/devices/{pci_addr}/numa_node得到设备所在 NUMA 节点。

代码还处理了一个真实硬件细节:在单 NUMA 节点系统上,该文件常被写为-1,此时显然存在 node 0,因此用max(value, 0)兜底为 0。

6.2 NUMA 节点 → 允许的 CPU 集合

_get_allowed_logical_cpu_indices_for_numa_node(binding.py)做了两层取交集

读 /sys/devices/system/node/node{N}/cpulist → 该节点全部 CPU 当前线程 os.sched_getaffinity(0) → 当前线程被允许的 CPU 结果 = 两者交集

也就是说,如果外层环境(如 cgroup/容器、taskset)已经限定了 CPU 集合,NUMA 绑定会尊重该限制,只在该限制与节点 CPU 的交集内做文章,而不会越界绑定。

6.3 拓扑归组:物理核与共享缓存

exclusive 模式需要知道“哪些逻辑 CPU 属于同一物理核”,core-complex 模式需要知道“哪些逻辑 CPU 共享最大级缓存”,分别对应两个读取函数:

  • _get_logical_cpu_indices_sharing_same_physical_core_as(binding.py):读取/sys/devices/system/cpu/cpu{N}/topology/thread_siblings_list,得到同一物理核的全部逻辑 CPU(含超线程兄弟核);
  • _get_logical_cpus_sharing_same_max_level_cache_as(binding.py):遍历/sys/devices/system/cpu/cpu{N}/cache/index*,过滤出类型为UnifiedData的缓存条目,取level最大者的shared_cpu_list,从而得到共享该核簇的全部 CPU。

6.4 socket 语义的推导

socket 模式下没有现成的“设备→socket” sysfs 文件,实现是间接推导的(binding.py):

  1. 取设备所在 NUMA 节点的任意一个允许 CPU,读/sys/devices/system/cpu/cpu{N}/topology/physical_package_id得到 socket(物理封装)序号;
  2. 遍历系统所有 NUMA 节点(读/sys/devices/system/node/possible),把physical_package_id相同的节点全部收拢为该 socket 的 NUMA 节点集合;
  3. 把所有这些节点的 CPU 取并集,作为该 worker 的绑定集合。

6.5 工具函数:sysfs 范围字符串 ↔ Python 整数集合

Linux sysfs 中的 CPU/节点列表通常写作"0-2,4,6-7"形式的范围串,源码提供了双向转换工具:

  • _get_set_of_int_from_ranges_str(binding.py):"0-2,4,6-7"{0,1,2,4,6,7}
  • _get_ranges_str_from_ints(binding.py):{0,1,2,4,6,7}"0-2,4,6-7",用于拼装--physcpubind参数。

七、前置条件与失效兜底(应用注意事项)

综合文档与源码,在使用 NUMA 绑定前需要确认以下前提,避免踩坑:

  1. Linux 系统:绑定依赖os.sched_setaffinity/os.sched_getaffinity/proc/sys文件系统,均为 Linux 特性。
  2. 必须安装numactl:即便走进程内线程绑定路径,代码为简单起见仍会统一检查shutil.which("numactl"),缺失时抛出RuntimeError("numactl CLI is required for NUMA binding")(见 binding.py)。
  3. 必须有可用的加速器_get_numa_node_index_for_device_index会先检查torch.accelerator.is_available(),否则抛RuntimeError("No accelerator available for NUMA binding")
  4. 绑定集合不能为空:如果设备所在节点无可用 CPU(例如 CPU 已被 cgroup/taskset 限制在外),会抛出RuntimeError("Must bind to a non-empty set of CPU indices")
  5. exclusive 模式的硬性约束:当某 NUMA 节点上的物理核数量少于挂在该节点的设备数量时无法均分,实现会直接报错(binding.py)。
  6. 超线程语义:exclusive 模式以物理核为单位切分,但切给某 worker 的物理核会包含其全部超线程逻辑核,避免同核兄弟线程干扰其他 worker。
  7. 默认失败即抛出:绑定异常默认会终止启动流程;只有显式设置should_fall_back_if_binding_fails=True才会降级为“不带绑定继续跑”(官方不建议常规使用)。

八、在 PyTorch 源码中继续深入

如果你希望进一步验证或扩展本文结论,可以顺着以下路径阅读:

  • 本文主文档:docs/source/elastic/numa.md(Sphinxautomodule挂载torch.numatorch.numa.binding,其正文即来自模块 docstring);
  • 核心实现:torch/numa/binding.py——AffinityModeNumaOptions及全部_xxx_get_logical_cpus_to_bind_to私有实现均在此文件;
  • 模块入口:torch/numa/init.py(空文件,仅作包名占位,公共 API 实际从binding.py导入);
  • torchrun 参数接线:torch/distributed/run.py 与 torch/distributed/run.py;
  • elastic 运行时接线:torch/distributed/elastic/multiprocessing/api.py(进程内绑定装饰器)、torch/distributed/elastic/multiprocessing/subprocess_handler/subprocess_handler.py(numactl 包装)、torch/distributed/elastic/multiprocessing/init.py(LaunchConfignuma_options字段声明);
  • torchrun 与 elastic 的配套文档:docs/source/elastic/run.md、docs/source/elastic/multiprocessing.md。

总的来说,PyTorch 的 NUMA 绑定是一条零代码侵入、纯启动期生效的性能优化路径:你只需要在torchrun上多传一个--numa-binding参数,或在LaunchConfig中加一个NumaOptions,启动器便会依据四种模式之一,为每个 worker 精确计算并施加 CPU 亲和性。理解node/socket/exclusive/core-complex的粒度差异,结合自己机器的设备-NUMA 拓扑,就能在多路服务器上做出正确选择。

【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 16:27:46

CVAT快捷键:把鼠标放回桌上的6个时刻

CVAT快捷键:把鼠标放回桌上的6个时刻 【免费下载链接】cvat Computer Vision Annotation Tool (CVAT) is a leading platform for building high-quality visual datasets for vision AI. It offers open-source, cloud, and enterprise products, as well as label…

作者头像 李华
网站建设 2026/9/10 16:24:46

Improvement Goal

Improvement Goal 【免费下载链接】oh-my-claudecode Teams-first Multi-agent orchestration for Claude Code 项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-claudecode Objective {specific objective} Target Metric Metric name: {name}Target value…

作者头像 李华
网站建设 2026/9/10 16:24:32

CANN/GE算子执行接口

aclopExecWithHandle 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、Tenso…

作者头像 李华
网站建设 2026/9/10 16:24:06

CANN/ge图引擎ToString函数

ToString 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前端的…

作者头像 李华