跳转至

CPU绑定

概述

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

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

为什么需要CPU绑定?

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

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

  • --numa-bind 被转换为 additional_config={"enable_cpu_binding": true}
  • --numa-bind-nodes--numa-bind-cpus 被忽略,因为昇腾根据NPU拓扑或全局逻辑NPU ID计算CPU池。

工作原理

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

输入 来源 用途
允许的CPU /proc/self/status Cpus_allowed_list 唯一可绑定的CPU。容器cpuset设置会被尊重。
逻辑NPU映射 npu-smi info -m 将卡/芯片ID映射到全局逻辑NPU ID,并给出total_logic_npus。在Ascend 950上,不报告Chip Logic ID,因此使用NPU ID作为逻辑ID。
运行中的NPU npu-smi info进程表,经ASCEND_RT_VISIBLE_DEVICES过滤 标识此工作进程使用的逻辑NPU。A2/A3进程行使用NPU Chip;Ascend 950进程行使用NPU ID
拓扑亲和性 npu-smi info -t topo topo_affinity模式提供NPU到CPU的亲和性信息。
CPU NUMA映射 lscpu -e=CPU,NODE 用于将单NUMA亲和性池扩展到下一个NUMA节点。
线程拓扑 lscpu Thread(s) per core 决定 Ascend 950 集群大小:1 线程每核心时为 8 个 CPU,2 线程每核心时为 16 个 CPU。
UVB 轮询线程 ps -Te 查找主机 uvb_poll_window_thread 线程以用于 Ascend 950 UVB CPU 绑定。Docker 容器必须使用 --pid=host 才能看到这些主机线程。

策略选择

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

设备类型 策略 原因
A3 global_slice A3使用HCCS卡间互联。每个NPU与所有NUMA节点的距离几乎相等,因此没有强烈的NPU到NUMA亲和性信号。基于全局逻辑NPU ID的分片提供确定性的、不重叠的CPU池以及工作进程间的CPU/NUMA隔离。
Ascend 950 topo_affinity Ascend 950 使用来自 npu-smi info -t topo 的 NPU 到 CPU 亲和性来选择亲和性 NUMA 节点,然后从该 NUMA 节点为每个工作进程分配一个 CPU 集群。它还按 NPU ID 而非 NPU Chip 报告进程行,跳过 IRQ 绑定,并绑定主机 UVB 轮询线程。
A2和Atlas 300推理产品 topo_affinity A2和Atlas 300推理产品通过npu-smi info -t topo提供NPU到CPU的亲和性信息,因此在可用时会使用此拓扑信号。

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

CPU池构建

global_slice

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

  1. 按以下顺序确定total_npus
  2. 来自total_logic_npusnpu-smi info -m
  3. 拓扑亲和性条目数量
  4. 运行中的NPU数量
  5. 计算:
  6. base = len(allowed_cpus) // total_npus
  7. extra = len(allowed_cpus) % total_npus
  8. 每个逻辑NPU获得一个确定性的分片:
  9. NPU ID < extra 的获得 base + 1 个CPU。
  10. 其余NPU ID获得 base 个CPU。
  11. 只有运行中的NPU会被实例化到npu_cpu_pool中。

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

global_slice 需要足够的 CPU 来满足所选设备的角色拆分:

  • 具有 IRQ 绑定的设备要求 base >= 5: 2 个 CPU 用于 SQ/CQ IRQ 绑定,至少 1 个 CPU 用于主工作进程,1 个 CPU 用于 ACL 线程,以及 1 个 CPU 用于释放线程。

topo_affinity

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

  1. 从所有逻辑NPU中构建候选NPU:
  2. 始终包含正在运行的NPU
  3. 仅当非运行NPU的亲和性与当前进程允许的cpuset重叠时,才包含它们
  4. 对于每个候选NPU,将拓扑亲和性与 allowed_cpus 取交集。
  5. 如果某个候选NPU的交集为空,则该rank的绑定失败。
  6. 如果亲和性CPU全部位于同一个NUMA节点上,则从下一个NUMA节点扩展CPU池,受 allowed_cpus 约束。
  7. 将具有相同扩展池的NPU分组,并将每个共享池在该组内平均分配。
  8. 在最终的 npu_cpu_pool 中仅保留正在运行的NPU。

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

