entrypoints/cli/serve.py: the vllm serve subcommand and its launch modes
Code version: vLLM @ 3e44c0d · 2026-10-22
Purpose#
vllm/entrypoints/cli/serve.py turns vllm serve ... into running processes: it decides how many API servers to start and which function starts them.
| File | vllm/entrypoints/cli/serve.py, 407 lines of Python. One class and three functions. |
|---|---|
| Main class | ServeSubcommand(CLISubcommand) (vllm/entrypoints/cli/serve.py:48-174). |
| Main functions | cmd_init (vllm/entrypoints/cli/serve.py:177-178), run_headless (vllm/entrypoints/cli/serve.py:181-266), run_multi_api_server (vllm/entrypoints/cli/serve.py:269-407). |
| Runs in | The CLI process, the first process of vllm serve, on the main thread. With one API server, this process then becomes the API server. |
| Called by | vllm/entrypoints/cli/main.py: it collects the subcommands, parses sys.argv, validates and dispatches (vllm/entrypoints/cli/main.py:100-117). |
| Calls | make_arg_parser, validate_parsed_serve_args, run_server, run_dp_supervisor, serve_grpc, CoreEngineProcManager, launch_core_engines, APIServerProcessManager, RustFrontendProcessManager, wait_for_completion_or_failure. |
| Not in this file | The flags themselves (FrontendArgs and EngineArgs), the HTTP server (launchers/api_server/entry.py, launchers/launcher.py), the engine start (v1/engine/utils.py), the API server processes (v1/utils.py). |
The page The OpenAI-compatible server: launch modes, app, serving classes and streaming explains what each launch mode serves. This page reads the file line by line.
Where it sits#
Who calls it#
cli/main.pyimports this module at start-up (vllm/entrypoints/cli/main.py:43-51) and callscmd_init()for the subcommand object (vllm/entrypoints/cli/main.py:100-109).subparser_initis called for everyvllmcommand, not only forserve, because all subparsers are built before parsing (same lines). Building the serve parser loads the general plugins, throughmake_arg_parserandAsyncEngineArgs.add_cli_args(How vLLM finds and loads plugins).- After parsing,
maincallsvalidate(args)and thenargs.dispatch_function(args), which isServeSubcommand.cmd(vllm/entrypoints/cli/main.py:111-117).
What it calls#
make_arg_parser: the positionalmodel_tag,--headless,--api-server-count,--config,--grpc, then allFrontendArgsandAsyncEngineArgsflags (vllm/entrypoints/common/cli_args.py:422-468).validate_parsed_serve_args: early checks that need no model (vllm/entrypoints/common/cli_args.py:503-549).- One of the five starters, by mode (
vllm/entrypoints/cli/serve.py:147-156).
Key classes and functions#
| Name | Line | What it does |
|---|---|---|
DESCRIPTION | vllm/entrypoints/cli/serve.py:39-45 | Help text; the default model is Qwen/Qwen3-0.6B (vllm/config/model.py:155). |
ServeSubcommand.cmd | vllm/entrypoints/cli/serve.py:54-156 | Resolves the mode and the API server count, then starts. |
ServeSubcommand.validate | vllm/entrypoints/cli/serve.py:158-159 | Calls validate_parsed_serve_args. |
ServeSubcommand.subparser_init | vllm/entrypoints/cli/serve.py:161-174 | Adds the serve subparser with all flags. |
cmd_init | vllm/entrypoints/cli/serve.py:177-178 | Returns [ServeSubcommand()] for cli/main.py. |
run_headless | vllm/entrypoints/cli/serve.py:181-266 | Starts engine cores (or workers of a multi-node rank) with no API server. |
run_multi_api_server | vllm/entrypoints/cli/serve.py:269-407 | Starts the engines and N API server processes (or the Rust frontend), then supervises them. |
Walk through the main methods#
cmd, part 1: model, gRPC, headless#
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.launchers.grpc_server import serve_grpc
uvloop.run(serve_grpc(args))
return
rust_frontend_path = (
envs.VLLM_RUST_FRONTEND_PATH if envs.VLLM_USE_RUST_FRONTEND else None
)
if args.headless:
if args.api_server_count is not None and args.api_server_count > 0:
raise ValueError(
f"--api-server-count={args.api_server_count} cannot be "
"used with --headless (no API servers are started in "
"headless mode)."
)
# Default to 0 in headless mode (no API servers)
args.api_server_count = 0- Lines 56-57. The positional model (
vllm serve MODEL) wins over--model. - Lines 59-63.
--grpcruns the gRPC server and returns; none of the checks below apply to it. - Lines 65-67. The Rust frontend path is set only when
VLLM_USE_RUST_FRONTENDis on. - Lines 69-77.
--headlessforces the API server count to 0; an explicit positive count with it raisesValueError.
cmd, part 2: the data-parallel load-balancing mode#
# Detect LB mode for defaulting api_server_count.
# Multi-port: --data-parallel-multi-port-external-lb
# External LB: --data-parallel-external-lb or --data-parallel-rank
# Hybrid LB: --data-parallel-hybrid-lb or --data-parallel-start-rank
is_external_lb = (
args.data_parallel_external_lb or args.data_parallel_rank is not None
)
# If --data_parallel_multi_port_external_lb and --data_parallel_hybrid_lb
# are unset, default to hybrid if --data-parallel-start-rank is set
is_hybrid_lb = is_multi_port = False
if (
not args.data_parallel_hybrid_lb
and not args.data_parallel_multi_port_external_lb
):
is_hybrid_lb = args.data_parallel_start_rank is not None
else:
is_hybrid_lb = args.data_parallel_hybrid_lb
is_multi_port = args.data_parallel_multi_port_external_lb
if sum([is_multi_port, is_external_lb, is_hybrid_lb]) > 1:
raise ValueError(
"Cannot use more than one data parallel load balancing mode. "
"Choose one of: --data-parallel-multi-port-external-lb, "
"--data-parallel-external-lb (or --data-parallel-rank), "
"--data-parallel-hybrid-lb (or --data-parallel-start-rank)."
)- Lines 83-85. External load balancing:
--data-parallel-external-lb, or any--data-parallel-rank. - Lines 90-97. Hybrid load balancing:
--data-parallel-hybrid-lb, or--data-parallel-start-rankwhen neither hybrid nor multi-port is set. - Lines 99-105. Two modes at once raise
ValueError.
cmd, part 3: the API server count and the dispatch#
if args.api_server_count is None:
if is_multi_port or is_external_lb or rust_frontend_path:
args.api_server_count = 1
elif is_hybrid_lb:
args.api_server_count = args.data_parallel_size_local or 1
if args.api_server_count > 1:
logger.info(
"Defaulting api_server_count to data_parallel_size_local "
"(%d) for hybrid LB mode.",
args.api_server_count,
)
else:
args.api_server_count = args.data_parallel_size
if args.api_server_count > 1:
logger.info(
"Defaulting api_server_count to data_parallel_size (%d).",
args.api_server_count,
)
elif rust_frontend_path and args.api_server_count > 1:
logger.warning(
"Ignoring --api-server-count=%d when using rust front-end process",
args.api_server_count,
)
args.api_server_count = 1
# Elastic EP currently only supports running with at most one API server.
if getattr(args, "enable_elastic_ep", False) and args.api_server_count > 1:
logger.warning(
"Elastic EP only supports running with with at most one API server. "
"Capping api_server_count from %d to 1.",
args.api_server_count,
)
args.api_server_count = 1
if is_multi_port:
run_dp_supervisor(args)
elif args.api_server_count < 1:
run_headless(args)
elif args.api_server_count > 1 or rust_frontend_path:
run_multi_api_server(args)
else:
# Single API server (this process).
args.api_server_count = None
uvloop.run(run_server(args))- Lines 114-115. Multi-port, external load balancing and the Rust frontend default to 1 API server.
- Lines 116-117. Hybrid: the local DP size.
- Lines 124-125. Otherwise (internal load balancing, the normal case): the full DP size. So
-dp 4on one node gives 4 API servers. - Lines 131-136. The Rust frontend ignores an explicit count above 1, with a warning.
- Lines 139-145. Elastic EP caps the count at 1.
- Lines 147-156. The dispatch. Only the last branch keeps the server in this process; it sets the count back to
Nonefirst.
run_headless#
def run_headless(args: argparse.Namespace):
if args.api_server_count > 1:
raise ValueError("api_server_count can't be set in headless mode")
if args.reasoning_parser_plugin and len(args.reasoning_parser_plugin) > 3:
ReasoningParserManager.import_reasoning_parser(args.reasoning_parser_plugin)
# Create the EngineConfig.
engine_args = vllm.AsyncEngineArgs.from_cli_args(args)
propagate_flash_late_interaction(args, engine_args)
usage_context = UsageContext.OPENAI_API_SERVER
vllm_config = engine_args.create_engine_config(
usage_context=usage_context, headless=True
)
if engine_args.data_parallel_hybrid_lb:
raise ValueError("data_parallel_hybrid_lb is not applicable in headless mode")
parallel_config = vllm_config.parallel_config
local_engine_count = parallel_config.data_parallel_size_local
if local_engine_count <= 0:
raise ValueError("data_parallel_size_local must be > 0 in headless mode")
shutdown_requested = False
# Catch SIGTERM and SIGINT to allow graceful shutdown.
def signal_handler(signum, frame):
nonlocal shutdown_requested
logger.debug("Received %d signal.", signum)
if not shutdown_requested:
shutdown_requested = True
raise SystemExit
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
if parallel_config.node_rank_within_dp > 0:
from vllm.version import __version__ as VLLM_VERSION
# Run headless workers (for multi-node PP/TP).
host = parallel_config.master_addr
head_node_address = f"{host}:{parallel_config.master_port}"
logger.info(
"Launching vLLM (v%s) headless multiproc executor, "
"with head node address %s for torch.distributed process group.",
VLLM_VERSION,
head_node_address,
)
executor = MultiprocExecutor(vllm_config, monitor_workers=False)
executor.start_worker_monitor(inline=True)
return- Lines 185-186. A reasoning parser plugin is imported here, because the engine cores need the reasoning parser for structured outputs (Structured outputs: JSON schemas, grammars and token bitmasks in vLLM).
- Lines 189-194. The config is made with
headless=Trueand the API server usage context. - Lines 202-203. A node with no local DP ranks cannot run headless.
- Lines 218-233. On a secondary node of one DP rank (multi-node TP or PP), this process starts only the workers of its node, through
MultiprocExecutor, and blocks in the worker monitor.
Otherwise it starts the local engine cores with CoreEngineProcManager, which connect to the head node's handshake address, and waits on their liveness (vllm/entrypoints/cli/serve.py:235-266). SIGTERM and SIGINT raise SystemExit once, and the shutdown then waits up to shutdown_timeout seconds (lines 205-216 and 260-266).
run_multi_api_server#
if num_api_servers > 1:
setup_multiprocess_prometheus()
shutdown_requested = False
# Catch SIGTERM and SIGINT to allow graceful shutdown.
def signal_handler(signum, frame):
nonlocal shutdown_requested
logger.debug("Received %d signal.", signum)
if not shutdown_requested:
shutdown_requested = True
raise SystemExit
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
listen_address, sock = setup_server(args, reuse_port=num_api_servers > 1)
# `--grpc-port` is only accepted for the Rust frontend, which inherits this
# listener like the HTTP one. gRPC follows the HTTP TCP host, or IPv4
# loopback when HTTP uses a Unix socket.
grpc_sock = None
if args.grpc_port is not None:
grpc_host = "127.0.0.1" if args.uds else (args.host or "")
grpc_sock = create_server_socket((grpc_host, args.grpc_port), reuse_port=False)
engine_args = vllm.AsyncEngineArgs.from_cli_args(args)
propagate_flash_late_interaction(args, engine_args)
engine_args._api_process_count = num_api_servers
engine_args._api_process_rank = -1
usage_context = UsageContext.OPENAI_API_SERVER
vllm_config = engine_args.create_engine_config(usage_context=usage_context)
if num_api_servers > 1 and envs.VLLM_ALLOW_RUNTIME_LORA_UPDATING:
raise ValueError(
"VLLM_ALLOW_RUNTIME_LORA_UPDATING cannot be used with api_server_count > 1"
)- Lines 282-283. More than one API server needs Prometheus multiprocess mode; a temporary directory is made unless the user set
PROMETHEUS_MULTIPROC_DIR(vllm/v1/metrics/prometheus.py:17-36). - Line 298. One listen socket, with
SO_REUSEPORTwhen there are several servers (vllm/entrypoints/launchers/launcher.py:340-355). - Lines 307-310. The engine config records the API process count; rank
-1marks the parent. - Line 313. The
VllmConfigis made here, in the supervisor, once. - Lines 315-318. Runtime LoRA updates are refused with several API servers.
from vllm.v1.engine.utils import bind_engine_zmq_listeners
zmq_listeners = bind_engine_zmq_listeners(vllm_config, num_api_servers)
addresses = zmq_listeners.addresses
with launch_core_engines(
vllm_config, executor_class, log_stats, addresses
) as engine_launch:
local_engine_manager = engine_launch.engine_manager
coordinator = engine_launch.coordinator
addresses = engine_launch.addresses
stats_update_address = (
coordinator.get_stats_publish_address() if coordinator else None
)
if rust_frontend_path:
if parallel_config.local_engines_only:
expected_engine_start_index = parallel_config.data_parallel_rank
expected_engine_count = parallel_config.data_parallel_size_local
else:
expected_engine_start_index = 0
expected_engine_count = parallel_config.data_parallel_size
# Start rust front-end process.
api_server_manager = RustFrontendProcessManager(
binary_path=rust_frontend_path,
sock=sock,
grpc_sock=grpc_sock,
args=args,
input_listener=zmq_listeners.inputs[0],
output_listener=zmq_listeners.outputs[0],
engine_start_index=expected_engine_start_index,
engine_count=expected_engine_count,
data_parallel_size=parallel_config.data_parallel_size,
stats_update_address=stats_update_address,
)
else:
# Start API server(s).
api_server_manager = APIServerProcessManager(
listen_address=listen_address,
sock=sock,
args=args,
num_servers=num_api_servers,
input_listeners=zmq_listeners.inputs,
output_listeners=zmq_listeners.outputs,
stats_update_address=stats_update_address,
tensor_queue=engine_launch.tensor_queue,
)
# Set frontend processes to watch during engine startup.
# If any of these processes exit before the engines are up, the engine startup
# will be aborted with an error.
engine_launch.watched_frontend_processes = api_server_manager.processes
# Wait for API servers.
try:
wait_for_completion_or_failure(
api_server_manager=api_server_manager,
engine_manager=local_engine_manager,
coordinator=coordinator,
)- Line 333. One input and one output ZMQ listener per API server, bound here.
- Lines 336-338.
launch_core_enginesis a context manager: it starts the engine cores (and the DP coordinator if needed) on entry, and waits for their handshake on exit (vllm/v1/engine/utils.py:1384-1395). - Line 354 / lines 368-377. Inside the block, the Rust frontend or the Python API servers start. So the API servers start while the engines load.
- Line 382. If an API server dies during the engine start-up, the start-up is aborted.
- Lines 386-390. The supervisor waits; any child that exits with a non-zero code raises
RuntimeError(vllm/v1/utils.py:575-594).
The finally block shuts down the API servers, then the engines, then the coordinator, all within one shutdown_timeout deadline after a signal (vllm/entrypoints/cli/serve.py:391-407).
Deep dive#
The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.
Deep diveWorked example: vllm serve M -dp 2 on one node
A dense model, one node, two GPUs, -dp 2, no other DP flags.
cmd: no gRPC, no Rust, not headless.is_external_lb,is_hybrid_lbandis_multi_portare all false (lines 83-97).api_server_countisNone, so it becomes the DP size, 2, and the log says "Defaulting api_server_count to data_parallel_size (2)." (lines 124-130).- 2 > 1, so
run_multi_api_serverruns (line 152). - The supervisor makes the Prometheus directory, binds port 8000 with
SO_REUSEPORT, and builds theVllmConfigwith_api_process_count = 2(lines 282-313). bind_engine_zmq_listeners(vllm_config, 2)binds two input and two output listeners (line 333).launch_core_enginesstarts 2 engine core processes and, because this is internal load balancing, the DP coordinator (vllm/v1/engine/utils.py:1226-1232). Each engine core starts one worker process (TP = 1 with DP > 1).APIServerProcessManagerspawnsApiServer_0andApiServer_1, each with its listener pair andclient_index0 or 1 (vllm/v1/utils.py:230-254).- On leaving the
withblock, the supervisor waits for both engines' ready messages, while it watches the two API server processes (line 382). - Each API server builds its own AsyncLLM, which connects to both engines, and then serves HTTP.
Result: 8 processes: the supervisor, 2 API servers, the DP coordinator, 2 engine cores and 2 workers. This matches the count in The process and thread architecture of vllm serve.
Deep diveProcesses and threads
| Mode | What the CLI process does | Children it starts |
|---|---|---|
| one API server | runs the asyncio loop of the API server (uvloop.run) | engine core processes, through AsyncLLM (vllm/entrypoints/launchers/api_server/entry.py:93-103) |
| several API servers | supervisor: waits on process sentinels (vllm/v1/utils.py:550-594) | engine cores, DP coordinator, ApiServer_i (spawn) |
| Rust frontend | supervisor | engine cores, the Rust binary |
| headless | engine manager: waits on engine liveness | local engine cores, or workers of a multi-node rank |
| multi-port external LB | DPSupervisor with its own aggregated health server (vllm/entrypoints/launchers/dp_supervisor.py:562-564) | one full server per local DP rank |
| gRPC | runs the gRPC server with AsyncLLM (vllm/entrypoints/launchers/grpc_server.py:56-85) | engine core processes |
The API server processes always use the spawn start method (vllm/v1/utils.py:204).
Deep diveError paths
| Situation | Result | Lines |
|---|---|---|
--headless with --api-server-count > 0 | ValueError | vllm/entrypoints/cli/serve.py:69-75 |
| two DP load-balancing modes | ValueError | vllm/entrypoints/cli/serve.py:99-105 |
| headless with hybrid load balancing | ValueError | vllm/entrypoints/cli/serve.py:196-197 |
| headless with no local DP ranks | ValueError | vllm/entrypoints/cli/serve.py:202-203 |
| Rust frontend with more than one API server | ValueError (only if reached; cmd caps it first) | vllm/entrypoints/cli/serve.py:277-280 |
VLLM_ALLOW_RUNTIME_LORA_UPDATING with several API servers | ValueError | vllm/entrypoints/cli/serve.py:315-318 |
--enable-auto-tool-choice without a parser | TypeError in validate | vllm/entrypoints/common/cli_args.py:523-525 |
| a child process dies | RuntimeError "Process ... died with exit code ..." | vllm/v1/utils.py:584-589 |
Deep diveEnvironment variables
| Variable | Default | Read at | Effect |
|---|---|---|---|
VLLM_USE_RUST_FRONTEND | off (vllm/envs.py:1454-1456) | vllm/entrypoints/cli/serve.py:65-67 | Use the Rust frontend binary instead of Python API servers. |
VLLM_RUST_FRONTEND_PATH | auto (vllm/envs.py:593) | same | Path of the Rust binary; auto means the vllm-rs file in the package folder (vllm/envs.py:604-615). |
VLLM_ALLOW_RUNTIME_LORA_UPDATING | off (vllm/envs.py:1197-1200) | vllm/entrypoints/cli/serve.py:315 | Refused with several API servers. |
PROMETHEUS_MULTIPROC_DIR | unset | vllm/v1/metrics/prometheus.py:21 | Kept if you set it (with a warning); else a temporary directory. |
Related concepts#
- The OpenAI-compatible server: launch modes, app, serving classes and streaming: what the started API server does.
- The process and thread architecture of vllm serve: the processes and the ZMQ links between them.
- Data parallel serving: several engine cores behind one endpoint: the load-balancing modes in depth.
- How vLLM finds and loads plugins: why building the parser loads the general plugins.
- Tour: Start-up of vllm serve: the full start-up of
vllm serve.
Check yourself#
- 1Witheasy
vllm serve Mand no DP flags, where does the HTTP server run?Show answer
In the CLI process itself:
api_server_countdefaults to the DP size, 1, so the last branch runsuvloop.run(run_server(args))(vllm/entrypoints/cli/serve.py:124-125,vllm/entrypoints/cli/serve.py:153-156). - 2What is the default number of API servers formedium
-dp 4 --data-parallel-external-lb, and why?Show answer
1. With an external load balancer, each
vllm serveserves its own rank, so the default is one API server (vllm/entrypoints/cli/serve.py:83-85,vllm/entrypoints/cli/serve.py:114-115). - 3Inmedium
run_multi_api_server, do the API server processes start before or after the engines are ready?Show answer
Before. They start inside the
with launch_core_engines(...)block, and the wait for the engines' ready messages runs when the block exits (vllm/entrypoints/cli/serve.py:336-382,vllm/v1/engine/utils.py:1384-1395). - 4On a second node you runhard
vllm serve M --headless --data-parallel-size 4 --data-parallel-size-local 2 --data-parallel-start-rank 2 --data-parallel-address HEAD(TP = 1). What does this process start, and what does it not start?Show answer
--headlesssets the API server count to 0 before the load-balancing checks, so the dispatch runsrun_headless(vllm/entrypoints/cli/serve.py:69-77,vllm/entrypoints/cli/serve.py:149-150). The start rank makes the local variableis_hybrid_lbtrue, but the count is already set, andrun_headlesschecks only the explicit--data-parallel-hybrid-lbflag (vllm/entrypoints/cli/serve.py:90-94,vllm/entrypoints/cli/serve.py:196-197). With TP = 1 this node is the first node of its DP ranks, so it starts the two local engine cores withCoreEngineProcManager; they connect to the head's handshake address (vllm/entrypoints/cli/serve.py:235-256). It starts no API server and opens no HTTP socket.