vLLM Explained

Deployment recipes: single GPU, TP, DP, multi-node, load balancers and security

AdvancedvLLM conceptsabout 15 min read + 3 min deep dives

Code version: vLLM @ 3e44c0d · 2026-10-22

In one sentence

Each deployment shape is one vllm serve command per node; the flags decide which processes start, which port answers, and what is protected.

The idea#

vllm serve has one command line, but it starts different sets of processes. The API server can run inside the CLI process or in several child processes. The EngineCore can hold the worker itself or drive one worker process per GPU. A node can also run only workers or only engine cores, with no HTTP port at all (--headless). The page The process and thread architecture of vllm serve explains the processes; this page gives the command for each common shape and what it starts.

The second half of the page is about production. It shows how a load balancer sees a server (/health), how to start and stop it, and which routes need a proxy. --api-key alone does not protect a vLLM server.

An everyday comparison. Think of a kitchen. One cook can do everything at one stove. A large dish can need several cooks at one stove (tensor parallelism). A busy restaurant opens more stoves, each with its own cooks (data parallelism), and a host at the door spreads the guests (the load balancer). The comparison stops at the door. vLLM's lock (the API key) closes only some of the entrances, so the building needs its own guard (a proxy).

The picture#

Figure: four shapes as processes (panels 1 and 2), and two independent servers behind a load balancer (panel 3). The process counts are derived from the launch code.
Open full size ↗

Read each shape from its command down. Blue boxes serve HTTP (or supervise), violet boxes schedule, green boxes own GPUs. The counts help you size the CPU: each box is at least one busy process.

In the code#

Step 1: how vllm serve picks the shape#

After the flags are parsed, ServeSubcommand.cmd chooses one of four launchers (entrypoints/cli/serve.py: the vllm serve subcommand and its launch modes):

        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))

The engine side follows the parallel sizes. The worker runs inside the engine core (the uni executor) only when TP, PP and DP are all 1 (vllm/config/parallel.py:1055, vllm/config/parallel.py:1099-1100). With any larger size, each engine core starts one worker process per GPU (Executors and workers: from the engine core to the GPUs).

Step 2: one GPU, and tensor parallelism on one node#

# Shape A: one GPU (2 processes)
vllm serve Qwen/Qwen3-8B
# Shape B: one model over 2 GPUs of one node (4 processes)
CUDA_VISIBLE_DEVICES=0,1 vllm serve Qwen/Qwen3-8B --tensor-parallel-size 2

-tp and -pp default to 1 (vllm/config/parallel.py:142, vllm/config/parallel.py:140). With TP = 2, the engine core uses the multiprocessing executor and two worker processes, Worker_TP0 and Worker_TP1 (Executors and workers: from the engine core to the GPUs). The attention heads must divide by the TP size (Tensor and pipeline parallelism in practice).

Step 3: data parallelism on one node, and DP + EP for MoE models#

# Shape C: two independent engine copies behind one port (8 processes)
vllm serve Qwen/Qwen3-8B --data-parallel-size 2
# More front-end processes for the same engines
vllm serve Qwen/Qwen3-8B --data-parallel-size 2 --api-server-count 4
# A MoE model: 8 DP ranks, experts spread over all 8 GPUs
vllm serve Qwen/Qwen3-30B-A3B --data-parallel-size 8 --enable-expert-parallel

With -dp N on one node, the CLI process becomes a supervisor. It starts N API servers on one port, a DP coordinator, and N engine cores (Data parallel serving: several engine cores behind one endpoint). For a dense model, the engine cores are independent: each rank is set up like DP = 1 (vllm/v1/engine/core.py:1534-1543). For a MoE model, the ranks are DPEngineCoreProcs that step together in waves. --enable-expert-parallel (off by default, vllm/config/parallel.py:190) shards the experts over the ranks instead of over the TP group (Expert parallelism and MoE serving). The DP coordinator runs for MoE models, and for dense models with internal or hybrid load balancing (vllm/config/vllm.py:854-874).

Step 4: one model over several nodes#

A model that does not fit one node needs TP or PP across nodes. With the default multiprocessing backend, run one command per node. The vLLM docs give this example for 2 nodes with 8 GPUs each (docs/serving/parallelism_scaling.md:143-159):

# node 0 (the head)
vllm serve /path/to/model --tensor-parallel-size 8 --pipeline-parallel-size 2 \
  --nnodes 2 --node-rank 0 --master-addr <HEAD_NODE_IP>
# node 1
vllm serve /path/to/model --tensor-parallel-size 8 --pipeline-parallel-size 2 \
  --nnodes 2 --node-rank 1 --master-addr <HEAD_NODE_IP> --headless

