vllm CLI : du terminal à AsyncLLM
Responsabilités
La commande vllm est le point d'entrée le plus utilisé : vllm serve Qwen/Qwen3-0.6B démarre un serveur compatible OpenAI, vllm bench throughput lance un benchmark, vllm run-batch fait du traitement par lot. L'implémentation vit dans vllm/entrypoints/cli/, et la fonction d'entrée est main()(main.py:17-97). Cette couche est très fine : argparse enregistre chaque sous-commande (subcommand) comme un subparser, argv est résolu en argparse.Namespace, puis le contrôle est dispatché vers la méthode statique cmd(args) de la sous-commande correspondante.
Le squelette des sous-commandes est abstrait dans CLISubcommand(types.py:13-29) : chaque sous-commande déclare name, implémente cmd(args) pour faire le travail, peut redéfinir validate(args) pour des vérifications préalables, et subparser_init(subparsers) enregistre ses flags sur un subparser du parser principal. Au démarrage, main() parcourt CMD_MODULES(main.py:30-37), appelle cmd_init() sur chaque module pour récupérer une liste de CLISubcommand, puis cmd.subparser_init(subparsers).set_defaults(dispatch_function=cmd.cmd)(main.py:86-89) attache le dispatcher au namespace. Après parser.parse_args(), args.dispatch_function(args)(main.py:94-95) cède le contrôle à la sous-commande.
serve est la sous-commande la plus importante et la chaîne la plus longue : CLI parser → make_arg_parser → AsyncEngineArgs.from_cli_args → create_engine_config → AsyncLLM.from_vllm_config → FastAPI build_app → uvicorn serve_http. Toute la chaîne est en v1, mais chaque étape est suffisamment découpée pour être testée isolément. La couche CLI ne touche pas aux poids du modèle ; son rôle se résume à « traduire argv en EngineArgs et confier le tout à l'aval ».
Motivation de conception
- Lazy load des modules de sous-commande : en tête de
main(), on trouve des importsimport vllm.entrypoints.cli.serveetc.(main.py:18-23) plutôt qu'un chargement global en début de module, car certaines sous-commandes (par exempleserve) tirent des modules lourds comme CUDA/torch, dontvllm benchn'a pas besoin. La docstring note explicitementmust be lazily loaded within main to avoid certain eager import breakage(main.py:3-6). - Abstraction CLISubcommand plutôt que if/elif :
main()énumère les modules de sous-commande viaCMD_MODULES(main.py:30-37). Chaque module expose une fonction factorycmd_init() -> list[CLISubcommand]. Ajouter une sous-commande (par exemplevllm launch render) se résume à écrire une sous-classe deCLISubcommandet à implémentercmd_init, sans toucher àmain(). vllm benchbascule la plateforme à l'avance :vllm bench throughputest un benchmark qui peut tourner sur CPU pur, maiscurrent_platformvaut par défautUnspecifiedPlatform, ce qui ferait échouer l'inférence du device type. Aussi, quandmain()détectesys.argv[1] == "bench", elle substitueCpuPlatform()àcurrent_platform(main.py:58-71).--omnidélègue tout : le flag--omnin'est pas une sous-commande vLLM, il transmet l'intégralité d'argv au paquetvllm-omni(main.py:42-55).find_specdétecte d'abord la présence du paquet, sinonsys.exit(1); s'il est installé,omni_main()prend le relais.- Modes multiples pour api_server_count :
ServeSubcommand.cmd(serve.py:49-148) choisit l'un des quatre modes selonapi_server_count,data_parallel_external_lbetVLLM_RUST_FRONTEND_PATH—run_servermono-processus, headless sans API server,run_multi_api_servermulti-processus API server,run_dp_supervisoravec LB externe — c'est l'aiguillage d'entrée de l'architecture multi-frontend de v1. - argparse réutilise intégralement le schéma EngineArgs :
make_arg_parser(cli_args.py:339-383) se contente de deux choses : ajouter quelques flags spécifiques serveur (--host,--port,--headless,--api-server-count,--config,--grpc), puis appelerFrontendArgs.add_cli_args(parser)+AsyncEngineArgs.add_cli_args(parser)(cli_args.py:380-381) pour enregistrer en une fois les centaines de champs d'EngineArgs. Ainsi, serve etLLM(...)restent naturellement alignés côté paramètres.
Fichiers clés
main():17-97— entrée principale du CLI : enregistre CMD_MODULES, attache dispatch_function, gère--omniet la bascule plateformebench.CLISubcommand:13-29— classe de base abstraite des sous-commandes :cmdstatique +validate+subparser_init.ServeSubcommand:44-47— définition de la sous-commandename = "serve".ServeSubcommand.cmd:49-148— décide entre headless / multi-api-server / dp_supervisor / mono-processus run_server.api_server_count 默认值推导:105-128— multi-port / external LB / hybrid LB / Rust frontend impliquent chacun une valeur par défaut différente pourcount.ServeSubcommand.subparser_init:153-166— appellemake_arg_parser(serve_parser)pour enregistrer tous les flags EngineArgs, et attache l'epilog.run_headless:173-180— entrée du mode headless : ne lance que EngineCore, pas d'API server.run_multi_api_server:257-390— mode multi API server : démarreAPIServerProcessManagerouRustFrontendProcessManagerpour gérer les sous-processus.make_arg_parser:339-383— assemble le parser serve : d'abord les flags server-only, puisFrontendArgs.add_cli_args+AsyncEngineArgs.add_cli_args.validate_parsed_serve_args:386-393—validate_chat_template(args): contrôle préalable du chat template.build_async_engine_client:78-105—AsyncEngineArgs.from_cli_args(args)→build_async_engine_client_from_engine_args, le véritable point où la sous-commande serve démarre le moteur.build_async_engine_client_from_engine_args:109-154—engine_args.create_engine_config()→AsyncLLM.from_vllm_config(...), transforme EngineArgs en un AsyncLLM vivant.run_server:685-698— entrée mono-processus :setup_serverrécupère le socket →run_server_worker.run_server_worker:701-723—async with build_async_engine_client→build_and_serve→ attend shutdown_task.setup_server:555-589— bind socket, validate_api_server_args, set_ulimit, avant le démarrage moteur pour éviter la concurrence de port avec Ray.build_and_serve:592-617—get_supported_tasks→build_app→init_app_state→serve_http.LaunchSubcommand:60-105—vllm launchimbrique des sub-subcommands, actuellementrender.RunBatchSubcommand:21-68—vllm run-batch, démarre Prometheus puisrun_batch_main(args)pour traiter du JSONL asynchrone.
Flux de données
Pour vllm serve Qwen/Qwen3-0.6B --tensor-parallel-size 2, la chaîne commence dans main()(main.py:17) : branche else, FlexibleArgumentParser + subparsers enregistrent toutes les sous-commandes, parser.parse_args() donne args.subparser == "serve". Ensuite cmds["serve"].validate(args)(main.py:91-92) lance validate_parsed_serve_args (contrôle du chat template), puis args.dispatch_function(args)(main.py:94-95) entre dans 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 (argument positionnel) est réécrit dans args.model, puis grpc / headless / multi_port / hybrid_lb / external_lb / Rust frontend sont aiguillés. Le cas le plus courant tombe sur la dernière branche uvloop.run(run_server(args))(serve.py:146-148), qui appelle 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()Dans build_async_engine_client(api_server.py:78-105), AsyncEngineArgs.from_cli_args(args) reconstitue EngineArgs à partir du Namespace, puis build_async_engine_client_from_engine_args(api_server.py:109-154) appelle engine_args.create_engine_config() pour obtenir un VllmConfig, puis AsyncLLM.from_vllm_config(...) démarre le moteur asynchrone. Une fois le moteur prêt, build_and_serve monte l'app FastAPI, les routes, les middlewares et uvicorn, puis serve_http bloque le thread principal jusqu'à SIGTERM.
Limites et échecs
- Paquet
--omnimanquant : sifind_spec("vllm_omni")renvoieNone,logger.error+sys.exit(1)(main.py:46-50), plutôt que de laisser l'utilisateur sur un ImportError ambigu. - Conflit headless et api_server_count : en mode
--headless, passer--api-server-count>0lève directement(serve.py:62-69) — headless est par construction le scénario multi-nœud sans API server. - Modes LB mutuellement exclusifs :
is_multi_port/is_external_lb/is_hybrid_lblevèvent une erreur si deux sont activés ensemble(serve.py:91-97) ; les trois topologies LB sont exclusives. - Plafond elastic EP : avec
enable_elastic_ep=Trueetapi_server_count > 1, on force le cap à 1(serve.py:131-137) avec un warning, l'EP élastique ne supportant pour l'instant qu'un seul API server. - Rust frontend ne supporte pas le multi API server : si
VLLM_RUST_FRONTEND_PATH+api_server_count > 1, on ignore et on remet à 1(serve.py:123-128) — le frontend Rust est multi-thread, pas multi-processus. - VLLM_ALLOW_RUNTIME_LORA_UPDATING + multi API server :
run_multi_api_serverdétecte la combinaison et lève une erreur(serve.py:293-296), car les mises à jour LoRA runtime ne peuvent pas se synchroniser entre plusieurs processus API server indépendants. - Validation des plugins tool_parser / reasoning_parser :
validate_api_server_args(api_server.py:536-551) vérifie que--tool-call-parser/reasoning_parserfigurent dans la liste enregistrée, sinonKeyError. - Bind du socket avant le moteur :
setup_servers'exécute avantbuild_async_engine_client(api_server.py:697-698), pour éviter la concurrence de port avec Ray (un commentaire renvoie à l'issue #8204). - SIGTERM pendant l'initialisation :
run_serverinstalle manuellementSIGTERM → KeyboardInterruptavant qu'uvicorn ne pose son propre signal handler(api_server.py:690-695) ; ainsi un SIGTERM reçu pendant l'initialisation du moteur permet de quitter immédiatement au lieu de rester bloqué.
Résumé
Le CLI vllm est un dispatcher très fin : main() utilise l'abstraction CLISubcommand pour enregistrer toutes les sous-commandes (serve / launch / openai / run-batch / bench / collect-env) comme subparsers, et cmd_init() offre à chaque module une fonction factory. La sous-commande la plus importante et la plus appelée, vllm serve, dans ServeSubcommand.cmd, aiguille vers quatre modes selon api_server_count et le mode LB ; le plus courant tombe sur run_server → build_async_engine_client → AsyncLLM.from_vllm_config, qui transforme réellement EngineArgs en moteur asynchrone vivant. La couche CLI ne touche ni aux poids ni à la passe avant ; elle ne fait que traduire argv proprement et choisir le bon mode d'exécution. Pour la façon dont EngineArgs assemble des centaines de champs en VllmConfig, voir /startup/engine-args ; pour la manière dont AsyncLLM, une fois démarré, achemine les requêtes vers EngineCore, voir /startup/llm-class et /engine/engine-core.
Voir la documentation officielle : Documentation vLLM · README