Prhub

#40569 [CPU][RISC-V] Auto-bind OMP threads and harden nobind path

原始 PR 作者 lyd1992 合并时间 2026-05-06 19:38 文件变更 1 提交数 6 评论 4 代码增减 +14 / -7

执行摘要

RISC-V 平台 OMP 线程绑定支持与 nobind 路径加固

RISC-V 平台启动 vLLM CPU worker 时,默认的 VLLM_CPU_OMP_THREADS_BIND=auto 会引发 RuntimeError("RISCV doesn't support auto CPU binding"),且 nobind 模式因 cpu_lists=[] 导致 IndexError。需要让 RISC-V 架构与 ARM 一样获得完整的 OMP 线程绑定支持,以确保 CPU 后端在该平台上的可用性和稳定性。

值得合并,改动量小、目标明确、测试充分(在 SG2044 硬件上验证了 auto 和 nobind 模式)。此 PR 是 RISC-V CPU 后端支持链中的一环,与 Issue #40119(RVV 注意力核)等形成功能基线。建议使用者关注后续 CPU 架构扩展模式。

讨论亮点

Review 中仅有一条实质讨论:

  • reviewer bigPYJ1151 指出作者最初在 configure_omp_envs 中添加的 local_rank >= len(self.cpu_lists)not self.cpu_lists[local_rank] 条件是多余的:nobind 时 skip_setup 已提前返回,手动/自动模式都有充分性检查。
  • 作者 lyd1992 同意并在后续 commit d8f8194 中移除了这些条件,使代码更简洁。
  • 结论清晰,无未解决疑虑。

实现拆解

本次变更仅涉及 vllm/utils/ompmultiprocessing.py 一个文件,分三个修复点:

  1. 扩展自动绑定分支(核心逻辑)
    - 在 __init__ 方法中,将 reserve_cpu_num 条件从 CpuArchEnum.ARM 扩展为 (CpuArchEnum.ARM, CpuArchEnum.RISCV),使得 RISC-V 也按 local_world_size 保留调度器核心。
    - 在 _parse_omp_threads_bind_env 方法中,将自动绑定的 ARM 分支条件从 cpu_arch == CpuArchEnum.ARM 改为 cpu_arch in (CpuArchEnum.ARM, CpuArchEnum.RISCV),RISC-V 与 ARM 共享“无 SMT,使用所有逻辑核心”的逻辑。
    - 原因:RISC-V 与 AArch64 一样没有 SMT,因此 ARM 的 CPU 选择策略(使用所有逻辑 CPU)完全适用。

  2. 加固 nobind 路径(bugfix)
    - 原 configure_omp_envs 中,当 VLLM_CPU_OMP_THREADS_BIND=nobind 时,skip_setup=True 导致 cpu_lists=[],但 local_world_size=1,随后 cpu_lists[local_rank] 触发 IndexError。
    - 作者最初添加了 local_rank >= len(self.cpu_lists)not self.cpu_lists[local_rank] 两个守护条件(commit af12451),但经 reviewer bigPYJ1151 指出这些是多余的,因为 skip_setup 已短路 nobind 路径,且手动/自动模式都有断言保证。随后作者在 commit d8f8194 中移除了这些冗余条件。

  3. 修复绑定日志循环并增强日志(代码质量)
    - 原日志循环 for i in range(self.local_world_size): ... self.cpu_lists[i]cpu_lists 长度不足时越界。
    - 改为 for i, cpus in enumerate(self.cpu_lists),安全迭代。
    - 同时增加日志输出 VLLM_CPU_OMP_THREADS_BINDauto_setupskip_setupreserve_cpu_num 等配置值,便于调试。

未涉及测试、配置、schema 或部署的改动。

文件 模块 状态 重要度
vllm/utils/ompmultiprocessing.py CPU 工具 modified 6.15

关键符号

OMPProcessManager.__init__ OMPProcessManager._parse_omp_threads_bind_env OMPProcessManager.configure_omp_envs

关键源码片段

vllm/utils/ompmultiprocessing.py core-logic

唯一变更文件,包含所有三个修复:RISC-V 自动绑定支持、nobind 路径保护、日志循环修复与增强。

# vllm/utils/ompmultiprocessing.py ( 关键变更片段 )class OMPProcessManager:
    def __init__(self, config: "VllmConfig"):
        if not current_platform.is_cpu():
            return
        # ... 初始化属性 ...
