Skip to content

LLMEngine:v1 引擎的同步薄壳

源码版本v0.25.1

职责

v1 架构里 LLMEngine 是给同步 API(LLM.generateLLM.chat)用的入口薄壳。它本身不跑前向、不调度、不分配 KV cache,只是把这三件事粘起来:用 InputProcessor 把外部进来的 prompt 加工成 EngineCoreRequest,用 OutputProcessorEngineCoreOutputs 翻译回 RequestOutput,真正干活的全丢给 EngineCore。在 __init__ 末尾那行 self.engine_core = EngineCoreClient.make_client(...)(llm_engine.py:105-111)才是引擎核心 (EngineCore) 真正入场的地方——make_client 根据是否多进程、是否 asyncio,在 InprocClientSyncMPClientAsyncMPClient 三种客户端里三选一。

这一层存在的意义是把 v0 那套 LLMEngine.add_request() / .step() 的同步用法保留下来:同进程模式下 InprocClient 直接持有 EngineCore 实例(core_client.py:286-292),get_output() 就地调 step_fn(),前后端共享内存、没有 ZMQ、没有 busy loop;多进程模式下 SyncMPClient 起一个后台 EngineCoreProc(core.py:896-897),LLMEngine 还是那一套同步接口,只是底下多了 ZMQ 桥接。所以 LLMEngine 的代码量很小(448 行),大量方法只是把调用透传给 engine_core(sleep/wake、profile、lora、reset_prefix_cache 都是这种一行透传)。

设计动机

  • 保留 v0 同步 API:LLM 类对外暴露 generate/chat 同步方法,内部 LLMEngine.add_request() + LLMEngine.step() 循环(llm_engine.py:296-334),不用 asyncio 也能跑通 v1。这是 v1 替换 v0 时不破坏离线推理工作流的关键。
  • 三种后端一个接口:make_client 的 3 选 1 逻辑(core_client.py:83-105)让 LLMEngine 的所有方法都不用关心是同进程、多进程同步、多进程异步,只要拿到一个 EngineCoreClient 实例就行,后端切换是构造期决定的。
  • 输入输出加工放在 shell 层:InputProcessor(input_processor.py:36)处理多模态 (multimodal)、tokenization、priority、trace_headers、LoRA;OutputProcessor(output_processor.py:417)负责 detokenize、流式切块、stop string 匹配、把 EngineCoreOutputs 转回 RequestOutput。这两件脏活脏数据都留在 shell 层,EngineCore 只看纯结构化的 EngineCoreRequest / EngineCoreOutputs
  • n>1 多采样扇出:SamplingParams.n 大于 1 时,add_requestParentRequest(parallel_sampling.py:13)把一个父请求拆成 n 个子请求分别塞进 EngineCore 和 OutputProcessor(llm_engine.py:279-294),输出层再把子请求的 token 流聚合回父请求。
  • 进程退出兜底:multiprocess_mode=False 时拿 driver model 的弱引用 finalizer(<SrcLink path="vllm/v1/engine/llm_engine.py" lines="129-133" label="llm_engine.py"/>),LLMEngine 被 GC 时调 _cleanup_instance_caches 释放 byte code hook 钉住的显存。

关键文件

  • LLMEngine class:48-49class LLMEngine: 定义,docstring 自称 Legacy LLMEngine for backwards compatibility.
  • LLMEngine.__init__:51-141 — 装配 renderer / InputProcessor / OutputProcessor / EngineCoreClient,再起 StatLoggerManager 和 cleanup finalizer。
  • make_client 调用点:105-111EngineCoreClient.make_client(multiprocess_mode=..., asyncio_mode=False, ...) 选后端。
  • from_vllm_config:143-158 — 从 VllmConfig 构造,multiprocess_modeenvs.VLLM_ENABLE_V1_MULTIPROCESSING 决定。
  • add_request:218-294 — 校验 request_id、input_processor.process_inputsn>1ParentRequest 扇出、最后 engine_core.add_request
  • step:296-334engine_core.get_output()output_processor.process_outputsabort_requestslogger_manager.record
  • abort_request:212-216 — 先在 OutputProcessor 标记,再让 EngineCore 把请求从调度器里移除。
  • sleep / wake_up:361-376 — 跨 renderer + engine_core 的 sleep/wake,StatLoggerManager 记录 sleep 状态。
  • do_log_stats_with_interval:394-401 — 用 VLLM_LOG_STATS_INTERVAL 节流打日志,避免每个 step 都 flush。
  • _get_driver_model_for_cleanup:431-434 — 沿 model_executor.driver_worker.model_runner.model 链取到 driver 模型,供 finalizer 释放显存。

数据流

一次同步推理就是 for req in requests: add_request(...) 然后 while has_unfinished_requests(): step()step() 的结构很直白:从 engine_core 拉一批 EngineCoreOutputs,交给 OutputProcessor 翻译,把因为 stop string 提前结束的请求丢回去 abort,顺手记一段统计。

