Prhub

#6086 [hardware] feat: add platform abstraction layer and plugin-based engine override system

原始 PR 作者 physics31415926 合并时间 2026-06-04 18:12 文件变更 27 提交数 48 评论 45 代码增减 +1719 / -149

执行摘要

新增平台抽象层与插件化引擎覆写系统

verl 目前原生支持 NVIDIA/AMD/Ascend,但其他训练后端(如 DeepSpeed、Bumblebee)应动态加载以避免维护在源码树中。PR 旨在提供一个统一的平台抽象层和插件系统,让新硬件可被动态注册而不修改 verl 源码,同时保持向后兼容。

值得精读。该 PR 是 verl 多芯片支持的基础设施重构,其 PlatformBase + PlatformRegistry 设计模式清晰,与 EngineRegistry 的 last-writer-wins 覆写配合形成可扩展插件体系。Review 讨论中关于检测策略、覆写策略的权衡也富有参考价值。

讨论亮点
  • 使用 VERL_USE_EXTERNAL_MODULES 替代 custom_engine_module:wuxibin89 指出已有 VERL_USE_EXTERNAL_MODULES 钩子(verl/__init__.py)用于动态加载外部模块,建议复用而非新增配置。作者接受并移除 custom_engine_module 字段。
  • 移除 CPU 后端:wuxibin89 认为 verl 不打算支持纯 CPU 后端,要求移除 PlatformCPU。作者照做,自动检测只在 CUDA/NPU 之间选择。
  • EngineRegistry 覆写策略:gemini-code-assist 指出 assert key not in … 阻止了插件覆写引擎,违反 last-writer-wins 设计。heavyrain-lzy 最初表示“覆写被禁止”,但后续提交改为允许覆写,最终版本已移除断言。
  • CUDA 平台检测的 nvidia-smi 回退:gemini-code-assist 建议 nvidia-smi 不可用时也应 fallback 到 torch.cuda.is_available()。heavyrain-lzy 坚持必须通过 smi 检测,因为 CPU-only Ray actor 下 torch.cuda.is_available() 可能返回 False。最终保留了 smi 优先逻辑。
  • Profiler 与 DistProfiler 分层:tardis-key 询问 PlatformBase 中的 profiler 方法与现有 DistProfiler 的关系。作者解释 PlatformBase 提供最低层设备抽象,DistProfiler 做高层调度,两者不重叠。

实现拆解

  1. 定义平台抽象接口:在 verl/plugin/platform/platform_base.py 中定义 PlatformBase 抽象基类,包含设备管理(device_namevendor_namecurrent_device 等)、随机数种子、内存管理、通信、Ray 集成等抽象方法。
  2. 实现平台注册与自动检测:在 platform_manager.py 中提供 PlatformRegistry 类,支持 @register(platform="xxx") 装饰器注册具体平台;_detect_platform_name() 按照环境变量 VERL_PLATFORM、注册列表逐项探测、最终 fallback 的顺序返回平台名称;缓存成全局单例。
  3. 内置平台实现PlatformCUDA(注册为 "nvidia")和 PlatformNPU(注册为 "huawei")分别托管 NVIDIA GPU 和 Ascend NPU,利用 is_platform_available(use_smi_check=True) 处理 CPU-only Ray actor 下的检测问题。
  4. 改造 EngineRegistry:在 verl/workers/engine/base.py 中修改 EngineRegistry.register() 允许同名引擎被覆盖(last-writer-wins),get_engine_cls() 使用 get_device_name() 自动匹配当前设备,移除原先的 VERL_ENGINE_DEVICE 环境变量。
  5. 重构设备工具函数verl/utils/device.py 中的所有公开函数(get_device_nameget_vendorset_expandable_segments 等)内部委托给 get_platform(),80+ 调用点无需修改。
  6. 测试与文档:新增 tests/plugin/test_platform_abstraction.py 覆盖平台注册、自动检测、mock 平台注入、engine 获取等;更新 docs/hardware/multi_chip_support.rst 描述架构;提供示例脚本 examples/grpo_trainer/run_qwen3-0.6b_fl.sh
