vLLM Explained

v1/engine/core_client.py: the engine core clients

AdvancedCode mapabout 10 min read + 4 min deep dives

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.

Filevllm/v1/engine/core_client.py, 2,162 lines of Python. Nine classes and one helper function.
Main classesEngineCoreClient (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 inThe 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 byAsyncLLM (make_async_mp_client) and LLMEngine (make_client).
Callspyzmq sockets, MsgpackEncoder / MsgpackDecoder, launch_core_engines in v1/engine/utils.py, the DP coordinator.
Not in this fileThe 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#

What it calls#

Key classes and functions#

NameLineWhat it does
EngineCoreClient.make_clientvllm/v1/engine/core_client.py:97-136Factory for the offline engine: sync or in-process.
EngineCoreClient.make_async_mp_clientvllm/v1/engine/core_client.py:138-173Factory for AsyncLLM: picks the class by the DP settings.
InprocClientvllm/v1/engine/core_client.py:364-494Wraps an EngineCore in the same process; no busy loop.
BackgroundResourcesvllm/v1/engine/core_client.py:497-584Sockets, tasks and the engine manager; the finalizer; validate_alive.
MPClient.__init__vllm/v1/engine/core_client.py:606-782Binds sockets, launches engines, waits for ready messages, starts the monitor.
MPClient._apply_ready_responsevllm/v1/engine/core_client.py:847-923Copies start-up values of each engine into the front-end config.
_process_utility_outputvllm/v1/engine/core_client.py:929-948Completes the future of a utility call.
SyncMPClientvllm/v1/engine/core_client.py:951-1140Blocking client with an output thread.
AsyncMPClient._ensure_output_queue_taskvllm/v1/engine/core_client.py:1187-1262Starts the task that reads the PULL socket.
AsyncMPClient._send_input / _send_input_messagevllm/v1/engine/core_client.py:1275-1294Encodes and sends one message to one engine.
AsyncMPClient._call_utility_asyncvllm/v1/engine/core_client.py:1316-1328A remote method call with a call id and a future.
AsyncMPClient.add_request_asyncvllm/v1/engine/core_client.py:1333-1336Sets client_index and sends ADD.
DPAsyncMPClient._ensure_stats_update_taskvllm/v1/engine/core_client.py:1513-1597Reads load counts and waves from the DP coordinator.
DPLBAsyncMPClient.get_core_engine_for_requestvllm/v1/engine/core_client.py:1743-1804Load balancing: picks the engine with the lowest score.
DPLBAsyncMPClient.abort_requests_asyncvllm/v1/engine/core_client.py:1866-1890Sends 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,
        )
  1. 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.
  2. Lines 158-164. DP > 1 with an external load balancer: DPAsyncMPClient, which sends everything to one engine core.
  3. Lines 165-169. DP > 1 otherwise: DPLBAsyncMPClient, which balances over all engine cores.
  4. 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)
  1. Lines 621-622. One ZMQ context with 2 I/O threads. The asyncio clients wrap it in a zmq.asyncio.Context.
  2. Lines 627-628. BackgroundResources holds everything that must be closed. A weakref.finalize calls 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
  1. Lines 673-679. A ROUTER socket for requests, bound by the client. A ROUTER sends each message to the peer whose identity is the first frame.
  2. Lines 680-682. A PULL socket for outputs. All engine cores push into it.
  3. Lines 686-693. The real bound addresses go to the engines.
  4. Lines 695-697. launch_core_engines starts 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()
  1. Lines 740-743. With internal load balancing, the client manages all DP ranks. With hybrid or external load balancing, only the local ones.
  2. 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).
  3. Lines 756-768. A blocking poll with timeout VLLM_ENGINE_READY_TIMEOUT_S. The error message names the variable.
  4. Lines 769-771. Each ready message carries the identity and an EngineCoreReadyResponse, which _apply_ready_response applies.
  5. Line 773. core_engine is the first engine: the default target of a DP = 1 client.
  6. 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"
        )
  1. Line 1211. Receive one multipart message without a copy.
  2. Line 1212. validate_alive raises EngineDeadError when the message is the single frame ENGINE_CORE_DEAD (vllm/v1/engine/core_client.py:581-584).
  3. Line 1213. Decode into EngineCoreOutputs.
  4. Lines 1214-1243 (first excerpt ends at 1214). A message with utility_output is a reply to a utility call, or one of two special notifications. It never goes into the queue.
  5. Lines 1245-1251. A subclass hook. DPLBAsyncMPClient uses it to count finished requests per engine (vllm/v1/engine/core_client.py:1810-1817).
  6. Lines 1253-1254. Only messages with outputs or stats go to outputs_queue.
  7. Lines 1255-1258. An exception goes into the queue. A cancel puts an EngineDeadError there. The next get_output_async() raises it (vllm/v1/engine/core_client.py:1264-1273).
  8. Lines 1260-1262. The task is named EngineCoreOutputQueueTask. The constructor starts it if a loop runs, so that early EXECUTOR_FAILED messages 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)
  1. Line 1284. The frames are the type byte (for example b"\x00" for ADD) and the encoded buffers.
  2. Line 1290. ensure_alive raises EngineDeadError when the dead flag is set (vllm/v1/engine/core_client.py:803-805).
  3. Line 1294. The engine identity goes first. The ROUTER uses it to route the message, and removes it before delivery. send_multipart of the asyncio socket returns an awaitable.
  4. Line 1334. client_index tells the engine core which output socket to use for this request.
  5. 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
  1. Line 1319. A 64-bit call id from a UUID.
  2. Lines 1320-1321. An asyncio future, stored under the call id.
  3. Lines 1322-1325. The message is UTILITY plus (client_index, call_id, method, args).
  4. 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
  1. Lines 1755-1759 (above the excerpt). A request with data_parallel_rank set, 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).
  2. Line 1768. The scan starts at eng_start_index, which depends on the client index, and rotates after each pick (line 1798).
  3. Lines 1777-1778. The base score is the larger of the coordinator's waiting + running and this client's in-flight count times the number of clients.
  4. 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%.
  5. 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 dive

