Skip to content

LLM と AsyncLLM: オフライン/サーブの二系統エントリ

源码版本v0.25.1

役割

LLM は vLLM がオフライン推論 (offline inference) シナリオ向けに提供する同期 Python API(llm.py:66-67)で、AsyncLLM はサーブシナリオで OpenAI API server が使う非同期ラッパー(async_llm.py:70-71)。両者とも EngineClient 抽象を継承するが、経由する EngineCoreClient 実装は異なる:LLM はデフォルトで SyncMPClient または InprocClient を経由し、AsyncLLMAsyncMPClient(または DP シナリオでは DPLBAsyncMPClient)を経由する。これら二つのクラス自体は直接 forward を走らせない——それらは InputProcessorOutputProcessorEngineCoreClient の三件セットのコンテナに過ぎない。

LLM.__init__(llm.py:176-222)はほぼすべての EngineArgs フィールドを kwarg として受け取り、それらを EngineArgs(model=..., tensor_parallel_size=..., ...)(llm.py:305-345)に再パッケージし、LLMEngine.from_engine_args(engine_args, usage_context=UsageContext.LLM_CLASS)(llm.py:349-351)に渡す。LLMEngine は v1 で後方互換性のために残された薄い殻(llm_engine.py:48-49)で、内部では EngineCoreClient.make_client(...)(llm_engine.py:105)で選ばれたクライアントを self.engine_core として保持する。そのため LLM.generate() を呼ぶと最終的には llm_engine.add_request + llm_engine.step のループになる。

AsyncLLM は別の組み立てパスを通る。build_async_engine_client_from_engine_args(api_server.py:109-154)はまず engine_args.create_engine_config()VllmConfig を取得し、その後 AsyncLLM.from_vllm_config(vllm_config, ...) を呼ぶ。後者は内部で直接 EngineCoreClient.make_async_mp_client(...)(async_llm.py:146-153)を呼んで AsyncMPClient(または DP シナリオでは DPLBAsyncMPClient)を起動する。AsyncLLM には常駐の output_handler コルーチン(async_llm.py:637-660)があり、継続的に get_output_async() で EngineCore の出力をフロントエンドに引き戻す。

設計動機

  • 同一の EngineArgs schema で二系統に対応:LLM 構築時は直接 EngineArgs(model=model, ...)(llm.py:305-345)を経由し、AsyncLLMAsyncEngineArgs.from_cli_args(args)(api_server.py:95)から来る——両者とも /startup/engine-args の同一のフィールド定義を再利用し、オフラインとサーブで挙動が一致する。
  • LLM は同期の薄い殻:LLM.generate_run_completion_render_and_add_requests_add_request(offline_utils.py:552-571) → llm_engine.add_request、その後 _run_enginewhile has_unfinished_requests(): step_outputs = self.llm_engine.step()(offline_utils.py:594-595)でループ駆動する。これによりスクリプトシナリオでは for 一つで書ききれ、ユーザーは asyncio に触れずに済む。
  • AsyncLLM はブロックを output_handler に隠す:add_request は即座に RequestOutputCollector(async_llm.py:280-297)を返し、実際の token ストリームはバックグラウンドの output_handlerengine_core.get_output_async() で引き戻され chunk 処理(async_llm.py:656-676)される。VLLM_V1_OUTPUT_PROC_CHUNK_SIZE が大きな出力の塊を切り分け、一つの巨大バッチでイベントループがブロックされるのを防ぐ。
  • LLMEngine は v1 では互換層に過ぎない:class LLMEngine の docstring に直接 Legacy LLMEngine for backwards compatibility.(llm_engine.py:48-49)と書かれている。その __init__EngineCoreClient.make_client(multiprocess_mode, asyncio_mode=False, ...) を呼び、同期マルチプロセス / 同プロセスの二つのモードを一つにまとめる。make_client(core_client.py:83-105)は二つの bool で InprocClient / SyncMPClient / AsyncMPClient を選ぶ。
  • make_async_mp_client が DP クライアントを自動選択:data_parallel_size > 1 かつ data_parallel_external_lb の場合は DPAsyncMPClient(各 DP rank に一つの client)、そうでなければ DPLBAsyncMPClient(内部 LB が全 DP rank にリクエストを分散)(core_client.py:116-132)。これが v1 データ並列のエントリ。
  • LLM.__init__ が設定を正規化:compilation_config が int の場合は自動で CompilationConfig(mode=CompilationMode(int)) に包み(llm.py:275-282)、kv_transfer_config が dict の場合は KVTransferConfig に変換(llm.py:245-262)し、worker_cls が class の場合は cloudpickle でシリアライズ(llm.py:238-243)する。これらの変換により SDK の呼び出し側のボイラープレートを減らす。
  • from_engine_args クラスメソッドがエントリを統一:LLM.from_engine_args(llm.py:387-389)、AsyncLLM.from_engine_args(async_llm.py:232-254)、LLMEngine.from_engine_args(llm_engine.py:161-186)の三つのクラスメソッドはすべて engine_args.create_engine_config()Executor.get_class(vllm_config) を経由し、異なるエントリでも VllmConfig の派生過程が完全に一致することを保証する。

