找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3096

积分

0

好友

418

主题
发表于 前天 05:27 | 查看: 23| 回复: 0

模型结构概念介绍

vllm 是目前最流行的 LLM 推理服务解决方案之一。它除了提供 LLM 推理的吞吐优化和性能优化外,也内置了各种各样的模型结构。相关的代码位于这个目录中:

https://github.com/vllm-project/vllm/tree/main/vllm/model_executor/models

此外,在 vllm 中,每一个模型结构类都需要在 registry.py 中进行注册。

vllm 的模型结构实现一直与 huggingface 的 transformers 库中的模型结构对标。例如,当 transformers 中增加了新的模型结构后,vllm 社区通常会跟进实现一个同名的类。或者,模型结构的供应方在向 transformers 提交代码后,也会同时更新 vllm 的仓库。

需要注意的是,模型结构和模型名称不是同一个概念。对于任何一个来自 huggingface 的模型,都可以通过查看它的 config.json 文件来确认其模型结构。以 Qwen2.5-32B 为例,可以看这个文件:

https://huggingface.co/Qwen/Qwen2.5-32B/blob/main/config.json

{
  "architectures": [
    "Qwen2ForCausalLM"
  ],
  ...
}

所以,Qwen2.5-32B 这个模型的模型结构是 Qwen2ForCausalLM。其对应的 transformers 源码路径为:

https://github.com/huggingface/transformers/blob/v5.1.0/src/transformers/models/qwen2/modeling_qwen2.py

vllm 在启动时,同样需要解析 config.json,以获取这个模型结构,然后找到在 vllm 中注册的同名模型结构类来加载模型权重。

在企业内部,为了针对特定业务、产品或解决特定问题,常常会产生自定义的模型结构。这些模型通常不会贡献给 vllm 官方(这没有太大意义)。算法工程师在训练阶段通常会基于 transformers 库中已有的模型结构进行“魔改”,来定义新的模型结构。而在模型正式上线时,出于性能考虑,需要改用 vllm 来提供推理服务,因此也必须在 vllm 中重新实现一遍该模型结构,否则 vllm 加载训练好的模型权重时就会报错:not supported

此时,有两种实现方式。一种是直接在 vllm 的 models 目录中编写自己的模型结构 Python 文件,然后在 registry.py 中进行注册,这被称为 内部集成。另一种是在其他包(例如一个独立的 PyPI 包)中实现新的模型结构并进行注册,这被称为 外部集成(官方称之为 Out-of-Tree Model Integration)。

内部集成虽然容易理解,但灵活性不足,主要体现在以下几个方面:

  1. 你基于某个特定版本的 vllm 自定义了模型结构,并修改了 registry.py 文件,然后在内部部署环境重新分发该版本的 vllm。后续如果需要升级到新的 vllm 版本(vllm 版本更新很快!),你可能又得将之前的代码在新版本的 vllm 上重新集成一次。
  2. vllm 可能是通过类似 conda(如 Mamba、Miniforge 等)的环境来分发的。这个环境通常很大,提前安装好了许多 PyPI 包,vllm 是其中之一。如果每次新增模型结构都需要修改 vllm 源码,那么这个庞大的环境也需要重新打包和分发。

因此,在 vllm 外部集成新的模型结构才是更明智的选择。

如何在 vllm 外部集成新模型结构?

如果你直接 Google 搜索 vllm new model 等关键词,可能会直接找到 vllm 0.6.0 版本中的一篇经典文档:

https://docs.vllm.ai/en/v0.6.0/models/adding_model.html#out-of-tree-model-integration

但文档最后只是轻描淡写地写了一句:

If you are running api server with vllm serve, you can wrap the entrypoint with the following code:

ModelRegistry.register_model("YourModelForCausalLM", YourModelForCausalLM)
import runpy
runpy.run_module('vllm.entrypoints.openai.api_server', run_name='__main__')

如果你是自己编写一个 Python 脚本,通过调用 vllm 的库来启动服务,那么在合适的代码位置注册模型自然比较方便。但如果你是使用 vllm serve 命令运行模型,则需要进行 warp the entrypoint,但如何实现 warp the entrypoint,文档却没有说明。作者似乎假定了用户都具备这方面的知识背景。

不过,在后来的 vllm 版本文档中,其实已经给出了更具体的例子,只不过它是被放在了 plugin(插件)系统的文档中。

官方例子解读

这个例子主要是为了演示 vllm 的 plugin 机制,顺便用外部集成模型类来举例,所以直接搜索可能不容易找到:

