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 |
|
唯一有资格绑定的 CPU。容器 cpuset 会被尊重。 |
逻辑 NPU 映射 |
|
将卡/芯片 ID 映射到全局逻辑 NPU ID,并提供 |
运行中的 NPU |
|
识别此工作进程使用的逻辑 NPU。A2/A3 进程行使用 |
拓扑亲和性 |
|
为 |
CPU NUMA 映射 |
|
用于将单 NUMA 亲和性池扩展到下一个 NUMA 节点。 |
策略选择#
绑定策略根据 Ascend 设备类型选择:
设备类型 |
策略 |
原因 |
|---|---|---|
A3 |
|
A3 使用 HCCS 卡间互联。每个 NPU 与所有 NUMA 节点的距离几乎相等,因此没有强烈的 NPU 到 NUMA 亲和性信号。基于全局逻辑 NPU ID 的分片提供了确定性的、不重叠的 CPU 池,以及工作进程间的 CPU/NUMA 隔离。 |
Ascend 950 |
|
Ascend 950 报告 NPU 到 NPU/NIC 的拓扑,但不在 |
A2 和 Atlas 300 推理产品 |
|
A2 和 Atlas 300 推理产品通过 |
如果选择了 topo_affinity 但拓扑亲和性不可用,分配器将回退到 global_slice。
CPU 池构建#
global_slice#
global_slice 专为没有有用 NPU 到 CPU 亲和性信号的设备设计,包括 A3 和 Ascend 950。由于 A3 的 HCCS 互联使得每个 NPU 到每个 NUMA 节点的距离几乎相同,拓扑亲和性不是一个有用的放置信号。Ascend 950 同样暴露 UB/NIC 拓扑但不暴露 CPU 亲和性。因此,分配器根据全局逻辑 NPU ID 对排序后的 allowed_cpus 列表进行分区。
按以下顺序确定
total_npus:来自
npu-smi info -m的total_logic_npus拓扑亲和性条目数量
运行中 NPU 数量
计算:
base = len(allowed_cpus) // total_npusextra = len(allowed_cpus) % total_npus
每个逻辑 NPU 获得一个确定性的分片:
NPU ID
< extra获得base + 1个 CPU。其余 NPU ID 获得
base个 CPU。
只有运行中的 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 用于释放线程。Ascend 950 跳过 IRQ 绑定且不预留 SQ/CQ IRQ CPU,因此需要
base >= 3:至少 1 个 CPU 用于主工作线程,1 个 CPU 用于 ACL 线程,1 个 CPU 用于释放线程。
topo_affinity#
topo_affinity 专为 A2、Atlas 300 推理产品及其他非 A3 设备类型设计。A2 和 Atlas 300 推理产品暴露了有意义的 NPU 到 CPU 亲和性信息,因此分配器在可用时从 NPU 拓扑亲和性开始,然后避免共享亲和性组的重叠。
从所有逻辑 NPU 构建候选 NPU:
始终包含运行中的 NPU
仅当非运行中 NPU 的亲和性与该进程的允许 cpuset 重叠时才包含它们
对于每个候选 NPU,将拓扑亲和性与
allowed_cpus取交集。如果某个候选 NPU 的交集为空,则此 rank 的绑定失败。
如果亲和性 CPU 都在一个 NUMA 节点上,则使用来自下一个 NUMA 节点的 CPU 扩展池,受
allowed_cpus约束。将具有相同扩展池的 NPU 分组,并在该组内均匀分割每个共享池。
在最终的
npu_cpu_pool中仅保留运行中的 NPU。
非运行候选步骤是有意设计的。它防止两个独立的单卡工作进程在它们可见的NPU共享相同拓扑亲和性时选择相同的CPU范围。
角色拆分#
构建CPU池后,分配器按角色进行拆分:
对于支持IRQ绑定的设备:
角色 |
CPU |
|---|---|
SQ/CQ中断 |
|
主工作进程及子线程 |
|
ACL线程 |
|
释放线程 |
|
对于Ascend 950:
角色 |
CPU |
|---|---|
主工作进程及子线程 |
|
ACL线程 |
|
释放线程 |
|
如果最终池中的CPU少于所选角色拆分所需的数量,则此rank的绑定失败,工作进程将从调用者处记录警告。对于支持IRQ绑定的设备,每个NPU至少需要5个CPU;对于Ascend 950,每个NPU至少需要3个CPU。
条件性主机调优#
应用CPU亲和性后,当环境支持时,CPU绑定还可以执行两个主机端调优步骤:
内存迁移使用
migratepages将工作进程的现有页面移动到选定的NUMA节点。这使工作进程更接近其读取的内存,并减少远程NUMA内存读取延迟。当
/proc/irq可写且IRQ文件可解析时,IRQ绑定将NPU中断处理放置在为相应NPU保留的CPU上。Ascend 950跳过此步骤,并将这些CPU分配给主工作进程。
这些是CPU绑定的条件性部分,而非独立的功能开关。如果缺少主机前提条件,该步骤将被跳过,而CPU线程绑定仍会继续。缺少migratepages仍可能使页面留在远程NUMA节点上,因此与完整的CPU绑定设置相比,延迟或吞吐量可能会下降。
示例#
具有640个CPU和16个NPU的A3推理服务器#
输入:
allowed_cpus = [0..639]total_logic_npus = 16running_npu_list = [0..15]
计算:
base = 640 // 16 = 40extra = 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的。
Logs#
The allocator logs the selected mode and allocation plan:
[cpu_bind_mode] mode=topo_affinity rank=0 visible_npus=[0]
The CPU allocation plan is as follows:
NPU0: main=[...] acl=[...] release=[...]
Limitations#
CPU binding runs only on ARM. It is skipped on x86_64.
每个最终NPU池必须有足够的CPU用于角色拆分:对于支持IRQ绑定的设备至少需要5个CPU,对于Ascend 950至少需要3个CPU。
global_sliceis deterministic and provides CPU/NUMA isolation when the cpuset is NUMA-aligned, but it cannot guarantee NUMA-local pools when CPU numbering or cpuset layout crosses NUMA boundaries.topo_affinitydepends on usable output fromnpu-smi info -t topo.IRQ绑定需要可写的
/proc/irq和可解析的PCI/IRQ信息。即使/proc/irq可写,Ascend 950也会跳过IRQ绑定,并且在其角色拆分中不保留IRQ CPU。Memory migration requires
migratepages; otherwise only memory migration is skipped. CPU affinity still applies, but performance may degrade because existing pages are not moved to the target NUMA node and may be read through higher-latency remote NUMA access.If an exception escapes the binding flow,
NPUWorkerlogs a warning and skips CPU binding for that rank.
References#
Implementation:
vllm_ascend/cpu_binding.pyWorker integration:
vllm_ascend/worker/worker.pyConfig:
vllm_ascend/ascend_config.pyanddocs/source/user_guide/configuration/additional_config.mdTests:
tests/ut/device_allocator/test_cpu_binding.py