LLMEngine:v1 エンジンの同期薄殻
役割
v1 アーキテクチャにおいて LLMEngine は同期 API(LLM.generate、LLM.chat)向けの入口となる薄殻です。自身は前向き計算もスケジューリングも KV cache の割り当ても行わず、次の 3 つをまとめるだけです: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 の 3 つから 1 つを選びます。
このレイヤーが存在する意義は 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 はすべて 1 行透伝です)。
設計動機
- v0 同期 API の保持:
LLMクラスは外部にgenerate/chat同期メソッドを晒し、内部ではLLMEngine.add_request()+LLMEngine.step()ループ(llm_engine.py:296-334)で回します。asyncio を使わずに v1 を動かせます。これが v1 が v0 を置き換える際にオフライン推論ワークフローを壊さないための鍵です。 - 3 つのバックエンドを 1 つのインターフェースで:
make_clientの 3 択ロジック(core_client.py:83-105)により、LLMEngineのすべてのメソッドは同プロセスか、多プロセス同期か、多プロセス非同期かを意識せず、EngineCoreClientインスタンスを 1 つ持てば済みます。バックエンドの切り替えは構築期に決まります。 - 入出力の加工を shell 層に置く:
InputProcessor(input_processor.py:36)はマルチモーダル (multimodal)、tokenization、priority、trace_headers、LoRA を処理します。OutputProcessor(output_processor.py:417)は detokenize、ストリーム区分け、stop string マッチ、EngineCoreOutputsからRequestOutputへの変換を担当します。この 2 つの汚れ仕事と汚れデータは shell 層に留まり、EngineCore は純粋に構造化されたEngineCoreRequest/EngineCoreOutputsだけを見ます。 n>1のマルチサンプル扇出:SamplingParams.nが 1 より大きい場合、add_requestはParentRequest(parallel_sampling.py:13)を使って 1 つの親リクエストを n 個の子リクエストに分割し、それぞれを EngineCore と OutputProcessor に流し込みます(llm_engine.py:279-294)。出力層で子リクエストの token ストリームを親リクエストに集約し直します。- プロセス退出時の兜底:
multiprocess_mode=Falseのとき driver model の弱参照 finalizer を持ちます(llm_engine.py:129-133)。LLMEngineが GC された際に_cleanup_instance_cachesを呼び出し、byte code hook が釘付けした GPU メモリを解放します。
主要ファイル
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 で GPU メモリを解放します。
データフロー
同期推論 1 回とは 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 オブジェクトを再利用して 1 回のコピーを省き、親リクエスト 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でさらに 1 回上書きします(llm_engine.py:174-176)。そのため多プロセスの on/off は設定層で固定され、実行期には切り替えられません。
まとめ
LLMEngine は v1 同期パス上の薄殻です。入力を検証し、InputProcessor / OutputProcessor でマルチモーダルとストリーミング出力を処理し、EngineCoreClient.make_client でバックエンドを選んだら、あとは add_request + step のループを回すだけです。重い仕事はすべて EngineCore に委譲し(同プロセスなら直接保持、多プロセスなら ZMQ 経由)、自身は v0 互換、ParentRequest 扇出、統計スロットル、sleep/wake 連動といった壳層の役割だけに集中します。さらに掘り下げるには /engine/engine-core でエンジンコアの組み立てを、/engine/engine-core-proc で ZMQ バックグラウンドプロセスの実装を、クライアントの 3 つの実装は /client/inproc-mp を、スケジューラ内部は /scheduler/scheduler を参照してください。