ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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/pytorchNUMANon-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 绑定最简单的方式是使用torchrunPyTorch 官方分布式启动器的--numa-binding参数。官方文档示例为torchrun --numa-bindingnode --nproc_per_node8 train.py该用法同时被记录在两份官方文档中docs/source/elastic/numa.md 模块 docstringtorch/distributed/run.py 中 torchrun 的 “NUMA Binding” 章节。命令行参数的取值只能是四种绑定模式之一与AffinityMode枚举的值一一对应node、socket、exclusive、core-complex。在 torch/distributed/run.py 中argparse 定义如下parser.add_argument( --numa-binding, --numa_binding, typestr, choices[mode.value for mode in _AffinityMode], # node / socket / exclusive / core-complex defaultNone, helpBind 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再透传给LaunchConfignuma_options ( None if args.numa_binding is None else _NumaOptions(affinity_mode_AffinityMode(args.numa_binding)) ) config LaunchConfig( ... numa_optionsnuma_options, ... )也就是说torchrun --numa-bindingnode ...与下面“编程式”写法在底层是同一回事。适用范围提示从源码看该功能是“加速器无关”的。绑定目标设备通过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 的核组有共享末级缓存核组的现代 CPU3.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 rank2 的 worker 绑定核 0–7local rank3 的 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 rank2 的 worker 绑核 0–7local rank3 的 worker 绑核 8–15。实现上binding.py先为该 NUMA 节点的每个逻辑 CPU 解析“与其共享同一最大级缓存max-level cache的逻辑 CPU 集合”再按缓存组聚合排序时优先给可用 CPU 更多的缓存组、其次给索引更小的组随后按设备在该节点内的相对序号% 缓存组数轮转分配。这可以理解为比 exclusive 更贴近硬件缓存拓扑的“共享末级缓存局部性”优化。四、编程式配置NumaOptions 与 LaunchConfig除命令行外官方推荐在LaunchConfigelastic_launch场景直接传NumaOptions。相关使用方式来源于 torch/distributed/run.py 与 elastic/multiprocessing/api.pyfrom 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_nodes1, max_nodes1, nproc_per_node8, rdzv_backendc10d, rdzv_endpointlocalhost:0, rdzv_configs{store_type: agent_only}, max_restarts0, monitor_interval1, start_methodspawn, # 或 fork numa_optionsNumaOptions( affinity_modeAffinityMode.NODE, should_fall_back_if_binding_failsFalse, ), ) elastic_launch(configconfig, entrypointtrain_main)()NumaOptions是定义在 binding.py 的 frozen dataclass全部配置项如下字段类型必填/默认含义affinity_modeAffinityMode必填采用哪种绑定模式见上文四种模式should_fall_back_if_binding_failsbool默认False为True时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_bindingbinding.py会把原始命令包装成由numactl前缀限定的命令用于Std/命令行形态的子进程启动。子进程处理模块 elastic/multiprocessing/subprocess_handler/subprocess_handler.py 正是调用它来改写启动参数。底层拼装逻辑在_assemble_numactl_command_argsbinding.pyreturn ( numactl, f--physcpubind{_get_ranges_str_from_ints(logical_cpu_indices)}, *original_command_args, )即最终效果相当于numactl --physcpubind0-7,16-23 python train.py --local_rank3 ...通过numactl启动的新进程天然带上 CPU 亲和性其后续创建的所有线程默认也继承该亲和性。5.2 路径二包装函数进程内设置亲和性对于在现有 Python 进程中直接派生 worker例如start_methodfork的场景源码提供了装饰器_maybe_wrap_with_numa_bindingbinding.py在调用被包装的函数worker 主函数之前先对当前进程内所有线程施加 NUMA 绑定。该路径被 elastic/multiprocessing/api.py 用于 fork 型多进程启动。真正执行绑定的函数是_bind_all_threads_in_current_process_to_logical_cpusbinding.py其实现值得细读先记录主线程原始亲和性os.sched_getaffinity(0)对当前线程0调用os.sched_setaffinity(0, logical_cpu_indices)——注释明确说明主线程“应总能绑定成功”因此放在 try 之外遍历/proc/self/task下所有线程 ID逐一查询其亲和性仅当某线程亲和性与主线程原始亲和性一致时才改写它以此防御性地避免覆盖那些已被其他机制如线程池单独设置了亲和性的线程线程已退出等导致的异常被吞掉。这种“只动继承默认亲和性的线程”的保守策略保证了即使进程内有第三方线程池管理线程NUMA 绑定也不会误伤。5.3 公共判定流程两条路径共享同一套“该绑到哪些逻辑 CPU”的判定管线_maybe_apply_numa_binding_to_current_process/_get_validated_logical_cpus_to_bind_tobinding.pydevice_index affinity_mode │ ▼ _get_logical_cpus_to_bind_to() ← 按四种模式分别计算 │ ▼ _raise_if_binding_invalid() ← 校验 numactl 存在 CPU 集合非空 │ ▼ numactl 包装命令行路径 或 sched_setaffinity进程内线程绑定路径成功或失败都会调用signpost_event(categorynuma_binding, nameapply_success/apply_exception, ...)上报埋点便于大规模灰度时观测成功率binding.py。六、底层原理设备→NUMA 节点→CPU 的映射是怎么算出来的这是整个实现最“硬核”的部分。NUMA 绑定不依赖任何假设全部通过读取 Linux sysfs 与内核调度接口完成核心映射链如下。6.1 从加速卡反查其所在 NUMA 节点_get_numa_node_index_for_device_indexbinding.py通过 PCI 拓扑反查设备所属 NUMA 节点通过torch.accelerator拿到当前加速器设备模块调用get_device_properties(device_index)读取pci_domain_id、pci_bus_id、pci_device_id格式化为 sysfs PCI 地址f{domain:04x}:{bus:02x}:{device:02x}.0例如0000:dc:00.0读取/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_nodebinding.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_asbinding.py读取/sys/devices/system/cpu/cpu{N}/topology/thread_siblings_list得到同一物理核的全部逻辑 CPU含超线程兄弟核_get_logical_cpus_sharing_same_max_level_cache_asbinding.py遍历/sys/devices/system/cpu/cpu{N}/cache/index*过滤出类型为Unified或Data的缓存条目取level最大者的shared_cpu_list从而得到共享该核簇的全部 CPU。6.4 socket 语义的推导socket 模式下没有现成的“设备→socket” sysfs 文件实现是间接推导的binding.py取设备所在 NUMA 节点的任意一个允许 CPU读/sys/devices/system/cpu/cpu{N}/topology/physical_package_id得到 socket物理封装序号遍历系统所有 NUMA 节点读/sys/devices/system/node/possible把physical_package_id相同的节点全部收拢为该 socket 的 NUMA 节点集合把所有这些节点的 CPU 取并集作为该 worker 的绑定集合。6.5 工具函数sysfs 范围字符串 ↔ Python 整数集合Linux sysfs 中的 CPU/节点列表通常写作0-2,4,6-7形式的范围串源码提供了双向转换工具_get_set_of_int_from_ranges_strbinding.py0-2,4,6-7→{0,1,2,4,6,7}_get_ranges_str_from_intsbinding.py{0,1,2,4,6,7}→0-2,4,6-7用于拼装--physcpubind参数。七、前置条件与失效兜底应用注意事项综合文档与源码在使用 NUMA 绑定前需要确认以下前提避免踩坑Linux 系统绑定依赖os.sched_setaffinity/os.sched_getaffinity与/proc、/sys文件系统均为 Linux 特性。必须安装numactl即便走进程内线程绑定路径代码为简单起见仍会统一检查shutil.which(numactl)缺失时抛出RuntimeError(numactl CLI is required for NUMA binding)见 binding.py。必须有可用的加速器_get_numa_node_index_for_device_index会先检查torch.accelerator.is_available()否则抛RuntimeError(No accelerator available for NUMA binding)。绑定集合不能为空如果设备所在节点无可用 CPU例如 CPU 已被 cgroup/taskset 限制在外会抛出RuntimeError(Must bind to a non-empty set of CPU indices)。exclusive 模式的硬性约束当某 NUMA 节点上的物理核数量少于挂在该节点的设备数量时无法均分实现会直接报错binding.py。超线程语义exclusive 模式以物理核为单位切分但切给某 worker 的物理核会包含其全部超线程逻辑核避免同核兄弟线程干扰其他 worker。默认失败即抛出绑定异常默认会终止启动流程只有显式设置should_fall_back_if_binding_failsTrue才会降级为“不带绑定继续跑”官方不建议常规使用。八、在 PyTorch 源码中继续深入如果你希望进一步验证或扩展本文结论可以顺着以下路径阅读本文主文档docs/source/elastic/numa.mdSphinxautomodule挂载torch.numa与torch.numa.binding其正文即来自模块 docstring核心实现torch/numa/binding.py——AffinityMode、NumaOptions及全部_xxx_get_logical_cpus_to_bind_to私有实现均在此文件模块入口torch/numa/init.py空文件仅作包名占位公共 API 实际从binding.py导入torchrun 参数接线torch/distributed/run.py 与 torch/distributed/run.pyelastic 运行时接线torch/distributed/elastic/multiprocessing/api.py进程内绑定装饰器、torch/distributed/elastic/multiprocessing/subprocess_handler/subprocess_handler.pynumactl 包装、torch/distributed/elastic/multiprocessing/init.pyLaunchConfig的numa_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),仅供参考
返回列表