Prhub

#49458 Hardware-agnostic model definition via HF transformer backend (1/N)

原始 PR 作者 bohnstingl 合并时间 2026-08-13 18:21 文件变更 10 提交数 12 评论 30 代码增减 +691 / -5

执行摘要

引入 hw-agnostic 层解析与 VLLM_USE_HW_AGNOSTIC 开关

PR body 提出的目标是实现硬件无关的模型定义:让 tail 模型的建模代码驻留在 HF transformers 中,通过 --model-impl transformers 后端在 vLLM 内执行。现有后端靠 patch 方式把 HF 特定层替换为 vLLM 原生实现,而这些实现往往依赖特定硬件 kernel;本 PR 通过 VLLM_USE_HW_AGNOSTIC=1 让 hw-agnostic 层优先被导入、失败才回退默认 vLLM 层,从而把层实现与具体硬件 kernel 解耦。作者明确说明其实现基础来自 #45470 的 DSv4 迁移工作。

值得精读。这是 transformers 后端走向硬件无关模型定义的奠基 PR,重点学习三点:一是 _resolve 的“优先导入 + 异常回退”通用解析模式,避免逐层手动映射;二是 CustomOp.__new__ 在实例化期做 OOT 替换的设计,实现外部覆盖而调用方无感;三是 review 中关于动态创建类与 CUDA Graphs 兼容性的经验教训。建议结合 #45470 的 DSv4 迁移与后续 2/N 系列一起阅读,跟踪该机制如何从基础设施走向真实模型落地。

讨论亮点

Review 中核心交锋如下:

  • 是否需要逐层选择:tdoublep 追问“Do we really need this complexity of selecting/de-selecting individual layers to be HW agnostic?”,作者随后把按层列表选择简化为布尔开关。
  • 环境变量命名不一致:tdoublep 指出 PR body 写的是 VLLM_USE_HW_AGNOSTIC,但初版代码定义的是 VLLM_HW_AGNOSTIC_LAYERS;hmellor 补充“In the test plan it explicitly says VLLM_USE_HW_AGNOSTIC=1”。最终统一为 VLLM_USE_HW_AGNOSTIC
  • 手动逐类映射的扩展性问题:hmellor 提出“It's not very scalable to need manual mapping for every layer class. Could we not have something generic that attempts to import everything first from HW agnostic and then from the rest of vLLM for any class name?” 作者据此引入通用 _resolve shim,tdoublep 认可“I think the approach looks better!”
  • 动态类与 CUDA Graphs / torch.compile 兼容性:hmellor 在 rms_norm.py 上提醒动态创建 TPAware 子类可能有问题,作者回应“I reverted this change.”。
  • 日志措辞:tdoublep 认为 “hw-agnostic vs vLLM” 的措辞暗示 hw-agnostic 不属于 vLLM,建议改为 “falling back to default”,作者已调整。
  • 未解决问题:zou3519 认为 “VLLM_USE_HW_AGNOSTIC=1 ux is a little weird”,作者询问替代方案但未获答复,留待后续。

实现拆解

  1. 环境开关:在 vllm/envs.py 中新增 VLLM_USE_HW_AGNOSTIC 布尔环境变量(默认 False,解析 true/1),作为 transformers 后端层选择的全局开关。

  2. 注册与替换基础设施:新增 vllm/model_executor/hw_agnostic/custom_op.py,定义 op_registry(内置)与 op_registry_oot(OOT 覆盖)两个注册表,以及 CustomOp(算子级)与 PluggableLayer(模块组合级)两个基类。二者都在 __new__ 中按类名查询 OOT 注册表,实现实例化期的整体替换;maybe_get_oot_by_class 提供不经过 __new__ 的查询入口。register / register_oot 装饰器分别登记内置与外部覆盖类。

  3. 首批 hw-agnostic 实现hw_agnostic/layers/activation.py 提供 SiluAndMul(SwiGLU 激活)及按 HF 激活名索引的 get_act_and_mul_fnhw_agnostic/layers/layernorm.py 提供 RMSNorm(含 _rms_norm_fused_add_rms_norm,均走 fp32 中间计算以保证跨后端数值一致)。

  4. transformers 后端解析入口:新增 vllm/model_executor/models/transformers/layers.py_resolve(module, name) 在开关开启时优先从 hw_agnostic.layers.<module> 导入符号,捕获 ImportError/AttributeError 后回退到 vllm.model_executor.layers;并在 import 期解析 RMSNormGemmaRMSNormget_act_and_mul_fn 按激活名逐名回退。同步改造 fusers/glu.pyfusers/rms_norm.py 的导入路径,使 GLU 融合与 RMSNorm 融合均经过新的层解析器。

  5. 测试配套:新增 tests/models/transformers/test_layer_registry.py,用 monkeypatch 注入伪 hw-agnostic 模块验证开关 0/1、缺失符号回退、未知激活名回退,并用随机初始化的 tiny Llama 以 VLLM_USE_HW_AGNOSTIC=0/1 分别端到端 serve,通过 apply_model 内省各层 provider,并用 greedy logprobs 对比校验输出一致。

