Skip to content

vllm CLI: ターミナルから AsyncLLM まで

源码版本v0.25.1

役割

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 として登録し、argvargparse.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_parserAsyncEngineArgs.from_cli_argscreate_engine_configAsyncLLM.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_platformUnspecifiedPlatform で、device type の推論に失敗する。そのため main()sys.argv[1] == "bench" を検出した時点で手動で current_platformCpuPlatform() に差し替える(main.py:58-71)。
  • --omni の全体委譲:--omni flag は 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_countdata_parallel_external_lbVLLM_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 の紐付け、--omnibench のプラットフォーム切替を処理。
  • CLISubcommand:13-29 — サブコマンド抽象基底クラス、cmd 静的メソッド + validate + subparser_init の三件セット。
  • ServeSubcommand:44-47name = "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-166make_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-393validate_chat_template(args) で chat template の事前チェック。
  • build_async_engine_client:78-105AsyncEngineArgs.from_cli_args(args)build_async_engine_client_from_engine_args、serve サブコマンドが実際にエンジンを起動する場所。
  • build_async_engine_client_from_engine_args:109-154engine_args.create_engine_config()AsyncLLM.from_vllm_config(...)、EngineArgs を生きた AsyncLLM に変換。
  • run_server:685-698 — 単一プロセスのエントリ、setup_server で socket を取得 → run_server_worker
  • run_server_worker:701-723async with build_async_engine_clientbuild_and_serve → shutdown_task を待機。
  • setup_server:555-589 — socket の bind、validate_api_server_args、set_ulimit。エンジン起動前に実行し Ray とのポート競合を避ける。
  • build_and_serve:592-617get_supported_tasksbuild_appinit_app_stateserve_http
  • LaunchSubcommand:60-105vllm launch はネストした sub-subcommands、現在は render サブコマンドを保持。
  • RunBatchSubcommand:21-68vllm 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 に入る:

python
# 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)を呼ぶ:

python
# 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_serverbuild_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 serveServeSubcommand.cmdapi_server_count と LB モードに基づき四つの実行モードに振り分け、最も一般的なものは run_serverbuild_async_engine_clientAsyncLLM.from_vllm_config と進み、このステップで EngineArgs を本当に生きた非同期エンジンに変換する。CLI 層は重みに触れず、forward も走らせず、argv を正しく翻訳して適切な実行モードを選ぶことだけを担う。EngineArgs がどのように数百のフィールドを VllmConfig に組み立てるかは /startup/engine-args を参照。AsyncLLM 起動後に内部でどうリクエストを EngineCore に送るかは /startup/llm-class/engine/engine-core を参照。

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