v1/engine/core_client.py: the engine core clients
Code version: vLLM @ 3e44c0d · 2026-10-22
Purpose#
vllm/v1/engine/core_client.py holds the engine core clients: the objects in the front end that send requests to the engine core and get its outputs back.
| File | vllm/v1/engine/core_client.py, 2,162 lines of Python. Nine classes and one helper function. |
|---|---|
| Main classes | EngineCoreClient (abstract, vllm/v1/engine/core_client.py:83), MPClient (vllm/v1/engine/core_client.py:594), AsyncMPClient (vllm/v1/engine/core_client.py:1143), DPAsyncMPClient (vllm/v1/engine/core_client.py:1461), DPLBAsyncMPClient (vllm/v1/engine/core_client.py:1702). For offline use: InprocClient and SyncMPClient. |
| Runs in | The API server process (asyncio clients) or the process of the offline LLM (sync and in-process clients). One monitor thread per MPClient that started engines. |
| Called by | AsyncLLM (make_async_mp_client) and LLMEngine (make_client). |
| Calls | pyzmq sockets, MsgpackEncoder / MsgpackDecoder, launch_core_engines in v1/engine/utils.py, the DP coordinator. |
| Not in this file | The engine side of the sockets (v1/engine/core.py), the process launch (v1/engine/utils.py), the coordinator process (v1/engine/coordinator.py), encoding details (v1/serial_utils.py). |
The page The engine client: AsyncLLM in the API server shows how AsyncLLM uses these clients. This page reads the clients themselves.
Where it sits#
The class tree has two branches: one in-process client, and the multiprocess clients that use ZMQ. The asyncio branch is the one of vllm serve:
EngineCoreClient abstract; factories
├── InprocClient EngineCore in this process
└── MPClient ZMQ, launch, ready wait
├── SyncMPClient blocking; offline LLM
└── AsyncMPClient asyncio; vllm serve, DP = 1
└── DPAsyncMPClient DP > 1, external LB
└── DPLBAsyncMPClient DP > 1, internal LB
Who calls it#
AsyncLLM.__init__callsEngineCoreClient.make_async_mp_client(vllm/v1/engine/async_llm.py:193-201). For every request it callsadd_request_async; the output handler callsget_output_asyncandabort_requests_async(vllm/v1/engine/async_llm.py:584,vllm/v1/engine/async_llm.py:880,vllm/v1/engine/async_llm.py:920).LLMEngine(offline) callsmake_clientwithasyncio_mode=False. That givesSyncMPClientor, without multiprocessing,InprocClient(vllm/v1/engine/core_client.py:128-136).
What it calls#
make_zmq_socketfor the ROUTER, PULL, XSUB and PAIR sockets (vllm/v1/engine/core_client.py:673-682,vllm/v1/engine/core_client.py:1524-1527).launch_core_engines, which starts the engine core processes and, when needed, the DP coordinator (vllm/v1/engine/core_client.py:695-702).MsgpackEncoderandMsgpackDecoder(EngineCoreOutputs)(vllm/v1/engine/core_client.py:731-732).- The engine manager's
monitor_engine_livenessin a thread (vllm/v1/engine/core_client.py:829-845).
Key classes and functions#
| Name | Line | What it does |
|---|---|---|
EngineCoreClient.make_client | vllm/v1/engine/core_client.py:97-136 | Factory for the offline engine: sync or in-process. |
EngineCoreClient.make_async_mp_client | vllm/v1/engine/core_client.py:138-173 | Factory for AsyncLLM: picks the class by the DP settings. |
InprocClient | vllm/v1/engine/core_client.py:364-494 | Wraps an EngineCore in the same process; no busy loop. |
BackgroundResources | vllm/v1/engine/core_client.py:497-584 | Sockets, tasks and the engine manager; the finalizer; validate_alive. |
MPClient.__init__ | vllm/v1/engine/core_client.py:606-782 | Binds sockets, launches engines, waits for ready messages, starts the monitor. |
MPClient._apply_ready_response | vllm/v1/engine/core_client.py:847-923 | Copies start-up values of each engine into the front-end config. |
_process_utility_output | vllm/v1/engine/core_client.py:929-948 | Completes the future of a utility call. |
SyncMPClient | vllm/v1/engine/core_client.py:951-1140 | Blocking client with an output thread. |
AsyncMPClient._ensure_output_queue_task | vllm/v1/engine/core_client.py:1187-1262 | Starts the task that reads the PULL socket. |
AsyncMPClient._send_input / _send_input_message | vllm/v1/engine/core_client.py:1275-1294 | Encodes and sends one message to one engine. |
AsyncMPClient._call_utility_async | vllm/v1/engine/core_client.py:1316-1328 | A remote method call with a call id and a future. |
AsyncMPClient.add_request_async | vllm/v1/engine/core_client.py:1333-1336 | Sets client_index and sends ADD. |
DPAsyncMPClient._ensure_stats_update_task | vllm/v1/engine/core_client.py:1513-1597 | Reads load counts and waves from the DP coordinator. |
DPLBAsyncMPClient.get_core_engine_for_request | vllm/v1/engine/core_client.py:1743-1804 | Load balancing: picks the engine with the lowest score. |
DPLBAsyncMPClient.abort_requests_async | vllm/v1/engine/core_client.py:1866-1890 | Sends each abort to the engine that has the request. |
Walk through the main methods#
make_async_mp_client: which class#
@staticmethod
@instrument(span_name="Overall Loading")
def make_async_mp_client(
vllm_config: VllmConfig,
executor_class: type[Executor],
log_stats: bool,
client_addresses: dict[str, Any] | None = None,
client_count: int = 1,
client_index: int = 0,
renderer: BaseRenderer | None = None,
) -> "AsyncMPClient":
parallel_config = vllm_config.parallel_config
client_args = (
vllm_config,
executor_class,
log_stats,
client_addresses,
client_count,
client_index,
)
if parallel_config.data_parallel_size > 1:
if parallel_config.data_parallel_external_lb:
# External load balancer - client per DP rank.
return DPAsyncMPClient(
*client_args,
renderer=renderer,
)
# Internal load balancer - client balances to all DP ranks.
return DPLBAsyncMPClient(
*client_args,
renderer=renderer,
)
return AsyncMPClient(
*client_args,
renderer=renderer,
)- Lines 150-157. All three classes take the same six arguments: the config, the executor class,
log_stats, and the client addresses, count and index of this API server. - Lines 158-164. DP > 1 with an external load balancer:
DPAsyncMPClient, which sends everything to one engine core. - Lines 165-169. DP > 1 otherwise:
DPLBAsyncMPClient, which balances over all engine cores. - Line 170. DP = 1:
AsyncMPClient.
There is no asyncio client without multiprocessing: make_client raises NotImplementedError for that case (vllm/v1/engine/core_client.py:114-118). So vllm serve always runs the engine core in its own process.
MPClient.__init__: sockets and launch#
# ZMQ setup.
sync_ctx = zmq.Context(io_threads=2)
self.ctx = zmq.asyncio.Context(sync_ctx) if asyncio_mode else sync_ctx
# This will ensure resources created so far are closed
# when the client is garbage collected, even if an
# exception is raised mid-construction.
self.resources = BackgroundResources(ctx=sync_ctx)
self._finalizer = weakref.finalize(self, self.resources)- Lines 621-622. One ZMQ context with 2 I/O threads. The asyncio clients wrap it in a
zmq.asyncio.Context. - Lines 627-628.
BackgroundResourcesholds everything that must be closed. Aweakref.finalizecalls it when the client is collected, even if the constructor fails half-way (lines 624-626).
Then the client binds its two sockets. In the multi-API-server case, the parent process has bound them already, and the client gets the listeners through client_addresses (vllm/v1/engine/core_client.py:643-669). In the single-server case, the client binds them itself and launches the engines:
else:
# Engines are managed by this client.
bind_addresses = get_engine_zmq_bind_addresses(vllm_config)
self.input_socket = self.resources.input_socket = make_zmq_socket(
self.ctx,
bind_addresses.inputs[0],
zmq.ROUTER,
bind=True,
router_handover=enable_input_socket_handover,
)
self.resources.output_socket = make_zmq_socket(
self.ctx, bind_addresses.outputs[0], zmq.PULL
)
# EngineZmqAddresses contains only endpoints resolved by the
# sockets that own the bind operation.
addresses = EngineZmqAddresses(
inputs=[self.input_socket.getsockopt(zmq.LAST_ENDPOINT).decode()],
outputs=[
self.resources.output_socket.getsockopt(
zmq.LAST_ENDPOINT
).decode()
],
)
with launch_core_engines(
vllm_config, executor_class, log_stats, addresses
) as engine_launch:
self.resources.coordinator = engine_launch.coordinator
self.resources.engine_manager = engine_launch.engine_manager
coordinator = engine_launch.coordinator
addresses = engine_launch.addresses
tensor_queue = engine_launch.tensor_queue- Lines 673-679. A
ROUTERsocket for requests, bound by the client. A ROUTER sends each message to the peer whose identity is the first frame. - Lines 680-682. A
PULLsocket for outputs. All engine cores push into it. - Lines 686-693. The real bound addresses go to the engines.
- Lines 695-697.
launch_core_enginesstarts the engine core processes (and the DP coordinator when needed).
Next the client computes which engines it talks to, and waits for each one:
# Client manages local+remote EngineCores in pure internal LB case.
# Client manages local EngineCores in hybrid and external LB case.
num_ranks = dp_local_size if parallel_config.local_engines_only else dp_size
self.engine_ranks_managed = (
[dp_rank] if offline_mode else list(range(dp_rank, dp_rank + num_ranks))
)
assert parallel_config.data_parallel_size_local <= len(
self.engine_ranks_managed
)
# ZMQ identity of each engine that this client will talk to.
self.core_engines: list[EngineIdentity] = [
rank.to_bytes(2, "little") for rank in self.engine_ranks_managed
]
# Wait for ready messages from each engine on the input socket.
identities = set(self.core_engines)
sync_input_socket = zmq.Socket.shadow(self.input_socket)
while identities:
if not sync_input_socket.poll(
timeout=VLLM_ENGINE_READY_TIMEOUT_S * 1000 # convert to ms
):
raise TimeoutError(
f"Timed out waiting for engine core processes to "
f"start. This is often caused by slow weight loading "
f"for large models. Waited "
f"{VLLM_ENGINE_READY_TIMEOUT_S}s (configured by "
f"VLLM_ENGINE_READY_TIMEOUT_S). To increase the "
f"timeout, set the environment variable: "
f"VLLM_ENGINE_READY_TIMEOUT_S=<seconds>"
)
identity, payload = sync_input_socket.recv_multipart()
identities.remove(identity)
self._apply_ready_response(payload)
self.core_engine: EngineIdentity = self.core_engines[0]
self.utility_results: dict[int, AnyFuture] = {}
# Start monitoring engine core processes for unexpected failures
self.start_engine_core_monitor()- Lines 740-743. With internal load balancing, the client manages all DP ranks. With hybrid or external load balancing, only the local ones.
- Lines 749-751. The ZMQ identity of each engine is its DP rank as 2 little-endian bytes. The engine core uses the same bytes as the identity of its DEALER socket (
vllm/v1/engine/core.py:1178-1179). - Lines 756-768. A blocking poll with timeout
VLLM_ENGINE_READY_TIMEOUT_S. The error message names the variable. - Lines 769-771. Each ready message carries the identity and an
EngineCoreReadyResponse, which_apply_ready_responseapplies. - Line 773.
core_engineis the first engine: the default target of a DP = 1 client. - Line 777. The monitor thread starts.
AsyncMPClient: the output task#
async def process_outputs_socket():
try:
while True:
frames = await output_socket.recv_multipart(copy=False)
resources.validate_alive(frames)
outputs: EngineCoreOutputs = decoder.decode(frames)
if outputs.utility_output: else:
_process_utility_output(
outputs.utility_output, utility_results
)
continue
if output_handler is not None:
assert _self_ref is not None
_self = _self_ref()
if not _self:
# Client has been garbage collected, abort.
return
await output_handler(_self, outputs)
if outputs.outputs or outputs.scheduler_stats:
outputs_queue.put_nowait(outputs)
except Exception as e:
outputs_queue.put_nowait(e)
except asyncio.CancelledError:
outputs_queue.put_nowait(EngineDeadError())
resources.output_queue_task = asyncio.create_task(
process_outputs_socket(), name="EngineCoreOutputQueueTask"
)- Line 1211. Receive one multipart message without a copy.
- Line 1212.
validate_aliveraisesEngineDeadErrorwhen the message is the single frameENGINE_CORE_DEAD(vllm/v1/engine/core_client.py:581-584). - Line 1213. Decode into
EngineCoreOutputs. - Lines 1214-1243 (first excerpt ends at 1214). A message with
utility_outputis a reply to a utility call, or one of two special notifications. It never goes into the queue. - Lines 1245-1251. A subclass hook.
DPLBAsyncMPClientuses it to count finished requests per engine (vllm/v1/engine/core_client.py:1810-1817). - Lines 1253-1254. Only messages with outputs or stats go to
outputs_queue. - Lines 1255-1258. An exception goes into the queue. A cancel puts an
EngineDeadErrorthere. The nextget_output_async()raises it (vllm/v1/engine/core_client.py:1264-1273). - Lines 1260-1262. The task is named
EngineCoreOutputQueueTask. The constructor starts it if a loop runs, so that earlyEXECUTOR_FAILEDmessages are not missed (vllm/v1/engine/core_client.py:1177-1185).
Sending a request#
def _send_input(
self,
request_type: EngineCoreRequestType,
request: Any,
engine: EngineIdentity | None = None,
) -> Awaitable[Any]:
if engine is None:
engine = self.core_engine
message = (request_type.value, *self.encoder.encode(request))
return self._send_input_message(message, engine)
def _send_input_message(
self, message: tuple[bytestr, ...], engine: EngineIdentity
) -> Awaitable[Any]:
self.ensure_alive()
# Any zero-copy tensor/ndarray frames are kept alive by zmq itself
# until it's finished sending them (there is a ref chain from the underlying
# memoryview back to the original owning tensor/ndarray).
return self.input_socket.send_multipart((engine,) + message, copy=False) async def add_request_async(self, request: EngineCoreRequest) -> None:
request.client_index = self.client_index
await self._send_input(EngineCoreRequestType.ADD, request)
self._ensure_output_queue_task()
async def abort_requests_async(self, request_ids: list[str]) -> None:
if request_ids and not self.resources.engine_dead:
await self._send_input(EngineCoreRequestType.ABORT, request_ids)- Line 1284. The frames are the type byte (for example
b"\x00"for ADD) and the encoded buffers. - Line 1290.
ensure_aliveraisesEngineDeadErrorwhen the dead flag is set (vllm/v1/engine/core_client.py:803-805). - Line 1294. The engine identity goes first. The ROUTER uses it to route the message, and removes it before delivery.
send_multipartof the asyncio socket returns an awaitable. - Line 1334.
client_indextells the engine core which output socket to use for this request. - Line 1339. Aborts are skipped when the engine is dead.
Utility calls: a small RPC system#
A utility call runs one method of EngineCore in the engine core process and returns its result. Control methods such as sleep, add_lora and collective_rpc use it.
async def _call_utility_async(
self, method: str, *args, engine: EngineIdentity
) -> Any:
call_id = uuid.uuid1().int >> 64
future = asyncio.get_running_loop().create_future()
self.utility_results[call_id] = future
message = (
EngineCoreRequestType.UTILITY.value,
*self.encoder.encode((self.client_index, call_id, method, args)),
)
await self._send_input_message(message, engine)
self._ensure_output_queue_task()
return await future- Line 1319. A 64-bit call id from a UUID.
- Lines 1320-1321. An asyncio future, stored under the call id.
- Lines 1322-1325. The message is
UTILITYplus(client_index, call_id, method, args). - Line 1328. The caller waits on the future. The output task completes it with
_process_utility_output(vllm/v1/engine/core_client.py:929-948).
On the engine side, the busy loop looks up the method by name on the EngineCore object and calls it. The result goes back as EngineCoreOutputs(utility_output=...) to the client that asked (vllm/v1/engine/core.py:1752-1765). If the method returns a Future, the reply waits for it (vllm/v1/engine/core.py:1826-1834). Three variants exist in the client: one engine (call_utility_async), all engines (call_utility_all_async), and all engines with a check that the results agree (call_utility_consensus_async) (vllm/v1/engine/core_client.py:1296-1314). DPLBAsyncMPClient changes call_utility_async: it calls all engines and returns the first result (vllm/v1/engine/core_client.py:1806-1808).
DPLBAsyncMPClient.get_core_engine_for_request: load balancing#
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 (above the excerpt). A request with
data_parallel_rankset, or a late-interaction pooling request, goes to a fixed engine. Streaming-input sessions stick to their engine (vllm/v1/engine/core_client.py:1750-1752). - Line 1768. The scan starts at
eng_start_index, which depends on the client index, and rotates after each pick (line 1798). - Lines 1777-1778. The base score is the larger of the coordinator's
waiting + runningand this client's in-flight count times the number of clients. - Lines 1779-1786. Waiting requests add a penalty that grows with the KV cache usage above 50%: from 0 at 50% to 3 × waiting at 100%.
- Line 1792. The local waiting count goes up at once, so that the next request sees the new load before the next coordinator update.
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 diveData flow: frames and types
| Direction | Frames | Payload type | Where |
|---|---|---|---|
| client → engine, ADD | identity, b"\x00", msgpack buffers | EngineCoreRequest | vllm/v1/engine/core_client.py:1333-1335 |
| client → engine, ABORT | identity, b"\x01", msgpack | list[str] of internal ids | vllm/v1/engine/core_client.py:1338-1340 |
| client → engine, UTILITY | identity, b"\x03", msgpack | (client_index, call_id, method, args) | vllm/v1/engine/core_client.py:1322-1325 |
| engine → client, ready | identity, payload | msgpack EngineCoreReadyResponse | vllm/v1/engine/core_client.py:847-855 |
| engine → client, outputs | msgpack buffers | EngineCoreOutputs | vllm/v1/engine/core_client.py:1211-1213 |
| engine → client, death | one frame b"ENGINE_CORE_DEAD" | none | vllm/v1/engine/core.py:1156, vllm/v1/engine/core_client.py:581-584 |
| coordinator → client (DP) | one buffer | msgpack (counts, wave, running); counts per engine are [waiting, running, kv_cache_usage] | vllm/v1/engine/core_client.py:1580-1590 |
Utility results come back as plain msgpack types unless VLLM_ALLOW_INSECURE_SERIALIZATION is set. Without the variable, the encoder sends (None, result), with no type information (vllm/v1/serial_utils.py:211-214), so a tuple or a set from the engine side reaches the client as a list. This is msgspec behaviour: without a target type, it decodes a msgpack array as a list (vllm/v1/serial_utils.py:366-378). For example, get_supported_tasks_async is typed as a tuple, but returns a list (vllm/v1/engine/core_client.py:1330-1331). (Derived.)
Deep diveProcesses, tasks and threads
| Client | Background work | Kind | Where |
|---|---|---|---|
every MPClient that launched engines | MPClientEngineMonitor: waits for an engine process to exit, sets engine_dead, shuts down | daemon thread | vllm/v1/engine/core_client.py:818-845 |
AsyncMPClient | EngineCoreOutputQueueTask | asyncio task | vllm/v1/engine/core_client.py:1260-1262 |
DPAsyncMPClient | stats task: XSUB socket to the coordinator, PAIR socket for scale events | asyncio task | vllm/v1/engine/core_client.py:1522-1597 |
SyncMPClient | EngineCoreOutputQueueThread: polls the PULL socket and a shutdown PAIR socket | daemon thread | vllm/v1/engine/core_client.py:985-1021 |
InprocClient | none: get_output() runs one engine step in the caller's thread | - | vllm/v1/engine/core_client.py:396-399 |
The background functions do not refer to the client object directly. They copy the parts they need, or keep a weakref. So the client can be garbage-collected, and its finalizer can close the sockets (vllm/v1/engine/core_client.py:1192-1200).
Deep diveWorked example: one load-balancing decision
Two API servers (client_count = 2) and two engine cores. The last coordinator snapshot for this client says:
| Engine | waiting | running | KV usage | this client's in-flight |
|---|---|---|---|---|
| 0 | 3 | 10 | 0.80 | 4 |
| 1 | 0 | 12 | 0.40 | 5 |
- Engine 0: base = max(2 × 4, 3 + 10) = 13. Penalty = 3 × 6.0 × max(0, 0.80 − 0.5) = 5.4. Score = 18.4.
- Engine 1: base = max(2 × 5, 0 + 12) = 12. No waiting requests, so no penalty. Score = 12.
- The client picks engine 1. It adds
client_count= 2 to engine 1's local waiting count, records the request inreqs_in_flight, and adds 1 toengine_inflightfor engine 1 (vllm/v1/engine/core_client.py:1792-1803). - When the engine reports the request in
finished_requests, the in-flight count goes down again (vllm/v1/engine/core_client.py:1814-1817). The engine core sends this set only for internal DP balancing, which turns oninclude_finished_set(vllm/v1/engine/core.py:1207-1226).
The coordinator publishes new counts every 100 ms by default when they changed (vllm/v1/engine/coordinator.py:152, vllm/v1/engine/coordinator.py:264-267). Between two snapshots, the local increments keep a burst of requests from going to the same engine.
Deep diveError paths: what raises, what only logs
| Situation | Result | Where |
|---|---|---|
| no ready message within the timeout | TimeoutError that names VLLM_ENGINE_READY_TIMEOUT_S | vllm/v1/engine/core_client.py:756-768 |
| the constructor fails half-way | the finalizer closes what exists; the exception goes on | vllm/v1/engine/core_client.py:779-782 |
ENGINE_CORE_DEAD received | engine_dead = True, EngineDeadError | vllm/v1/engine/core_client.py:581-584 |
| an engine process exits | warning "engine core exited unexpectedly"; flag set; client shuts down | vllm/v1/engine/core_client.py:829-838 |
| a send after the engine died | EngineDeadError from ensure_alive | vllm/v1/engine/core_client.py:803-805 |
| any error after death | replaced by EngineDeadError(suppress_context=True), so the root cause stays clear | vllm/v1/engine/core_client.py:797-801 |
| a utility method fails in the engine | the future gets Exception(failure_message) | vllm/v1/engine/core_client.py:933-937 |
| the caller of a utility call was cancelled | the result is dropped; a failure is logged as an error | vllm/v1/engine/core_client.py:941-948 |
| engines disagree in a consensus call | RuntimeError | vllm/v1/engine/core_client.py:1308-1314 |
Deep diveEnvironment variables
| Variable | Default | Read at | Effect |
|---|---|---|---|
VLLM_ENGINE_READY_TIMEOUT_S | 600 (vllm/envs.py:812-814) | imported once at module load (vllm/v1/engine/core_client.py:29) | Seconds to wait for each engine's ready message. Set it before vLLM is imported. |
VLLM_WORKER_SHUTDOWN_TIMEOUT_SECONDS | 5 (vllm/envs.py:1865-1867) | vllm/v1/engine/core_client.py:525-527 | Timeout for the engine manager shutdown in the finalizer. |
VLLM_ALLOW_INSECURE_SERIALIZATION | 0 (vllm/envs.py:1710-1712) | vllm/v1/serial_utils.py:213 | Allows pickled objects and typed utility results on the ZMQ channel. |
Related concepts#
- The engine client: AsyncLLM in the API server: how AsyncLLM uses this client.
- The process and thread architecture of vllm serve: the sockets and processes on both sides.
- Data parallel serving: several engine cores behind one endpoint: load balancing modes, waves and the DP coordinator.
- v1/engine/core.py: EngineCore and the engine core process: the engine side of the sockets.
- Processes, ZMQ and shared memory: ZMQ socket types in general.
Check yourself#
- 1Which client class doeseasy
vllm serveuse with one engine core, and which socket types does it bind?Show answer
AsyncMPClient(vllm/v1/engine/core_client.py:170-173). It binds aROUTERsocket for requests and aPULLsocket for outputs (vllm/v1/engine/core_client.py:673-682). - 2How does the client know which engine core a message on the ROUTER socket goes to?medium
Show answer
The first frame is the engine identity: the DP rank as 2 little-endian bytes (
vllm/v1/engine/core_client.py:749-751,vllm/v1/engine/core_client.py:1294). Each engine core sets the same bytes as the identity of its DEALER socket (vllm/v1/engine/core.py:1178-1179). - 3A utility reply and a step output both arrive on the PULL socket. How does the output task tell them apart?medium
Show answer
A utility reply has
utility_outputset. The task completes the future of its call id and does not put the message intooutputs_queue(vllm/v1/engine/core_client.py:1214-1243). - 4Two engines: engine 0 has waiting 2, running 6, KV usage 0.9; engine 1 has waiting 0, running 9. One API server, and no request in flight. Which engine doeshard
DPLBAsyncMPClientpick?Show answer
Engine 0: base max(1 × 0, 8) = 8, penalty 2 × 6.0 × 0.4 = 4.8, score 12.8. Engine 1: base max(0, 9) = 9, no penalty, score 9. It picks engine 1 (
vllm/v1/engine/core_client.py:1777-1789).