文件 模块 状态 重要度
verl/plugin/platform/platform_base.py 平台层 added 9.08
verl/plugin/platform/platform_manager.py 平台层 added 8.96
verl/plugin/platform/platform_npu.py 平台层 added 8.81
verl/utils/device.py 工具层 modified 8.58
tests/plugin/test_platform_abstraction.py 测试 added 7.42
verl/workers/engine/base.py 训练引擎 modified 7.34

关键符号

PlatformBase.check_smi_command PlatformBase.device_name PlatformBase.vendor_name PlatformBase.is_available PlatformBase.current_device PlatformRegistry.register PlatformRegistry.get PlatformRegistry.registered_names _detect_platform_name get_platform get_device_name get_vendor set_expandable_segments EngineRegistry.register EngineRegistry.get_engine_cls

关键源码片段

verl/plugin/platform/platform_base.py dependency-wiring

定义了平台抽象层的核心接口 PlatformBase,所有硬件后端必须继承此类,是整个多芯片架构的基石。

# verl/plugin/platform/platform_base.py
# 硬件无关的抽象基类,所有平台(CUDA, NPU, 等)必须实现这些方法。import abc
import os
import shutil
import subprocess
from typing import Optionalclass PlatformBase(abc.ABC):
    """Hardware-agnostic interface for accelerator backends."""
​
    # ------------------------------------------------------------------
    # Core device management ( 核心设备管理 )
    # ------------------------------------------------------------------