--nnodes, --node-rank and --master-addr are parallel flags (vllm/engine/arg_utils.py:1165-1168). --nnodes must divide the total world size, and it needs the mp DP backend (vllm/engine/arg_utils.py:2379-2402). On node 1, run_headless sees that this node is not the first node of its DP group. Then it only starts a MultiprocExecutor for the local workers, with no engine core and no API server (vllm/entrypoints/cli/serve.py:218-231). The alternative is a Ray cluster with --distributed-executor-backend ray and one command on the head node (docs/serving/parallelism_scaling.md:122-130).

Step 5: data parallelism over several nodes#

To run DP ranks on several nodes with one HTTP endpoint, start the head node with the API servers and the other nodes with --headless. The vLLM docs give this example for DP = 4 over 2 nodes (docs/serving/data_parallel_deployment.md:40-49):

# node 0, address 10.99.48.128: API servers + DP ranks 0 and 1
vllm serve $MODEL --data-parallel-size 4 --data-parallel-size-local 2 \
  --data-parallel-address 10.99.48.128 --data-parallel-rpc-port 13345
# node 1: DP ranks 2 and 3, no API server
vllm serve $MODEL --headless --data-parallel-size 4 --data-parallel-size-local 2 \
  --data-parallel-start-rank 2 \
  --data-parallel-address 10.99.48.128 --data-parallel-rpc-port 13345
Flag (short form)MeaningWhere
--data-parallel-size (-dp)DP ranks in totalvllm/engine/arg_utils.py:1230-1232
--data-parallel-size-local (-dpl)DP ranks on this nodevllm/engine/arg_utils.py:1248-1253
--data-parallel-start-rank (-dpr)first rank of this node; without --headless it turns on hybrid load balancingvllm/engine/arg_utils.py:1242-1247, vllm/engine/arg_utils.py:2478-2480
--data-parallel-address (-dpa), --data-parallel-rpc-port (-dpp)the head node and its fixed RPC port, the same on all nodesvllm/engine/arg_utils.py:1254-1266
--data-parallel-backend (-dpb)mp (default) or rayvllm/engine/arg_utils.py:1267-1273
--data-parallel-hybrid-lb (-dph)each node runs its own API servers for its own ranksvllm/engine/arg_utils.py:1274-1278
--data-parallel-external-lb (-dpe), --data-parallel-rank (-dpn)one server per DP rank behind your own router; MoE models onlyvllm/engine/arg_utils.py:1233-1241, vllm/engine/arg_utils.py:1279-1283

The head node runs the API servers (by default one per DP rank, so 4 here), the DP coordinator, and its local engine cores (vllm/entrypoints/cli/serve.py:124-130). On node 1, run_headless starts only the local engine cores, which connect to the head address (vllm/entrypoints/cli/serve.py:235-256).

Step 6: disaggregated prefill and decode#

Disaggregated serving runs prefill and decode on separate vLLM instances and moves the KV cache with a KV connector. Each instance is one of the shapes above, with --kv-transfer-config (vllm/engine/arg_utils.py:1870-1872) and a router in front. The vLLM docs list the reasons, for example control of tail ITL (docs/features/disagg_prefill.md:8-13). The page Disaggregated prefill: prefill and decode on different instances explains the request flow, and KV connectors the connectors.

Step 7: behind a load balancer: independent servers, /health and readiness#

For a dense model, the simplest scale-out is a set of independent servers behind any HTTP load balancer. vLLM refuses the external DP flags for dense models and says why:

        if (
            self.data_parallel_size > 1
            and data_parallel_external_lb
            and not model_config.is_moe
        ):
            raise ValueError(
                "Non-MoE models do not support external data parallel mode. "
                "For external load balancing, launch independent vLLM "
                "instances without --data-parallel-* arguments."
            )

So run one plain vllm serve per GPU group, each with its own port or host, and no --data-parallel-* flag. For MoE models, external load balancing keeps one DP group: each server gets --data-parallel-size N --data-parallel-rank i and its own port (docs/serving/data_parallel_deployment.md:104-127).

The load balancer needs a health signal:

@router.get("/health", response_class=Response)
async def health(raw_request: Request) -> Response:
    """Health check."""
    client = engine_client(raw_request)
    if client is None:
        # Render-only servers have no engine; they are always healthy.
        return Response(status_code=200)
    try:
        await client.check_health()
        return Response(status_code=200)
    except EngineDeadError:
        return Response(status_code=503)

The readiness probe for vllm serve is GET /health; the vLLM docs use it for both the liveness and the readiness probe in Kubernetes (docs/deployment/k8s.md:244-255). GET /v1/models also answers only when the server is up, and it confirms the served name. The docs warn that a startup probe with a low failureThreshold kills the container during start-up (docs/deployment/k8s.md:436-443). The log then shows "KeyboardInterrupt: terminated". That text comes from the SIGTERM handler that run_server installs during start-up (vllm/entrypoints/launchers/api_server/entry.py:170-175).