# inside `setup.py` file
from setuptools import setup

setup(name='vllm_add_dummy_model',
    version='0.1',
    packages=['vllm_add_dummy_model'],
    entry_points={
        'vllm.general_plugins':
            ["register_dummy_model = vllm_add_dummy_model:register"]
    })

# inside `vllm_add_dummy_model/__init__.py` file
def register():
    from vllm import ModelRegistry

    if "MyLlava" not in ModelRegistry.get_supported_archs():
        ModelRegistry.register_model(
            "MyLlava",
            "vllm_add_dummy_model.my_llava:MyLlava",
        )

这就是 warp the entrypoint 的方式,需要实现两个文件。

也可以不用新增 module 目录,直接用 vllm_add_dummy_model.py 来注册模型,但需要把 setup.py 中的 packages=['vllm_add_dummy_model'] 改成 py_modules=['vllm_add_dummy_model']

首先,你需要创建一个自己的 PyPI 包,在这个包中实现注册新模型结构的功能,即调用 vllm 的 ModelRegistryregister_model 方法。第一个参数是模型结构名,第二个参数是模型结构类的导入路径(import path)。关于导入路径,下文也会介绍。另外,这里其实也可以直接使用类本身进行注册,即:

# inside `vllm_add_dummy_model/__init__.py` file
from vllm_add_dummy_model.my_llava import MyLlava
def register():
    from vllm import ModelRegistry

    if "MyLlava" not in ModelRegistry.get_supported_archs():
        ModelRegistry.register_model(
            "MyLlava",
            MyLlava
        )

然后,你需要在这个 PyPI 包的 setup.py 文件中,设置 entry_points 参数,来指定刚才那个注册函数(register)的导入路径:

    entry_points={
        'vllm.general_plugins':
            ["register_dummy_model = vllm_add_dummy_model:register"]
    })

导入路径介绍

这里简单介绍一下导入路径,这是 Python 的官方语法。格式是 路径:符号,冒号前面是模块或包路径,冒号后面指定要导入的具体符号(如函数、类)。对于 vllm 注册模型,这里导入的符号只要是可调用(callable)的即可,既可以是函数,也可以是拥有 __call__ 方法的类。

再说明一下路径部分。对于 xxx:registerxxx 可以是一个名为 xxx.py 的文件,也可以是一个名为 xxx 的目录,然后在该目录下的 __init__.py 文件中定义 register 函数。当然,路径也可以更深,用点号分割,例如 xxx.yyy:register。这可以表示 xxx 目录下有一个 yyy.py 文件,yyy.py 里有一个 register 函数;也可以表示 xxx 目录下有一个 yyy 子目录,yyy 子目录的 __init__.py 文件里有 register 函数。

生效方式1

那么,这个 PyPI 包如何生效呢?假设这个包名为 vllm_add_dummy_model,只要将它和 vllm 通过 pip install 安装到相同的 Python 环境即可。

不过,这会带来一个副作用:如果一台机器上有多个模型在提供服务,并且它们使用同一个 conda 环境,那么执行 pip install 会污染整个 conda 环境。因此,如果同一台机器上要服务多个模型,那么这些模型的模型结构都需要统一提交到 vllm_add_dummy_model 的代码仓库中。

另外,如果我们能方便地修改 conda 环境(即可以自由地安装新 PyPI 包到部署环境的 conda 中),那自然没问题。但如果可以这样做,那么我们直接修改 vllm 的源码,加上新的模型结构就可以了。然而,这并不能解决我们前面提到的灵活性问题。如果你希望获得更好的灵活性,我建议使用下面的方式。

生效方式2

对于 vllm 而言,最常用的启动方式是使用 vllm serve 命令。也有使用 Ray 进行服务化的情况,即在 Ray 的 Actor 中使用 vllm 的 AsyncLLM 提供服务,通过 serve run 命令启动。无论如何,在不修改 vllm 源码、不更新 conda 环境的情况下,要向 vllm 注册模型,可以像下面这样做。

在生产环境中,conda 环境(包含各类第三方 PyPI 包)和自定义代码通常是分离的(如果不是,建议改造成分离的)。在开发环境中,进入前面提到的新增模型结构的代码目录,然后执行:

python setup.py bdist_wheel

这时候会生成 egg-info 目录,例如 vllm_add_dummy_model.egg-info

然后,我们将我们的模型文件、注册文件、以及这个 egg-info 目录一起打包(或者直接使用上述命令生成的 .whl 文件),分发到生产环境即可。

