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) 自身は 3 つのものをまとめます:model_executor(実行器、つまり executor_class のインスタンス、ワーカーをまたいだ前向き計算を担当)、scheduler(スケジューラ、waiting/running キュー、連続バッチング (continuous batching)、プリエンプションを担当)、KVCacheManager(KV キャッシュマネージャ、_initialize_kv_caches でプロファイリング後に各 GPU が KV cache に使えるメモリ量を決定)。コンストラクタでこの 3 点を初期化し(core.py:122-158)、その後 step 関数ポインタを self.step または self.step_with_batch_queue に定め(core.py:221-223)、以降のループで繰り返し呼び出します。
step() がやることは 1 つだけです。スケジュール、前向き計算、出力の回収(core.py:479-508)です。具体的には scheduler.schedule() で SchedulerOutput を取り出し、model_executor.execute_model(..., non_block=True) が即座に future を返し、その隙にスケジューラが grammar bitmask を計算し、future の result() が取れたら scheduler.update_from_output() に渡して token をリクエストに書き戻し、EngineCoreOutputs を生成します。これが v1 の 1 イテレーションで、すべての上位 API(LLM.generate、AsyncLLMEngine.generate)は最終的にこの数十行に行き着きます。
設計動機
なぜ LLMEngine / EngineCore / EngineCoreProc / EngineCoreClient を 4 層に分けるのか?
- プロセス分離:
EngineCoreProcはEngineCoreのサブクラス(core.py:896-897)で、独立したバックグラウンドプロセスに投げて走らせます。フロントエンドの Python がクラッシュしても、前向き計算中の worker を道連れにせず、worker の OOM が API プロセスを一緒に巻き込むこともありません。 - ZMQ の疎結合:前後端は ZMQ socket + 2 つの
queue.Queueで橋渡し(core.py:915-916)します。ZMQ は socket IO の際に GIL を解放するため、GPU forward と本当に重ねられます。入出力はそれぞれ独立スレッド(core.py:980-1001)を持ち、シリアライズ/デシリアライズも forward の裏に隠せます。 - 同プロセスの兜底:多プロセスを有効にしないとき
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 に 1 つの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で取得、メモリプロファイリング、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— 同期多プロセス / 非同期多プロセス / 同プロセスの 3 択。
データフロー
リクエストがクライアントから入ってくると、まず 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 を取り出し、1 件ずつ 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 関数本体は次の 3 つの部分からなります:
# 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 転送待ち)、
time.sleep(0.001)で自発的に GIL を譲り(core.py:1311-1315)、バックグラウンド転送スレッドが餓死しないようにします。 - 同プロセスモードには 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) の 3 点を組み立て、step() 一手でスケジュール + 前向き計算 + 回収を繰り返します。サブクラス EngineCoreProc はそれを独立プロセスに放り込み、ZMQ + 2 つの queue で前後端を橋渡しし、GPU 前向き計算とネットワーク IO を本当に重ね合わせます。フロントエンドがどのクライアント(InprocClient、MPClient、AsyncMPClient)を使うかで同期になるか非同期になるかが決まりますが、エンジンコアのレイヤーはすべてのフロントエンドに対して同じコードです。さらに中を見るには /engine/llm-engine で LLMEngine という薄殻が何を包んでいるか、/engine/engine-core-proc で ZMQ のレイヤーを深掘り、クライアントの 3 つの実装は /client/inproc-mp、スケジューラ内部は /scheduler/scheduler を参照してください。