vllm CLI: ターミナルから AsyncLLM まで
役割
vllm コマンドはユーザーが最もよく触れるエントリである。vllm serve Qwen/Qwen3-0.6B で OpenAI 互換サーバーを起動し、vllm bench throughput でベンチマークを走らせ、vllm run-batch でバッチ処理を行う。実装は vllm/entrypoints/cli/ 以下にあり、エントリ関数は main()(main.py:17-97)。この層の役割は非常に薄い:argparse でサブコマンド (subcommand) を subparser として登録し、argv を argparse.Namespace に解析し、対応するサブコマンドの cmd(args) 静的メソッドへディスパッチするだけだ。
サブコマンドの骨格抽象は CLISubcommand(types.py:13-29)にある:各サブコマンドは name を宣言し、cmd(args) を実装して実際の処理を行い、必要に応じて validate(args) をオーバーライドして事前チェックを行い、subparser_init(subparsers) で自身の flag をメイン parser のサブ parser に登録する。main() 起動時に CMD_MODULES(main.py:30-37)を巡回し、各モジュールに対して cmd_init() を呼んで CLISubcommand リストを取得し、cmd.subparser_init(subparsers).set_defaults(dispatch_function=cmd.cmd)(main.py:86-89)で dispatcher を namespace に紐付ける。parser.parse_args() の後、args.dispatch_function(args)(main.py:94-95)で制御をサブコマンドに渡す。
serve は最も重要なサブコマンドで、チェーンが最も長い:CLI parser → make_arg_parser → AsyncEngineArgs.from_cli_args → create_engine_config → AsyncLLM.from_vllm_config → FastAPI build_app → uvicorn serve_http。チェーン全体が v1 側にあるが、各小ステップは単体テスト可能なように細分化されている。CLI 層自体はモデル重みに触れず、その役割は「argv を EngineArgs に翻訳して下流に渡す」ことだ。
設計動機
- サブコマンドモジュールの遅延読み込み:
main()の先頭でimport vllm.entrypoints.cli.serveなどの import 文(main.py:18-23)を使い、トップレベルで一括読み込みしない。一部のサブコマンド(serveなど)は CUDA/torch などの重いモジュールを引っ張るため、vllm benchがそれらを必要としない場合には初期化しなくて済むようにするため。docstring にmust be lazily loaded within main to avoid certain eager import breakage(main.py:3-6)と明記されている。 - if/elif ではなく CLISubcommand 抽象:
main()はCMD_MODULESリスト(main.py:30-37)で全サブコマンドモジュールを列挙し、各モジュールはcmd_init() -> list[CLISubcommand]ファクトリ関数だけを外部に公開する。これにより新サブコマンド(vllm launch renderなど)の追加はCLISubcommandサブクラスを一つ書いてcmd_initを実装するだけで済み、main()は一切変更不要。 vllm benchの事前プラットフォーム切替:vllm bench throughputは純 CPU でも走るベンチマークだが、vLLM のデフォルトcurrent_platformはUnspecifiedPlatformで、device type の推論に失敗する。そのためmain()はsys.argv[1] == "bench"を検出した時点で手動でcurrent_platformをCpuPlatform()に差し替える(main.py:58-71)。--omniの全体委譲:--omniflag は vLLM 自身のサブコマンドではなく、argv 全体をvllm-omniパッケージ(main.py:42-55)に渡す。find_specでパッケージのインストール有無を先に探り、未インストールならsys.exit(1)、インストールされていればomni_main()に全面委譲。- API server count のマルチモード:
ServeSubcommand.cmd(serve.py:49-148)はapi_server_count、data_parallel_external_lb、VLLM_RUST_FRONTEND_PATHいくつかの変数に基づき四つの実行モードから一つを選ぶ——単一プロセスrun_server、headless で API server なし、run_multi_api_serverで複数 API server プロセス、run_dp_supervisorで外部 LB——これが v1 マルチフロントエンドアーキテクチャのエントリ振り分け。 - argparse が EngineArgs schema を完全再利用:
make_arg_parser(cli_args.py:339-383)は二つだけのことをする:サーバー専用 flag(--host、--port、--headless、--api-server-count、--config、--grpc)をいくつか追加し、その後FrontendArgs.add_cli_args(parser)+AsyncEngineArgs.add_cli_args(parser)(cli_args.py:380-381)でEngineArgsの数百フィールドを一括登録する。これにより serve とLLM(...)のパラメータが自然と一致する。
主要ファイル
main():17-97— CLI 総エントリ、CMD_MODULES の登録、dispatch_function の紐付け、--omniとbenchのプラットフォーム切替を処理。CLISubcommand:13-29— サブコマンド抽象基底クラス、cmd静的メソッド +validate+subparser_initの三件セット。ServeSubcommand:44-47—name = "serve"のサブコマンド定義。ServeSubcommand.cmd:49-148— headless / multi-api-server / dp_supervisor / 単一プロセス run_server のどれを走らせるか決定。api_server_count デフォルト値推導:105-128— multi-port / external LB / hybrid LB / Rust frontend がそれぞれ異なる count にデフォルト。ServeSubcommand.subparser_init:153-166—make_arg_parser(serve_parser)を呼んで全 EngineArgs flag を登録、ついでに epilog を付与。run_headless:173-180— headless モードのエントリ、EngineCore だけ起動し API server は起動しない。run_multi_api_server:257-390— マルチ API server モード、APIServerProcessManagerまたはRustFrontendProcessManagerを起動して子プロセスを管理。make_arg_parser:339-383— serve parser を組み立て、server-only flag を先に追加し、FrontendArgs.add_cli_args+AsyncEngineArgs.add_cli_argsを呼ぶ。validate_parsed_serve_args:386-393—validate_chat_template(args)で chat template の事前チェック。build_async_engine_client:78-105—AsyncEngineArgs.from_cli_args(args)→build_async_engine_client_from_engine_args、serve サブコマンドが実際にエンジンを起動する場所。build_async_engine_client_from_engine_args:109-154—engine_args.create_engine_config()→AsyncLLM.from_vllm_config(...)、EngineArgs を生きた AsyncLLM に変換。run_server:685-698— 単一プロセスのエントリ、setup_serverで socket を取得 →run_server_worker。run_server_worker:701-723—async with build_async_engine_client→build_and_serve→ shutdown_task を待機。setup_server:555-589— socket の bind、validate_api_server_args、set_ulimit。エンジン起動前に実行し Ray とのポート競合を避ける。build_and_serve:592-617—get_supported_tasks→build_app→init_app_state→serve_http。LaunchSubcommand:60-105—vllm launchはネストした sub-subcommands、現在はrenderサブコマンドを保持。RunBatchSubcommand:21-68—vllm run-batch、Prometheus を起動後run_batch_main(args)で JSONL を非同期処理。
データフロー
vllm serve Qwen/Qwen3-0.6B --tensor-parallel-size 2 が通るチェーン:まず main()(main.py:17)で else ブランチに入り、FlexibleArgumentParser + subparsers が全サブコマンドを組み立て、parser.parse_args() で args.subparser == "serve" を得る。次に cmds["serve"].validate(args)(main.py:91-92)が validate_parsed_serve_args で chat template をチェックし、その後 args.dispatch_function(args)(main.py:94-95)で ServeSubcommand.cmd に入る:
# vllm/entrypoints/cli/serve.py L49-L148
@staticmethod
def cmd(args: argparse.Namespace) -> None:
# If model is specified in CLI (as positional arg), it takes precedence
if hasattr(args, "model_tag") and args.model_tag is not None:
args.model = args.model_tag
if getattr(args, "grpc", False):
from vllm.entrypoints.grpc_server import serve_grpc
uvloop.run(serve_grpc(args))
return
if args.headless:
...
args.api_server_count = 0
# Detect LB mode for defaulting api_server_count.
is_external_lb = (
args.data_parallel_external_lb or args.data_parallel_rank is not None
)
...
if args.api_server_count is None:
if is_multi_port or is_external_lb or envs.VLLM_RUST_FRONTEND_PATH:
args.api_server_count = 1
elif is_hybrid_lb:
args.api_server_count = args.data_parallel_size_local or 1
else:
args.api_server_count = args.data_parallel_size
...
if is_multi_port:
run_dp_supervisor(args)
elif args.api_server_count < 1:
run_headless(args)
elif args.api_server_count > 1 or envs.VLLM_RUST_FRONTEND_PATH:
run_multi_api_server(args)
else:
# Single API server (this process).
args.api_server_count = None
uvloop.run(run_server(args))model_tag(位置引数)が args.model に書き換えられ、その後 grpc / headless / multi_port / hybrid_lb / external_lb / Rust frontend の各モードに振り分けられる。最もよくあるのは最後の uvloop.run(run_server(args))(serve.py:146-148)で、その中で run_server_worker(api_server.py:701-723)を呼ぶ:
# vllm/entrypoints/openai/api_server.py L712-L721
async with build_async_engine_client(
args,
client_config=client_config,
) as engine_client:
shutdown_task = await build_and_serve(
engine_client, listen_address, sock, args, **uvicorn_kwargs
)
# NB: Await server shutdown only after the backend context is exited
try:
await shutdown_task
finally:
sock.close()build_async_engine_client(api_server.py:78-105)内で AsyncEngineArgs.from_cli_args(args) が Namespace を EngineArgs に戻し、その後 build_async_engine_client_from_engine_args(api_server.py:109-154)が engine_args.create_engine_config() で VllmConfig を得て、さらに AsyncLLM.from_vllm_config(...) で非同期エンジンを起動する。エンジンが構築された後 build_and_serve が FastAPI app、ルーティング、middleware、uvicorn をすべて取り付け、serve_http が SIGTERM までメインスレッドをブロックする。
境界と失敗
--omniパッケージ不在:find_spec("vllm_omni")がNoneを返した場合、logger.error+sys.exit(1)(main.py:46-50)し、ユーザーを曖昧な ImportError で止めない。- headless と api_server_count の衝突:
--headlessモードで--api-server-count>0を渡すと直接 raise(serve.py:62-69)する。headless は元々 multi-node で API server を起動しないシナリオだから。 - LB モードの相互排他:
is_multi_port/is_external_lb/is_hybrid_lbの二つを同時に有効にすると直接 raise(serve.py:91-97)する。三種類の LB トポロジは相互排他。 - Elastic EP の上限:
enable_elastic_ep=Trueかつapi_server_count > 1の場合、1 に強制 cap(serve.py:131-137)し warning を出す。弾性 EP は現時点で単一 API server しかサポートしない。 - Rust frontend はマルチ API server 非サポート:
VLLM_RUST_FRONTEND_PATH+api_server_count > 1の場合は無視されて 1 に戻される(serve.py:123-128)。Rust frontend はマルチプロセスではなくマルチスレッドだから。 - VLLM_ALLOW_RUNTIME_LORA_UPDATING + マルチ API server:
run_multi_api_serverはこの二つの組合せを検出すると直接 raise(serve.py:293-296)する。実行時 LoRA 更新は複数の独立した API server プロセス間で同期できない。 - tool_parser / reasoning_parser プラグインの検証:
validate_api_server_args(api_server.py:536-551)は--tool-call-parser/reasoning_parserが登録済みリストにあるかチェックし、なければKeyError。 - bind socket をエンジン起動より前に:
setup_serverはbuild_async_engine_clientより前に実行(api_server.py:697-698)し、Ray とのポート競合を避ける(コードコメントは issue #8204 を明示)。 - SIGTERM による初期化中断:
run_serverは uvicorn が自身の signal handler をインストールする前に手動でSIGTERM → KeyboardInterruptを取り付け(api_server.py:690-695)、エンジン初期化中に SIGTERM を受け取った場合に即座に終了でき、ハングしないようにする。
まとめ
vllm CLI は非常に薄い dispatcher である:main() は CLISubcommand 抽象で全サブコマンド(serve / launch / openai / run-batch / bench / collect-env)を subparser として登録し、cmd_init() が各モジュールにファクトリエントリを提供する。最も重要で最もよく呼ばれる vllm serve は ServeSubcommand.cmd で api_server_count と LB モードに基づき四つの実行モードに振り分け、最も一般的なものは run_server → build_async_engine_client → AsyncLLM.from_vllm_config と進み、このステップで EngineArgs を本当に生きた非同期エンジンに変換する。CLI 層は重みに触れず、forward も走らせず、argv を正しく翻訳して適切な実行モードを選ぶことだけを担う。EngineArgs がどのように数百のフィールドを VllmConfig に組み立てるかは /startup/engine-args を参照。AsyncLLM 起動後に内部でどうリクエストを EngineCore に送るかは /startup/llm-class と /engine/engine-core を参照。
公式資料: vLLM ドキュメント · README