主要ファイル

データフロー

オフライン LLM.generate(prompts)

generate(llm.py:422)は runner_type を検証した後 OfflineInferenceMixin._run_completion に委譲し、後者は _render_and_add_requests(offline_utils.py:523)を呼ぶ。その中で各 prompt が renderer で加工されて EngineInput になり _add_request に渡される:

python
# vllm/entrypoints/offline_utils.py L552-L571
def _add_request(
    self,
    prompt: EngineInput,
    params: SamplingParams | PoolingParams,
    lora_request: LoRARequest | None = None,
    priority: int = 0,
) -> str:
    if isinstance(params, SamplingParams):
        # We only care about the final output
        params.output_kind = RequestOutputKind.FINAL_ONLY

    request_id = str(next(self.request_counter))

    return self.llm_engine.add_request(
        request_id,
        prompt,
        params,
        lora_request=lora_request,
        priority=priority,
    )

params.output_kind = RequestOutputKind.FINAL_ONLY に注意——オフラインシナリオは最終結果だけを気にし、中間 token ストリームは不要、これにより EngineCore の出力側は chunked-streaming のオーバーヘッドを一式省ける。_add_request の後、_run_engine が同期ループで EngineCore を駆動する:

python
# vllm/entrypoints/offline_utils.py L590-L599
# Run the engine.
outputs: list[_O] = []
total_in_toks = 0
total_out_toks = 0
while self.llm_engine.has_unfinished_requests():
    step_outputs = self.llm_engine.step()
    for output in step_outputs:
        assert isinstance(output, output_type)
        if output.finished:
            outputs.append(output)

LLMEngine.step() 内部は engine_core.get_output() + output_processor.process_outputs の薄いラッパー。engine_coreInprocClient の場合は直接同期的に step_fn() を呼び、SyncMPClient の場合は ZMQ を受信してブロックする。全体のフローに asyncio がないため、スクリプトで for output in llm.generate(...) と書いても動く。

サーブ AsyncLLM.add_request

サーバーサイドの AsyncLLM は全く別のパスを通る。build_async_engine_client_from_engine_args(api_server.py:109-154)は AsyncEngineArgsVllmConfig に変換し、さらに AsyncLLM.from_vllm_config(async_llm.py:203-229)を呼ぶ。コンストラクタのキーステップ:

python
# vllm/v1/engine/async_llm.py L146-L153
# EngineCore (starts the engine in background process).
self.engine_core = EngineCoreClient.make_async_mp_client(
    vllm_config=vllm_config,
    executor_class=executor_class,
    log_stats=self.log_stats,
    client_addresses=client_addresses,
    client_count=client_count,
    client_index=client_index,
)

make_async_mp_client(core_client.py:109-132)は DP トポロジに基づき AsyncMPClient または DPLBAsyncMPClient を選ぶ。その後 output_handler コルーチンがバックグラウンドで継続実行:

