声明: 纯兴趣爱好,如有疏漏敬请谅解。

源码版本: v0.21.0

学习相关源码路径: 

https://github.com/vllm-project/vllm/blob/v0.21.0/vllm/entrypoints/cli/main.py

学习总结:

源码的全局入口,即: 命令行启动入口(如何找到入口源码,请参见上一篇)。

main.py主要在做什么,个人理解总结:

根据调用vllm的命令行参数,去做函数分发。即: 根据你传入不同参数,执行不同启动函数。

命令总结:

(注: 执行命令别忘了激活你的环境)

vllm                         --不带任何参数、打印help信息

vllm -v                    --查询版本

vllm -version          --查询版本

vllm serve              --在线推理服务

vllm bench             --性能基准测试 / 跑分

vllm chat                --命令行直接聊天

vllm complete        --命令行补全测试

vllm launch            --启动vLLM 的独立底层组件

vllm collect-env     --自动把你电脑的配置、版本、环境全部打包输出

vllm run-batch       --批量跑提示词 + 把结果写入文件

流程梳理:

接下来一步一步梳理 vLLM 命令行源码启动的核心流程。看是如何支持这些命令的。

1. 模块列表变量: CMD_MODULES

  声明模块类型的列表变量。

    CMD_MODULES = [
        vllm.entrypoints.cli.openai,
        vllm.entrypoints.cli.serve,
        vllm.entrypoints.cli.launch,
        vllm.entrypoints.cli.benchmark.main,
        vllm.entrypoints.cli.collect_env,
        vllm.entrypoints.cli.run_batch,
    ]

这些模块和带参数的启动命令是一一对应的, 比如:serve对应vllm.entrypoints.cli.serve模块, 对应源码https://github.com/vllm-project/vllm/blob/v0.21.0/vllm/entrypoints/cli/serve.py

重点: 这是下面分发逻辑要用的基础变量。

2. 调用函数cli_env_setup()

逻辑相对简单,在没有设置环境变量VLLM_WORKER_MULTIPROC_METHOD时,追加环境变量,值为: spawn。

非重点: 分发逻辑不用,可理解只是设置参数。

3. omni

如果在启动参数增加了omni参数,则启动流程就委托给omni。此命令是在v0.19.1版本后新增的,本次先不做深入展开。

4. bench预处理 

只有启动参数是bench才会触发,例: vllm bench。

核心逻辑: 指定current_platform,未设置则默认指定为CpuPlatform()。

(注:current_platform在这里用了python的魔方方法https://github.com/vllm-project/vllm/blob/v0.21.0/vllm/platforms/__init__.py#L262)


非重点: 分发逻辑不用,可理解只是设置参数。

5.parser命令解释器

创建FlexibleArgumentParser命令行解释器,本质上是ArgumentParser。FlexibleArgumentParser继承了ArgumentParser,对部分函数做了功能增强。

parser = FlexibleArgumentParser(
    description="vLLM CLI",
    epilog=VLLM_SUBCMD_PARSER_EPILOG.format(subcmd="[subcommand]"),
)

增加-v  --version参数

parser.add_argument(
    "-v",
    "--version",
    action="version",
    version=importlib.metadata.version("vllm"),
)

增加非必须子命令

subparsers = parser.add_subparsers(required=False, dest="subparser")

至此,可以看到代码中是如何接收-v --version参数与子命令。接下来继续学习,子命令与代码逻辑是如何关联。

重点: parser命令解释器与接下来的逻辑相关

6.关联逻辑

关联逻辑代码:

cmds = {}
for cmd_module in CMD_MODULES:
    new_cmds = cmd_module.cmd_init()
    for cmd in new_cmds:
        cmd.subparser_init(subparsers).set_defaults(dispatch_function=cmd.cmd)
        cmds[cmd.name] = cmd
    args = parser.parse_args()
    if args.subparser in cmds:
        cmds[args.subparser].validate(args)

大体的思路是,循环模块变量,将子命令关联到各个函数。本来想在这里展开讲,当时发现东西不少。由于篇幅有限,在这里先不展开讲,准备放到下一篇详细讲解下关联代码逻辑。

在这里先了解核心机制,执行完这段代码,每个模块已经关联好子命令。

7. 分发

if hasattr(args, "dispatch_function"):
    args.dispatch_function(args)
else:
    parser.print_help()

代码的最后几行。

判断启动命令是否包含子命令,未包含则直接打印帮助信息:parser.print_help()。

若包含子命令,则进入相应模块的入口函数。

知识点总结

由于我算是python小白,可能总结的知识点会非常简单,还请见谅

1. 在函数内import模块

Python 允许在任何地方写 import,包括函数内部、if 内部、循环内部。

和顶部 import 有什么区别

顶部:

        程序启动时就加载

        全局都能用

        启动慢一点

函数内:

        调用函数时才加载

        只在函数内能用

        启动速度更快

        避免循环依赖

2. sys.argv

用终端调用python时,输入的所有内容变成的列表。

举个例子, 假设你的python代码:

import sys
print(sys.argv)

调用命令: python test.py arg1 arg2 arg3 test

输出结果: ['test.py', 'arg1', 'arg2', 'arg3', '123']

3. __init__.py中__all__的作用

当别人 from 模块 import * 时,只能导入这里列出来的名字!没写的一律不让导!

4. Python魔法方法

def __getattr__(name: str):

当访问一个不存在的属性时触发。

比如在vllm源码中会使用: platforms.current_platform。

因为没定义current_platform,所以会进入__getattr__方法。

常用于懒加载,只有用到的时候才会进行初始化。

5. *args, **kwargs参数

def __init__(self, *args, **kwargs):

 self:当前创建的解析器对象

 *args:所有不带名字的参数

 **kwargs:所有带名字的关键字参数(字典类型)

6. ArgumentParser

Python 官方自带的命令行参数解析工具

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