文件 模块 状态 重要度
vllm/model_executor/hw_agnostic/custom_op.py 算子注册 added 9.17
vllm/model_executor/models/transformers/layers.py 层解析器 added 8.18
vllm/model_executor/hw_agnostic/layers/layernorm.py 归一化层 added 8.82
vllm/model_executor/hw_agnostic/layers/activation.py 激活函数 added 7.56
tests/models/transformers/test_layer_registry.py 回归测试 added 7.89
vllm/envs.py 环境配置 modified 5.13
vllm/model_executor/models/transformers/fusers/glu.py GLU 融合 modified 5.0
vllm/model_executor/models/transformers/fusers/rms_norm.py 归一化融合 modified 4.94

关键符号

maybe_get_oot_by_class PluggableLayer.__new__ PluggableLayer.register PluggableLayer.register_oot CustomOp.__new__ CustomOp.register CustomOp.register_oot CustomOp.forward_native CustomOp.forward_oot _resolve get_act_and_mul_fn RMSNorm.forward_native SiluAndMul.forward_native

关键源码片段

vllm/model_executor/hw_agnostic/custom_op.py core-logic

整个 hw-agnostic 机制的核心:定义 op_registry / op_registry_oot 注册表、CustomOp / PluggableLayer 基类及 register / register_oot 装饰器,在 __new__ 中实现按类名的 OOT 实例化替换,是所有后续 hw-agnostic 层的地基。

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""hw-agnostic 基础设施:CustomOp / PluggableLayer 的注册与 OOT 替换。OOT(out-of-tree)注册表允许硬件厂商或后端以同名类覆盖 vLLM 默认实现;
实例化时通过 __new__ 按类名查找覆盖,调用方无需感知替换。
"""
import torch.nn as nn# 两个注册表均按注册名索引到类:
# - op_registry : vLLM 内置(in-tree)算子 / 层
# - op_registry_oot : 外部覆盖(out-of-tree),优先于内置实现
op_registry: dict[str, type] = {}
op_registry_oot: dict[str, type] = {}
​
​
def maybe_get_oot_by_class(class_type: type) -> type:
    """按类名查询 OOT 覆盖;无覆盖时原样返回原类。"""
    class_name = class_type.__name__
    if class_name in op_registry_oot:
        return op_registry_oot[class_name]
    return class_type
​
​
class CustomOp(nn.Module):
    """自定义算子基类:在 __new__ 中做 OOT 替换分发。    OOT 覆盖发生在实例化期,子模块组合、参数初始化等完全由覆盖类接管;
    forward 则在 forward_native(原生)与 forward_oot(插件覆盖)间选择。
    """
​
    def __new__(cls, *args, **kwargs):
        try:
            op_name = cls.__name__ # 经 @CustomOp.register 注册后才有 name
        except AttributeError:
            raise TypeError(
                f"Cannot instantiate '{cls.__name__}': its 'name' attribute "
                f"was not set, possibly because it was not decorated with "
                f"@CustomOp.register, or it's the CustomOp base class itself."
            ) from None
​
        # 同名 OOT 算子存在时,改实例化 OOT 类,其余参数原样透传
        if op_name not in op_registry_oot:
            op_cls_to_instantiate = cls
        else:
            op_cls_to_instantiate = op_registry_oot[op_name]
            logger.debug(
                "Instantiating custom op: %s using %s",
                op_name,
                str(op_cls_to_instantiate),
            )
        return super().__new__(op_cls_to_instantiate)
​
    def forward(self, *args, **kwargs):
        return self._forward_method(*args, **kwargs)
​
    def forward_native(self, *args, **kwargs):
        """PyTorch 原生实现;OOT 插件可覆盖 forward_oot 来替换它。"""
        raise NotImplementedError
​
    def forward_oot(self, *args, **kwargs):
        # 默认回退到原生实现,即未覆盖时行为不变
        return self.forward_native(*args, **kwargs)
​
    @classmethod
    def register(cls, name: str, dynamic_arg_dims=None):
        """注册内置算子:@CustomOp.register('rms_norm')"""
        def decorator(op_cls):
            assert name not in op_registry, f"Duplicate op name: {name}"
            op_cls.name = name # __new__ 依赖该属性查出可覆盖名
            op_cls._dynamic_arg_dims = dynamic_arg_dims
            op_registry[name] = op_cls
            return op_cls
​
        return decorator
​
    @classmethod
    def register_oot(cls, _decorated_op_cls=None, name: str | None = None):
        """注册 OOT 覆盖,支持带括号/不带括号两种装饰器写法。"""
        def decorator(op_cls):
            reg_name = name if name is not None else cls.__name__
            assert reg_name not in op_registry_oot, f"Duplicate op name: {reg_name}"
            op_cls.name = reg_name
            op_registry_oot[reg_name] = op_cls
            return op_cls
​
        if _decorated_op_cls is None:
            return decorator # @CustomOp.register_oot() 或 (name=...)
        if isinstance(_decorated_op_cls, type):
            return decorator(_decorated_op_cls) # @CustomOp.register_oot
        raise TypeError("Decorator can only be applied to classes.")
vllm/model_executor/models/transformers/layers.py core-logic

transformers 后端的层解析入口:以 _resolve 实现 hw-agnostic 优先、默认回退的逐符号解析,并在模块导入期解析 RMSNorm / GemmaRMSNorm,是 fusers 改造与测试的锚点。

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Transformers 后端的层解析入口。当 VLLM_USE_HW_AGNOSTIC 开启时,层符号优先从 hw_agnostic.layers.<module>
导入;未移植的符号自动回退到 vllm.model_executor.layers。<module>。
每次解析都会记录实际来源,便于用户判断哪些层跑在 hw-agnostic 实现上。
"""
import importlibimport vllm.envs as envs
from vllm.logger import init_loggerlogger = init_logger(__name__)_HW_PKG = "vllm.model_executor.hw_agnostic.layers"
_VLLM_PKG = "vllm.model_executor.layers"
​
​
def _resolve(module: str, name: str):
    """从 hw-agnostic 模块取 name;未启用或符号缺失时回退到 vLLM。"""
    if envs.VLLM_USE_HW_AGNOSTIC:
        try:
            obj = getattr(importlib.import_module(f"{_HW_PKG}.{module}"), name)
            logger.info("Using hw-agnostic layer: %s", name)
            return obj
        except (ImportError, AttributeError):
            # 模块未移植或符号不存在:按层回退,避免整体切换失败
            logger.warning(
                "hw-agnostic layer %s is not available; falling back to default",
                name,
            )
    return getattr(importlib.import_module(f"{_VLLM_PKG}.{module}"), name)
