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