​
    @staticmethod
    def check_smi_command(cmd: str) -> bool:
        """运行 SMI 命令(如 nvidia-smi),返回是否成功。用于自动检测时区分同类硬件。"""
        cmd_path = shutil.which(cmd)
        if cmd_path is None:
            common_paths = [
                f"/usr/bin/{cmd}",
                f"/usr/local/bin/{cmd}",
                f"/usr/local/cuda/bin/{cmd}",
            ]
            for path in common_paths:
                if os.path.isfile(path) and os.access(path, os.X_OK):
                    cmd_path = path
                    break
            if cmd_path is None:
                return False
        try:
            result = subprocess.run([cmd_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10)
            return result.returncode == 0
        except (subprocess.TimeoutExpired, OSError):
            return False
​
    @property
    @abc.abstractmethod
    def device_name(self) -> str:
        """返回设备类型字符串,如 ``'cuda'``, ``'npu'``。"""
        ...
​
    @property
    @abc.abstractmethod
    def vendor_name(self) -> str:
        """返回硬件厂商名称,如 ``'nvidia'``, ``'huawei'``。"""
        ...
​
    @property
    @abc.abstractmethod
    def device_module(self) -> ModuleType:
        """返回 torch.<device> 模块,如 torch.cuda。"""
        ...
​
    @abc.abstractmethod
    def is_available(self) -> bool:
        """返回当前进程是否可用该设备。"""
        ...
​
    def is_platform_available(self, use_smi_check=False) -> bool:
        """返回当前主机是否支持该平台(用于自动检测)。当 use_smi_check=True 时可采用宽松检查。"""
        return self.is_available()
​
    @abc.abstractmethod
    def current_device(self) -> int:
        """返回当前设备索引。"""
        ...
​
    @abc.abstractmethod
    def device_count(self) -> int:
        """返回可用设备数量。"""
        ...
​
    @abc.abstractmethod
    def set_device(self, device_index: int) -> None:
        """选择指定索引的设备。"""
        ...
​
    @abc.abstractmethod
    def synchronize(self, device_index: Optional[int] = None) -> None:
        """等待设备完成所有操作。"""
        ...
verl/plugin/platform/platform_manager.py dependency-wiring

提供 PlatformRegistry 注册器、自动检测逻辑和全局平台单例,是插件的入口管理组件。

# verl/plugin/platform/platform_manager.py
# 平台注册器与自动检测import logging
import osfrom .platform_base import PlatformBaselogger = logging.getLogger(__name__)
_current_platform: PlatformBase | None = Noneclass PlatformRegistry:
    """将平台名称映射到 PlatformBase 子类的注册表。"""
​
    _platforms: dict[str, type[PlatformBase]] = {}
​
    @classmethod
    def register(cls, platform: str):
        """类装饰器:注册一个 PlatformBase 子类。
        用法示例:
            @PlatformRegistry.register(platform="nvidia")
            class PlatformCUDA(PlatformBase): ...
        """
        def decorator(platform_cls: type[PlatformBase]) -> type[PlatformBase]:
            assert issubclass(platform_cls, PlatformBase), f"{platform_cls.__name__} must be a subclass of PlatformBase"
            name = platform.strip().lower()
            if name in cls._platforms:
                logger.info("PlatformRegistry: overriding %s (%s -> %s)",
                            name, cls._platforms[name].__name__, platform_cls.__name__)
            cls._platforms[name] = platform_cls
            return platform_cls
        return decorator
​
    @classmethod
    def get(cls, name: str) -> type[PlatformBase] | None:
        """按名称获取已注册的平台类。"""
        return cls._platforms.get(name.strip().lower())
​
    @classmethod
    def registered_names(cls) -> tuple[str, ...]:
        """返回所有已注册的平台名称。"""
        return tuple(cls._platforms.keys())
​
​
def _detect_platform_name() -> str:
    """探测当前环境并返回最佳平台名称。
    检测顺序:
    1. VERL_PLATFORM 环境变量(显式指定)
    2. 遍历已注册平台,调用 is_platform_available(use_smi_check=True)
    3. 若均不可用,fallback 为 "nvidia"
    """
    env_platform = os.environ.get("VERL_PLATFORM", "").strip().lower()
    if env_platform:
        logger.info("Platform override from VERL_PLATFORM: %s", env_platform)
        return env_platform
​
    names = PlatformRegistry.registered_names()
    logger.info("Registered platforms: %s", names)
​
    for name in names:
        platform_cls = PlatformRegistry.get(name)
        if platform_cls is None:
            continue
        try:
            instance = platform_cls()
            if instance.is_platform_available(use_smi_check=True):
                logger.info("Auto-detected platform: %s", name)
                return name
        except Exception as e:
            logger.debug("Platform '%s' detection failed: %s", name, e)
            continue
​
    logger.warning("No supported accelerator detected. Registered: %s. Falling back to 'nvidia'.", names)
    return "nvidia"
​
​
def get_platform() -> PlatformBase:
    """返回当前平台单例(首次调用时自动检测)。"""
    global _current_platform
    if _current_platform is None:
        name = _detect_platform_name()
        _current_platform = _create_platform(name)
    return _current_platform
verl/plugin/platform/platform_npu.py dependency-wiring

华为 Ascend NPU 平台的实现,展示了平台类如何注册并实现抽象方法,是内置双平台之一。

# verl/plugin/platform/platform_npu.py
# 华为 Ascend NPU 平台实现import logging
import os
from types import ModuleType
from typing import Optionalimport torchfrom .platform_base import PlatformBase
from .platform_manager import PlatformRegistrylogger = logging.getLogger(__name__)def _ensure_torch_npu() -> bool:
    """尝试导入 torch_npu,使 torch.npu 可用。"""
    if hasattr(torch, "npu"):
        return True
    try:
        import torch_npu # noqa: F401
        return hasattr(torch, "npu")
    except Exception as e:
        logger.debug("The current machine has no torch.npu, because: %s", e)
    return False_ensure_torch_npu() # 模块加载时即尝试导入,加速后续检查@PlatformRegistry.register(platform="huawei")
class PlatformNPU(PlatformBase):
    """华为 Ascend NPU 平台。"""
​
    @property
    def device_name(self) -> str:
        return "npu"
​
    @property
    def vendor_name(self) -> str:
        return "huawei"
​
    @property
    def device_module(self) -> ModuleType:
        return torch.npu
​
    def is_available(self) -> bool:
        return torch.npu.is_available()
​
    def is_platform_available(self, use_smi_check=False) -> bool:
        """宽松检测:当 use_smi_check=True 时,仅需 torch_npu 可导入即返回 True。"""
        if not _ensure_torch_npu():
            return False
        if use_smi_check:
            return True # torch_npu 已导入,NPU 环境确认
        return torch.npu.is_available()
​
    def current_device(self) -> int:
        return torch.npu.current_device()
​
    def device_count(self) -> int:
        return torch.npu.device_count()
​
    def set_device(self, device_index: int) -> None:
        torch.npu.set_device(device_index)
​
    def synchronize(self, device_index: Optional[int] = None) -> None:
        torch.npu.synchronize(device_index)
​
    # ...... ( 其余方法遵循相同模式 )

评论区精华

使用 VERL_USE_EXTERNAL_MODULES 替代 custom_engine_module 设计

wuxibin89 指出已有 VERL_USE_EXTERNAL_MODULES 钩子用于动态加载外部模块,建议复用。

结论:接受,移除 custom_engine_module 相关字段,改用 VERL_USE_EXTERNAL_MODULES。 · 已解决

移除 CPU 后端 设计

wuxibin89 认为 verl 不支持纯 CPU 训练,要求移除 PlatformCPU。

结论:接受,移除 PlatformCPU 类,自动检测只覆盖 CUDA/NPU。 · 已解决

EngineRegistry 是否允许覆写 设计

gemini-code-assist 指出 assert 阻止插件覆写引擎。heavyrain-lzy 最初表示禁止覆写,但后期 PR 改为 last-writer-wins。

结论:最终允许覆写,移除 assert,改为日志提示。 · 已解决

CUDA 平台检测的 nvidia-smi 回退策略 正确性

gemini-code-assist 建议 nvidia-smi 不可用时 fallback 到 torch.cuda.is_available()。heavyrain-lzy 坚持必须通过 smi 检测,因为 CPU-only Ray actor 下 torch.cuda.is_available() 可能返回 False。

结论:保留 smi 优先逻辑,不添加 torch.cuda.is_available() fallback。 · 已解决

Profiler 方法与 DistProfiler 的关系 设计

tardis-key 询问 PlatformBase 中的 profiler_start/stop 与现有 DistProfiler 的协作关系。作者解释 PlatformBase 提供最底层设备抽象,DistProfiler 做高层调度,两者不重叠。tardis-key 担忧多 profiler 场景下的耦合。

结论:作者维持分层设计,认为 Profiler 是设备能力的一部分。tardis-key 未完全认同,但未继续争论,设计保持不变。 · 已采纳

风险与影响

  1. 回归风险verl/utils/device.py 重构为委托调用,80+ 外部调用点虽未改动签名,但若 get_platform() 返回的平台与预期不符(如自动检测误判),可能导致 torch.cudatorch.npu 替代而引发属性错误。
  2. 自动检测准确性is_platform_available(use_smi_check=True) 在 CPU-only actor 下依赖 smi 命令,若 smi 未安装或路径不对,检测可能失败 fallback 到 "nvidia",造成后续调用错误。
  3. 外部插件安全VERL_USE_EXTERNAL_MODULES 允许任意 Python 模块被导入,可能引入恶意代码或版本冲突,需用户自行保证插件可信。
  4. 多平台冲突:若同时安装多个平台插件且自动检测优先顺序不明确,可能注册了错误平台。_detect_platform_name 按注册顺序探测,用户需确保环境变量 VERL_PLATFORM 显式指定。
  • 用户影响:现有用户无需动作,verl.utils.device 保持向后兼容。新用户如需多芯片支持,只需安装对应插件并设置 VERL_USE_EXTERNAL_MODULES
  • 系统影响:架构上清晰分离了设备抽象与训练引擎,降低了添加新硬件门槛。EngineRegistry 的 last-writer-wins 使得插件可以完全替换默认引擎。
  • 团队影响:平台抽象层文档完善(docs/hardware/multi_chip_support.rst),便于协作者贡献新后端。未来设备相关改动应通过 PlatformBase 扩展而非直接修改 device.py
核心路径变更 自动检测误判 外部插件安全 缺少端到端测试覆盖所有硬件

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论