对于 Ascend 950,拓扑亲和性的使用方式不同:

  1. 将所有可见的主机 uvb_poll_window_thread 线程绑定到除 CPU0 之外的 NUMA0 CPU,受 allowed_cpus 约束。Docker 容器必须使用 --pid=host 才能使这些主机线程可见。
  2. 使用拓扑亲和性来识别每个 NPU 的单个亲和性 NUMA 节点。
  3. Thread(s) per core 解析 lscpu,当值为 1 时将集群大小设置为 8 个 CPU,值为 2 时设置为 16 个 CPU。
  4. 将每个亲和性 NUMA 的排序后的允许 CPU 列表分割成连续的集群。
  5. 按排序后的逻辑 NPU ID 分配集群,包括共享相同亲和性 NUMA 的隐藏 NPU。
  6. 在最终的 npu_cpu_pool 中仅保留正在运行的 NPU。

如果 Ascend 950 的拓扑亲和性缺失、跨越多个 NUMA 节点、集群过少或报告了不支持的 Thread(s) per core,则工作进程 CPU 绑定将被跳过,不会向工作进程抛出异常。

角色划分

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

对于支持IRQ绑定的设备:

角色 CPU
SQ/CQ IRQ pool[0], pool[1]
主工作进程及子线程 pool[2:-2]
ACL线程 pool[-2]
释放线程 pool[-1]

对于Ascend 950:

角色 CPU
主工作进程及子线程 整个分配的集群
ACL 线程 不单独绑定
释放线程 不单独绑定

如果最终池中的 CPU 数量少于所选角色拆分所需的数量,则该 rank 的绑定失败,工作进程会从调用者处记录一条警告。对于具有 IRQ 绑定的设备,每个 NPU 至少需要 5 个 CPU。Ascend 950 要求每个工作进程有一个完整的集群。

条件性主机调优

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

  • 内存迁移使用 migratepages 将工作进程的现有页面移动到选定的 NUMA 节点。这使工作进程更接近其读取的内存,并减少远程 NUMA 内存读取延迟。
  • /proc/irq 可写且 IRQ 文件可解析时,IRQ 绑定将 NPU IRQ 处理放置在为相应 NPU 保留的 CPU 上。Ascend 950 跳过此步骤。

这些是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池 IRQ 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 topo_affinity

来自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=[...]

Ascend 950 使用不同的角色拆分,因此其计划日志不包括 ACL 或释放字段。当找到匹配的线程时,UVB 轮询线程绑定会单独报告:

[cpu_bind_mode] mode=topo_affinity rank=0 visible_npus=[0]
The CPU allocation plan is as follows:
Ascend 950 NPU0: worker=[...]
[cpu_bind_ascend_950] uvb_poll_window_thread tids=[...] cpus=[...]

限制

  • CPU 绑定仅在 ARM 上运行。在 x86_64 上跳过。
  • 每个最终的 NPU 池必须有足够的 CPU 用于其角色拆分:对于具有 IRQ 绑定的设备,至少需要 5 个 CPU。Ascend 950 要求每个工作进程有一个完整的 CPU 集群。
  • global_slice 是确定性的,当 cpuset 与 NUMA 对齐时提供 CPU/NUMA 隔离,但当 CPU 编号或 cpuset 布局跨越 NUMA 边界时,它无法保证 NUMA 本地池。
  • topo_affinity 依赖于 npu-smi info -t topo 的可用输出。
  • IRQ 绑定需要可写的 /proc/irq 和可解析的 PCI/IRQ 信息。即使 /proc/irq 可写,Ascend 950 也会跳过 IRQ 绑定。
  • Ascend 950 UVB 轮询线程绑定需要能够看到主机 PID 命名空间。Docker 容器必须使用 --pid=host 创建;否则可能找不到 uvb_poll_window_thread
  • 内存迁移需要 migratepages;否则仅跳过内存迁移。CPU 亲和性仍然适用,但性能可能会下降,因为现有页面不会移动到目标 NUMA 节点,并且可能通过更高延迟的远程 NUMA 访问读取。
  • 如果异常从绑定流程中逃逸,NPUWorker 会记录一条警告并跳过该 rank 的 CPU 绑定。

参考

  • 实现: vllm_ascend/cpu_binding.py
  • 工作进程集成: 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