vllm 启动时,只需要 cd 到这个打包文件解压后的目录,然后执行 vllm serve 命令。此时,vllm 就能识别新的模型结构了。

setuptools 的入口点(Entry Points)

setuptools 本是用于构建 Python 项目的第三方库,不过早已成为事实标准。

setuptools 工具图标

setup.py 是 setuptools 中用来描述构建方式的“配置”文件,其性质相当于 C++ 中的 Makefile(make)或者 BUILD(Bazel)。当然,因为要构建的是 Python 项目,所以能玩的花样会更多一点,入口点(Entry Points)机制就是其中之一。我们前面已经见识过,通过 entry_points 语法可以注册入口点,从而执行我们的自定义代码。

setuptools 内置了一些入口点类型,例如 console_scriptsgui_scripts

setup(
    # ...,
    entry_points={
        'console_scripts': [
            'hello-world = timmins:hello_world',
        ]
    }
)

另外,我们也可以自定义入口点的类型。

那么,入口点注册的插件什么时候被执行呢?

有两种方式:第一种是用户显式调用,第二种是主程序动态发现与加载时执行。

console_scripts 这种内置插件就需要用户显式调用。例如上面注册的名为 hello-worldconsole_scripts 类型插件,实际上是帮你在系统的 PATH 路径里创建了一个名为 hello-world 的命令。在终端输入 hello-world 即可执行插件 timmins:hello_world

$ hello-world
Hello world

不过,入口点的强大之处在于第二种执行方式,这是实现 Python 插件系统的核心。插件(plugin)一词在维基百科中是这样定义的:

它无需重新构建系统即可扩展现有软件系统的功能。插件功能是系统可定制性的一种方式。

例如,vllm 注册模型时用到的 vllm.general_plugins,其实就是 vllm 内部定义的一个名为 general_plugins 的入口点类型,在 vllm 中就直接称呼为插件(plugin)。下面我将以 vllm.general_plugins 的源码为例,对插件的实现方式进行抛砖引玉的介绍。

vllm.general_plugins 插件

在 vllm 仓库的 vllm/plugins/__init__.py 文件中,定义了变量 DEFAULT_PLUGINS_GROUP,其值就是字符串 vllm.general_plugins

# Default plugins group will be loaded in all processes(process0, engine core
# process and worker processes)
DEFAULT_PLUGINS_GROUP = "vllm.general_plugins"

看名字是“插件组”(plugins group),之所以叫“组”,是因为一个插件组可以注册多个回调函数。

除了 vllm.general_plugins 以外,vllm 还支持如下几种插件:

# IO processor plugins group will be loaded in process0 only
IO_PROCESSOR_PLUGINS_GROUP = "vllm.io_processor_plugins"
# Platform plugins group will be loaded in all processes when
# `vllm.platforms.current_platform` is called and the value not initialized,
PLATFORM_PLUGINS_GROUP = "vllm.platform_plugins"
# Stat logger plugins group will be loaded in process0 only when serve vLLM with
# async mode.
STAT_LOGGER_PLUGINS_GROUP = "vllm.stat_logger_plugins"

vllm 的其他几种插件本文不过多介绍,简而言之:

  • vllm.io_processor_plugins: 用来实现自定义的 IO 处理器的插件。
  • vllm.platform_plugins: 支持在新的平台(例如新型 GPU)上使用 vllm 的插件。
  • STAT_LOGGER_PLUGINS_GROUP: 自定义日志插件。

load_plugins_by_group()

插件在 vllm 中都是通过 load_plugins_by_group 函数加载的,它有一个参数 group

def load_plugins_by_group(group: str) -> dict[str, Callable[[], Any]]:
    from importlib.metadata import entry_points

    allowed_plugins = envs.VLLM_PLUGINS

    discovered_plugins = entry_points(group=group)
    if len(discovered_plugins) == 0:
        logger.debug("No plugins for group %s found.", group)
        return {}

    # Check if the only discovered plugin is the default one
    is_default_group = group == DEFAULT_PLUGINS_GROUP
    # Use INFO for non-default groups and DEBUG for the default group
    log_level = logger.debug if is_default_group else logger.info

    log_level("Available plugins for group %s:", group)
    for plugin in discovered_plugins:
        log_level("- %s -> %s", plugin.name, plugin.value)

    if allowed_plugins is None:
        log_level(
            "All plugins in this group will be loaded. "
            "Set `VLLM_PLUGINS` to control which plugins to load."
        )

    plugins = dict[str, Callable[[], Any]]()
    for plugin in discovered_plugins:
        if allowed_plugins is None or plugin.name in allowed_plugins:
            if allowed_plugins is not None:
                log_level("Loading plugin %s", plugin.name)

            try:
                func = plugin.load()
                plugins[plugin.name] = func
            except Exception:
                logger.exception("Failed to load plugin %s", plugin.name)

    return plugins

