vllm的启动流程
vllm的启动流程
记录一下阅读源码时发现的问题
GPUModelRunner.py
“FIXME(woosuk): Fix warmup for LoRA.”
现在似乎这个model runner还没有对LoRA Adapter做Graph Capture
1. LLMEngine的初始化
从一个/root/autodl-tmp/vllm/examples/basic/offline_inference/basic.py为入口看一下vllm的启动/初始化流程:
1 | |
进入到LLM类中,构造函数中有一个有关键的llm_engine成员这个是我们模型推理的引擎部分,这里我们主要看一下from_engine_args()这个方法。
1 | |
1 | |
llm_engine到这里就是一个大致的初始化,并没有看到加载模型权重等一系列方法,这是因为vllm是一个C-S架构的项目,从后续分析generate()这一方法的时候我们能够看到初始化计算后端等一系列逻辑,不过需要注意的是这里LLMEngine有一个executor_class,后续初始化执行器的时候会按照这个配置来初始化执行器。
2. generate()流程解析
2.1 MPClient (Multi-Processes Client)
前一节初始化LLM类后,计算backend其实还并没有初始化,实际调用generate()方法时vllm才会实现后端的初始化流程,下边是generate()方法。
1 | |
在_run_completion()中_add_completion_requests()做的是将prompts变化为Sequence数据结构再添加为reqeust,这里LLM持有一个reqeust id的列表。根据调用链一路向下,最后调用到了LLMEngine._add_requests()方法。
1 | |
这里并没有贴出完整的代码,我们只需要关注这里request首先被预处理,并在LLMEngine中被assign的一个id,返回给上层,接着由self.engine_core.add_request(request)继续向下传递。
EngineCoreClient的继承结构:
1 | |
InProcClent是最简易的版本,直接持有EngineCore对象;推理直接调用add_request并没有网络通信开销。
MPClient是多进程版本,通过zmq库以及socket实现通信,也是工业场景应用最多的版本,第一次阅读源码,应该重点关注这个类的实现类。
Async/Sync MPClient异步/同步版本,是MPCLient的实现类。
DPAsyncMPClient数据并行+外部负载均衡版本。
DPLBAsyncMPClient数据并行+内部负载均衡版本。
EngineCoreClient是 vLLM v1 引擎架构中的通信抽象层,负责在上层引擎(LLMEngine/AsyncLLM)和底层EngineCore(执行实际推理)之间传递请求和输出。它定义了统一的接口(add_request、get_output、abort_requests等),屏蔽了不同部署模式下的通信细节。
make_client 工厂方法的选择逻辑
multiprocess_mode |
asyncio_mode |
data_parallel_size |
external_lb |
选择的类 |
|---|---|---|---|---|
False |
False |
任意 | - | InprocClient |
True |
False |
任意 | - | SyncMPClient |
True |
True |
1 | - | AsyncMPClient |
True |
True |
>1 | True |
DPAsyncMPClient |
True |
True |
>1 | False |
DPLBAsyncMPClient |
2.2 launch_core_engines
在MPClient的构造方法中,launch_core_engines初始化了真正的EngineCore,不过这里对Ray和其他的计算后端实现有一点点区别。
1 | |
看到Ray的情况下代码中执行流会被提前返回给之前的with语句(大概是因为Ray拥有自己的计算调度方式)所以需要在最顶层进行分离。接着会保存所有需要握手的engine_core实例,并初始化本地的一系列的EngineCore,在CoreEngineProcManager内部其实就直接启动了一系列的本地进程。
1 | |
2.3 Client
回到我们2.1节所说的Client中,以SyncMPClient为例:
1 | |
这里做的就是直接将request加发送给后端的EngineCore,EngineCore再根据Scheduler分配具体的调度逻辑(Scheduler后面再解析),并附带上Tracker,那么这里队列中的请求是什么时候被处理的呢?我们需要回到之前的launch_core_engine中,这里有一个CoreEngineProcManager,我们进入查看他的构造函数,发现他在构造函数中启动了一系列的进程。
1 | |
执行的目标方法是EngineCoreProc.run_engine_core,这里看到其中有一个run_busy_loop不难猜到这里就是从队列中获取request的循环了。
不过有意思的地方是这里做了退出信号的拦截,这里handle_signal()直接对本地的状态进行的更新,但是呢,如果只对本地状态做更新的话,engine在获取queue时其实是阻塞式的,如果队列为空,那么也检测不到状态更新会一直卡在busy_loop中,所以这里是直接向队列中发送了一个关闭的请求。
1 | |
在每一个Proc的run_busy_loop做了两件事,第一是处理来自Client的控制请求,第二是获取模型的输出。
1 | |
_process_input_queue代码:
1 | |
下边是一段解释
1 | |
_process_engine_step是真正执行推理的步骤,大致流程如下,具体的我们后续再进行分析。
1 | |
DeepWiki生成的接口层到CoreEngine的代码关系。