CPU 绑定¶
概述¶
CPU 绑定是面向 ARM 服务器上 vLLM worker 的一种 Ascend 原生主机端优化。从 vllm-ascend v0.18.0rc1 开始,它通过 enable_cpu_binding=True 默认启用。
该特性不会改变模型执行逻辑或数值结果。它仅在主机环境允许时,控制 worker 进程、关键运行时线程、内存页和 NPU IRQ 的 CPU 放置。通过将主 worker、ACL 和 release 线程保持在专用 CPU 范围内,它有助于降低繁忙主机上因调度器抢占带来的上下文切换开销。
为什么需要 CPU 绑定?¶
在多插槽 ARM 系统上,Linux 调度器可能会将 worker 线程放置在与 worker 所驱动 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 |
将 card/chip ID 映射到全局逻辑 NPU ID,并提供 total_logic_npus。在 950PR&950DT Products 上,不会报告 Chip Logic ID,因此使用 NPU ID 作为逻辑 ID。 |
| 运行中的 NPU | npu-smi info 进程表,按 ASCEND_RT_VISIBLE_DEVICES 过滤 |
标识此 worker 进程使用的逻辑 NPU。A2/A3 进程行使用 NPU Chip;950PR&950DT Products 进程行使用 NPU ID。 |
| 拓扑亲和性 | npu-smi info -t topo |
为 topo_affinity 模式提供 NPU 到 CPU 的亲和性信息。 |
| CPU NUMA 映射 | lscpu -e=CPU,NODE |
用于将单 NUMA 亲和性池扩展到下一个 NUMA 节点。 |
| 线程拓扑 | lscpu Thread(s) per core |
决定 950PR&950DT Products 的集群大小:每核 1 线程时为 8 个 CPU,每核 2 线程时为 16 个 CPU。 |
| UVB 轮询线程 | ps -Te |
为 950PR&950DT Products UVB CPU 绑定查找主机 uvb_poll_window_thread 线程。Docker 容器必须使用 --pid=host 才能看到这些主机线程。 |
策略选择¶
绑定策略由 Ascend 设备类型决定:
| 设备类型 | 策略 | 原因 |
|---|---|---|
| A3 | global_slice |
A3 使用 HCCS 卡间互联。每个 NPU 到所有 NUMA 节点的距离几乎相等,因此不存在强烈的 NPU 到 NUMA 亲和性信号。基于全局逻辑 NPU ID 的切片可提供确定性、不重叠的 CPU 池,以及 worker 之间的 CPU/NUMA 隔离。 |
| 950PR&950DT Products | topo_affinity |
950PR&950DT Products 使用来自 npu-smi info -t topo 的 NPU 到 CPU 亲和性来选择亲和 NUMA 节点,然后从该 NUMA 节点为每个 worker 分配一个 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: - 来自
npu-smi info -m的total_logic_npus - 拓扑亲和性条目数量
- 运行中的 NPU 数量
- 计算:
base = len(allowed_cpus) // total_npusextra = len(allowed_cpus) % total_npus- 每个逻辑 NPU 获得一个确定性的切片:
- NPU ID
< extra的 NPU 获得base + 1个 CPU。 - 其余 NPU ID 获得
base个 CPU。 - 只有运行中的 NPU 会被具体化到
npu_cpu_pool中。
这是关键属性:两个具有相同 cpuset 但可见 NPU ID 不同的独立 worker 进程,仍然会获得不重叠的 CPU 池,因为两个进程都针对相同的全局 NPU ID 空间进行切片。在 NUMA 对齐的 cpuset 下,这还提供了worker 之间的 CPU/NUMA 隔离,因此一个 worker 不会与另一个 worker 共享相同的 CPU 或 NUMA 切片。
global_slice 需要足够的 CPU 来满足所选设备的角色拆分:
- 需要 IRQ 绑定的设备要求
base >= 5: 2 个 CPU 用于 SQ/CQ IRQ 绑定,至少 1 个 CPU 用于主 worker,1 个 CPU 用于 ACL 线程,1 个 CPU 用于 release 线程。
topo_affinity¶
topo_affinity 专为 A2、Atlas 300 推理产品、950PR&950DT Products 以及其他非 A3 设备类型设计。A2 和 Atlas 300 推理产品暴露了有意义的 NPU 到 CPU 亲和性信息,因此分配器在可用时从 NPU 拓扑亲和性出发,并避免共享亲和性组之间的重叠。
- 从所有逻辑 NPU 构建候选 NPU:
- 始终包含运行中的 NPU
- 仅当其亲和性与本进程的允许 cpuset 重叠时,才包含非运行中的 NPU
- 对于每个候选 NPU,将拓扑亲和性与
allowed_cpus取交集。 - 如果某个候选 NPU 的交集为空,则该 rank 的绑定失败。
- 如果亲和性 CPU 全部位于一个 NUMA 节点上,则使用下一个 NUMA 节点的 CPU 扩展该池,并受
allowed_cpus约束。 - 将具有相同扩展池的 NPU 分组,并将每个共享池在该组内平均拆分。
- 最终
npu_cpu_pool中仅保留运行中的 NPU。
未运行候选步骤是有意为之。它可以防止两个独立的单卡工作进程在可见 NPU 共享相同拓扑亲和性时选择相同的 CPU 范围。
对于 950PR&950DT Products,拓扑亲和性的使用方式不同:
- 将所有可见的主机
uvb_poll_window_thread线程绑定到 NUMA0 的 CPU(除 CPU0 外),并受allowed_cpus约束。Docker 容器必须使用--pid=host,才能使这些主机线程可见。 - 使用拓扑亲和性确定每个 NPU 的唯一亲和 NUMA 节点。
- 从
lscpu解析Thread(s) per core,当该值为 1 时将集群大小设置为 8 个 CPU,当该值为 2 时设置为 16 个 CPU。 - 将每个亲和 NUMA 的已排序允许 CPU 列表拆分为连续集群。
- 按排序后的逻辑 NPU ID 分配集群,包括共享相同亲和 NUMA 的隐藏 NPU。
- 在最终
npu_cpu_pool中仅保留运行中的 NPU。
如果 950PR&950DT Products 的拓扑亲和性缺失、跨越多个 NUMA 节点、集群数量过少,或报告了不支持的 Thread(s) per core,则跳过工作进程 CPU 绑定,且不会向工作进程抛出异常。
角色拆分¶
构建 CPU 池后,分配器会按角色对其进行拆分:
对于具有 IRQ 绑定的设备:
| 角色 | CPU |
|---|---|
| SQ/CQ IRQ | pool[0], pool[1] |
| 主工作进程及子线程 | pool[2:-2] |
| ACL 线程 | pool[-2] |
| 释放线程 | pool[-1] |
对于 950PR&950DT Products:
| 角色 | CPU |
|---|---|
| 主工作进程及子线程 | 整个已分配集群 |
| ACL 线程 | 未单独绑定 |
| 释放线程 | 未单独绑定 |
如果最终池中的 CPU 数量少于所选角色拆分所需的数量,则该 rank 的绑定会失败,并且工作进程会记录来自调用方的警告。对于具有 IRQ 绑定的设备,每个 NPU 至少需要 5 个 CPU。950PR&950DT Products 要求每个工作进程拥有完整的一个集群。
条件性主机调优¶
在应用 CPU 亲和性之后,如果环境支持,CPU 绑定还可以应用两个主机侧调优步骤:
- 内存迁移使用
migratepages将工作进程已有的页面移动到所选 NUMA 节点。这会使工作进程更接近其读取的内存,并降低远程 NUMA 内存读取延迟。 - IRQ 绑定会在
/proc/irq可写且可以解析 IRQ 文件时,将 NPU IRQ 处理放置在为对应 NPU 保留的 CPU 上。950PR&950DT Products 会跳过此步骤。
这些是 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。
A2 topo_affinity 与隐藏同亲和性 NPU¶
来自 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=[...]
950PR&950DT Products 使用不同的角色拆分,因此其计划日志不包含 ACL 或 release 字段。当找到匹配的线程时,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。950PR&950DT Products 要求每个工作进程拥有完整的一个 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可写,950PR&950DT Products 也会跳过 IRQ 绑定。 - 950PR&950DT Products 的 UVB 轮询线程绑定要求能够看到主机 PID 命名空间。Docker 容器必须使用
--pid=host创建;否则可能找不到uvb_poll_window_thread。 - 内存迁移需要
migratepages;否则仅跳过内存迁移。CPU 亲和性仍会应用,但性能可能会下降,因为已有页面不会被移动到目标 NUMA 节点,并且可能通过延迟更高的远程 NUMA 访问进行读取。 - 如果异常从绑定流程中逃逸,
NPUWorker会记录一条警告,并跳过该 rank 的 CPU 绑定。
参考¶
- 实现:
vllm_ascend/cpu_binding.py - Worker 集成:
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
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 vl/vllm-ascend