CPU 绑定#

概述#

CPU 绑定是针对 ARM 服务器上 vLLM 工作进程的 Ascend 原生主机端优化从 vllm-ascend v0.18.0rc1 开始,通过 enable_cpu_binding=True 默认启用。

该功能不会改变模型执行逻辑或数值结果。它仅在主机环境允许时,控制工作进程、关键运行时线程、内存页和 NPU IRQ 的 CPU 放置。通过将主工作线程、ACL 线程和释放线程保持在专用 CPU 范围内,有助于减少繁忙主机上因调度器抢占导致的上下文切换开销。

为什么需要 CPU 绑定?#

在多插槽 ARM 系统上,Linux 调度器可能将工作线程放置在远离工作进程所驱动 NPU 的 CPU 上。这会增加跨 NUMA 流量、增加线程抢占并引入延迟抖动。因此,Ascend 后端拥有自己的 CPU 分配策略,旨在减少跨 NUMA 流量、减少线程抢占并提高延迟稳定性,而不是依赖上游 GPU NUMA 绑定标志。

这也是上游 NUMA 标志在 Ascend 上被适配的原因:

  • --numa-bind 被转换为 additional_config={"enable_cpu_binding": true}

  • --numa-bind-nodes--numa-bind-cpus 被忽略,因为 Ascend 根据 NPU 拓扑或全局逻辑 NPU ID 计算 CPU 池。

工作原理#

分配器根据运行时主机状态推导其计划:

输入

来源

用途

允许的 CPU

/proc/self/status Cpus_allowed_list

唯一有资格绑定的 CPU。容器 cpuset 会被尊重。

逻辑 NPU 映射

npu-smi info -m

将卡/芯片 ID 映射到全局逻辑 NPU ID,并提供 total_logic_npus

运行中的 NPU

npu-smi info 进程表,经 ASCEND_RT_VISIBLE_DEVICES 过滤

识别此工作进程使用的逻辑 NPU。

拓扑亲和性

npu-smi info -t topo

topo_affinity 模式提供 NPU 到 CPU 的亲和性。

CPU NUMA 映射

lscpu -e=CPU,NODE

用于将单 NUMA 亲和性池扩展到下一个 NUMA 节点。

策略选择#

绑定策略根据 Ascend 设备类型选择:

设备类型

策略

原因

A3

global_slice

A3 使用 HCCS 卡间互联。每个 NPU 与所有 NUMA 节点的距离几乎相等,因此没有强烈的 NPU 到 NUMA 亲和性信号。基于全局逻辑 NPU ID 的分片提供了确定性的、不重叠的 CPU 池,以及工作进程间的 CPU/NUMA 隔离。

A2、Atlas 300 推理产品及其他非 A3 设备类型

topo_affinity

A2 和 Atlas 300 推理产品通过 npu-smi info -t topo 提供 NPU 到 CPU 的亲和性信息。非 A3 设备类型在可用时使用此拓扑信号。

如果选择了 topo_affinity 但拓扑亲和性不可用,分配器将回退到 global_slice

CPU 池构建#

global_slice#

global_slice 专为 A3 设计。由于 A3 的 HCCS 互联使得每个 NPU 到每个 NUMA 节点的距离几乎相同,拓扑亲和性不是一个有用的放置信号。因此,分配器根据全局逻辑 NPU ID 对排序后的 allowed_cpus 列表进行分区。

  1. 按以下顺序确定 total_npus

    • 来自 npu-smi info -mtotal_logic_npus

    • 拓扑亲和性条目数量

    • 运行中 NPU 数量

  2. 计算:

    • base = len(allowed_cpus) // total_npus

    • extra = len(allowed_cpus) % total_npus

  3. 每个逻辑 NPU 获得一个确定性的分片:

    • NPU ID < extra 获得 base + 1 个 CPU。

    • 其余 NPU ID 获得 base 个 CPU。

  4. 只有运行中的 NPU 会被实例化到 npu_cpu_pool 中。

这是关键特性:两个独立的工作进程,即使具有相同的 cpuset 但不同的可见 NPU ID,仍然获得不重叠的 CPU 池,因为两个进程都针对相同的全局 NPU ID 空间进行分片。使用 NUMA 对齐的 cpuset,这也提供了工作进程间的 CPU/NUMA 隔离,因此一个工作进程不会与另一个工作进程共享相同的 CPU 或 NUMA 分片。

global_slice 要求 base >= 5,因为每个 NPU 池保留:

  • 2 个 CPU 用于 SQ/CQ IRQ 绑定

  • 至少 1 个 CPU 用于主工作线程

  • 1 个 CPU 用于 ACL 线程

  • 1 个 CPU 用于释放线程

topo_affinity#

