LLM と AsyncLLM: オフライン/サーブの二系統エントリ
役割
LLM は vLLM がオフライン推論 (offline inference) シナリオ向けに提供する同期 Python API(llm.py:66-67)で、AsyncLLM はサーブシナリオで OpenAI API server が使う非同期ラッパー(async_llm.py:70-71)。両者とも EngineClient 抽象を継承するが、経由する EngineCoreClient 実装は異なる:LLM はデフォルトで SyncMPClient または InprocClient を経由し、AsyncLLM は AsyncMPClient(または DP シナリオでは DPLBAsyncMPClient)を経由する。これら二つのクラス自体は直接 forward を走らせない——それらは InputProcessor、OutputProcessor、EngineCoreClient の三件セットのコンテナに過ぎない。
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)を経由し、AsyncLLMはAsyncEngineArgs.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_engineがwhile 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_handlerでengine_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 クラス:66-67—class LLM(BeamSearchOfflineMixin, PoolingOfflineMixin, OfflineInferenceMixin)定義。LLM.__init__:176-222— 40 近い kwarg +**kwargsを受け取り、内部で dict/int を対応する config インスタンスに変換。EngineArgs 組み立て:305-345— LLM kwarg をすべてEngineArgs(model=..., ...)に詰め込み、log_non_default_argsを呼ぶ。LLMEngine.from_engine_args:349-353—usage_context=UsageContext.LLM_CLASS、llm_engine.model_configとsupported_tasksを取得。LLM.from_engine_args:387-389—cls(**vars(engine_args))でコンストラクタを一行で再利用。LLM.generate:422-485—runner_type == "generate"を検証、デフォルトの sampling params、_run_completionに委譲。_add_request:552-571—params.output_kind = RequestOutputKind.FINAL_ONLY、llm_engine.add_requestを呼ぶ。_run_engine:573-595—while self.llm_engine.has_unfinished_requests(): step_outputs = self.llm_engine.step()の同期ループ。LLMEngine クラス:48-49—Legacy LLMEngine for backwards compatibility.の v1 薄殻。make_client 呼び出し:105-111—self.engine_core = EngineCoreClient.make_client(multiprocess_mode, asyncio_mode=False, ...)。LLMEngine.from_engine_args:161-186—engine_args.create_engine_configを走らせ、VLLM_ENABLE_V1_MULTIPROCESSINGでマルチプロセスを有効にするか決定。make_client:83-105— 二つの bool でInprocClient/SyncMPClient/AsyncMPClientを選択。make_async_mp_client:109-132— DP シナリオでDPAsyncMPClientまたはDPLBAsyncMPClientを自動選択。AsyncLLM クラス:70-71—class AsyncLLM(EngineClient):定義。AsyncLLM.__init__:73-176—InputProcessor、OutputProcessor、EngineCoreClient.make_async_mp_clientの三件セットの組み立て。AsyncLLM.from_vllm_config:203-229—VllmConfig+Executor.get_classから直接構築、サーバーサイドエントリ。AsyncLLM.from_engine_args:232-254—engine_args.create_engine_config→Executor.get_class→cls(...)。_run_output_handler:637-676— バックグラウンドコルーチン、get_output_async+output_processor.process_outputs、chunk size はVLLM_V1_OUTPUT_PROC_CHUNK_SIZEで制御。build_async_engine_client:78-105—AsyncEngineArgs.from_cli_args→build_async_engine_client_from_engine_args。build_async_engine_client_from_engine_args:109-154—engine_args.create_engine_config→AsyncLLM.from_vllm_config、finally でasync_llm.shutdown(timeout=vllm_config.shutdown_timeout)を呼ぶ。AsyncLLM.shutdown:259-271—shutdown_prometheus、renderer.shutdown、engine_core.shutdown、output_handlerをキャンセル。
データフロー
オフライン 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 に渡される:
# 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 を駆動する:
# 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_core が InprocClient の場合は直接同期的に 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)は AsyncEngineArgs を VllmConfig に変換し、さらに AsyncLLM.from_vllm_config(async_llm.py:203-229)を呼ぶ。コンストラクタのキーステップ:
# 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 コルーチンがバックグラウンドで継続実行:
# 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.chatとLLM.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_argsのfinallyブロックで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_configdict の検証:LLM.__init__が dict をKVTransferConfigに変換する際に失敗するとlogger.errorの後ValueError(f"Invalid 'kv_transfer_config' provided: {e}")(llm.py:253-262)を raise し、元の ValidationError をより親切なエラーに包む。swap_spaceは非推奨:kwargsにswap_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_clientはparallel_config.data_parallel_sizeとdata_parallel_external_lb(core_client.py:126-132)だけを見て、実行時の LB モード切替はサポートしない——トポロジは起動時に固定される。
まとめ
LLM と AsyncLLM は 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