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。

    • 故障处理:绑定过程中的任何异常都会记录为警告,并且跳过该等级的绑定

执行流程(简化版)#

  1. 功能入口:当 enable_cpu_binding 为 true 时,工作进程初始化会调用 bind_cpus(local_rank)

  2. CPU 架构门控:如果 CPU 不是 ARM,则记录日志并跳过绑定。

  3. 收集设备信息

    • npu-smi info -m 映射逻辑 NPU ID。

    • 从 npu-smi info 进程表中检测运行中的 NPU ID。

    • 从 /proc/self/status 读取 cpuset。

    • npu-smi info -t topo 读取拓扑亲和性。

  4. 构建 CPU 池

    • 对 A3 设备使用 global_slice;对 A2 和 Atlas 300 推理产品使用 topo_affinity

    • 如果缺少拓扑亲和性,则回退到 global_slice。

    • 确保每个 NPU 至少有 5 个 CPU。

  5. 分配按角色划分的 CPU

    • 保留前两个 CPU 用于 IRQ 绑定。

    • main: pool[2:-2]

    • acl: pool[-2]

    • release: pool[-1]

  6. 绑定线程

    • 主进程被固定到 main CPU。

    • ACL 线程(以 acl_thread 命名)被固定到 acl CPU。

    • 释放线程(以 release_thread 命名)被固定到 release CPU。

  7. 绑定 NPU IRQ(可选)

    • 如果 /proc/irq 可写,则将 SQ/CQ IRQ 绑定到池中的前两个 CPU。

    • 可能会停止 irqbalance 以防止覆盖。

  8. 内存绑定(可选)

    • 如果 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

IRQ: 0-1, Main: 2-37, ACL: 38, Release: 39

1

40-79

IRQ: 40-41, Main: 42-77, ACL: 78, Release: 79

...

...

...

15

600-639

IRQ: 600-601, Main: 602-637, ACL: 638, Release: 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

IRQ: 0-1, Main: 2-3, ACL: 4, Release: 5

1

6-11

IRQ: 6-7, Main: 8-9, ACL: 10, Release: 11

2

12-17

IRQ: 12-13, Main: 14-15, ACL: 16, Release: 17

3

18-23

IRQ: 18-19, Main: 20-21, ACL: 22, Release: 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

IRQ: 0-1, Main: 2-3, ACL: 4, Release: 5

1

6-11

IRQ: 6-7, Main: 8-9, ACL: 10, Release: 11

2

12-16

IRQ: 12-13, Main: 14, ACL: 15, Release: 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

IRQ: 10-11, Main: 12, ACL: 13, Release: 14

3

15-19

IRQ: 15-16, Main: 17, ACL: 18, Release: 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

IRQ: 0-1, Main: 2-5, ACL: 6, Release: 7

1

8-15

IRQ: 8-9, Main: 10-13, ACL: 14, Release: 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