​
​
# import 期解析:模型构建时直接引用这里的 RMSNorm / GemmaRMSNorm
RMSNorm = _resolve("layernorm", "RMSNorm")
GemmaRMSNorm = _resolve("layernorm", "GemmaRMSNorm")
​
​
def get_act_and_mul_fn(act_fn_name: str):
    """按激活名取 fused activation-and-mul 算子,逐名回退。    与 _resolve 不同的是,这里按名称参数化:某个激活没有 hw-agnostic
    实现时单独回退,不影响其他激活的解析结果。
    """
    if envs.VLLM_USE_HW_AGNOSTIC:
        try:
            from vllm.model_executor.hw_agnostic.layers.activation import (
                get_act_and_mul_fn as hw_fn,
            )
            fn = hw_fn(act_fn_name)
            logger.info_once("Using hw-agnostic activation: %s", act_fn_name)
            return fn
        except (ImportError, KeyError):
            logger.warning_once(
                "hw-agnostic activation %s is not available; falling back to vLLM",
                act_fn_name,
            )
    from vllm.model_executor.layers.activation import get_act_and_mul_fn as vllm_fn
​
    return vllm_fn(act_fn_name)
vllm/model_executor/hw_agnostic/layers/layernorm.py core-logic

首个 hw-agnostic 层实例,展示 CustomOp 子类的写法:fp32 中间计算保证跨后端一致,并内置融合残差的 RMSNorm 路径。

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""hw-agnostic RMSNorm:纯 PyTorch 实现,不依赖任何定制 kernel。"""
import torch
import torch.nn as nn
from torch import Tensorfrom vllm.model_executor.hw_agnostic.custom_op import CustomOp
​
​
@CustomOp.register("rms_norm")
class RMSNorm(CustomOp):
    """``x -> w * x / sqrt(E[x^2] + eps)``;传入 residual 时融合
    ``residual += x`` 并返回 (normalized, residual)。"""
​
    def __init__(self, hidden_size: int, eps: float = 1e-6,
                 var_hidden_size: int | None = None, has_weight: bool = True,
                 dtype: torch.dtype | None = None):
        super().__init__()
        self.hidden_size = hidden_size
        self.variance_epsilon = eps
        # 只有与 hidden_size 不同时才保留覆盖,减少分支
        self.variance_size_override = (
            None if var_hidden_size == hidden_size else var_hidden_size
        )
        weight_dtype = dtype or torch.get_default_dtype()
        self.has_weight = has_weight
        weight = torch.ones(hidden_size, dtype=weight_dtype)
        if has_weight:
            self.weight = nn.Parameter(weight) # 常规可训练权重
        else:
            # 无需权重时注册为 buffer,避免被当作可训参数
            self.register_buffer("weight", weight, persistent=False)
​
    def _rms_norm(self, x: Tensor, weight: Tensor | None, epsilon: float,
                  variance_size: int | None = None) -> Tensor:
        """在 fp32 上计算均方根归一化,保证各后端数值一致。"""
        orig_dtype = x.dtype
        x = x.to(torch.float32)
        x_var = x if variance_size is None else x[..., :variance_size]
        variance = x_var.pow(2).mean(dim=-1, keepdim=True)
        x = x * torch.rsqrt(variance + epsilon)
        if weight is not None:
            x = x.to(weight.dtype) * weight # 权重按自身 dtype 乘回
        return x.to(orig_dtype)
​
    def _fused_add_rms_norm(self, x: Tensor, x_residual: Tensor,
                            weight: Tensor | None, epsilon: float,
                            variance_size: int | None = None):
        """融合残差加法与 RMSNorm,并回写更新后的残差。"""
        orig_dtype = x.dtype
        x = x.to(torch.float32)
        x = x + x_residual.to(torch.float32)
        x_residual = x.to(orig_dtype) # 返回原始精度,供后续层复用
        x_var = x if variance_size is None else x[..., :variance_size]
        variance = x_var.pow(2).mean(dim=-1, keepdim=True)
        x = x * torch.rsqrt(variance + epsilon)
        if weight is not None:
            x = x.to(weight.dtype) * weight
        return x.to(orig_dtype), x_residual
​
    def forward_native(self, x, residual=None):
        weight = self.weight if self.has_weight else None
        epsilon = self.variance_epsilon
        variance_size = self.variance_size_override
        if residual is None:
            return self._rms_norm(x, weight, epsilon, variance_size)
        return self._fused_add_rms_norm(
            x, residual, weight, epsilon, variance_size
        )

评论区精华

逐层选择 hw-agnostic 的复杂度是否必要 设计

tdoublep 在 layer_registry.py 上提问: > Do we really need this complexity of selecting/de-selecting individual layers to be HW agnostic?

结论:作者将按层列表选择的方案简化为布尔开关 VLLM_USE_HW_AGNOSTIC。 · 已解决

环境变量命名不一致 question

tdoublep 指出 PR body 写的是 VLLM_USE_HW_AGNOSTIC,但初版代码定义的是 VLLM_HW_AGNOSTIC_LAYERS;hmellor 补充: > In the test plan it explicitly says VLLM_USE_HW_AGNOSTIC=1

结论:统一为布尔环境变量 VLLM_USE_HW_AGNOSTIC。 · 已解决

手动逐类映射的扩展性问题 设计

hmellor 提出: > It's not very scalable to need manual mapping for every layer class. Could we not have something generic that attempts to import everything first from HW agnostic and then from the rest of vLLM for any class name? 作者随后引入通用 _resolve shim,tdoublep 回复 "I think the approach looks better!"。

结论:采用通用 _resolve 导入回退机制替代逐层手动映射。 · 已解决

动态创建类与 CUDA Graphs / torch.compile 兼容性 正确性

hmellor 在 rms_norm.py 上提醒: > IIRC I did it this way because CUDA Graphs/torch compile didn't like dynamically created classes 作者回应 "I reverted this change."。

结论:回退动态创建 TPAware 子类的改动,保留静态类定义。 · 已解决

日志措辞 hw-agnostic vs vLLM 的语义 style

tdoublep 认为日志措辞暗示 hw-agnostic 不属于 vLLM: > I'm not use I like `hw-agnostic` vs `vLLM` since it implies the hw-agnostic code is not part of vLLM? What about just "falling back to default"? 作者调整了日志语句。

结论:回退日志措辞改为 falling back to default。 · 已解决

VLLM_USE_HW_AGNOSTIC=1 的 UX 疑问 question

zou3519 在测试文件上评论: > Doesn't need to be fixed anytime soon, but the VLLM_USE_HW_AGNOSTIC=1 ux is a little weird to me 作者询问首选替代方案,但未获答复。

结论:未解决,作为已知 UX 疑问留待后续。 · 待处理

风险与影响

  • 回退机制依赖异常捕获vllm/model_executor/models/transformers/layers.py_resolve 仅捕获 ImportError/AttributeError,若 hw-agnostic 模块存在但实现有语义偏差,不会触发回退,可能静默产生输出差异;tiny Llama 测试覆盖了 logprobs 一致性,但覆盖面有限。
  • import 顺序敏感RMSNorm = _resolve(...) 在模块导入期执行,VLLM_USE_HW_AGNOSTIC 必须在 import 前设置;多进程 worker 默认 fork 场景下存在 import 顺序风险(测试依赖 VLLM_WORKER_MULTIPROC_METHOD=spawn 规避)。
  • 全局开关影响面VLLM_USE_HW_AGNOSTIC=1 会作用于该进程所有 transformers 后端模型,hw-agnostic RMSNorm 走 fp32 逐元素计算,性能与数值路径均与融合 kernel 不同。
  • fuser 核心路径改造fusers/glu.pyfusers/rms_norm.py 的导入路径变更位于 transformers 后端图融合核心路径,回归可能导致 GLU 融合或残差归一化行为变化;现有测试未覆盖多卡与 cudagraph 组合。
  • __new__ 全局替换custom_op.py 基于类名的全局 OOT 注册,若不同语义算子同名,可能互相覆盖;注册时机(import 顺序)影响替换是否生效。
  • 用户侧:默认关闭,现有行为完全不变;开启后,transformers 后端模型的 RMSNormSiluAndMul 换用纯 PyTorch 实现,便于在缺少定制 kernel 的硬件上运行。
  • 系统侧:确立了 hw_agnostic 包结构、CustomOp/PluggableLayer 的 OOT 注册模式与 _resolve 逐符号回退约定,为后续更多层“hw-agnostic 化”铺路,是系列 PR(1/N)的架构基础。
  • 团队侧:形成“先试 hw-agnostic、失败回退默认、日志报告来源”的协作约定;hmellor 在批准时已提示,随层数增长可能需要重新审视 layers.py 的数据结构。
环境开关全局生效 回退依赖异常捕获 import 顺序敏感 fuser 核心路径改造 实验性开关默认关闭

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论