topo_affinity 专为 A2、Atlas 300 推理产品及其他非 A3 设备类型设计。A2 和 Atlas 300 推理产品暴露了有意义的 NPU 到 CPU 亲和性信息,因此分配器在可用时从 NPU 拓扑亲和性开始,然后避免共享亲和性组的重叠。

  1. 从所有逻辑 NPU 构建候选 NPU:

    • 始终包含运行中的 NPU

    • 仅当非运行中 NPU 的亲和性与该进程的允许 cpuset 重叠时才包含它们

  2. 对于每个候选 NPU,将拓扑亲和性与 allowed_cpus 取交集。

  3. 如果某个候选 NPU 的交集为空,则此 rank 的绑定失败。

  4. 如果亲和性 CPU 都在一个 NUMA 节点上,则使用来自下一个 NUMA 节点的 CPU 扩展池,受 allowed_cpus 约束。

  5. 将具有相同扩展池的 NPU 分组,并在该组内均匀分割每个共享池。

  6. 在最终的 npu_cpu_pool 中仅保留运行中的 NPU。

非运行候选步骤是有意设计的。它防止两个独立的单卡工作进程在它们可见的NPU共享相同拓扑亲和性时选择相同的CPU范围。

角色拆分#

构建CPU池后,分配器按角色进行拆分:

角色

CPU

SQ/CQ中断

pool[0]pool[1]

主工作进程及子线程

pool[2:-2]

ACL线程

pool[-2]

释放线程

pool[-1]

如果最终池中的CPU少于5个,则此rank的绑定失败,工作进程将从调用者处记录警告。

条件性主机调优#

应用CPU亲和性后,当环境支持时,CPU绑定还可以执行两个主机端调优步骤:

  • 内存迁移使用migratepages将工作进程的现有页面移动到选定的NUMA节点。这使工作进程更接近其读取的内存,并减少远程NUMA内存读取延迟。

  • /proc/irq可写且IRQ文件可解析时,IRQ绑定将NPU中断处理放置在为相应NPU保留的CPU上。

这些是CPU绑定的条件性部分,而非独立的功能开关。如果缺少主机前提条件,该步骤将被跳过,而CPU线程绑定仍会继续。缺少migratepages仍可能使页面留在远程NUMA节点上,因此与完整的CPU绑定设置相比,延迟或吞吐量可能会下降。

示例#

具有640个CPU和16个NPU的A3推理服务器#

输入:

  • allowed_cpus = [0..639]

  • total_logic_npus = 16

  • running_npu_list = [0..15]

计算:

  • base = 640 // 16 = 40

  • extra = 0

  • 驱动逻辑NPU i的工作进程i接收CPU切片[i * 40 .. i * 40 + 39]

全局切片视图:

CPU range: 0                                                             639
           |-- worker0/NPU0 --|-- worker1/NPU1 --| ... |-- worker15/NPU15 --|
           |      0-39        |      40-79       | ... |      600-639       |

每个工作进程切片内的角色拆分:

40-CPU worker slice
| IRQ CPUs | main worker process and subthreads | ACL thread | release thread |
|  c0-c1   |              c2-c37                |    c38     |      c39       |

具体示例:

工作进程

逻辑NPU

CPU池

中断CPU

主CPU

ACL CPU

释放CPU

0

0

0-39

0-1

2-37

38

39

1

1

40-79

40-41

42-77

78

79

...

...

...

...

...

...

...

15

15

600-639

600-601

602-637

638

639

即使不同工作进程共享相同的cpuset,此布局也保持确定性,因为切分是基于全局逻辑NPU ID的。

具有隐藏相同亲和性NPU的A2拓扑亲和性#

来自A2拓扑的输入:

  • NPU0亲和性:144-167

  • NPU2亲和性:144-167

  • 进程A仅看到NPU0

  • 进程B仅看到NPU2

  • 两个进程都有allowed_cpus = [144..191]

分配器将隐藏的相同亲和性NPU作为候选包含在每个进程中,拆分共享的扩展池,然后在最终池中仅保留可见的NPU。

最终池:

进程

可见NPU

最终CPU池

A

0

144-167

B

2

168-191

即使两个工作进程作为独立的单卡服务启动,这也能避免CPU池重叠。

日志#

分配器记录所选模式和分配计划:

[cpu_bind_mode] mode=topo_affinity rank=0 visible_npus=[0]
The CPU allocation plan is as follows:
NPU0: main=[...] acl=[...] release=[...]

限制#

  • CPU绑定仅在ARM上运行。在x86_64上跳过。

  • 每个最终 NPU 池必须至少有 5 个 CPU。

  • global_slice 是确定性的,当 cpuset 与 NUMA 对齐时提供 CPU/NUMA 隔离,但当 CPU 编号或 cpuset 布局跨越 NUMA 边界时,无法保证 NUMA 本地池。

  • topo_affinity 依赖于 npu-smi info -t topo 的可用输出。

  • IRQ 绑定需要可写的 /proc/irq 和可解析的 PCI/IRQ 信息。

  • 内存迁移需要 migratepages;否则仅跳过内存迁移。CPU 亲和性仍然生效,但性能可能下降,因为现有页面不会移动到目标 NUMA 节点,可能通过更高延迟的远程 NUMA 访问读取。

  • 如果绑定流程中发生异常,NPUWorker 记录警告并跳过该 rank 的 CPU 绑定。

参考#

  • 实现:vllm_ascend/cpu_binding.py

  • Worker 集成:vllm_ascend/worker/worker.py

  • 配置:vllm_ascend/ascend_config.pydocs/source/user_guide/configuration/additional_config.md

  • 测试:tests/ut/device_allocator/test_cpu_binding.py