主要逻辑是,先通过 entry_points() 函数获取到发现的插件(discovered_plugins)。然后遍历它们,构造一个 dict 类型的对象返回,dict 的 key 是插件名,value 是回调函数。

这里的关键就是 importlib.metadata 模块的 entry_points() 函数,它的主要作用是查询当前 Python 环境中所有已安装包所注册的入口点。该函数自 Python 3.10 引入,返回值是 EntryPoint 类型。在 for plugin in discovered_plugins: 循环中,plugin 就是 EntryPoint 类型,plugin.name 就是插件的名称。以之前的代码为例:

    entry_points={
        'vllm.general_plugins':
            ["register_dummy_model = vllm_add_dummy_model:register"]
    })

register_dummy_model 就是 plugin.nameplugin.load() 就是通过导入路径 vllm_add_dummy_model:register 得到具体的 func。也就是从字符串转换成了可调用对象(函数或类)。

load_general_plugins()

load_plugins_by_group() 是通用的加载插件组的函数,前面提到的 vllm 几种插件它都能加载。对于 vllm.general_plugins 类型的插件,还有一个特殊的封装——load_general_plugins()

这个函数有两个功能:1) 保证 vllm.general_plugins 插件的幂等性,即加载多次不会有副作用。所以它通过全局变量 plugins_loaded 来识别当前是否执行过插件加载,如果已加载过则直接退出。2) 通过 load_plugins_by_group() 只是获取了插件回调函数的 dict 对象,这些回调函数并没有被真正执行,所以在这里遍历并执行一遍。

def load_general_plugins():
    """WARNING: plugins can be loaded for multiple times in different
    processes. They should be designed in a way that they can be loaded
    multiple times without causing issues.
    """
    global plugins_loaded
    if plugins_loaded:
        return
    plugins_loaded = True

    plugins = load_plugins_by_group(group=DEFAULT_PLUGINS_GROUP)
    # general plugins, we only need to execute the loaded functions
    for func in plugins.values():
        func()

再来看一下 load_general_plugins() 被调用的地方,它在 vllm 代码中有多处:

vllm 代码中多处调用 load_general_plugins 函数的截图

这符合前面所说的,自定义插件由主程序控制加载时机。Python 官方库 importlib.metadata 提供了环境内所有入口点的识别功能(entry_points 函数),然后 vllm 自行控制在什么位置去识别这些入口点并加载注册的插件。

EngineCore

看截图有这么多 load_general_plugins() 被调用的地方,是因为 vllm.general_plugins 不仅可以用来注册新模型结构,也可以用来做其他事情。我们这里关注的重点是在 core.py 中:

class EngineCore:
    """Inner loop of vLLM's Engine."""

    def __init__(
        self,
        vllm_config: VllmConfig,
        executor_class: type[Executor],
        log_stats: bool,
        executor_fail_callback: Callable | None = None,
    ):
        # plugins need to be loaded at the engine/scheduler level too
        from vllm.plugins import load_general_plugins

        load_general_plugins()

        self.vllm_config = vllm_config
        if vllm_config.parallel_config.data_parallel_rank == 0:
            logger.info(
                "Initializing a V1 LLM engine (v%s) with config: %s",
                VLLM_VERSION,
                vllm_config,
            )

        self.log_stats = log_stats

        # Setup Model.
        self.model_executor = executor_class(vllm_config)
        ...

在 vllm 的 EngineCore 中,初始化 model_executor(准备加载模型)之前,会调用 load_general_plugins() 来加载 vllm.general_plugins 注册的插件。这样一来,通过外部插件注册的新模型结构也就能被 vllm 感知并使用了。

这种方法有效解决了直接修改 vllm 源码带来的维护负担和升级困难,为在企业内部灵活部署和管理自定义大模型提供了一种优雅的解决方案。如果你在模型架构设计或部署中遇到了类似问题,不妨到 云栈社区 的技术文档板块寻找更多相关思路和实践经验。




上一篇:中兴通讯AI战略解析:“连接+算力”双轮驱动,全栈方案如何撬动政企市场
下一篇:半导体介质刻蚀中氟碳气体的核心作用:平衡蚀刻与钝化的化学工具箱
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-16 17:01 , Processed in 0.994513 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表