A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 15, 16:00 UTC
[V1] DP scale-out (1/N): Use zmq ROUTER/DEALER sockets for input queue (#15906)
Signed-off-by: Nick Hill <nhill@redhat.com>
Nick Hill committed
Apr 4, 2025 at 12:56 UTC
651cf0fec19ed1da44ad266066f86b74e5246006
4 files changed
+113
-69
vllm/utils.py
+25
-10
@@ -2189,6 +2189,8 @@ def make_zmq_socket(
2189
ctx: Union[zmq.asyncio.Context, zmq.Context], # type: ignore[name-defined]
2190
path: str,
2191
socket_type: Any,
2192
+ bind: Optional[bool] = None,
2193
+ identity: Optional[bytes] = None,
2194
) -> Union[zmq.Socket, zmq.asyncio.Socket]: # type: ignore[name-defined]
2195
"""Make a ZMQ socket with the proper bind/connect semantics."""
2196
@@ -2207,16 +2209,24 @@ def make_zmq_socket(
2209
else:
2210
buf_size = -1 # Use system default buffer size
2211
2210
- if socket_type == zmq.constants.PULL:
2211
- socket.setsockopt(zmq.constants.RCVHWM, 0)
2212
- socket.setsockopt(zmq.constants.RCVBUF, buf_size)
2212
+ if bind is None:
2213
+ bind = socket_type != zmq.PUSH
2214
+
2215
+ if socket_type in (zmq.PULL, zmq.DEALER, zmq.ROUTER):
2216
+ socket.setsockopt(zmq.RCVHWM, 0)
2217
+ socket.setsockopt(zmq.RCVBUF, buf_size)
2218
+
2219
+ if socket_type in (zmq.PUSH, zmq.DEALER, zmq.ROUTER):
2220
+ socket.setsockopt(zmq.SNDHWM, 0)
2221
+ socket.setsockopt(zmq.SNDBUF, buf_size)
2222
+
2223
+ if identity is not None:
2224
+ socket.setsockopt(zmq.IDENTITY, identity)
2225
+
2226
+ if bind:
2227
socket.bind(path)
2214
- elif socket_type == zmq.constants.PUSH:
2215
- socket.setsockopt(zmq.constants.SNDHWM, 0)
2216
- socket.setsockopt(zmq.constants.SNDBUF, buf_size)
2217
- socket.connect(path)
2228
else:
2219
- raise ValueError(f"Unknown Socket Type: {socket_type}")
2229
+ socket.connect(path)
2230
2231
return socket
2232
@@ -2225,14 +2235,19 @@ def make_zmq_socket(
2235
def zmq_socket_ctx(
2236
path: str,
2237
socket_type: Any,
2238
+ bind: Optional[bool] = None,
2239
linger: int = 0,
2240
+ identity: Optional[bytes] = None,
2241
) -> Iterator[zmq.Socket]:
2242
"""Context manager for a ZMQ socket"""
2243
2244
ctx = zmq.Context() # type: ignore[attr-defined]
2245
try:
2234
- yield make_zmq_socket(ctx, path, socket_type)
2235
-
2246
+ yield make_zmq_socket(ctx,
2247
+ path,
2248
+ socket_type,
2249
+ bind=bind,
2250
+ identity=identity)
2251
except KeyboardInterrupt:
2252
logger.debug("Got Keyboard Interrupt.")
2253
vllm/v1/engine/core.py
+16
-12
@@ -318,6 +318,11 @@ class EngineCoreProc(EngineCore):
318
):
319
super().__init__(vllm_config, executor_class, log_stats)
320
321
+ self.step_fn = (self.step if self.batch_queue is None else
322
+ self.step_with_batch_queue)
323
+
324
+ self.global_unfinished_reqs = False
325
+
326
# Background Threads and Queues for IO. These enable us to
327
# overlap ZMQ socket IO with GPU since they release the GIL,
328
# and to overlap some serialization/deserialization with the
@@ -327,22 +332,16 @@ class EngineCoreProc(EngineCore):
332
Any]] = queue.Queue()
333
self.output_queue: queue.Queue[EngineCoreOutputs] = queue.Queue()
334
threading.Thread(target=self.process_input_socket,
330
- args=(input_path, ),
335
+ args=(input_path, engine_index),
336
daemon=True).start()
337
threading.Thread(target=self.process_output_socket,
338
args=(output_path, engine_index),
339
daemon=True).start()
340
336
- self.global_unfinished_reqs = False
337
-
338
- self.step_fn = (self.step if self.batch_queue is None else
339
- self.step_with_batch_queue)
340
-
341
@staticmethod
342
def run_engine_core(*args,
343
dp_rank: int = 0,
344
local_dp_rank: int = 0,
345
- ready_pipe,
345
**kwargs):
346
"""Launch EngineCore busy loop in background process."""
347
@@ -377,9 +376,6 @@ class EngineCoreProc(EngineCore):
376
else:
377
engine_core = EngineCoreProc(*args, **kwargs)
378
380
- # Send Readiness signal to EngineClient.
381
- ready_pipe.send({"status": "READY"})
382
-
379
engine_core.run_busy_loop()
380
381
except SystemExit:
@@ -476,14 +472,22 @@ class EngineCoreProc(EngineCore):
472
and not isinstance(v, p.annotation) else v
473
for v, p in zip(args, arg_types))
474
479
- def process_input_socket(self, input_path: str):
475
+ def process_input_socket(self, input_path: str, engine_index: int):
476
"""Input socket IO thread."""
477
478
# Msgpack serialization decoding.
479
add_request_decoder = MsgpackDecoder(EngineCoreRequest)
480
generic_decoder = MsgpackDecoder()
481
+ identity = engine_index.to_bytes(length=2, byteorder="little")
482
+
483
+ with zmq_socket_ctx(input_path,
484
+ zmq.DEALER,
485
+ identity=identity,
486
+ bind=False) as socket:
487
+
488
+ # Send ready message to front-end once input socket is connected.
489
+ socket.send(b'READY')
490
486
- with zmq_socket_ctx(input_path, zmq.constants.PULL) as socket:
491
while True:
492
# (RequestType, RequestData)
493
type_frame, data_frame = socket.recv_multipart(copy=False)
vllm/v1/engine/core_client.py
+71
-37
@@ -8,7 +8,7 @@ import threading
8
import uuid
9
import weakref
10
from abc import ABC, abstractmethod
11
-from collections.abc import Awaitable, Sequence
11
+from collections.abc import Awaitable
12
from concurrent.futures import Future
13
from dataclasses import dataclass, field
14
from threading import Thread
@@ -35,6 +35,8 @@ AnyFuture = Union[asyncio.Future[Any], Future[Any]]
35
36
_R = TypeVar('_R') # Return type for collective_rpc
37
38
+STARTUP_POLL_PERIOD_MS = 10000
39
+
40
41
class EngineCoreClient(ABC):
42
"""
@@ -261,15 +263,13 @@ class CoreEngine:
263
vllm_config: VllmConfig,
264
executor_class: type[Executor],
265
log_stats: bool,
264
- ctx: Union[zmq.Context, zmq.asyncio.Context],
266
+ input_path: str,
267
output_path: str,
268
index: int = 0,
269
local_dp_rank: int = 0,
270
):
269
- # Paths and sockets for IPC.
270
- input_path = get_open_zmq_ipc_path()
271
- self.input_socket = make_zmq_socket(ctx, input_path,
272
- zmq.constants.PUSH)
271
+ self.index = index
272
+ self.identity = index.to_bytes(length=2, byteorder="little")
273
try:
274
# Start EngineCore in background process.
275
self.proc_handle = BackgroundProcHandle(
@@ -291,14 +291,9 @@ class CoreEngine:
291
# Ensure socket is closed if process fails to start.
292
self.close()
293
294
- def send_multipart(self, msg_parts: Sequence):
295
- return self.input_socket.send_multipart(msg_parts, copy=False)
296
-
294
def close(self):
295
if proc_handle := getattr(self, "proc_handle", None):
296
proc_handle.shutdown()
300
- if socket := getattr(self, "input_socket", None):
301
- socket.close(linger=0)
297
298
299
@dataclass
@@ -309,6 +304,7 @@ class BackgroundResources:
304
ctx: Union[zmq.Context]
305
core_engines: list[CoreEngine] = field(default_factory=list)
306
output_socket: Optional[Union[zmq.Socket, zmq.asyncio.Socket]] = None
307
+ input_socket: Optional[Union[zmq.Socket, zmq.asyncio.Socket]] = None
308
shutdown_path: Optional[str] = None
309
310
def __call__(self):
@@ -321,6 +317,8 @@ class BackgroundResources:
317
# aren't explicitly closed first.
318
if self.output_socket is not None:
319
self.output_socket.close(linger=0)
320
+ if self.input_socket is not None:
321
+ self.input_socket.close(linger=0)
322
if self.shutdown_path is not None:
323
# We must ensure that the sync output socket is
324
# closed cleanly in its own thread.
@@ -387,21 +385,51 @@ class MPClient(EngineCoreClient):
385
386
# Paths and sockets for IPC.
387
self.output_path = get_open_zmq_ipc_path()
388
+ input_path = get_open_zmq_ipc_path()
389
+ self.input_socket = make_zmq_socket(self.ctx,
390
+ input_path,
391
+ zmq.ROUTER,
392
+ bind=True)
393
+ self.resources.input_socket = self.input_socket
394
395
new_core_engine = lambda index, local_dp_rank=None: CoreEngine(
392
- vllm_config, executor_class, log_stats, self.ctx, self.output_path,
393
- index, local_dp_rank)
396
+ vllm_config, executor_class, log_stats, input_path, self.
397
+ output_path, index, local_dp_rank)
398
399
# Start engine core process(es).
400
self._init_core_engines(vllm_config, new_core_engine,
401
self.resources.core_engines)
402
403
# Wait for engine core process(es) to start.
400
- for engine in self.resources.core_engines:
401
- engine.proc_handle.wait_for_startup()
404
+ self._wait_for_engine_startup()
405
406
self.utility_results: dict[int, AnyFuture] = {}
407
408
+ def _wait_for_engine_startup(self):
409
+ # Get a sync handle to the socket which can be sync or async.
410
+ sync_input_socket = zmq.Socket.shadow(self.input_socket)
411
+
412
+ # Wait for engine core process(es) to send ready messages.
413
+ identities = set(eng.index for eng in self.resources.core_engines)
414
+ while identities:
415
+ while not sync_input_socket.poll(timeout=STARTUP_POLL_PERIOD_MS):
416
+ logger.info("Waiting for %d core engine proc(s) to start: %s",
417
+ len(identities), identities)
418
+ eng_id_bytes, msg = sync_input_socket.recv_multipart()
419
+ eng_id = int.from_bytes(eng_id_bytes, byteorder="little")
420
+ if eng_id not in identities:
421
+ raise RuntimeError(f"Unexpected or duplicate engine: {eng_id}")
422
+ if msg != b'READY':
423
+ raise RuntimeError(f"Engine {eng_id} failed: {msg.decode()}")
424
+ logger.info("Core engine process %d ready.", eng_id)
425
+ identities.discard(eng_id)
426
+
427
+ # Double check that the process are running.
428
+ for engine in self.resources.core_engines:
429
+ proc = engine.proc_handle.proc
430
+ if proc.exitcode is not None:
431
+ raise RuntimeError(f"Engine proc {proc.name} not running")
432
+
433
def _init_core_engines(
434
self,
435
vllm_config: VllmConfig,
@@ -494,9 +522,10 @@ class SyncMPClient(MPClient):
522
return self.outputs_queue.get()
523
524
def _send_input(self, request_type: EngineCoreRequestType, request: Any):
497
- # (RequestType, SerializedRequest)
498
- msg = (request_type.value, self.encoder.encode(request))
499
- self.core_engine.send_multipart(msg)
525
+ # (Identity, RequestType, SerializedRequest)
526
+ msg = (self.core_engine.identity, request_type.value,
527
+ self.encoder.encode(request))
528
+ self.input_socket.send_multipart(msg, copy=False)
529
530
def call_utility(self, method: str, *args) -> Any:
531
call_id = uuid.uuid1().int >> 64
@@ -625,30 +654,34 @@ class AsyncMPClient(MPClient):
654
assert self.outputs_queue is not None
655
return await self.outputs_queue.get()
656
628
- async def _send_input(self, request_type: EngineCoreRequestType,
629
- request: Any) -> None:
630
- await self.core_engine.send_multipart(
631
- (request_type.value, self.encoder.encode(request)))
657
+ def _send_input(self,
658
+ request_type: EngineCoreRequestType,
659
+ request: Any,
660
+ engine: Optional[CoreEngine] = None) -> Awaitable[None]:
661
+ if engine is None:
662
+ engine = self.core_engine
663
633
- self._ensure_output_queue_task()
664
+ message = (request_type.value, self.encoder.encode(request))
665
+ return self._send_input_message(message, engine)
666
+
667
+ def _send_input_message(self, message: tuple[bytes, bytes],
668
+ engine: CoreEngine) -> Awaitable[None]:
669
+ message = (engine.identity, ) + message # type: ignore[assignment]
670
+ return self.input_socket.send_multipart(message, copy=False)
671
672
async def call_utility_async(self, method: str, *args) -> Any:
673
return await self._call_utility_async(method,
674
*args,
675
engine=self.core_engine)
676
640
- async def _call_utility_async(
641
- self,
642
- method: str,
643
- *args,
644
- engine: CoreEngine,
645
- ) -> Any:
677
+ async def _call_utility_async(self, method: str, *args,
678
+ engine: CoreEngine) -> Any:
679
call_id = uuid.uuid1().int >> 64
680
future = asyncio.get_running_loop().create_future()
681
self.utility_results[call_id] = future
682
message = (EngineCoreRequestType.UTILITY.value,
683
self.encoder.encode((call_id, method, args)))
651
- await engine.send_multipart(message)
684
+ await self._send_input_message(message, engine)
685
self._ensure_output_queue_task()
686
return await future
687
@@ -657,6 +690,7 @@ class AsyncMPClient(MPClient):
690
# tokenized.
691
request.prompt = None
692
await self._send_input(EngineCoreRequestType.ADD, request)
693
+ self._ensure_output_queue_task()
694
695
async def abort_requests_async(self, request_ids: list[str]) -> None:
696
if len(request_ids) > 0:
@@ -761,15 +795,15 @@ class DPAsyncMPClient(AsyncMPClient):
795
self.reqs_in_flight[request.request_id] = chosen_engine
796
chosen_engine.num_reqs_in_flight += 1
797
if self.num_engines_running >= len(self.core_engines):
764
- await chosen_engine.send_multipart(msg)
798
+ await self._send_input_message(msg, chosen_engine)
799
else:
800
# Send request to chosen engine and dp start loop
801
# control message to all other engines.
802
self.num_engines_running += len(self.core_engines)
803
await asyncio.gather(*[
770
- engine.send_multipart(msg if engine is
771
- chosen_engine else self.start_dp_msg)
772
- for engine in self.core_engines
804
+ self._send_input_message(
805
+ msg if engine is chosen_engine else self.start_dp_msg,
806
+ engine) for engine in self.core_engines
807
])
808
809
self._ensure_output_queue_task()
@@ -794,7 +828,7 @@ class DPAsyncMPClient(AsyncMPClient):
828
# sure to start the other engines:
829
self.num_engines_running = len(self.core_engines)
830
coros = [
797
- engine.send_multipart(self.start_dp_msg)
831
+ self._send_input_message(self.start_dp_msg, engine)
832
for engine in self.core_engines
833
if not engine.num_reqs_in_flight
834
]
@@ -820,5 +854,5 @@ class DPAsyncMPClient(AsyncMPClient):
854
855
async def _abort_requests(self, request_ids: list[str],
856
engine: CoreEngine) -> None:
823
- await engine.send_multipart((EngineCoreRequestType.ABORT.value,
824
- self.encoder.encode(request_ids)))
857
+ await self._send_input(EngineCoreRequestType.ABORT, request_ids,
858
+ engine)
vllm/v1/utils.py
+1
-10
@@ -105,12 +105,9 @@ class BackgroundProcHandle:
105
process_kwargs: dict[Any, Any],
106
):
107
context = get_mp_context()
108
- self.reader, writer = context.Pipe(duplex=False)
108
110
- assert ("ready_pipe" not in process_kwargs
111
- and "input_path" not in process_kwargs
109
+ assert ("input_path" not in process_kwargs
110
and "output_path" not in process_kwargs)
113
- process_kwargs["ready_pipe"] = writer
111
process_kwargs["input_path"] = input_path
112
process_kwargs["output_path"] = output_path
113
@@ -122,12 +119,6 @@ class BackgroundProcHandle:
119
input_path, output_path)
120
self.proc.start()
121
125
- def wait_for_startup(self):
126
- # Wait for startup.
127
- if self.reader.recv()["status"] != "READY":
128
- raise RuntimeError(f"{self.proc.name} initialization failed. "
129
- "See root cause above.")
130
-
122
def shutdown(self):
123
self._finalizer()
124