Skip to content

EngineCore:v1 引擎真正核心

源码版本v0.25.1

職責

LLMEngine 在 v1 架構裡只是一個薄殼:它拿著 InputProcessor 把外部進來的 prompt 加工成 EngineCoreRequest,拿著 OutputProcessorEngineCoreOutputs 翻譯回 RequestOutput,真正幹活的全丟給 EngineCoreLLMEngine.__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.stepself.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.generateAsyncLLMEngine.generate)最後都落在這幾十行上。

設計動機

為什麼要把 LLMEngine / EngineCore / EngineCoreProc / EngineCoreClient 拆成四層?

  • 行程隔離:EngineCoreProcEngineCore 的子類(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 做負載均衡。

關鍵檔案

資料流

一條請求從客戶端進來,先在 LLMEngine 裡被 InputProcessor 加工成 EngineCoreRequest,然後 engine_core.add_request(...) 丟下去。在多行程模式下,這個呼叫最終被 MPClient 序列化經 ZMQ 發到 EngineCoreProc 的 input socket,process_input_sockets 執行緒解碼後 put_nowaitinput_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)。

python
# 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 函數本體就是這麼三段:

python
# 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=Trueexecute_model 立刻返回 future,主執行緒在這個空檔裡算 grammar bitmask——這是 v1 把排程和 worker 真正並發起來的關鍵動作。

邊界與失敗

  • EngineCoreProc 行程崩潰:run_engine_coreexcept 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

對照官方資料:vLLM 文件 · README