EngineCore:v1 引擎真正核心
職責
LLMEngine 在 v1 架構裡只是一個薄殼:它拿著 InputProcessor 把外部進來的 prompt 加工成 EngineCoreRequest,拿著 OutputProcessor 把 EngineCoreOutputs 翻譯回 RequestOutput,真正幹活的全丟給 EngineCore。LLMEngine.__init__ 裡那行 self.engine_core = EngineCoreClient.make_client(...)(llm_engine.py:105-111),才是引擎核心 (EngineCore) 真正入場的地方。
引擎核心 (EngineCore) 自己把三件事拼到一起:model_executor(執行器,即 executor_class 實例,負責跨 worker 跑前向)、scheduler(排程器,負責 waiting/running 佇列、連續批次 (continuous batching)、搶佔)、KVCacheManager(KV 快取管理器,通過 _initialize_kv_caches 在 profiling 之後確定每張卡能給 KV cache 多少顯存)。構造函數里先把這三件套初始化好(core.py:122-158),然後把 step 函數指針定成 self.step 或 self.step_with_batch_queue(core.py:221-223),後面循環裡就反覆調它。
step() 幹一件事:排程、跑前向、回收輸出(core.py:479-508)。具體是 scheduler.schedule() 拿到 SchedulerOutput,model_executor.execute_model(..., non_block=True) 立刻返回 future,期間排程器順手把 grammar bitmask 算好,future 一 result() 出來就交給 scheduler.update_from_output() 把 token 寫回 request 併產出 EngineCoreOutputs。這就是 v1 的一個迭代,所有上層 API(LLM.generate、AsyncLLMEngine.generate)最後都落在這幾十行上。
設計動機
為什麼要把 LLMEngine / EngineCore / EngineCoreProc / EngineCoreClient 拆成四層?
- 行程隔離:
EngineCoreProc是EngineCore的子類(core.py:896-897),把它扔到一個獨立的後臺行程裡跑,前端的 Python 崩了不會拖垮已經在跑前向的 worker,worker OOM 也不會把 API 行程一起帶走。 - ZMQ 解耦:前後端用 ZMQ socket + 兩個
queue.Queue橋接(core.py:915-916),ZMQ 在 socket IO 時釋放 GIL,可以和 GPU forward 真正重疊;輸入和輸出各有獨立執行緒(core.py:980-1001),序列化/反序列化也能藏到前向背後。 - 同行程兜底:不開多行程時
InprocClient直接self.engine_core = EngineCore(...)(core_client.py:286-287),get_output()就地調step_fn(),保留 v0 那種LLMEngine.add_request()/.step()的同步用法。 - 非同步入口:
AsyncMPClient走多行程 + asyncio,伺服器場景下AsyncLLM通過它把 EngineCore 包成可await的物件,前端不阻塞。 - DP 擴展:
run_engine_core在多 DP rank 時給每個 rank 起一個EngineCoreProc(core.py:1192-1200),MoE 走DPEngineCoreProc,非 MoE 當作獨立 DP=1,前端再由DPLBAsyncMPClient做負載均衡。
關鍵檔案
EngineCore class:96-97—class EngineCore:定義,docstring 寫明Inner loop of vLLM's Engine.EngineCore.__init__:99-237— 裝配 model_executor、_initialize_kv_caches、Scheduler、batch_queue、prefix caching hasher。_initialize_kv_caches:240-295— 拿get_kv_cache_specs、profile 顯存、組裝KVCacheConfig。EngineCore.step:479-508— 排程 + execute_model + update_from_output 的核心 30 行。step_with_batch_queue:519-535— PP 場景下用 batch queue 非同步交疊多批,優先填滿佇列。EngineCoreProc class:896-897—ZMQ-wrapper for running EngineCore in background process.run_engine_core:1154-1224— 行程入口,起 EngineCoreProc、註冊 SIGTERM/SIGINT、調run_busy_loop。run_busy_loop:1259-1267—while self._handle_shutdown(): _process_input_queue(); _process_engine_step()。_process_engine_step:1300-1317— 調step_fn()、把 outputs 塞進output_queue、調post_step。LLMEngine.make_client:105-111— LLMEngine 通過EngineCoreClient.make_client選 Inproc/MP/AsyncMP。EngineCoreClient.make_client:83-105— 同步多行程 / 非同步多行程 / 同行程三選一。
資料流
一條請求從客戶端進來,先在 LLMEngine 裡被 InputProcessor 加工成 EngineCoreRequest,然後 engine_core.add_request(...) 丟下去。在多行程模式下,這個呼叫最終被 MPClient 序列化經 ZMQ 發到 EngineCoreProc 的 input socket,process_input_sockets 執行緒解碼後 put_nowait 進 input_queue(core.py:915-916)。主循環 run_busy_loop 醒來後從佇列裡取出來,交給 _handle_client_request 分發(core.py:1372-1405),ADD 類型最終落到 self.scheduler.add_request(request)(core.py:372-403)。
# vllm/v1/engine/core.py L1259-L1267
def run_busy_loop(self):
"""Core busy loop of the EngineCore."""
while self._handle_shutdown():
# 1) Poll the input queue until there is work to do.
self._process_input_queue()
# 2) Step the engine core and return the outputs.
self._process_engine_step()
raise SystemExit_process_engine_step(core.py:1300-1317)拿到 step_fn() 返回的 outputs,逐條 output_queue.put_nowait(output)。output 執行緒再把它們 ZMQ 回送前端,MPClient.get_output() 反序列化交給 LLMEngine.step() 的 engine_core.get_output() 呼叫(llm_engine.py:302-314),最後 OutputProcessor.process_outputs 把 token 流拼回 RequestOutput。
step 函數本體就是這麼三段:
# vllm/v1/engine/core.py L488-L508
if not self.scheduler.has_requests():
return {}, False
scheduler_output = self.scheduler.schedule(self._should_throttle_prefills())
future = self.model_executor.execute_model(scheduler_output, non_block=True)
grammar_output = self.scheduler.get_grammar_bitmask(scheduler_output)
with (
self.log_error_detail(scheduler_output),
self.log_iteration_details(scheduler_output),
):
model_output = future.result()
if model_output is None:
model_output = self.model_executor.sample_tokens(grammar_output)
# Before processing the model output, process any aborts that happened
# during the model execution.
self._process_aborts_queue()
engine_core_outputs = self.scheduler.update_from_output(
scheduler_output, model_output
)non_block=True 讓 execute_model 立刻返回 future,主執行緒在這個空檔裡算 grammar bitmask——這是 v1 把排程和 worker 真正並發起來的關鍵動作。
邊界與失敗
- EngineCoreProc 行程崩潰:
run_engine_core在except Exception裡調engine_core._send_engine_dead()(core.py:1229-1235),往output_queue塞一個ENGINE_CORE_DEAD字節串(core.py:1470-1474),前端MPClient收到後拋錯,不會無限等下去。 - Executor 失敗回調:構造時把
executor_fail_callback註冊到model_executor(core.py:124-125),worker 內部報錯時回調往input_queue塞一條EXECUTOR_FAILED,主循環_handle_client_request拿到就raise RuntimeError("Executor failed.")(core.py:1400-1401)。 - Shutdown 模式:
shutdown_timeout == 0是 abort 模式,立刻把所有在飛請求標成FINISHED_ABORTED;非 0 是 drain 模式,等所有在飛請求跑完(core.py:1329-1360)。shutdown 中新進來的 ADD 和 UTILITY 會被顯式拒絕(core.py:1407-1432)。 - 非因果注意力層:有些 attention 層(如 Prefix LM)標了
non_causal=True,在_initialize_kv_caches裡檢測到後強制關掉 chunked prefill 和 prefix caching(core.py:255-269),否則 KV cache 會被寫髒。 - batch_queue 讓出 CPU:某個 step 沒跑前向但 scheduler 還有未完成請求時(比如等遠端 KV transfer),
time.sleep(0.001)主動讓 GIL(core.py:1311-1315),免得後臺傳輸執行緒餓死。 - in-process 模式無 busy loop:
InprocClient.get_output直接同步調step_fn(core_client.py:289-292),不跑run_busy_loop,這意味著同行程模式下沒有 input/output 執行緒,add_request 是同步進 scheduler。
小結
引擎核心 (EngineCore) 是 v1 的發動機:它把排程器 (scheduler)、執行器 (executor)、KV 快取 (KV cache) 三件套裝配到一起,用 step() 這一招反覆排程 + 前向 + 回收。子類 EngineCoreProc 把它扔進獨立行程,用 ZMQ + 兩個 queue 橋接前後端,讓 GPU 前向和網路 IO 真正重疊。前端用什麼客戶端(InprocClient、MPClient、AsyncMPClient)決定了它是同步跑還是非同步跑,但引擎核心這一層對所有前端都是同一份代碼。繼續往裡看可以去 /engine/llm-engine 看 LLMEngine 這層薄殼到底包了什麼,或者去 /engine/engine-core-proc 深入 ZMQ 那一層,客戶端三種實現見 /client/inproc-mp,排程器內部見 /scheduler/scheduler。