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 列表进行分区。
- 按以下顺序确定
total_npus: - 来自
total_logic_npus的npu-smi info -m - 拓扑亲和性条目数量
- 运行中的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 用于释放线程。
topo_affinity¶
topo_affinity 专为 A2、Atlas 300 推理产品、Ascend 950 和其他非 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的步骤是有意为之。它防止两个独立的单卡工作进程在其可见NPU共享相同的拓扑亲和性时,选择相同的CPU范围。
对于 Ascend 950,拓扑亲和性的使用方式不同:
- 将所有可见的主机
uvb_poll_window_thread线程绑定到除 CPU0 之外的 NUMA0 CPU,受allowed_cpus约束。Docker 容器必须使用--pid=host才能使这些主机线程可见。 - 使用拓扑亲和性来识别每个 NPU 的单个亲和性 NUMA 节点。
- 从
Thread(s) per core解析lscpu,当值为 1 时将集群大小设置为 8 个 CPU,值为 2 时设置为 16 个 CPU。 - 将每个亲和性 NUMA 的排序后的允许 CPU 列表分割成连续的集群。
- 按排序后的逻辑 NPU ID 分配集群,包括共享相同亲和性 NUMA 的隐藏 NPU。
- 在最终的
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 = 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池 | 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.py和docs/source/user_guide/configuration/additional_config.md - 测试:
tests/ut/device_allocator/test_cpu_binding.py