LLMEngine:v1 引擎的同步薄殼
職責
v1 架構裡 LLMEngine 是給同步 API(LLM.generate、LLM.chat)用的入口薄殼。它本身不跑前向、不排程、不分配 KV cache,只是把這三件事粘起來:用 InputProcessor 把外部進來的 prompt 加工成 EngineCoreRequest,用 OutputProcessor 把 EngineCoreOutputs 翻譯回 RequestOutput,真正幹活的全丟給 EngineCore。在 __init__ 末尾那行 self.engine_core = EngineCoreClient.make_client(...)(llm_engine.py:105-111)才是引擎核心 (EngineCore) 真正入場的地方——make_client 根據是否多行程、是否 asyncio,在 InprocClient、SyncMPClient、AsyncMPClient 三種客戶端裡三選一。
這一層存在的意義是把 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_request用ParentRequest(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-49—class LLMEngine:定義,docstring 自稱Legacy LLMEngine for backwards compatibility.。LLMEngine.__init__:51-141— 裝配 renderer / InputProcessor / OutputProcessor / EngineCoreClient,再起 StatLoggerManager 和 cleanup finalizer。make_client 调用点:105-111—EngineCoreClient.make_client(multiprocess_mode=..., asyncio_mode=False, ...)選後端。from_vllm_config:143-158— 從VllmConfig構造,multiprocess_mode由envs.VLLM_ENABLE_V1_MULTIPROCESSING決定。add_request:218-294— 校驗 request_id、input_processor.process_inputs、n>1走ParentRequest扇出、最後engine_core.add_request。step:296-334—engine_core.get_output()→output_processor.process_outputs→abort_requests→logger_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,順手記一段統計。
# 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_request 在 n>1 時走扇出分支(llm_engine.py:279-294):
# 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_id和EngineCoreRequest.request_id不一致,以後者為準,前者被忽略。 - DP 模式下的 dummy batch:資料並行 (data parallel) 外部啟動器模式下,某個 rank 沒活但其他 rank 還在跑時,
has_unfinished_requests_dp把should_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_interval用VLLM_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_config裡multiprocess_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。