Skip to content

LLMEngine:v1 エンジンの同期薄殻

源码版本v0.25.1

役割

v1 アーキテクチャにおいて LLMEngine は同期 API(LLM.generateLLM.chat)向けの入口となる薄殻です。自身は前向き計算もスケジューリングも KV cache の割り当ても行わず、次の 3 つをまとめるだけです:InputProcessor で外部から入ってきた prompt を EngineCoreRequest に加工し、OutputProcessorEngineCoreOutputsRequestOutput に変換し、本当の仕事はすべて EngineCore に投げます。__init__ 末尾の self.engine_core = EngineCoreClient.make_client(...)(llm_engine.py:105-111)がエンジンコア (EngineCore) の本当の入り口で、make_client は多プロセスかどうか、asyncio かどうかに基づいて InprocClientSyncMPClientAsyncMPClient の 3 つから 1 つを選びます。

このレイヤーが存在する意義は v0 の LLMEngine.add_request() / .step() という同期用法を残すことです。同プロセスモードでは InprocClientEngineCore のインスタンスを直接保持し(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_requestParentRequest(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-49class LLMEngine: 定義、docstring は Legacy LLMEngine for backwards compatibility. と自称しています。
  • LLMEngine.__init__:51-141 — renderer / InputProcessor / OutputProcessor / EngineCoreClient を組み立て、StatLoggerManager と cleanup finalizer を起動します。
  • make_client 调用点:105-111EngineCoreClient.make_client(multiprocess_mode=..., asyncio_mode=False, ...) でバックエンドを選びます。
  • from_vllm_config:143-158VllmConfig から構築、multiprocess_modeenvs.VLLM_ENABLE_V1_MULTIPROCESSING で決まります。
  • add_request:218-294 — request_id 検証、input_processor.process_inputsn>1 の場合は ParentRequest 扇出、最後に engine_core.add_request
  • step:296-334engine_core.get_output()output_processor.process_outputsabort_requestslogger_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-401VLLM_LOG_STATS_INTERVAL でログをスロットルし、毎 step の flush を回避します。
  • _get_driver_model_for_cleanup:431-434model_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 で戻し、ついでに統計を記録します。

python
# 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_abortengine_core.abort_requests に渡します。stop string は detokenize して初めて分かるため、EngineCore 自身は気づきません。第 4 ステップは logger_manager が非空かつ scheduler_stats がある場合に record + do_log_stats_with_interval を呼び出します。

add_requestn>1 のとき扇出ブランチに入ります(llm_engine.py:279-294):

python
# 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_requestEngineCoreRequest を直接受け取ると warning_once で deprecated を報告し、v0.18 以降は Renderer.render_cmpl() / render_chat() を使うよう促します(llm_engine.py:235-248)。渡された request_idEngineCoreRequest.request_id が一致しない場合は後者が優先され、前者は無視されます。
  • DP モード下の dummy batch:データ並列 (data parallel) の外部起動器モードで、ある rank には仕事がないが他 rank がまだ走っているとき、has_unfinished_requests_dpshould_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_intervalVLLM_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_configmultiprocess_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 を参照してください。

公式資料:vLLM 文档 · README