Shutdown. --shutdown-timeout N (default 0) lets running requests finish for up to N seconds after a stop signal; 0 aborts them (vllm/engine/arg_utils.py:1930-1935, vllm/config/vllm.py:453-457). Set it a little below the grace period of your orchestrator (a practical rule).

Step 8: Docker, Kubernetes and the recipes folder#

The repository documents containers and orchestrators; this site does not repeat them:

Step 9: security: what --api-key protects#

A secure deployment therefore puts a proxy in front of vLLM (a practical rule that the vLLM docs also give, docs/usage/security.md:280-286):

  1. Let the proxy pass only the routes your clients need, for example /v1/chat/completions and /v1/models. Block /invocations, /ping, /scale_elastic_ep, /generative_scoring and everything else from outside.
  2. Do the authentication and rate limits in the proxy. Keep --api-key as a second check.
  3. Expose /health and /metrics only to your orchestrator and your monitoring.
  4. Bind vLLM to a private address (--host), not to all interfaces.

Other switches that matter for security, all off by default:

SwitchRisk when onWhere
VLLM_SERVER_DEV_MODE=1adds development routes such as /sleep, /reset_prefix_cache, /collective_rpc and /server_info; the server logs "SECURITY WARNING"vllm/entrypoints/launchers/api_server/routers.py:46-49, vllm/entrypoints/serve/__init__.py:43-47
--trust-remote-coderuns Python code from the model repositoryvllm/config/model.py:197-199
--trust-request-mm-kwargslets clients change multimodal processing (CPU, memory)vllm/entrypoints/common/cli_args.py:98-102
--trust-request-chat-templatelets clients send their own chat templatevllm/entrypoints/common/cli_args.py:94-97
--allowed-local-media-pathrequests can read files from that server foldervllm/config/model.py:231-234
VLLM_ALLOW_RUNTIME_LORA_UPDATING=1clients can load LoRA adapters through HTTPvllm/envs.py:1197-1200

Media URLs are fetched by the server. Limit them with --allowed-media-domains (vllm/config/model.py:235-237) and VLLM_MEDIA_URL_ALLOW_REDIRECTS=0 (default 1, vllm/envs.py:992-994); a single download is limited to VLLM_MAX_MEDIA_DOWNLOAD_SIZE_MB, default 256 (vllm/envs.py:987-989). VLLM_MAX_N_SEQUENCES (default 16384) caps n per request (vllm/envs.py:1140-1142). Traffic between nodes (PyTorch distributed, KV transfer, DP) is not encrypted; the docs require an isolated network for it (docs/usage/security.md:3-10).

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 dive

Process counts per shape (derived)

CommandProcessesCount
vllm serve MCLI = API server; engine core with the worker inside2
vllm serve M -tp 2API server; engine core; 2 workers4
vllm serve M -tp 4API server; engine core; 4 workers6
vllm serve M -dp 2 (dense)CLI supervisor; 2 API servers; coordinator; 2 engine cores; 2 workers8
vllm serve M -dp 2 -tp 2 (dense)CLI supervisor; 2 API servers; coordinator; 2 engine cores; 4 workers10
vllm serve M -dp 2 --api-server-count 4CLI supervisor; 4 API servers; coordinator; 2 engine cores; 2 workers10
Step 4, node 0 / node 1API server; engine core; 8 workers / CLI executor monitor; 8 workers10 / 9
Step 5, node 0 / node 1CLI supervisor; 4 API servers; coordinator; 2 engine cores; 2 workers / CLI; 2 engine cores; 2 workers10 / 5

Sources: the launchers (vllm/entrypoints/cli/serve.py:147-156), the executor choice (vllm/config/parallel.py:1055, vllm/config/parallel.py:1099-1100), the coordinator (vllm/config/vllm.py:854-874, vllm/v1/engine/coordinator.py:101-115). The counts leave out helper processes. One is the model-registry inspection subprocess on a cache miss. Another is the resource tracker process that Python's spawn start method can add (general Python knowledge). The vLLM docs count "2 + N" processes for N GPUs and 17 for DP = 4, TP = 2 (docs/configuration/optimization.md:432-436, docs/configuration/optimization.md:456-457). The code gives 2 for one GPU, because the worker runs in the engine core. For DP = 4, TP = 2 it gives 18: the docs leave out the CLI supervisor.

Deep dive

Worked example: from one GPU to two nodes

