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 14, 22:35 UTC
[BugFix] Harden distributed DP startup (#21538)
Signed-off-by: Nick Hill <nhill@redhat.com>
Nick Hill committed
Aug 1, 2025 at 22:40 UTC
881e1af43a1bb7b4bedd373e413eb7ad9dc9f920
3 files changed
+56
-20
vllm/utils/__init__.py
+3
@@ -2794,6 +2794,9 @@ def make_zmq_socket(
2794
if linger is not None:
2795
socket.setsockopt(zmq.LINGER, linger)
2796
2797
+ if socket_type == zmq.XPUB:
2798
+ socket.setsockopt(zmq.XPUB_VERBOSE, True)
2799
+
2800
# Determine if the path is a TCP socket with an IPv6 address.
2801
# Enable IPv6 on the zmq socket if so.
2802
scheme, host, _ = split_zmq_path(path)
vllm/v1/engine/coordinator.py
+12
@@ -172,6 +172,18 @@ class DPCoordinatorProc:
172
bind=True,
173
) as publish_back:
174
175
+ # Wait until all engines subscribe.
176
+ for _ in self.engines:
177
+ if publish_back.recv() != b'\x01':
178
+ logger.error(
179
+ "DP Coordinator received unexpected message while "
180
+ "waiting for engines to subscribe")
181
+ return
182
+ # Send ready message to engines.
183
+ publish_back.send(b"READY")
184
+
185
+ logger.info("All engine subscriptions received by DP coordinator")
186
+
187
poller = zmq.Poller()
188
poller.register(publish_front, zmq.POLLIN)
189
poller.register(output_back, zmq.POLLIN)
vllm/v1/engine/core.py
+41
-20
@@ -461,8 +461,11 @@ class EngineCoreProc(EngineCore):
461
self.has_coordinator = addresses.coordinator_output is not None
462
self.frontend_stats_publish_address = (
463
addresses.frontend_stats_publish_address)
464
+ logger.debug("Has DP Coordinator: %s, stats publish address: %s",
465
+ self.has_coordinator,
466
+ self.frontend_stats_publish_address)
467
# Only publish request queue stats to coordinator for "internal"
465
- # LB mode.
468
+ # and "hybrid" LB modes .
469
self.publish_dp_lb_stats = (
470
self.has_coordinator
471
and not vllm_config.parallel_config.data_parallel_external_lb)
@@ -472,25 +475,38 @@ class EngineCoreProc(EngineCore):
475
super().__init__(vllm_config, executor_class, log_stats,
476
executor_fail_callback)
477
478
+ # Background Threads and Queues for IO. These enable us to
479
+ # overlap ZMQ socket IO with GPU since they release the GIL,
480
+ # and to overlap some serialization/deserialization with the
481
+ # model forward pass.
482
+ # Threads handle Socket <-> Queues and core_busy_loop uses Queue.
483
+ ready_event = threading.Event()
484
+ input_thread = threading.Thread(target=self.process_input_sockets,
485
+ args=(addresses.inputs,
486
+ addresses.coordinator_input,
487
+ identity, ready_event),
488
+ daemon=True)
489
+ input_thread.start()
490
+
491
+ self.output_thread = threading.Thread(
492
+ target=self.process_output_sockets,
493
+ args=(addresses.outputs, addresses.coordinator_output,
494
+ self.engine_index),
495
+ daemon=True)
496
+ self.output_thread.start()
497
+
498
+ # Don't complete handshake until DP coordinator ready message is
499
+ # received.
500
+ while not ready_event.wait(timeout=10):
501
+ if not input_thread.is_alive():
502
+ raise RuntimeError(
503
+ "Input socket thread died during startup")
504
+ assert addresses.coordinator_input is not None
505
+ logger.info("Waiting for READY message from DP Coordinator...")
506
+
507
self.step_fn = (self.step if self.batch_queue is None else
508
self.step_with_batch_queue)
509
478
- # Background Threads and Queues for IO. These enable us to
479
- # overlap ZMQ socket IO with GPU since they release the GIL,
480
- # and to overlap some serialization/deserialization with the
481
- # model forward pass.
482
- # Threads handle Socket <-> Queues and core_busy_loop uses Queue.
483
- threading.Thread(target=self.process_input_sockets,
484
- args=(addresses.inputs, addresses.coordinator_input,
485
- identity),
486
- daemon=True).start()
487
- self.output_thread = threading.Thread(
488
- target=self.process_output_sockets,
489
- args=(addresses.outputs, addresses.coordinator_output,
490
- self.engine_index),
491
- daemon=True)
492
- self.output_thread.start()
493
-
510
@contextmanager
511
def _perform_handshakes(
512
self,
@@ -505,10 +521,10 @@ class EngineCoreProc(EngineCore):
521
522
For DP=1 or offline mode, this is with the colocated front-end process.
523
508
- For DP>1 with internal loadbalancing this is with the shared front-end
524
+ For DP>1 with internal load-balancing this is with the shared front-end
525
process which may reside on a different node.
526
511
- For DP>1 with external or hybrid loadbalancing, two handshakes are
527
+ For DP>1 with external or hybrid load-balancing, two handshakes are
528
performed:
529
- With the rank 0 front-end process which retrieves the
530
DP Coordinator ZMQ addresses and DP process group address.
@@ -772,7 +788,7 @@ class EngineCoreProc(EngineCore):
788
789
def process_input_sockets(self, input_addresses: list[str],
790
coord_input_address: Optional[str],
775
- identity: bytes):
791
+ identity: bytes, ready_event: threading.Event):
792
"""Input socket IO thread."""
793
794
# Msgpack serialization decoding.
@@ -809,9 +825,14 @@ class EngineCoreProc(EngineCore):
825
# back to us.
826
input_socket.send(b'')
827
poller.register(input_socket, zmq.POLLIN)
828
+
829
if coord_socket is not None:
830
+ # Wait for ready message from coordinator.
831
+ assert coord_socket.recv() == b"READY"
832
poller.register(coord_socket, zmq.POLLIN)
833
834
+ ready_event.set()
835
+ del ready_event
836
while True:
837
for input_socket, _ in poller.poll():
838
# (RequestType, RequestData)