Data parallel serving: several engine cores behind one endpoint
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#
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_kwargsEach 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 plainEngineCoreProc. 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- 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. - Lines 269-275. For lockstep MoE engines, wait at least 50 ms so that the counts of one step arrive from all ranks.
- 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- Lines 1755-1759. A request may name its rank. The serving layer takes it from the
X-data-parallel-rankheader (vllm/entrypoints/generate/base/serving.py:216-228). Then no score is computed. - 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). - 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.
- Lines 1779-1786. Waiting requests cost extra when the KV cache is more than half full: up to 3 × waiting at 100 % usage.
- Line 1792. The chosen engine's waiting count grows by
client_countat once, so the next request sees it before the next publication. - 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- 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. - Lines 2502-2505. All ranks agree whether any rank still has unfinished requests.
- 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 diveWorked 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):
| Engine | waiting w | running r | KV usage | in-flight (this server) | C × in-flight |
|---|---|---|---|---|---|
| 0 | 2 | 10 | 0.90 | 2 | 8 |
| 1 | 0 | 8 | 0.40 | 1 | 4 |
| 2 | 1 | 7 | 0.60 | 1 | 4 |
| 3 | 0 | 9 | 0.30 | 2 | 8 |
The three decisions (score = max(C × in-flight, w + r) + 6 × w × max(0, kv − 0.5)):
- 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.
- 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.
- 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 diveThe load-balancing modes and their flags
| Mode | How you select it | Default API servers | Who picks the engine | Coordinator |
|---|---|---|---|---|
| internal (default) | -dp N only | N (the DP size) | each API server, score of Step 4; it sees all ranks | yes |
| hybrid | --data-parallel-hybrid-lb, or --data-parallel-start-rank without --headless | --data-parallel-size-local | API servers on each node, only for the node's own ranks; an outside balancer spreads across nodes | yes |
| external | --data-parallel-external-lb or --data-parallel-rank | 1 | an outside router; each rank has its own endpoint | MoE only (waves) |
| multi-port external | --data-parallel-multi-port-external-lb | 1 per local rank, under a node supervisor | an outside router | as 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 diveMulti-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
| Flag | Short | Meaning |
|---|---|---|
--data-parallel-size-local | -dpl | DP ranks on this node. 0 on node 0 means "API servers only" (docs/serving/data_parallel_deployment.md:51-60). |
--data-parallel-start-rank | -dpr | First DP rank of this node. Without --headless it turns on hybrid mode (vllm/engine/arg_utils.py:2478-2480). |
--data-parallel-address | -dpa | Address of the head node. Unset: --master-addr, else 127.0.0.1 (vllm/engine/arg_utils.py:2526-2544). |
--data-parallel-rpc-port | -dpp | Fixed 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 diveThe 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 diveWaves in detail: one MoE request on an idle -dp 2 server
- Both engines are idle:
engines_runningis false, sohas_work()is false, and each busy loop blocks on its input queue (vllm/v1/engine/core.py:1596-1602). - The request reaches engine 0.
add_requestsees an idle, unpaused engine, setsengines_runningand sendsstart_waveto the coordinator (vllm/v1/engine/core.py:2360-2373). - 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). - 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). - 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). - 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 diveProcess counts and per-rank limits
| Command (one node, dense) | API | CLI sup. | coord. | engines | workers | total |
|---|---|---|---|---|---|---|
-dp 2 | 2 | 1 | 1 | 2 | 2 | 8 |
-tp 2 -dp 2 | 2 | 1 | 1 | 2 | 4 | 10 |
-dp 4 | 4 | 1 | 1 | 4 | 4 | 14 |
-dp 4 --api-server-count 1 | 1 (the CLI process) | 0 | 1 | 4 | 4 | 10 |
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_UPDATINGwith more than one API server raisesValueError(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-rankor 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#
- 1Witheasy
-dp 4and no other DP flag, how many API server processes run, and why?Show answer
Four. In internal mode, an unset
--api-server-countbecomes the DP size (vllm/entrypoints/cli/serve.py:124-125). - 2Which 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). - 3When 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_coordinatoris also false for a dense model in external load-balancing mode (vllm/config/vllm.py:870-874), butvllm serverefuses that combination at start-up (vllm/engine/arg_utils.py:2445-2454). - 4An engine has waiting = 4, running = 6, KV usage 0.75. An API server withhard
client_count2 has 3 requests in flight on it. What is the score?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). - 5Why 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 everydp_sync_intervalsteps, 16 by default (vllm/v1/engine/core.py:2542-2559).