Data flow: frames and types

DirectionFramesPayload typeWhere
client → engine, ADDidentity, b"\x00", msgpack buffersEngineCoreRequestvllm/v1/engine/core_client.py:1333-1335
client → engine, ABORTidentity, b"\x01", msgpacklist[str] of internal idsvllm/v1/engine/core_client.py:1338-1340
client → engine, UTILITYidentity, b"\x03", msgpack(client_index, call_id, method, args)vllm/v1/engine/core_client.py:1322-1325
engine → client, readyidentity, payloadmsgpack EngineCoreReadyResponsevllm/v1/engine/core_client.py:847-855
engine → client, outputsmsgpack buffersEngineCoreOutputsvllm/v1/engine/core_client.py:1211-1213
engine → client, deathone frame b"ENGINE_CORE_DEAD"nonevllm/v1/engine/core.py:1156, vllm/v1/engine/core_client.py:581-584
coordinator → client (DP)one buffermsgpack (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 dive

Processes, tasks and threads

ClientBackground workKindWhere
every MPClient that launched enginesMPClientEngineMonitor: waits for an engine process to exit, sets engine_dead, shuts downdaemon threadvllm/v1/engine/core_client.py:818-845
AsyncMPClientEngineCoreOutputQueueTaskasyncio taskvllm/v1/engine/core_client.py:1260-1262
DPAsyncMPClientstats task: XSUB socket to the coordinator, PAIR socket for scale eventsasyncio taskvllm/v1/engine/core_client.py:1522-1597
SyncMPClientEngineCoreOutputQueueThread: polls the PULL socket and a shutdown PAIR socketdaemon threadvllm/v1/engine/core_client.py:985-1021
InprocClientnone: 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 dive

Worked example: one load-balancing decision

Two API servers (client_count = 2) and two engine cores. The last coordinator snapshot for this client says:

EnginewaitingrunningKV usagethis client's in-flight
03100.804
10120.405
  1. Engine 0: base = max(2 × 4, 3 + 10) = 13. Penalty = 3 × 6.0 × max(0, 0.80 − 0.5) = 5.4. Score = 18.4.
  2. Engine 1: base = max(2 × 5, 0 + 12) = 12. No waiting requests, so no penalty. Score = 12.
  3. The client picks engine 1. It adds client_count = 2 to engine 1's local waiting count, records the request in reqs_in_flight, and adds 1 to engine_inflight for engine 1 (vllm/v1/engine/core_client.py:1792-1803).
  4. 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 on include_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 dive

Error paths: what raises, what only logs

SituationResultWhere
no ready message within the timeoutTimeoutError that names VLLM_ENGINE_READY_TIMEOUT_Svllm/v1/engine/core_client.py:756-768
the constructor fails half-waythe finalizer closes what exists; the exception goes onvllm/v1/engine/core_client.py:779-782
ENGINE_CORE_DEAD receivedengine_dead = True, EngineDeadErrorvllm/v1/engine/core_client.py:581-584
an engine process exitswarning "engine core exited unexpectedly"; flag set; client shuts downvllm/v1/engine/core_client.py:829-838
a send after the engine diedEngineDeadError from ensure_alivevllm/v1/engine/core_client.py:803-805
any error after deathreplaced by EngineDeadError(suppress_context=True), so the root cause stays clearvllm/v1/engine/core_client.py:797-801
a utility method fails in the enginethe future gets Exception(failure_message)vllm/v1/engine/core_client.py:933-937
the caller of a utility call was cancelledthe result is dropped; a failure is logged as an errorvllm/v1/engine/core_client.py:941-948
engines disagree in a consensus callRuntimeErrorvllm/v1/engine/core_client.py:1308-1314
Deep dive

Environment variables

VariableDefaultRead atEffect
VLLM_ENGINE_READY_TIMEOUT_S600 (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_SECONDS5 (vllm/envs.py:1865-1867)vllm/v1/engine/core_client.py:525-527Timeout for the engine manager shutdown in the finalizer.
VLLM_ALLOW_INSECURE_SERIALIZATION0 (vllm/envs.py:1710-1712)vllm/v1/serial_utils.py:213Allows pickled objects and typed utility results on the ZMQ channel.

Check yourself#

  1. 1
    Which client class does vllm serve use with one engine core, and which socket types does it bind?
    easy
    Show answer

    AsyncMPClient (vllm/v1/engine/core_client.py:170-173). It binds a ROUTER socket for requests and a PULL socket for outputs (vllm/v1/engine/core_client.py:673-682).

  2. 2
    How 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).

  3. 3
    A 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_output set. The task completes the future of its call id and does not put the message into outputs_queue (vllm/v1/engine/core_client.py:1214-1243).

  4. 4
    Two 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 does DPLBAsyncMPClient pick?
    hard
    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).