python
# vllm/v1/engine/llm_engine.py L296-L314
def step(self) -> list[RequestOutput | PoolingRequestOutput]:
    if self.should_execute_dummy_batch:
        self.should_execute_dummy_batch = False
        self.engine_core.execute_dummy_batch()
        return []

    # 1) Get EngineCoreOutput from the EngineCore.
    with record_function_or_nullcontext("llm_engine step: get_output"):
        outputs = self.engine_core.get_output()

    # 2) Process EngineCoreOutputs.
    with record_function_or_nullcontext("llm_engine step: process_outputs"):
        iteration_stats = IterationStats() if self.log_stats else None
        processed_outputs = self.output_processor.process_outputs(
            outputs.outputs,
            engine_core_timestamp=outputs.timestamp,
            iteration_stats=iteration_stats,
        )
        self.output_processor.update_scheduler_stats(outputs.scheduler_stats)

后面还有第 3、4 步(llm_engine.py:317-332):第 3 步把 processed_outputs.reqs_to_abort 交给 engine_core.abort_requests,因为 stop string 在 detokenize 之后才看得到,EngineCore 自己不知道;第 4 步在 logger_manager 非空且有 scheduler_stats 时调 record + do_log_stats_with_interval

add_requestn>1 时走扇出分支(llm_engine.py:279-294):

python
# vllm/v1/engine/llm_engine.py L279-L294
# Fan out child requests (for n>1).
parent_req = ParentRequest(request)
for idx in range(n):
    request_id, child_params = parent_req.get_child_info(idx)
    child_request = request if idx == n - 1 else copy(request)
    child_request.request_id = request_id
    child_request.sampling_params = child_params

    # Make a new RequestState and queue.
    self.output_processor.add_request(
        child_request, prompt_text, parent_req, idx
    )
    # Add the request to EngineCore.
    self.engine_core.add_request(child_request)

return req_id

最后一个 child 复用原 request 对象省一次拷贝,父请求 ID 由 ParentRequest.get_child_info(idx) 生成,OutputProcessor 拿到父引用和 idx 后能再把子输出聚合回去。

边界与失败

  • EngineCoreRequest 已废弃:add_request 收到 EngineCoreRequest 直接传进来会 warning_once 报 deprecated,说 v0.18 起要改用 Renderer.render_cmpl() / render_chat()(llm_engine.py:235-248);如果传入的 request_idEngineCoreRequest.request_id 不一致,以后者为准,前者被忽略。
  • DP 模式下的 dummy batch:数据并行 (data parallel) 外部启动器模式下,某个 rank 没活但其他 rank 还在跑时,has_unfinished_requests_dpshould_execute_dummy_batch 置为 True(llm_engine.py:197-203),step() 开头先跑一个 dummy batch 占位(llm_engine.py:297-300),让 collective 通信不挂。
  • 同进程模式才能拿 model_executor:if not multiprocess_mode: 分支里才设 self.model_executor = self.engine_core.engine_core.model_executor(llm_engine.py:123-125);多进程模式下跨进程拿不到这个对象,所以下游代码访问 model_executor 之前要确认 multiprocess_mode=False
  • 统计日志节流:do_log_stats_with_intervalVLLM_LOG_STATS_INTERVAL 控制最少间隔(llm_engine.py:394-401),_last_log_time 是首次访问时懒初始化的实例属性,绕开了 __init__ 里设初值的麻烦。
  • sleep 联动 renderer:sleep(level>=1) 先清 renderer 的多模态缓存再调 engine_core.sleep(llm_engine.py:361-367),因为唤醒 (wake_up) 后多模态 embedding 可能失效,得让 renderer 重新算。
  • EngineCore 选后端由环境变量决定:from_vllm_configmultiprocess_mode=envs.VLLM_ENABLE_V1_MULTIPROCESSING(llm_engine.py:157),from_engine_args 再叠加一次(llm_engine.py:174-176),所以多进程开关在配置层就定死了,运行期不能切。

小结

LLMEngine 是 v1 同步路径上的薄壳:校验入参、用 InputProcessor / OutputProcessor 处理多模态和流式输出、用 EngineCoreClient.make_client 选后端,然后 add_request + step 的循环就完事了。它把所有重活都委派给 EngineCore(同进程直接持有,多进程走 ZMQ),自己只关心 v0 兼容、ParentRequest 扇出、统计节流、sleep/wake 联动这几件壳层的事。继续往下看可以去 /engine/engine-core 看引擎核心装配,去 /engine/engine-core-proc 看 ZMQ 后台进程实现,客户端三种实现见 /client/inproc-mp,调度器内部见 /scheduler/scheduler

对照官方资料:vLLM 文档 · README