​
        # at least reserve 1/local_world_size(for ARM/RISC-V) core for scheduler
        # proc as always use MP executor
        # RISC-V 无 SMT,与 ARM 一样需要为调度器保留 local_world_size 个核心
        self.reserve_cpu_num = (
            self.local_world_size
            if current_platform.get_cpu_architecture()
            in (CpuArchEnum.ARM, CpuArchEnum.RISCV) # 扩展条件:包含 RISCV
            else 1
        )
        # reserve at one more core for nixl_connector under p/d case
        if config.kv_transfer_config:
            self.reserve_cpu_num += 1
        # ...
​
    def _parse_omp_threads_bind_env(self):
        vllm_mask = envs.VLLM_CPU_OMP_THREADS_BIND
        self.skip_setup = vllm_mask == "nobind"
        self.auto_setup = vllm_mask == "auto"
        self.reserved_cpu_list = []
        self.cpu_lists = []
​
        if self.auto_setup:
            cpu_arch = current_platform.get_cpu_architecture()
            if cpu_arch == CpuArchEnum.POWERPC:
                # For POWERPC SMT-8/4/2
                cpu_list, reserve_list = self._get_autobind_cpu_ids(
                    lambda cpus: [cpu for cpu in cpus if cpu.id % 8 < 4]
                )
            elif cpu_arch in (CpuArchEnum.X86, CpuArchEnum.S390X):
                # For x86/S390X SMT-2, use 1 logical CPU per physical core
                cpu_list, reserve_list = self._get_autobind_cpu_ids(
                    lambda cpus: cpus[-1:]
                )
            elif cpu_arch in (CpuArchEnum.ARM, CpuArchEnum.RISCV):
                # For AArch64 / RISC-V, no SMT, use all logical CPUs
                cpu_list, reserve_list = self._get_autobind_cpu_ids(lambda cpus: cpus)
            else:
                cpu_list, reserve_list = [], []
                raise RuntimeError(f"{cpu_arch} doesn't support auto CPU binding.")
            # ... 构建 cpu_lists 和 reserved_cpu_list ...
        elif not self.skip_setup:
            # manual mode: 解析用户提供的 CPU 列表
            # ...
        else:
            # nobind mode: cpu_lists 保持为空,后续 configure_omp_envs 因 skip_setup=True 跳过
            msg = (
                "OpenMP thread binding info: \n"
                f"\tVLLM_CPU_OMP_THREADS_BIND={vllm_mask!r}, "
                f"auto_setup={self.auto_setup}, skip_setup={self.skip_setup}\n"
                f"\tlocal_world_size={self.local_world_size}, "
                f"reserve_cpu_num={self.reserve_cpu_num}\n"
            )
            # 使用 enumerate 安全遍历 cpu_lists,避免越界
            for i, cpus in enumerate(self.cpu_lists):
                msg += f"\tlocal_rank={i}, core ids={cpus}\n"
            msg += f"\treserved_cpus={self.reserved_cpu_list}"
            logger.info(msg)
​
    @contextmanager
    def configure_omp_envs(self, rank: int, local_rank: int):
        # nobind 时 skip_setup=True,直接 yield 返回,不访问 cpu_lists
        if not current_platform.is_cpu() or self.skip_setup:
            yield
            return
        # ... 正常绑定路径 ...

评论区精华

configure_omp_envs 中的冗余守护条件 设计

reviewer bigPYJ1151 指出作者添加的 `local_rank >= len(self.cpu_lists)` 和 `not self.cpu_lists[local_rank]` 条件是多余的,因为 nobind 路径已被 `skip_setup` 短路,手动 / 自动模式都有检查。

结论:作者同意并移除了冗余条件,使代码更简洁。 · 已解决

风险与影响

变动集中在一个文件中,影响范围限于 CPU 后端的 OMP 线程绑定逻辑。风险较低:

  • 回归风险小:RISC-V 分支是 ADDITIVE 的,不影响已有 x86/ARM/POWERPC 路径;日志循环改用 enumerate 是安全的重构。
  • 性能风险无:逻辑等价,仅扩展条件判断。
  • 兼容性风险:无,功能新增且无 API/配置变更。
  • 用户影响:RISC-V 平台的 vLLM CPU 用户现在可以正常使用默认 auto 绑定模式和 nobind 模式,此前无法启动;x86/ARM 用户无感知。
  • 系统影响:无。
  • 团队影响:对 CPU 后端维护者而言,此 PR 降低了 RISC-V 平台的维护成本,为后续 RVV 注意力核(Issue #40119)等特性提供基础。
  • 影响程度:中等,RISC-V 用户的关键阻塞被移除,但整体用户基数小。

关联 Issue

#40119 [CPU][RISC-V] Add RVV-optimized attention kernels for RISC-V Vector Extension

完整报告

参与讨论