A team serves an 8B dense model. The steps below are advice; each one names the flag and the shape.

  1. One GPU (shape A). Check the start-up log for the maximum concurrency (Tuning vLLM: throughput and latency knobs, their real defaults and what each one does). Put a proxy in front (Step 9).
  2. More traffic, one node with 4 GPUs. The model fits one GPU. So use -dp 4 for four copies behind one port (Step 3), or four independent servers behind your own load balancer (Step 7). -dp 4 gives 4 API servers by default; the log line then stops, so use /metrics (Metrics and observability: how to watch a vLLM server).
  3. Two nodes. Either one server per node behind an external load balancer, or one DP group over both nodes with --headless on node 1 (Step 5). For a dense model, the first is simpler: a failure stays on one node.
  4. A model that does not fit one node: TP inside the node, PP or TP across nodes (Step 4), on a fast and isolated network.
Deep dive

What each process of a shape logs

Each process puts a prefix on its log lines, so you can tell them apart in one container log. The prefixes are (APIServer pid=...), (EngineCore pid=...) (EngineCore_DP0 and so on with DP) and Worker_TP0 and so on for the workers (vllm/entrypoints/launchers/api_server/entry.py:166, vllm/v1/engine/core.py:1467-1472, vllm/v1/engine/utils.py:284). A worker name gets a _DP, _PP or _TP part only when that size is above 1 (vllm/v1/executor/multiproc_executor.py:1101-1112). With several API servers, they are named ApiServer_0, ApiServer_1, ... (vllm/v1/utils.py:248-254). The page Tour: Start-up of vllm serve follows the start-up log of shape A line by line.

What can go wrong#

  • "Non-MoE models do not support external data parallel mode." You used --data-parallel-rank or --data-parallel-external-lb with a dense model; start independent servers instead (Step 7).
  • Pods restart during start-up. The probe gives up before the model is loaded and compiled; raise the startup probe threshold (Step 7).
  • A dead engine stays in the pool. /health answers 503, but the load balancer checks only the TCP port. Use an HTTP health check on /health.
  • Text generation without a key. /invocations is open on every server (Step 9).
  • Multi-node hangs at start-up. Wrong network interface or blocked ports; see Troubleshooting vLLM: symptom, cause and fix.
  • Two servers on one GPU fail to start. Each --gpu-memory-utilization is a share of the whole GPU, and the second server finds too little free memory (vllm/v1/worker/utils.py:761-770).

Why it matters. The shape decides the process count, the CPU need, the failure domain and which signals you can read. The security section decides who can use your GPUs. Both are deployment decisions that no tuning flag can fix later.

Check yourself#

  1. 1
    Which paths does --api-key protect?
    easy
    Show answer

    Only paths that start with /v1, /v2, /inference or /cohere (vllm/entrypoints/serve/middleware/authenticate.py:11, vllm/entrypoints/serve/middleware/authenticate.py:59-61). /invocations, /ping, /scale_elastic_ep, /generative_scoring, /health, /metrics and the other routes need no key.

  2. 2
    What does GET /health answer after the engine core process has died?
    easy
    Show answer

    503, because check_health() raises EngineDeadError (vllm/entrypoints/serve/instrumentator/health.py:29-33, vllm/v1/engine/async_llm.py:1133-1134). Soon after, the watchdog stops the server unless VLLM_KEEP_ALIVE_ON_ENGINE_DEATH is set (vllm/entrypoints/launchers/launcher.py:325-337).

  3. 3
    You start vllm serve M -tp 8 -pp 2 --nnodes 2 --node-rank 1 --master-addr X --headless on the second node. Which processes run there?
    medium
    Show answer

    run_headless sees that this node is not the first node of its DP group. It starts only a MultiprocExecutor with the local workers: the CLI process plus 8 worker processes, with no engine core and no API server (vllm/entrypoints/cli/serve.py:218-231).

  4. 4
    Why does vllm serve M -dp 2 print no "Avg prompt throughput" log line, while vllm serve M does?
    medium
    Show answer

    -dp 2 defaults to 2 API servers (vllm/entrypoints/cli/serve.py:124-130), and with more than one API server the log-line logger is not added (vllm/v1/metrics/loggers.py:1384-1389). Prometheus still works.

  5. 5
    You want four independent copies of a dense 8B model on one 4-GPU node, behind your company's load balancer, with one port each. Which commands do you use, and why not --data-parallel-external-lb?
    hard
    Show answer

    Four plain commands, for example CUDA_VISIBLE_DEVICES=i vllm serve M --port 800i for i = 0 to 3, each without --data-parallel-* flags. The external DP mode raises ValueError for non-MoE models, because dense DP ranks are independent anyway (vllm/engine/arg_utils.py:2445-2454, vllm/v1/engine/core.py:1538-1543). Each server is shape A with 2 processes; the load balancer probes each /health.