vLLM Explained

Data parallel serving: several engine cores behind one endpoint

AdvancedvLLM conceptsabout 10 min read + 7 min deep dives

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

In one sentence

Data parallelism runs several full engine cores; the API servers pick one per request, and a coordinator shares the load counts.

The idea#

Data parallelism runs several copies of the model, and each copy serves other requests (Parallelism basics). In vllm serve, one copy is one engine core process with its own workers, scheduler and KV cache. Each copy is one data parallel rank. With -dp 4 there are four engine cores, but the clients still see one HTTP endpoint.

Three questions follow. Which process decides where a request goes? How does it know the load of each engine? And do the engines need to wait for each other? vLLM answers them like this:

  • The API servers decide. Each one scores every engine and sends the request to the lowest score. This is the default load-balancing mode, internal load balancing.
  • The engines report their queue lengths and KV cache usage to a DP coordinator. The coordinator publishes them to all API servers, by default every 100 ms when they change.
  • The engines of a dense model are independent. The engines of an MoE model must run every forward pass together, because the expert layers exchange tokens across ranks. They step in waves and run dummy batches when they have no work.

An everyday comparison. Think of a supermarket with four checkouts and four entrance doors. A guide at each door sends each customer to the checkout with the shortest line. A screen above the doors shows the line lengths and refreshes every few seconds. Each guide also remembers whom she sent recently, because the screen is a little late. The comparison stops here. For MoE models, the checkouts scan in lockstep, and an idle cashier must "scan" an empty basket while others work.

The picture#

Figure: -dp 4 with a dense model. The processes and their channels (panel 1), and the loop that turns engine counts into a routing decision (panel 2).
Open full size ↗

Read panel 1 from top to bottom. API servers send requests down to any engine core. Outputs come back to the API server that sent the request. The pink dashed path is the slow load loop. Panel 2 shows that the API server combines two sources. The coordinator's published counts can be 100 ms old; its own count of requests in flight is exact.

In the code#

Step 1: one flag, N engine cores and N API servers#

