CPU 绑定#
概述#
CPU 绑定将 vLLM Ascend 工作进程和关键线程固定到特定的 CPU 核心,以减少 CPU-NPU 跨 NUMA 流量,并在多进程工作负载下稳定延迟。它专为运行 Ascend NPU 的 ARM 服务器设计,启用后会在工作进程初始化期间自动执行。
背景#
在多插槽 ARM 系统上,操作系统调度器可能会将 vLLM 线程放置在远离本地 NPU 的 CPU 上,从而导致 NUMA 跨域流量和延迟抖动。CPU 绑定强制执行一种确定性的 CPU 放置策略,并可选地将 NPU IRQ 绑定到同一个 CPU 池。这与其他性能特性(如图模式或动态批处理)不同,因为它纯粹是主机端的亲和性策略,不改变模型执行逻辑。
设计与工作原理#
关键概念#
允许的 CPU 列表:来自 /proc/self/status (Cpus_allowed_list) 的 cpuset。所有分配都受限于此列表。
运行中的 NPU 列表:从 npu-smi 进程列表中提取的逻辑 NPU ID,可选地由 ASCEND_RT_VISIBLE_DEVICES 过滤。
每个 NPU 的 CPU 池:根据绑定模式分配给每个逻辑 NPU ID 的 CPU 列表。
绑定模式与设备行为:
设备类型
默认模式
描述
A3 (无亲和性)
global_slice根据全局逻辑 NPU 总数均匀分割允许的 CPU 列表,确保每个 NPU 被分配一个连续的 CPU 核心段。这可以防止多个进程组之间的 CPU 核心重叠。
A2 / Atlas 300 推理产品 / 其他
topo_affinity基于 NPU 拓扑亲和性 (
npu-smi info -t topo) 分配 CPU。如果多个 NPU 被分配到单个 NUMA 节点(可能导致带宽争用),则 CPU 分配会扩展到相邻的 NUMA 节点。默认:启用 (enable_cpu_binding = true)。
回退:如果 NPU 拓扑亲和性不可用,则使用 global_slice。
故障处理:绑定过程中的任何异常都会记录为警告,并且跳过该等级的绑定。
执行流程(简化版)#
功能入口:当
enable_cpu_binding为 true 时,工作进程初始化会调用bind_cpus(local_rank)。CPU 架构门控:如果 CPU 不是 ARM,则记录日志并跳过绑定。
收集设备信息:
从
npu-smi info -m映射逻辑 NPU ID。从 npu-smi info 进程表中检测运行中的 NPU ID。
从 /proc/self/status 读取 cpuset。
从
npu-smi info -t topo读取拓扑亲和性。
构建 CPU 池:
对 A3 设备使用 global_slice;对 A2 和 Atlas 300 推理产品使用 topo_affinity。
如果缺少拓扑亲和性,则回退到 global_slice。
确保每个 NPU 至少有 5 个 CPU。
分配按角色划分的 CPU:
保留前两个 CPU 用于 IRQ 绑定。
main: pool[2:-2]acl: pool[-2]release: pool[-1]
绑定线程:
主进程被固定到
mainCPU。ACL 线程(以 acl_thread 命名)被固定到
aclCPU。释放线程(以 release_thread 命名)被固定到
releaseCPU。
绑定 NPU IRQ(可选):
如果 /proc/irq 可写,则将 SQ/CQ IRQ 绑定到池中的前两个 CPU。
可能会停止 irqbalance 以防止覆盖。
内存绑定(可选):
如果 migratepages 可用,则将 ACL 线程的内存迁移到 NPU 的 NUMA 节点。
分配方案示例#
分配方案直接来源于每个 NPU 的 CPU 池,然后按角色划分:
IRQ CPU: pool[0], pool[1]
main: pool[2:-2]acl: pool[-2]release: pool[-1]
以下是反映实际代码路径的具体示例。
示例 1:具有 640 个 CPU 和 16 个 NPU 的 A3 推理服务器#
allowed_cpus = [0..639] (640 个 CPU)
NUMA 节点 = 0..7 (8 个 NUMA 节点,对称布局)
total_npus = 16
running_npu_list = [0..15]
base = 640 // 16 = 40, extra = 0
每个 NPU 获得一个 40 个 CPU 的池。
NPU ID |
分配的 CPU 核心 (global_slice) |
角色划分 (IRQ/Main/ACL/Release) |
|---|---|---|
0 |
0-39 |
|
1 |
40-79 |
|
... |
... |
... |
15 |
600-639 |
|
即使多个进程共享同一个 cpuset,此布局也保持确定性,因为切片是基于全局逻辑 NPU ID 的。
示例 2:A3 global_slice,均匀分割#
输入:
allowed_cpus = [0..23] (24个CPU)
NUMA 节点 = 0..1 (2个NUMA节点,对称布局;NUMA0 = 0..11, NUMA1 = 12..23)
total_npus = 4 (来自 npu-smi info -m)
running_npu_list = [0, 1, 2, 3]
全局切片:
base = 24 // 4 = 6, extra = 0
每个NPU获得一个包含6个CPU的池。
NPU ID |
分配的 CPU 核心 (global_slice) |
角色划分 (IRQ/Main/ACL/Release) |
|---|---|---|
0 |
0-5 |
|
1 |
6-11 |
|
2 |
12-17 |
|
3 |
18-23 |
|
示例 3: A3 global_slice,余数分配#
输入:
allowed_cpus = [0..16] (17个CPU)
NUMA 节点 = 0..1 (2个NUMA节点,对称布局;NUMA0 = 0..7, NUMA1 = 8..16)
total_npus = 3
running_npu_list = [0, 1, 2]
全局切片:
base = 17 // 3 = 5, extra = 2
NPU0 池大小 = 6 (base+1)
NPU1 池大小 = 6 (base+1)
NPU2 池大小 = 5 (base)
NPU ID |
分配的 CPU 核心 (global_slice) |
角色划分 (IRQ/Main/ACL/Release) |
|---|---|---|
0 |
0-5 |
|
1 |
6-11 |
|
2 |
12-16 |
|
注意:当池大小恰好为5时,main 只有一个CPU (pool[2])。如果任何池小于5,绑定将引发错误。
NUMA 分析:
在上述对称NUMA布局中 (NUMA0 = 0..7, NUMA1 = 8..16),NPU0保持在NUMA0内,NPU2保持在NUMA1内,但NPU1跨越了NUMA0 (6,7) 和 NUMA1 (8..11)。这是对有序cpuset进行全局切片的直接结果;余数分配不强制NUMA边界。
如果cpuset编号在NUMA节点间交错(非对称布局),跨NUMA池可能更早发生。这就是为什么推荐对称NUMA布局以获得最佳局部性。
已知限制与未来改进#
使用当前的 global_slice 策略,某些CPU/NPU布局无法避免跨NUMA池。未来的增强应将NUMA节点边界纳入切片逻辑,以便池尽可能保持在单个NUMA节点内。
示例 4: 使用NPU可见子集的 global_slice#
输入:
total_npus = 8 (来自 npu-smi info -m)
running_npu_list = [2, 3] (由 ASCEND_RT_VISIBLE_DEVICES 过滤)
allowed_cpus = [0..39] (40个CPU)
NUMA 节点 = 0..3 (4个NUMA节点,对称布局;0..9, 10..19, 20..29, 30..39)
全局切片:
base = 40 // 8 = 5, extra = 0
只有可见的逻辑NPU获得池,但切片使用全局NPU ID,因此不同进程不会重叠。
NPU ID |
分配的 CPU 核心 (global_slice) |
角色划分 (IRQ/Main/ACL/Release) |
|---|---|---|
2 |
10-14 |
|
3 |
15-19 |
|
示例 5: 具有NUMA扩展的 A2/Atlas 300 推理产品 topo_affinity#
输入:
npu_affinity = {0: [0..7], 1: [0..7]} (来自
npu-smi info -t topo)allowed_cpus = [0..15] (16个CPU)
NUMA 节点 = 0..1 (2个NUMA节点;NUMA0 = 0..7, NUMA1 = 8..15)
NUMA 扩展:
两个NPU都在NUMA0上,因此每个池扩展到最近的NUMA节点以减少争用。
NPU0 扩展到 NUMA1 -> [0..15]
NPU1 扩展到 NUMA1 -> [0..15]
由于两个池相同,分配器应用跨NPU的平均分配以避免重叠。对于池 [0..15] 和 2个NPU,最终池变为:
NPU ID |
分配的CPU核心 (topo_affinity) |
角色划分 (IRQ/Main/ACL/Release) |
|---|---|---|
0 |
0-7 |
|
1 |
8-15 |
|
示例 6: 每个NPU的最小CPU数#
输入:
total_npus = 2
allowed_cpus = [0..7] (8个CPU)
NUMA 节点 = 0..1 (2个NUMA节点,对称布局;NUMA0 = 0..3, NUMA1 = 4..7)
结果:
base = 4,小于5,因此绑定失败,错误信息为:"用于IRQ/ACL/REL预留绑定的CPU不足..."
NPU ID |
分配的CPU核心 |
角色划分 (IRQ/Main/ACL/Release) |
|---|---|---|
0 |
不适用 |
绑定错误(每个NPU的CPU不足) |
1 |
不适用 |
绑定错误(每个NPU的CPU不足) |
要解决此问题,要么减少 total_npus,要么扩大 cpuset,使每个NPU至少有5个CPU。
日志记录与验证#
日志显示选定的绑定模式和分配计划,例如:
[cpu_bind_mode] mode=global_slice rank=0 visible_npus=[...]CPU分配计划如下:...
启动后,您可以通过 taskset 或
/proc/<pid>/status验证亲和性。
限制与注意事项#
仅限ARM:在非ARM CPU上跳过绑定。
最小CPU要求:每个逻辑NPU至少需要5个CPU。如果cpuset更小,绑定将失败并报错。
NUMA对称性假设:为获得最佳局部性,当前策略假设cpuset在NUMA节点间均匀分布,且CPU编号与NUMA布局对齐;否则NUMA局部性可能不理想。
示例(对称布局):2个NUMA节点,共64个CPU。NUMA0 = CPU 0–31,NUMA1 = CPU 32–63,cpuset为0–63。对于4个逻辑NPU,全局切片为每个NPU分配16个CPU (0–15, 16–31, 32–47, 48–63),因此每个NPU的CPU池都保持在单个NUMA节点内。
运行时依赖项:
需要 npu‑smi 和 lscpu 命令。
IRQ绑定需要对 /proc/irq 的写访问权限。
内存绑定需要 migratepages;否则将跳过此步骤。
IRQ副作用:可能会停止 irqbalance 服务以避免覆盖绑定。
每进程行为:仅使用当前 rank 的 NPU 进行 IRQ 绑定,以避免跨进程覆盖。
调试日志#
使用标准的 vLLM 日志配置来启用调试日志。当启用调试级别时,绑定过程会发出调试消息(例如 [cpu_global_slice] ...)。
参考资料#
CPU 绑定实现:vllm_ascend/cpu_binding.py (
DeviceInfo,CpuAlloc,bind_cpus)Worker 集成:vllm_ascend/worker/worker.py (
NPUWorker._init_device)附加配置选项:docs/source/user_guide/configuration/additional_config.md (
enable_cpu_binding)测试:tests/ut/device_allocator/test_cpu_binding.py