Prhub

#39781 [CPU] Refactor CPU affinity and memory management

vllm-project/vllm · 作者 bigPYJ1151 · 合并时间 2026-04-17 21:01

分析状态 已生成
文件变更 14提交数 16 · 评论 62
代码增减 +723 / -425
cpu refactor performance v1

执行摘要

重构 CPU 亲和性与内存管理,修复性能回归并支持自动 KV 缓存大小分析。

PR 描述指出,此重构旨在基于 #36487 修复 CPU 亲和性相关的功能和性能回归,并支持自动 KV 缓存大小分析。讨论中提及了之前版本中存在的 OMP 设置问题和跨架构兼容性问题,例如 ARM 多 rank 性能下降和 s390x 书籍(book)拓扑处理。

建议技术管理者和工程师精读 OMPProcessManager 类的设计,理解其如何适配不同 OpenMP 库和架构;同时关注 csrc/cpu/utils.cpp 中的 NUMA 代码风险,并在部署前进行多架构测试。

讨论亮点
  • C++ NUMA 位掩码安全:gemini-code-assist[bot] 指出 csrc/cpu/utils.cpp 中直接 XOR 操作 bitmask->maskp 在大规模 NUMA 系统上不安全,可能只处理第一个字,但 bigPYJ1151 回应“不是问题”,未完全解决。
  • OMP 环境设置争议:kot-begemot-uk 强调必须同时设置 OMP_PLACESOMP_PROC_BIND,但 bigPYJ1151 解释采用 OMP_PLACES={{0},{1},...} 格式可显式绑定,决策为使用后者。
  • ARM 多 rank 性能回归:fadara01 报告 ARM 上双 rank(tp=2)性能下降,建议每 rank 保留一核;代码随后更新,在 ARM 架构下恢复每 rank 保留一核的策略。
  • s390x 书籍(book)处理:R3hankhan123 和 wjhrdy 讨论 s390x 的书籍拓扑,决定在其它 PR(如 #39191)中实现,此处使用 core 字段作为基础。
  • 导入缺失和代码风格:gemini-code-assist[bot] 发现 vllm/platforms/cpu.py 缺失 globCpuArchEnum 导入,已修复;同时修正了断言消息拼接和 OMP_PLACES 重叠问题。

实现拆解

  1. 创建集中式 CPU 资源工具库:新增 vllm/utils/cpu_resource_utils.py,定义 LogicalCPUInfoMemoryNodeInfo 数据类,提供 get_memory_affinityparse_id_listget_memory_node_info 等函数,统一解析系统 CPU 拓扑和内存节点信息。
  2. 重构 OMP 多进程管理器:重写 vllm/utils/ompmultiprocessing.py 中的 OMPProcessManager 类,根据检测到的 OpenMP 库(IOMP、GOMP 或标准 OMP)动态设置环境变量(如 KMP_AFFINITYGOMP_CPU_AFFINITYOMP_PLACES),并支持架构特定的核心保留策略(如 ARM 多 rank 时每 rank 保留一核)。
  3. 清理平台 CPU 代码:修改 vllm/platforms/cpu.py,移除重复的 LogicalCPUInfo 定义和内存计算逻辑,转而导入 cpu_resource_utils 工具,简化平台配置和 OMP 管理器集成。
  4. 增强 CPU Worker 内存绑定:在 vllm/v1/worker/cpu_worker.py__init__determine_available_memory 方法中,添加基于 NUMA 节点的内存绑定(通过 torch.ops._C.init_cpu_memory_env)和动态 KV 缓存大小计算,利用 gpu_memory_utilization 配置自动调整。
  5. 实现 C++ 层 NUMA 绑定:在 csrc/cpu/utils.cpp 中新增 init_cpu_memory_env 函数,使用 libnuma API 进行内存页面迁移和绑定策略(MEMBIND 或 INTERLEAVE),但注意位掩码操作存在安全风险。
  6. 配套更新:调整 vllm/v1/executor/multiproc_executor.py 以使用新的 OMP 管理器,更新 vllm/config/cache.py 移除旧配置,修改测试文件(如 tests/models/language/generation/test_common.py)和 CI 脚本(如 .buildkite/scripts/hardware_ci/run-cpu-distributed-smoke-test.sh)以确保兼容性。
文件 模块 状态 重要度
vllm/utils/cpu_resource_utils.py 工具库 added 8.96
vllm/utils/ompmultiprocessing.py 多进程管理 modified 8.65
vllm/platforms/cpu.py 平台层 modified 8.62
vllm/v1/worker/cpu_worker.py Worker 实现 modified 7.45
csrc/cpu/utils.cpp C++ 核心 modified 6.89
vllm/utils/ompmultiprocessing.py core-logic

重构的 OMP 进程管理器,负责线程亲和性设置和跨 OpenMP 库兼容性,是性能优化的核心。

class OMPProcessManager:
    def __init__(self, config: "VllmConfig"):
        # 初始化时检测 OpenMP 库类型和架构,设置保留核心数
        self.use_iomp = "libiomp" in os.getenv("LD_PRELOAD", "")
        self.use_gomp = "libgomp" in os.getenv("LD_PRELOAD", "")
        self.reserve_cpu_num = 1 # 默认保留一核
        if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
            # ARM 架构下每 local_world_size 保留一核,以优化多 rank 性能
            self.reserve_cpu_num = self.local_world_size
        self._parse_omp_threads_bind_env() # 解析环境变量配置
​
    @contextmanager
    def configure_omp_envs(self, rank: int, local_rank: int):
        """上下文管理器,为指定 rank 设置 OpenMP 环境变量。"""
        cpu_list = [str(i) for i in self.cpu_lists[local_rank]]
        envs_dict = {}
        if self.use_iomp:
            # Intel OpenMP 设置
            envs_dict["KMP_AFFINITY"] = f"granularity=fine,explicit,proclist=[{','.join(cpu_list)}]"
        elif self.use_gomp:
            # GNU OpenMP 设置
            envs_dict["GOMP_CPU_AFFINITY"] = " ".join(cpu_list)
        else:
            # 标准 OpenMP 设置,使用非重叠的 places 格式
            envs_dict["OMP_PLACES"] = "{" + "},{}".join(cpu_list) + "}"
            envs_dict["OMP_PROC_BIND"] = "true"
        envs_dict["OMP_NUM_THREADS"] = str(len(cpu_list))
        # 备份并设置环境变量,确保线程亲和性
        old_envs = {k: os.environ.get(k) for k in envs_dict}
        try:
            for k, v in envs_dict.items():
                os.environ[k] = v
            yield
        finally:
            for k, v in old_envs.items():
                if v is None:
                    os.environ.pop(k, None)
                else:
                    os.environ[k] = v
vllm/v1/worker/cpu_worker.py core-logic

CPU Worker 新增内存节点绑定和动态 KV 缓存大小分析,直接关系到推理时的内存分配性能。

def __init__(self, vllm_config: VllmConfig, local_rank: int, rank: int, distributed_init_method: str, is_driver_worker: bool = False):
    # 获取允许的内存节点和 CPU 列表
    allowed_memory_nodes = get_visible_memory_node()
    allowed_cpu_list = get_allowed_cpu_list()
    cpu_core = allowed_cpu_list[0] # 假设每个 worker 绑定到第一个可用 CPU
    # 检查 NUMA 节点一致性
    if cpu_core.numa_node not in allowed_memory_nodes:
        logger.warning(f"Node {cpu_core.numa_node} 不在可用内存节点 {allowed_memory_nodes} 中。")
    # 调用 C++ 函数初始化 CPU 内存环境,绑定到指定 NUMA 节点
    torch.ops._C.init_cpu_memory_env([cpu_core.numa_node])
    # 计算请求的 CPU 内存基于 gpu_memory_utilization
    memory_status = get_memory_node_info(cpu_core.numa_node)
    memory_fraction = vllm_config.cache_config.gpu_memory_utilization
    self.requested_cpu_memory = math.ceil(memory_status.total_memory * memory_fraction)
    available_memory = memory_status.available_memory
    # 验证内存是否充足,否则抛出错误
    if vllm_config.cache_config.kv_cache_memory_bytes is None and self.requested_cpu_memory > available_memory:
        raise ValueError(f"可用内存不足,请降低 --gpu-memory-utilization。")
    super().__init__(vllm_config, local_rank, rank, distributed_init_method, is_driver_worker)

关键符号

LogicalCPUInfo get_memory_affinity parse_id_list get_memory_node_info OMPProcessManager configure_omp_envs init_cpu_memory_env

评论区精华

C++ NUMA 位掩码安全 正确性

gemini-code-assist[bot] 指出直接操作 bitmask->maskp 不安全,可能只处理第一个字,bigPYJ1151 回应“不是问题”。

结论:未完全解决,代码中保留风险。 · unresolved

OMP 环境设置设计 设计

kot-begemot-uk 认为必须设置 OMP_PROC_BIND,bigPYJ1151 解释采用 OMP_PLACES 非重叠格式已足够。

结论:采用 bigPYJ1151 的方案,使用 OMP_PLACES={{0},{1},...} 格式。 · 已解决

ARM 多 rank 性能优化 性能

fadara01 报告双 rank 性能下降,建议每 rank 保留一核;代码更新为 ARM 架构下每 local_world_size 保留一核。

结论:已修复,性能恢复。 · 已解决

s390x 书籍(book)处理 设计

R3hankhan123 和 wjhrdy 讨论 s390x 的书籍拓扑,决定在其它 PR 中实现,此处使用 core 字段。

结论:延迟处理,相关功能在 PR #39191 中。 · pending

风险与影响

  • C++ 代码安全风险csrc/cpu/utils.cpp 中的位掩码操作可能在大规模 NUMA 节点系统上导致未定义行为,需验证兼容性。
  • 架构特定兼容性:s390x 的书籍(book)和 PowerPC 的 SMT-8 处理未完全覆盖,可能影响性能或功能。
  • OMP 库兼容性:不同 OpenMP 实现(IOMP、GOMP、标准 OMP)的环境变量设置可能冲突,尤其在嵌套环境中。
  • 回归风险:尽管修复了 ARM 性能问题,但其它架构(如 x86)的线程绑定策略变更可能引入新性能波动。
  • 测试覆盖不足:改动涉及多个核心模块,但测试文件变更较少,可能缺乏对边缘案例(如多 NUMA 节点、低内存场景)的验证。
  • 用户影响:CPU 后端用户将体验到改进的线程亲和性和内存局部性,可能提升推理性能,但需要确保系统 NUMA 配置正确。
  • 系统影响:重构影响 CPU 后端的整个资源管理链路,从进程启动到内存分配,可能改变多进程执行器的行为。
  • 团队影响:引入了集中化的 CPU 工具库,便于后续维护和扩展,但增加了对 OpenMP 和 NUMA 知识的依赖。
  • 影响程度:中等至高,因为核心路径变更且跨模块,但主要限于 CPU 后端,不直接影响 GPU 或其它设备。
核心路径变更 C++ 代码安全风险 架构特定兼容性 测试覆盖不足

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

执行摘要

  • 一句话:重构 CPU 亲和性与内存管理,修复性能回归并支持自动 KV 缓存大小分析。
  • 推荐动作:建议技术管理者和工程师精读 OMPProcessManager 类的设计,理解其如何适配不同 OpenMP 库和架构;同时关注 csrc/cpu/utils.cpp 中的 NUMA 代码风险,并在部署前进行多架构测试。

功能与动机

PR 描述指出,此重构旨在基于 #36487 修复 CPU 亲和性相关的功能和性能回归,并支持自动 KV 缓存大小分析。讨论中提及了之前版本中存在的 OMP 设置问题和跨架构兼容性问题,例如 ARM 多 rank 性能下降和 s390x 书籍(book)拓扑处理。

实现拆解

  1. 创建集中式 CPU 资源工具库:新增 vllm/utils/cpu_resource_utils.py,定义 LogicalCPUInfoMemoryNodeInfo 数据类,提供 get_memory_affinityparse_id_listget_memory_node_info 等函数,统一解析系统 CPU 拓扑和内存节点信息。
  2. 重构 OMP 多进程管理器:重写 vllm/utils/ompmultiprocessing.py 中的 OMPProcessManager 类,根据检测到的 OpenMP 库(IOMP、GOMP 或标准 OMP)动态设置环境变量(如 KMP_AFFINITYGOMP_CPU_AFFINITYOMP_PLACES),并支持架构特定的核心保留策略(如 ARM 多 rank 时每 rank 保留一核)。
  3. 清理平台 CPU 代码:修改 vllm/platforms/cpu.py,移除重复的 LogicalCPUInfo 定义和内存计算逻辑,转而导入 cpu_resource_utils 工具,简化平台配置和 OMP 管理器集成。
  4. 增强 CPU Worker 内存绑定:在 vllm/v1/worker/cpu_worker.py__init__determine_available_memory 方法中,添加基于 NUMA 节点的内存绑定(通过 torch.ops._C.init_cpu_memory_env)和动态 KV 缓存大小计算,利用 gpu_memory_utilization 配置自动调整。
  5. 实现 C++ 层 NUMA 绑定:在 csrc/cpu/utils.cpp 中新增 init_cpu_memory_env 函数,使用 libnuma API 进行内存页面迁移和绑定策略(MEMBIND 或 INTERLEAVE),但注意位掩码操作存在安全风险。
  6. 配套更新:调整 vllm/v1/executor/multiproc_executor.py 以使用新的 OMP 管理器,更新 vllm/config/cache.py 移除旧配置,修改测试文件(如 tests/models/language/generation/test_common.py)和 CI 脚本(如 .buildkite/scripts/hardware_ci/run-cpu-distributed-smoke-test.sh)以确保兼容性。

关键文件:

  • vllm/utils/cpu_resource_utils.py(模块 工具库;类别 source;类型 core-logic;符号 LogicalCPUInfo, _int, json_decoder, MemoryNodeInfo): 新增的集中式 CPU 资源工具库,定义核心数据结构和解析函数,为后续模块提供统一接口。
  • vllm/utils/ompmultiprocessing.py(模块 多进程管理;类别 source;类型 core-logic;符号 OMPProcessManager, _parse_omp_threads_bind_env, configure_omp_envs): 重构的 OMP 进程管理器,负责线程亲和性设置和跨 OpenMP 库兼容性,是性能优化的核心。
  • vllm/platforms/cpu.py(模块 平台层;类别 source;类型 dependency-wiring;符号 CpuPlatform, get_device_total_memory, check_and_update_config): 平台 CPU 代码清理,集成新工具并移除重复逻辑,影响设备内存计算和配置检查。
  • vllm/v1/worker/cpu_worker.py(模块 Worker 实现;类别 source;类型 core-logic;符号 init, determine_available_memory): CPU Worker 新增内存节点绑定和动态 KV 缓存大小分析,直接关系到推理时的内存分配性能。
  • csrc/cpu/utils.cpp(模块 C++ 核心;类别 source;类型 core-logic;符号 init_cpu_memory_env): C++ 层实现 NUMA 内存绑定,通过 libnuma 进行页面迁移和内存策略设置,但存在位掩码安全风险。

关键符号:LogicalCPUInfo, get_memory_affinity, parse_id_list, get_memory_node_info, OMPProcessManager, configure_omp_envs, init_cpu_memory_env

关键源码片段

vllm/utils/ompmultiprocessing.py

重构的 OMP 进程管理器,负责线程亲和性设置和跨 OpenMP 库兼容性,是性能优化的核心。

class OMPProcessManager:
    def __init__(self, config: "VllmConfig"):
        # 初始化时检测 OpenMP 库类型和架构,设置保留核心数
        self.use_iomp = "libiomp" in os.getenv("LD_PRELOAD", "")
        self.use_gomp = "libgomp" in os.getenv("LD_PRELOAD", "")
        self.reserve_cpu_num = 1 # 默认保留一核
        if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
            # ARM 架构下每 local_world_size 保留一核,以优化多 rank 性能
            self.reserve_cpu_num = self.local_world_size
        self._parse_omp_threads_bind_env() # 解析环境变量配置
​
    @contextmanager
    def configure_omp_envs(self, rank: int, local_rank: int):
        """上下文管理器,为指定 rank 设置 OpenMP 环境变量。"""
        cpu_list = [str(i) for i in self.cpu_lists[local_rank]]
        envs_dict = {}
        if self.use_iomp:
            # Intel OpenMP 设置
            envs_dict["KMP_AFFINITY"] = f"granularity=fine,explicit,proclist=[{','.join(cpu_list)}]"
        elif self.use_gomp:
            # GNU OpenMP 设置
            envs_dict["GOMP_CPU_AFFINITY"] = " ".join(cpu_list)
        else:
            # 标准 OpenMP 设置,使用非重叠的 places 格式
            envs_dict["OMP_PLACES"] = "{" + "},{}".join(cpu_list) + "}"
            envs_dict["OMP_PROC_BIND"] = "true"
        envs_dict["OMP_NUM_THREADS"] = str(len(cpu_list))
        # 备份并设置环境变量,确保线程亲和性
        old_envs = {k: os.environ.get(k) for k in envs_dict}
        try:
            for k, v in envs_dict.items():
                os.environ[k] = v
            yield
        finally:
            for k, v in old_envs.items():
                if v is None:
                    os.environ.pop(k, None)
                else:
                    os.environ[k] = v

vllm/v1/worker/cpu_worker.py

CPU Worker 新增内存节点绑定和动态 KV 缓存大小分析,直接关系到推理时的内存分配性能。

def __init__(self, vllm_config: VllmConfig, local_rank: int, rank: int, distributed_init_method: str, is_driver_worker: bool = False):
    # 获取允许的内存节点和 CPU 列表
    allowed_memory_nodes = get_visible_memory_node()
    allowed_cpu_list = get_allowed_cpu_list()
    cpu_core = allowed_cpu_list[0] # 假设每个 worker 绑定到第一个可用 CPU
    # 检查 NUMA 节点一致性
    if cpu_core.numa_node not in allowed_memory_nodes:
        logger.warning(f"Node {cpu_core.numa_node} 不在可用内存节点 {allowed_memory_nodes} 中。")
    # 调用 C++ 函数初始化 CPU 内存环境,绑定到指定 NUMA 节点
    torch.ops._C.init_cpu_memory_env([cpu_core.numa_node])
    # 计算请求的 CPU 内存基于 gpu_memory_utilization
    memory_status = get_memory_node_info(cpu_core.numa_node)
    memory_fraction = vllm_config.cache_config.gpu_memory_utilization
    self.requested_cpu_memory = math.ceil(memory_status.total_memory * memory_fraction)
    available_memory = memory_status.available_memory
    # 验证内存是否充足,否则抛出错误
    if vllm_config.cache_config.kv_cache_memory_bytes is None and self.requested_cpu_memory > available_memory:
        raise ValueError(f"可用内存不足,请降低 --gpu-memory-utilization。")
    super().__init__(vllm_config, local_rank, rank, distributed_init_method, is_driver_worker)

评论区精华

  • C++ NUMA 位掩码安全:gemini-code-assist[bot] 指出 csrc/cpu/utils.cpp 中直接 XOR 操作 bitmask->maskp 在大规模 NUMA 系统上不安全,可能只处理第一个字,但 bigPYJ1151 回应“不是问题”,未完全解决。
  • OMP 环境设置争议:kot-begemot-uk 强调必须同时设置 OMP_PLACESOMP_PROC_BIND,但 bigPYJ1151 解释采用 OMP_PLACES={{0},{1},...} 格式可显式绑定,决策为使用后者。
  • ARM 多 rank 性能回归:fadara01 报告 ARM 上双 rank(tp=2)性能下降,建议每 rank 保留一核;代码随后更新,在 ARM 架构下恢复每 rank 保留一核的策略。
  • s390x 书籍(book)处理:R3hankhan123 和 wjhrdy 讨论 s390x 的书籍拓扑,决定在其它 PR(如 #39191)中实现,此处使用 core 字段作为基础。
  • 导入缺失和代码风格:gemini-code-assist[bot] 发现 vllm/platforms/cpu.py 缺失 globCpuArchEnum 导入,已修复;同时修正了断言消息拼接和 OMP_PLACES 重叠问题。

    • C++ NUMA 位掩码安全 (correctness): 未完全解决,代码中保留风险。
    • OMP 环境设置设计 (design): 采用 bigPYJ1151 的方案,使用 OMP_PLACES={{0},{1},...} 格式。
    • ARM 多 rank 性能优化 (performance): 已修复,性能恢复。
    • s390x 书籍(book)处理 (design): 延迟处理,相关功能在 PR #39191 中。

风险与影响

  • 风险:- C++ 代码安全风险csrc/cpu/utils.cpp 中的位掩码操作可能在大规模 NUMA 节点系统上导致未定义行为,需验证兼容性。
  • 架构特定兼容性:s390x 的书籍(book)和 PowerPC 的 SMT-8 处理未完全覆盖,可能影响性能或功能。
  • OMP 库兼容性:不同 OpenMP 实现(IOMP、GOMP、标准 OMP)的环境变量设置可能冲突,尤其在嵌套环境中。
  • 回归风险:尽管修复了 ARM 性能问题,但其它架构(如 x86)的线程绑定策略变更可能引入新性能波动。
  • 测试覆盖不足:改动涉及多个核心模块,但测试文件变更较少,可能缺乏对边缘案例(如多 NUMA 节点、低内存场景)的验证。
  • 影响:- 用户影响:CPU 后端用户将体验到改进的线程亲和性和内存局部性,可能提升推理性能,但需要确保系统 NUMA 配置正确。
  • 系统影响:重构影响 CPU 后端的整个资源管理链路,从进程启动到内存分配,可能改变多进程执行器的行为。
  • 团队影响:引入了集中化的 CPU 工具库,便于后续维护和扩展,但增加了对 OpenMP 和 NUMA 知识的依赖。
  • 影响程度:中等至高,因为核心路径变更且跨模块,但主要限于 CPU 后端,不直接影响 GPU 或其它设备。
  • 风险标记:核心路径变更, C++ 代码安全风险, 架构特定兼容性, 测试覆盖不足

关联脉络

  • PR #36487 参考重构基础: 此 PR 基于 #36487 进行 CPU 亲和性重构。
  • PR #39191 提及用于 s390x books: 讨论中提及 #39191 用于处理 s390x 书籍拓扑。

参与讨论