--data-parallel-size (-dp) sets the number of engine cores; the default is 1 (vllm/engine/arg_utils.py:1230-1232, vllm/config/parallel.py:147-149). The engine launcher starts one process per local DP rank, named EngineCore_DP<rank>:


        self.processes: list[BaseProcess] = []
        local_dp_ranks = []
        for index in range(local_engine_count):
            local_index = local_start_index + index
            global_index = start_index + index

            # Start EngineCore in background process.
            local_dp_ranks.append(local_index)
            self.processes.append(
                context.Process(
                    target=EngineCoreProc.run_engine_core,
                    name=f"EngineCore_DP{global_index}" if is_dp else "EngineCore",
                    kwargs=common_kwargs

Each rank gets its own GPUs: local rank r takes devices r × world_size and up. The docstring example is "[2, 3] for local_dp_rank=1" with world_size=2 (vllm/v1/engine/utils.py:441-445). The number of API server processes has its own default, set in the CLI by the load-balancing mode:

        # Default api_server_count if not explicitly set.
        # - Multi-port: 1 (supervisor spawns one server per local DP rank)
        # - Rust frontend: 1 (not applicable as it's multithreaded)
        # - External LB: 1 (external LB handles distribution)
        # - Hybrid LB: Use local DP size (internal LB for local ranks only)
        # - Internal LB: Use full DP size
        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,
                    )

So internal load balancing gets one API server per DP rank. With more than one API server, the CLI process stays alive as a supervisor and serves no HTTP (vllm/entrypoints/cli/serve.py:149-156). The API servers share one listen socket with SO_REUSEPORT, so the kernel spreads the TCP connections over them (vllm/entrypoints/cli/serve.py:298). The page The process and thread architecture of vllm serve counts all processes.

Step 2: dense model or MoE model#

The engine core process chooses its class at start-up:

            parallel_config.data_parallel_index = dp_rank
            if data_parallel and vllm_config.model_config.is_moe:
                # Set data parallel rank for this engine process.
                parallel_config.data_parallel_rank = dp_rank
                engine_core = DPEngineCoreProc(*args, **kwargs)
            else:
                # Non-MoE DP ranks are completely independent, so treat like DP=1.
                # Note that parallel_config.data_parallel_index will still reflect
                # the original DP rank.
                parallel_config.reconfigure_for_independent_dp_rank()
                engine_core = EngineCoreProc(*args, engine_index=dp_rank, **kwargs)
  • Dense model. reconfigure_for_independent_dp_rank() sets the DP size to 1 and the DP rank to 0 (vllm/config/parallel.py:1192-1201). The engine is a plain EngineCoreProc. It has its own NCCL world and never waits for the other ranks. It sleeps when it has no work.
  • MoE model. The engine is a DPEngineCoreProc. Its workers form one NCCL world with all DP ranks, so the expert layers can exchange tokens (Process groups: how vLLM slices its ranks). Step 5 explains the lockstep.

Step 3: the DP coordinator#

The coordinator is a separate process. It exists only when it has a job:

        """Determine if the DPCoordinator process is needed.

        The DPCoordinator is needed in two cases:
        1. For MoE models with DP > 1: to handle wave coordination
           (even in external LB mode, since wave coordination runs in the coordinator)
        2. For non-MoE models in internal/hybrid LB mode: to collect and publish
           queue stats for load balancing across DP ranks

        Returns:
            True if DPCoordinator process is needed, False otherwise.

        """
        # For non-MoE models, only need coordinator in internal/hybrid LB mode
        # (for stats collection).
        return self.parallel_config.data_parallel_size > 1 and (
            self.model_config is None
            or self.model_config.is_moe
            or not self.parallel_config.data_parallel_external_lb
        )

Only DP rank 0 starts it, and only in online serving. Wave coordination is turned on only for MoE models (vllm/v1/engine/utils.py:1227-1232, vllm/v1/engine/utils.py:1260-1265). The engines publish their counts only in internal and hybrid mode (vllm/v1/engine/core.py:1207-1213). When the counts change, an engine sends SchedulerStats with waiting count, running count and KV cache usage to client index −1, the coordinator (vllm/v1/engine/core.py:1622-1633).

            while True:
                elapsed = int(time.time() * 1000) - last_publish_time
                # Send at stats_update_interval_ms interval if the stats have
                # changed, or otherwise every 5 seconds.
                wait_for = self.stats_update_interval_ms if stats_changed else 5000

                # Wait at least 50ms to ensure we've received all stats for
                # the current step. Only applicable to lockstep (MoE) DP;
                # non-lockstep engines have no synchronized step boundaries.
                if self.enable_wave_coordination and last_step_counts is None:
                    min_timeout = 50
                else:
                    min_timeout = 0

                events = poller.poll(timeout=max(min_timeout, wait_for - elapsed))
                if not events:
                    # Poller timeout - publish current stats to front-ends.
                    if last_step_counts is not None:
                        engine_req_counts_list = last_step_counts
                        last_step_counts = None
                    else:
                        engine_req_counts_list = self._get_engine_counts()
                        stats_changed = False

                    to_publish = (engine_req_counts_list, current_wave, engines_running)
                    publish_front.send(msgspec.msgpack.encode(to_publish))
                    last_publish_time = int(time.time() * 1000)
                    continue
  1. Line 267. Publish at most every stats_update_interval_ms (100 by default, vllm/v1/engine/coordinator.py:152) when something changed, otherwise every 5 s.
  2. Lines 269-275. For lockstep MoE engines, wait at least 50 ms so that the counts of one step arrive from all ranks.
  3. Lines 287-288. The message is (counts, current_wave, engines_running), sent on an XPUB socket to every API server.

The class docstring says the engines send "just waiting and running queue lengths" (vllm/v1/engine/coordinator.py:29-31). The code also stores kv_cache_usage (vllm/v1/engine/coordinator.py:404-406).

Step 4: the API server picks an engine#

With DP > 1 and internal load balancing, AsyncLLM uses a DPLBAsyncMPClient (vllm/v1/engine/core_client.py:158-173). A background task keeps the newest published counts in lb_engines (vllm/v1/engine/core_client.py:1578-1590). Each new request runs this choice:

        # Engines are in rank order.
        if (eng_index := request.data_parallel_rank) is None and (
            eng_index := get_late_interaction_engine_index(
                request.pooling_params, len(self.core_engines)
            )
        ) is None:
            current_counts = self.lb_engines
            # TODO use P2C alg for larger DP sizes
            num_engines = len(current_counts)
            min_score: float = sys.maxsize
            eng_index = 0
            for i in range(num_engines):
                # Start from client_index to help with balancing when engines
                # are empty.
                idx = (self.eng_start_index + i) % num_engines
                waiting, running, kv_cache_usage = current_counts[idx]
                # Estimate engine load as the greater of the coordinator's
                # latest (waiting + running) snapshot and this client's own
                # in-flight count (scaled by the number of clients). The
                # in-flight floor is exact and can't be erased by a snapshot
                # rebind, so a burst spreads round-robin even when snapshots
                # race with routing decisions; the snapshot raises the score
                # when other clients or stale requests load the engine.
                inflight = self.engine_inflight[self.core_engines[idx]]
                score: float = max(self.client_count * inflight, waiting + running)
                if waiting:
                    # Waiting requests are penalized in proportion to KV cache
                    # pressure: a queue on a KV-bound engine drains slowly, so
                    # new requests should strongly prefer other engines. With
                    # low KV usage the queue is transient (e.g. mid-burst) and
                    # the penalty stays off, preserving exact round-robin.
                    # Ramps from 0 at <=50% usage to 3x waiting at 100%.
                    score += waiting * 6.0 * max(0.0, kv_cache_usage - 0.5)
                if score < min_score:
                    min_score = score
                    eng_index = idx
            # Increment local waiting count for better balancing between stats
            # updates from the coordinator (which happen every 100ms).
            current_counts[eng_index][0] += self.client_count
            # Rotate the scan start so that ties (equal scores, e.g. right
            # after a coordinator stats reset when engines look equally loaded)
            # don't systematically favor the same engine. This removes the
            # fixed tie-break bias without affecting load-aware decisions when
            # scores actually differ.
            self.eng_start_index = (self.eng_start_index + 1) % num_engines
  1. Lines 1755-1759. A request may name its rank. The serving layer takes it from the X-data-parallel-rank header (vllm/entrypoints/generate/base/serving.py:216-228). Then no score is computed.
  2. Lines 1765-1768. The scan starts at eng_start_index, which differs per API server: (engines × client_index) // client_count (vllm/v1/engine/core_client.py:1739-1741).
  3. Lines 1777-1778. The base score is the larger of two loads. One is this API server's in-flight requests times the number of API servers; the other is waiting + running.
  4. Lines 1779-1786. Waiting requests cost extra when the KV cache is more than half full: up to 3 × waiting at 100 % usage.
  5. Line 1792. The chosen engine's waiting count grows by client_count at once, so the next request sees it before the next publication.
  6. Line 1798. The start index rotates by one, so equal scores do not always pick the same engine.

The in-flight count drops when the engine reports the request as finished (vllm/v1/engine/core_client.py:1810-1817). add_request_async also stamps the request with the current wave and this API server's client_index. So the engine can send the outputs back to the right API server (vllm/v1/engine/core_client.py:1599-1609).

Step 5: MoE engines step together in waves#

The expert layers of all ranks must meet in every forward pass (docs/serving/data_parallel_deployment.md:9, Expert parallelism). So DPEngineCoreProc changes the busy loop:


            local_unfinished_reqs = self.scheduler.has_unfinished_requests()
            if not executed:
                if not local_unfinished_reqs and not self.engines_running:
                    # All engines are idle.
                    continue

                # Execute a dummy pass when no ready requests ran, unless the
                # engine is sleeping.
                elif not self.model_executor.is_sleeping:
                    with self.capture_iteration_details(None) as iteration_details:
                        self.execute_dummy_batch()
                    if iteration_details is not None and not self.has_coordinator:
                        stats = self._make_iteration_details_stats(iteration_details)
                        self.output_queue.put_nowait(
                            (0, EngineCoreOutputs(scheduler_stats=stats))
                        )

            # 3) All-reduce operation to determine global unfinished reqs.
            self.engines_running = self._has_global_unfinished_reqs(
                local_unfinished_reqs
            )

            if not self.engines_running:
                if self.dp_rank == 0 or not self.has_coordinator:
                    # Notify client that we are pausing the loop.
                    logger.debug(
                        "Wave %d finished, pausing engine loop.", self.current_wave
                    )
                    # In the coordinator case, dp rank 0 sends updates to the
                    # coordinator. Otherwise (offline spmd case), each rank
                    # sends the update to its colocated front-end process.
                    client_index = -1 if self.has_coordinator else 0
                    self.output_queue.put_nowait(
                        (
                            client_index,
                            EngineCoreOutputs(wave_complete=self.current_wave),
                        )
                    )
                # Increment wave count and reset step counter.
                self.current_wave += 1
                self.step_counter = 0
  1. Lines 2486-2495. The engine ran no real step. If no rank is running, it waits. Otherwise it runs execute_dummy_batch(), so the other ranks' collectives do not hang.
  2. Lines 2502-2505. All ranks agree whether any rank still has unfinished requests.
  3. Lines 2507-2525. When none has, the wave ends. Rank 0 tells the coordinator, and every rank increments its wave number and resets its step counter.

The agreement is a CPU all-reduce over a gloo group that the engine core process makes for itself (vllm/config/parallel.py:735-743). It does not run every step: only at the first step of a wave and then every dp_sync_interval steps, 16 by default (vllm/v1/engine/core.py:2542-2559, vllm/config/parallel.py:163-164). A new request on an idle engine starts the next wave: the engine tells the coordinator, and the coordinator sends START_DP_WAVE to the others (vllm/v1/engine/core.py:2360-2373, vllm/v1/engine/coordinator.py:426-440).

Step 6: metrics per engine#

Every Prometheus metric of the engines has an engine label with the DP rank as a string, next to model_name (vllm/v1/metrics/loggers.py:499-504). An engine core sends its scheduler stats of a step to only one front end (vllm/v1/core/sched/scheduler.py:2516-2521). With more than one API server, vLLM turns off the periodic log line, because each API server sees only part of the stats (vllm/v1/metrics/loggers.py:1384-1389). Prometheus then runs in multiprocess mode with a shared directory for all API server processes (vllm/entrypoints/cli/serve.py:282-283).

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

Worked example: -dp 4 on one node, three requests

vllm serve meta-llama/Llama-3.1-8B-Instruct -dp 4      # one node, 4 GPUs, dense model

Processes. --api-server-count is unset, so it becomes 4. The coordinator runs, because the mode is internal. Each engine core has one worker process: world_size_across_dp is 4, so the executor backend is "mp" (vllm/config/parallel.py:1055-1061). Total (derived): 1 CLI supervisor + 4 API servers + 1 coordinator + 4 engine cores + 4 workers = 14.

The state. Three requests arrive at ApiServer_0 (client_index 0, client_count C = 4), so the scan starts at engine 0. The latest published counts and this API server's in-flight requests are (illustrative):

Enginewaiting wrunning rKV usagein-flight (this server)C × in-flight
02100.9028
1080.4014
2170.6014
3090.3028

The three decisions (score = max(C × in-flight, w + r) + 6 × w × max(0, kv − 0.5)):

  1. Request 1, scan 0, 1, 2, 3. e0 = max(8, 12) + 6 × 2 × 0.4 = 16.8. e1 = max(4, 8) = 8. e2 = max(4, 8) + 6 × 1 × 0.1 = 8.6. e3 = max(8, 9) = 9. Engine 1 wins. Its waiting count becomes 0 + 4 = 4, its in-flight count 2, and the start index 1.
  2. Request 2, scan 1, 2, 3, 0. e1 = max(8, 12) = 12, with no KV penalty because 0.40 < 0.5. e2 = 8.6, e3 = 9, e0 = 16.8. Engine 2 wins. Its waiting count becomes 5, its in-flight count 2, and the start index 2.
  3. Request 3, scan 2, 3, 0, 1. e2 = max(8, 12) + 6 × 5 × 0.1 = 15; e3 = 9; e0 = 16.8; e1 = 12. Engine 3 wins.

Engine 0 gets nothing: it has a queue and a nearly full KV cache. Engine 2 was a good choice once, but the local +4 on its waiting count and the KV penalty pushed request 3 elsewhere. The next publication from the coordinator, within about 100 ms, replaces these local guesses with real counts. (Derived from vllm/v1/engine/core_client.py:1754-1798.)

Deep dive

The load-balancing modes and their flags

ModeHow you select itDefault API serversWho picks the engineCoordinator
internal (default)-dp N onlyN (the DP size)each API server, score of Step 4; it sees all ranksyes
hybrid--data-parallel-hybrid-lb, or --data-parallel-start-rank without --headless--data-parallel-size-localAPI servers on each node, only for the node's own ranks; an outside balancer spreads across nodesyes
external--data-parallel-external-lb or --data-parallel-rank1an outside router; each rank has its own endpointMoE only (waves)
multi-port external--data-parallel-multi-port-external-lb1 per local rank, under a node supervisoran outside routeras external

Sources: mode detection and defaults (vllm/entrypoints/cli/serve.py:79-130), flags (vllm/engine/arg_utils.py:1233-1292), engine-args checks (vllm/engine/arg_utils.py:2436-2510), coordinator rule (Step 3). Each API server of the hybrid and external modes manages only its local engines (vllm/config/parallel.py:654-659).

Deep dive

Multi-node data parallelism with the mp backend

Run one vllm serve per node. The docs example puts DP ranks 0-1 on the head node and 2-3 on a second node (docs/serving/data_parallel_deployment.md:39-49):

# Node 0 (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: no API server, DP ranks 2 and 3
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
FlagShortMeaning
--data-parallel-size-local-dplDP ranks on this node. 0 on node 0 means "API servers only" (docs/serving/data_parallel_deployment.md:51-60).
--data-parallel-start-rank-dprFirst DP rank of this node. Without --headless it turns on hybrid mode (vllm/engine/arg_utils.py:2478-2480).
--data-parallel-address-dpaAddress of the head node. Unset: --master-addr, else 127.0.0.1 (vllm/engine/arg_utils.py:2526-2544).
--data-parallel-rpc-port-dppFixed port for the ZMQ handshake, the same on all nodes; default 29550 (vllm/config/parallel.py:161-162). Only used with remote engines; one node uses ipc:// (vllm/engine/arg_utils.py:2546-2552).
--headless–No API server on this node; --api-server-count > 0 then raises (vllm/entrypoints/cli/serve.py:69-77).

A headless node runs run_headless: it builds the config and starts its local engine cores. They connect to the head node's handshake address tcp://<address>:<rpc port> (vllm/entrypoints/cli/serve.py:235-256). On a node that only holds the second half of a TP/PP replica (node_rank_within_dp > 0), it starts only a MultiprocExecutor for those workers (vllm/entrypoints/cli/serve.py:218-232). Flag names are from vllm/engine/arg_utils.py:1243-1266.

Deep dive

The Ray backend for data parallelism

With --data-parallel-backend ray (-dpb, default mp, vllm/engine/arg_utils.py:1267-1273), one command on any node starts all DP ranks as Ray actors. CoreEngineActorManager places them (vllm/v1/engine/utils.py:493), and the engine cores run as DPMoEEngineCoreActor or EngineCoreActor (vllm/v1/engine/core.py:2799, vllm/v1/engine/core.py:2822). The docs list the differences: no per-node command, no --data-parallel-address or --data-parallel-rpc-port, and VLLM_RAY_DP_PACK_STRATEGY="span" when one replica spans nodes (docs/serving/data_parallel_deployment.md:62-75). --nnodes > 1 needs the mp backend (vllm/engine/arg_utils.py:2379-2386). This site does not cover Ray further.

Deep dive

Waves in detail: one MoE request on an idle -dp 2 server

  1. Both engines are idle: engines_running is false, so has_work() is false, and each busy loop blocks on its input queue (vllm/v1/engine/core.py:1596-1602).
  2. The request reaches engine 0. add_request sees an idle, unpaused engine, sets engines_running and sends start_wave to the coordinator (vllm/v1/engine/core.py:2360-2373).
  3. The coordinator moves to "running" and sends START_DP_WAVE (wave, exclude=0) to the engines; engine 0 is excluded because it started the wave (vllm/v1/engine/coordinator.py:426-440, vllm/v1/engine/coordinator.py:447-456).
  4. Engine 1 starts its loop for the wave (vllm/v1/engine/core.py:2406-2424). With no requests, each of its steps is a dummy batch: one uniform-decode token through the model (vllm/v1/worker/gpu_worker.py:1454-1460).
  5. In every forward, the workers of both ranks first agree on a padded token count and a CUDA graph mode. They use a CPU all-reduce of a [6, dp_size] int32 tensor (vllm/v1/worker/gpu/dp_utils.py:65-74). Model Runner V2 calls this before it prepares the inputs (vllm/v1/worker/gpu/model_runner.py:1806-1821).
  6. At step 1 and every 16th step, the engines run the "unfinished?" all-reduce. Assume the request is done on engine 0 and nothing waits anywhere. Then the result is false: the wave ends, rank 0 sends wave_complete, and both engines block again (vllm/v1/engine/core.py:2502-2525).

So an idle MoE rank can run up to 15 dummy steps after the last request ends somewhere (derived from the sync interval). Larger --dp-sync-interval means fewer CPU all-reduces but more dummy steps at the end of a wave.

Deep dive

Process counts and per-rank limits

Command (one node, dense)APICLI sup.coord.enginesworkerstotal
-dp 2211228
-tp 2 -dp 22112410
-dp 44114414
-dp 4 --api-server-count 11 (the CLI process)014410

The counts are derived from the rules on The process and thread architecture of vllm serve. Limits per rank: --max-num-seqs applies to each DP rank, but --max-num-queued-reqs and --max-num-queued-tokens count over all ranks that an API server routes to (docs/serving/data_parallel_deployment.md:27-29). The KV cache of each rank is separate, so a prefix-cache hit needs the request to reach the rank that holds the blocks.

What can go wrong#

  • The API servers time out at start-up. With more than one API server, they wait for the engines with VLLM_ENGINE_READY_TIMEOUT_S, 600 s by default; a slow model load can exceed it (vllm/v1/engine/core_client.py:753-768).
  • Runtime LoRA updates are refused. VLLM_ALLOW_RUNTIME_LORA_UPDATING with more than one API server raises ValueError (vllm/entrypoints/cli/serve.py:316-319).
  • Prefix-cache hits get lost. The score looks at load, not at prefixes, so a repeated prompt can land on another rank. Use X-data-parallel-rank or an external router if you need affinity.
  • Idle MoE ranks still use the GPU. During a wave, an idle rank runs dummy batches (vllm/v1/engine/core.py:2486-2495).
  • Missing log line. With several API servers, the periodic "Avg prompt throughput" line is gone by design; use /metrics (vllm/v1/metrics/loggers.py:1384-1389).

Why it matters. DP is the main way to add throughput when one copy of the model fits. With the routing score and the MoE lockstep, you can predict where a request goes and why an idle GPU is busy.

Check yourself#

  1. 1
    With -dp 4 and no other DP flag, how many API server processes run, and why?
    easy
    Show answer

    Four. In internal mode, an unset --api-server-count becomes the DP size (vllm/entrypoints/cli/serve.py:124-125).

  2. 2
    Which engine core class does a dense model get with DP > 1, and which does an MoE model get?
    easy
    Show answer

    Dense: EngineCoreProc, after the DP size is reset to 1. MoE: DPEngineCoreProc (vllm/v1/engine/core.py:1534-1543).

  3. 3
    When does the DP coordinator process not exist, even with DP > 1?
    medium
    Show answer

    Only DP rank 0 (the head node) starts it, and never in offline mode (vllm/v1/engine/utils.py:1230-1232). needs_dp_coordinator is also false for a dense model in external load-balancing mode (vllm/config/vllm.py:870-874), but vllm serve refuses that combination at start-up (vllm/engine/arg_utils.py:2445-2454).

  4. 4
    An engine has waiting = 4, running = 6, KV usage 0.75. An API server with client_count 2 has 3 requests in flight on it. What is the score?
    hard
    Show answer

    max(2 × 3, 4 + 6) = 10, plus 4 × 6 × max(0, 0.75 − 0.5) = 6. Score 16 (vllm/v1/engine/core_client.py:1777-1786).

  5. 5
    Why can an MoE rank with no requests keep its GPU busy for a while, and what controls how long?
    hard
    Show answer

    During a wave every rank must join each forward pass, so an idle rank runs dummy batches (vllm/v1/engine/core.py:2486-2495). The wave ends only after an all-reduce shows no unfinished requests. That all-reduce runs at the first step and then every dp_sync_interval steps, 16 by default (vllm/v1/engine/core.py:2542-2559).