python
# vllm/v1/engine/async_llm.py L656-L676
async def output_handler():
    try:
        while True:
            # 1) Pull EngineCoreOutputs from the EngineCore.
            outputs = await engine_core.get_output_async()
            num_outputs = len(outputs.outputs)

            iteration_stats = (
                IterationStats() if (log_stats and num_outputs) else None
            )

            # Split outputs into chunks of at most
            # VLLM_V1_OUTPUT_PROC_CHUNK_SIZE, so that we don't block the
            # event loop for too long.
            engine_core_outputs = outputs.outputs
            for start in range(0, num_outputs, chunk_size):
                end = start + chunk_size
                outputs_slice = engine_core_outputs[start:end]
                # 2) Process EngineCoreOutputs.
                processed_outputs = output_processor.process_outputs(
                    outputs_slice, outputs.timestamp, iteration_stats

リクエスト側の OpenAI ルートは async_llm.add_request(request_id, prompt, params, ...)(async_llm.py:280-297)を呼んで即座に RequestOutputCollector を取得し、後はそのストリームを await すればよい。EngineCore 側は forward を走らせ、出力は ZMQ 経由で AsyncMPClient に戻り、さらに output_handler によって chunk 処理されて collector に push される——チェーン全体が完全非同期で、API server は一つの遅いリクエストでブロックされない。

境界と失敗

  • LLM.generate は generate runner 以外をサポートしない:runner_type != "generate" の場合は直接 raise(llm.py:465-471)し、--runner generate への切り替を提示。LLM.chatLLM.enqueue_chat も同様にチェック(llm.py:684-689)。
  • LLM(data_parallel_size>1) は単一プロセスを許可しない:_dp_size > 1 かつ external_launcher でなく、TPU でもない場合は直接 raise(llm.py:295-303)し、examples/features/data_parallel/data_parallel_offline.py のマルチプロセス案を提示。さもないとハングする。
  • renderer_num_workers > 1 はオフライン LLM では無効:LLM は同期 renderer パスを通る。マルチ worker スレッドプールは vllm serve / AsyncLLM の非同期パスだけで消費される。オフラインシナリオでは明示的に warning_once(llm.py:370-379)し、ユーザーがマルチスレッドを有効にしたと誤解しないようにする。
  • EngineDeadError:AsyncLLM.add_request の冒頭で self.errored(async_llm.py:300-301)を検査し、EngineCore プロセスが既に死んでいる場合(バックグラウンドで ENGINE_CORE_DEAD が発行済み)は直接 EngineDeadError(async_llm.py:1054-1058)を投げ、新リクエストを受け付けない。
  • async_llm.shutdown の兜底:build_async_engine_client_from_engine_argsfinally ブロックで async_llm.shutdown(timeout=vllm_config.shutdown_timeout)(api_server.py:152-154)を呼び、build 過程でエラーが投げられてもバックグラウンドプロセスと ZMQ socket をクリーンアップ。AsyncLLM.__del__(async_llm.py:256-257)も兜底で shutdown を呼び、GC 時のリークを防ぐ。
  • kv_transfer_config dict の検証:LLM.__init__ が dict を KVTransferConfig に変換する際に失敗すると logger.error の後 ValueError(f"Invalid 'kv_transfer_config' provided: {e}")(llm.py:253-262)を raise し、元の ValidationError をより親切なエラーに包む。
  • swap_space は非推奨:kwargsswap_space が含まれる場合は pop して DeprecationWarning(llm.py:224-233)を出す。将来削除予定。
  • output_handler が起動しない退化ケース:__init__ 時に asyncio.get_running_loop()RuntimeError(実行中のイベントループがない)を投げた場合は output_handler をスキップ(async_llm.py:170-176)。この場合、ユーザーは自身で明示的に start_engine_loop=True 相当のものを呼ぶか、AsyncLLM をイベントループに入れてから await する必要がある。
  • DP クライアント選択は静的:make_async_mp_clientparallel_config.data_parallel_sizedata_parallel_external_lb(core_client.py:126-132)だけを見て、実行時の LB モード切替はサポートしない——トポロジは起動時に固定される。

まとめ

LLMAsyncLLM は vLLM が上位(スクリプトと OpenAI server)に公開する二つのメインエントリであり、どちらも EngineClient 抽象の実装だが経由する EngineCoreClient が異なる。LLM は同期パスを通り、内部は LLMEngine + SyncMPClient/InprocClient で、ユーザーが手にするのは for output in llm.generate(...) 形式の同期イテレータ。AsyncLLM は非同期パスを通り、内部は AsyncMPClient/DPLBAsyncMPClient + バックグラウンドの output_handler コルーチンで、API server の各 HTTP リクエストはただ一つの RequestOutputCollector を await する。両者とも EngineArgs.create_engine_config() から VllmConfig を取得するため、設定側は完全に一致し、違いはクライアント選択とイベントループモデルだけにある。EngineArgs / VllmConfig がどう組み立てられるかは /startup/engine-args、CLI のチェーンがどう AsyncLLM.from_vllm_config に到達するかは /startup/cli、EngineCore のバックグラウンドプロセスと ZMQ 内部は /engine/engine-core、三種類の EngineCoreClient 実装の詳細は /client/inproc-mp を参照。

公式資料: vLLM ドキュメント · README