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, 11:00 UTC
[V1] DP scale-out (2/N): Decouple engine process management and comms (#15977)
Signed-off-by: Nick Hill <nhill@redhat.com>
Nick Hill committed
May 13, 2025 at 10:48 UTC
55aa7af9947244bdc427bfae01e750b1c157bd8f
10 files changed
+525
-252
tests/async_engine/test_async_llm_engine.py
+1
-1
@@ -41,7 +41,7 @@ class MockEngine:
41
self.abort_request_calls = 0
42
self.request_id = None
43
# Ugly, remove dependency when possible
44
- self.parallel_config = ParallelConfig(1, 1, False)
44
+ self.parallel_config = ParallelConfig()
45
self.model_config = MockModelConfig()
46
47
async def step_async(self, virtual_engine):
tests/v1/engine/test_engine_core_client.py
+8
-7
@@ -18,9 +18,10 @@ from vllm.platforms import current_platform
18
from vllm.usage.usage_lib import UsageContext
19
from vllm.v1.engine import EngineCoreRequest
20
from vllm.v1.engine.core import EngineCore
21
-from vllm.v1.engine.core_client import (AsyncMPClient, CoreEngine,
22
- EngineCoreClient, SyncMPClient)
21
+from vllm.v1.engine.core_client import (AsyncMPClient, EngineCoreClient,
22
+ SyncMPClient)
23
from vllm.v1.executor.abstract import Executor
24
+from vllm.v1.utils import CoreEngineProcManager
25
26
from ...distributed.conftest import MockSubscriber
27
from ...utils import create_new_process_for_each_test
@@ -348,13 +349,13 @@ def test_startup_failure(monkeypatch: pytest.MonkeyPatch):
349
350
# Monkey-patch to extract core process pid while it's starting.
351
core_proc_pid = [None]
351
- ce_ctor = CoreEngine.__init__
352
+ cepm_ctor = CoreEngineProcManager.__init__
353
353
- def patched_ce_ctor(self, *args, **kwargs):
354
- ce_ctor(self, *args, **kwargs)
355
- core_proc_pid[0] = self.proc_handle.proc.pid
354
+ def patched_cepm_ctor(self: CoreEngineProcManager, *args, **kwargs):
355
+ cepm_ctor(self, *args, **kwargs)
356
+ core_proc_pid[0] = self.processes[0].pid
357
357
- m.setattr(CoreEngine, "__init__", patched_ce_ctor)
358
+ m.setattr(CoreEngineProcManager, "__init__", patched_cepm_ctor)
359
360
t = time.time()
361
engine_args = EngineArgs(model=MODEL_NAME)
vllm/config.py
+19
-22
@@ -1668,25 +1668,17 @@ class ParallelConfig:
1668
data_parallel_size: int = 1
1669
"""Number of data parallel groups. MoE layers will be sharded according to
1670
the product of the tensor parallel size and data parallel size."""
1671
+ data_parallel_size_local: int = 1
1672
+ """Number of local data parallel groups."""
1673
data_parallel_rank: int = 0
1674
"""Rank of the data parallel group."""
1673
- _data_parallel_rank_local: Optional[int] = field(default=None, init=False)
1674
- """Private field to store the local rank of the data parallel group."""
1675
-
1676
- @property
1677
- def data_parallel_rank_local(self) -> int:
1678
- """Local rank of the data parallel group, defaults to global rank."""
1679
- if self._data_parallel_rank_local is None:
1680
- return self.data_parallel_rank
1681
- return self._data_parallel_rank_local
1682
-
1683
- @data_parallel_rank_local.setter
1684
- def data_parallel_rank_local(self, value: int) -> None:
1685
- """Set the local rank of the data parallel group."""
1686
- self._data_parallel_rank_local = value
1687
-
1675
+ data_parallel_rank_local: Optional[int] = None
1676
+ """Local rank of the data parallel group,
1677
+ set only in SPMD mode."""
1678
data_parallel_master_ip: str = "127.0.0.1"
1679
"""IP of the data parallel master."""
1680
+ data_parallel_rpc_port: int = 29550
1681
+ """Port for data parallel messaging."""
1682
data_parallel_master_port: int = 29500
1683
"""Port of the data parallel master."""
1684
enable_expert_parallel: bool = False
@@ -1734,13 +1726,16 @@ class ParallelConfig:
1726
1727
world_size: int = field(init=False)
1728
"""world_size is TPxPP, it affects the number of workers we create."""
1737
- world_size_across_dp: int = field(init=False)
1738
- """world_size_across_dp is TPxPPxDP, it is the size of the world
1739
- including data parallelism."""
1729
1730
rank: int = 0
1731
"""Global rank in distributed setup."""
1732
1733
+ @property
1734
+ def world_size_across_dp(self) -> int:
1735
+ """world_size_across_dp is TPxPPxDP, it is the size of the world
1736
+ including data parallelism."""
1737
+ return self.world_size * self.data_parallel_size
1738
+
1739
def get_next_dp_init_port(self) -> int:
1740
"""
1741
We might need to initialize process groups in multiple
@@ -1800,10 +1795,14 @@ class ParallelConfig:
1795
self.world_size = self.pipeline_parallel_size * \
1796
self.tensor_parallel_size
1797
1803
- if self.data_parallel_size > 1:
1798
+ if self.data_parallel_size_local > self.data_parallel_size:
1799
+ raise ValueError(
1800
+ f"data_parallel_size_local ({self.data_parallel_size_local}) "
1801
+ f"must be <= data_parallel_size ({self.data_parallel_size})")
1802
+
1803
+ if self.data_parallel_size > 1 or self.data_parallel_size_local == 0:
1804
# Data parallel was specified in the engine args.
1805
self.data_parallel_master_port = get_open_port()
1806
- # TODO multi-node
1806
else:
1807
# Otherwise fall back to env vars (e.g. for offline SPMD case).
1808
self.data_parallel_size = envs.VLLM_DP_SIZE
@@ -1812,8 +1811,6 @@ class ParallelConfig:
1811
self.data_parallel_master_ip = envs.VLLM_DP_MASTER_IP
1812
self.data_parallel_master_port = envs.VLLM_DP_MASTER_PORT
1813
1815
- self.world_size_across_dp = self.world_size * self.data_parallel_size
1816
-
1814
if self.distributed_executor_backend == "external_launcher":
1815
import os
1816
os.environ["VLLM_ENABLE_V1_MULTIPROCESSING"] = "0"
vllm/distributed/utils.py
+2
-1
@@ -22,6 +22,7 @@ from torch.distributed.rendezvous import rendezvous
22
23
import vllm.envs as envs
24
from vllm.logger import init_logger
25
+from vllm.utils import get_tcp_uri
26
27
logger = init_logger(__name__)
28
@@ -303,7 +304,7 @@ def stateless_init_torch_distributed_process_group(
304
always formed with process 1, 2, ..., 8, and the additional communication
305
channel is formed with process 9 and 10.
306
"""
306
- init_method = f"tcp://{host}:{port}"
307
+ init_method = get_tcp_uri(host, port)
308
backend = Backend(backend) # it is basically string
309
timeout = _get_default_timeout(backend)
310
vllm/engine/arg_utils.py
+38
@@ -283,6 +283,9 @@ class EngineArgs:
283
pipeline_parallel_size: int = ParallelConfig.pipeline_parallel_size
284
tensor_parallel_size: int = ParallelConfig.tensor_parallel_size
285
data_parallel_size: int = ParallelConfig.data_parallel_size
286
+ data_parallel_size_local: Optional[int] = None
287
+ data_parallel_address: Optional[str] = None
288
+ data_parallel_rpc_port: Optional[int] = None
289
enable_expert_parallel: bool = ParallelConfig.enable_expert_parallel
290
max_parallel_loading_workers: Optional[
291
int] = ParallelConfig.max_parallel_loading_workers
@@ -596,6 +599,21 @@ class EngineArgs:
599
**parallel_kwargs["tensor_parallel_size"])
600
parallel_group.add_argument("--data-parallel-size", "-dp",
601
**parallel_kwargs["data_parallel_size"])
602
+ parallel_group.add_argument('--data-parallel-size-local',
603
+ '-dpl',
604
+ type=int,
605
+ help='Number of data parallel replicas '
606
+ 'to run on this node.')
607
+ parallel_group.add_argument('--data-parallel-address',
608
+ '-dpa',
609
+ type=str,
610
+ help='Address of data parallel cluster '
611
+ 'head-node.')
612
+ parallel_group.add_argument('--data-parallel-rpc-port',
613
+ '-dpp',
614
+ type=int,
615
+ help='Port for data parallel RPC '
616
+ 'communication.')
617
parallel_group.add_argument(
618
"--enable-expert-parallel",
619
**parallel_kwargs["enable_expert_parallel"])
@@ -1019,10 +1037,30 @@ class EngineArgs:
1037
# but we should not do this here.
1038
placement_group = ray.util.get_current_placement_group()
1039
1040
+ # Local DP size defaults to global DP size if not set.
1041
+ data_parallel_size_local = self.data_parallel_size if (
1042
+ self.data_parallel_size_local
1043
+ is None) else self.data_parallel_size_local
1044
+
1045
+ # DP address, used in multi-node case for torch distributed group
1046
+ # and ZMQ sockets.
1047
+ data_parallel_address = self.data_parallel_address if (
1048
+ self.data_parallel_address
1049
+ is not None) else ParallelConfig.data_parallel_master_ip
1050
+
1051
+ # This port is only used when there are remote data parallel engines,
1052
+ # otherwise the local IPC transport is used.
1053
+ data_parallel_rpc_port = self.data_parallel_rpc_port if (
1054
+ self.data_parallel_rpc_port
1055
+ is not None) else ParallelConfig.data_parallel_rpc_port
1056
+
1057
parallel_config = ParallelConfig(
1058
pipeline_parallel_size=self.pipeline_parallel_size,
1059
tensor_parallel_size=self.tensor_parallel_size,
1060
data_parallel_size=self.data_parallel_size,
1061
+ data_parallel_size_local=data_parallel_size_local,
1062
+ data_parallel_master_ip=data_parallel_address,
1063
+ data_parallel_rpc_port=data_parallel_rpc_port,
1064
enable_expert_parallel=self.enable_expert_parallel,
1065
max_parallel_loading_workers=self.max_parallel_loading_workers,
1066
disable_custom_all_reduce=self.disable_custom_all_reduce,
vllm/entrypoints/cli/serve.py
+79
-2
@@ -1,14 +1,24 @@
1
# SPDX-License-Identifier: Apache-2.0
2
3
import argparse
4
+import signal
5
6
import uvloop
7
8
+import vllm.envs as envs
9
+from vllm import AsyncEngineArgs
10
from vllm.entrypoints.cli.types import CLISubcommand
11
from vllm.entrypoints.openai.api_server import run_server
12
from vllm.entrypoints.openai.cli_args import (make_arg_parser,
13
validate_parsed_serve_args)
11
-from vllm.utils import FlexibleArgumentParser
14
+from vllm.logger import init_logger
15
+from vllm.usage.usage_lib import UsageContext
16
+from vllm.utils import FlexibleArgumentParser, get_tcp_uri
17
+from vllm.v1.engine.core import EngineCoreProc
18
+from vllm.v1.engine.core_client import CoreEngineProcManager
19
+from vllm.v1.executor.abstract import Executor
20
+
21
+logger = init_logger(__name__)
22
23
24
class ServeSubcommand(CLISubcommand):
@@ -24,7 +34,10 @@ class ServeSubcommand(CLISubcommand):
34
if hasattr(args, 'model_tag') and args.model_tag is not None:
35
args.model = args.model_tag
36
27
- uvloop.run(run_server(args))
37
+ if args.headless:
38
+ run_headless(args)
39
+ else:
40
+ uvloop.run(run_server(args))
41
42
def validate(self, args: argparse.Namespace) -> None:
43
validate_parsed_serve_args(args)
@@ -42,6 +55,18 @@ class ServeSubcommand(CLISubcommand):
55
nargs='?',
56
help="The model tag to serve "
57
"(optional if specified in config)")
58
+ serve_parser.add_argument(
59
+ "--headless",
60
+ action='store_true',
61
+ default=False,
62
+ help="Run in headless mode. See multi-node data parallel "
63
+ "documentation for more details.")
64
+ serve_parser.add_argument(
65
+ '--data-parallel-start-rank',
66
+ '-dpr',
67
+ type=int,
68
+ default=0,
69
+ help='Starting data parallel rank for secondary nodes.')
70
serve_parser.add_argument(
71
"--config",
72
type=str,
@@ -57,3 +82,55 @@ class ServeSubcommand(CLISubcommand):
82
83
def cmd_init() -> list[CLISubcommand]:
84
return [ServeSubcommand()]
85
+
86
+
87
+def run_headless(args: argparse.Namespace):
88
+
89
+ # Create the EngineConfig.
90
+ engine_args = AsyncEngineArgs.from_cli_args(args)
91
+ usage_context = UsageContext.OPENAI_API_SERVER
92
+ vllm_config = engine_args.create_engine_config(usage_context=usage_context)
93
+
94
+ if not envs.VLLM_USE_V1:
95
+ raise RuntimeError("Headless mode is only supported for V1")
96
+
97
+ parallel_config = vllm_config.parallel_config
98
+ local_engine_count = parallel_config.data_parallel_size_local
99
+ host = parallel_config.data_parallel_master_ip
100
+ port = engine_args.data_parallel_rpc_port # add to config too
101
+ input_address = get_tcp_uri(host, port)
102
+
103
+ if local_engine_count <= 0:
104
+ raise RuntimeError("data_parallel_size_local must be > 0 in "
105
+ "headless mode")
106
+
107
+ # Catch SIGTERM and SIGINT to allow graceful shutdown.
108
+ def signal_handler(signum, frame):
109
+ logger.debug("Received %d signal.", signum)
110
+ raise SystemExit
111
+
112
+ signal.signal(signal.SIGTERM, signal_handler)
113
+ signal.signal(signal.SIGINT, signal_handler)
114
+
115
+ logger.info(
116
+ "Launching %d data parallel engine(s) in headless mode, "
117
+ "with head node address %s.", local_engine_count, input_address)
118
+
119
+ # Create the engines.
120
+ engine_manager = CoreEngineProcManager(
121
+ target_fn=EngineCoreProc.run_engine_core,
122
+ local_engine_count=local_engine_count,
123
+ start_index=args.data_parallel_start_rank,
124
+ local_start_index=0,
125
+ vllm_config=vllm_config,
126
+ on_head_node=False,
127
+ input_address=input_address,
128
+ executor_class=Executor.get_class(vllm_config),
129
+ log_stats=not engine_args.disable_log_stats,
130
+ )
131
+
132
+ try:
133
+ engine_manager.join_first()
134
+ finally:
135
+ logger.info("Shutting down.")
136
+ engine_manager.close()
vllm/utils.py
+4
@@ -613,6 +613,10 @@ def is_valid_ipv6_address(address: str) -> bool:
613
614
615
def get_distributed_init_method(ip: str, port: int) -> str:
616
+ return get_tcp_uri(ip, port)
617
+
618
+
619
+def get_tcp_uri(ip: str, port: int) -> str:
620
# Brackets are not permitted in ipv4 addresses,
621
# see https://github.com/python/cpython/issues/103848
622
return f"tcp://[{ip}]:{port}" if ":" in ip else f"tcp://{ip}:{port}"
vllm/v1/engine/core.py
+122
-67
@@ -1,5 +1,4 @@
1
# SPDX-License-Identifier: Apache-2.0
2
-import json
2
import os
3
import queue
4
import signal
@@ -23,7 +22,7 @@ from vllm.logging_utils.dump_input import dump_engine_exception
22
from vllm.lora.request import LoRARequest
23
from vllm.transformers_utils.config import (
24
maybe_register_config_serialize_by_value)
26
-from vllm.utils import resolve_obj_by_qualname, zmq_socket_ctx
25
+from vllm.utils import make_zmq_socket, resolve_obj_by_qualname, zmq_socket_ctx
26
from vllm.v1.core.kv_cache_utils import (get_kv_cache_config,
27
unify_kv_cache_configs)
28
from vllm.v1.core.sched.interface import SchedulerInterface
@@ -43,6 +42,7 @@ from vllm.version import __version__ as VLLM_VERSION
42
logger = init_logger(__name__)
43
44
POLLING_TIMEOUT_S = 2.5
45
+HANDSHAKE_TIMEOUT_MINS = 5
46
47
_R = TypeVar('_R') # Return type for collective_rpc
48
@@ -348,9 +348,9 @@ class EngineCoreProc(EngineCore):
348
349
def __init__(
350
self,
351
- input_path: str,
352
- output_path: str,
351
vllm_config: VllmConfig,
352
+ on_head_node: bool,
353
+ input_address: str,
354
executor_class: type[Executor],
355
log_stats: bool,
356
engine_index: int = 0,
@@ -360,28 +360,91 @@ class EngineCoreProc(EngineCore):
360
executor_fail_callback = lambda: input_queue.put_nowait(
361
(EngineCoreRequestType.EXECUTOR_FAILED, b''))
362
363
- super().__init__(vllm_config, executor_class, log_stats,
364
- executor_fail_callback)
365
-
366
- self.step_fn = (self.step if self.batch_queue is None else
367
- self.step_with_batch_queue)
368
- self.engines_running = False
369
-
370
- # Background Threads and Queues for IO. These enable us to
371
- # overlap ZMQ socket IO with GPU since they release the GIL,
372
- # and to overlap some serialization/deserialization with the
373
- # model forward pass.
374
- # Threads handle Socket <-> Queues and core_busy_loop uses Queue.
375
- self.input_queue = input_queue
376
- self.output_queue = queue.Queue[Union[EngineCoreOutputs, bytes]]()
377
- threading.Thread(target=self.process_input_socket,
378
- args=(input_path, engine_index),
379
- daemon=True).start()
380
- self.output_thread = threading.Thread(
381
- target=self.process_output_socket,
382
- args=(output_path, engine_index),
383
- daemon=True)
384
- self.output_thread.start()
363
+ # Create input socket.
364
+ input_ctx = zmq.Context()
365
+ identity = engine_index.to_bytes(length=2, byteorder="little")
366
+ input_socket = make_zmq_socket(input_ctx,
367
+ input_address,
368
+ zmq.DEALER,
369
+ identity=identity,
370
+ bind=False)
371
+ try:
372
+ # Register engine with front-end.
373
+ output_address = self.startup_handshake(
374
+ input_socket, on_head_node, vllm_config.parallel_config)
375
+
376
+ # Update config which may have changed from the handshake.
377
+ vllm_config.__post_init__()
378
+
379
+ # Set up data parallel environment.
380
+ self._init_data_parallel(vllm_config)
381
+
382
+ # Initialize engine core and model.
383
+ super().__init__(vllm_config, executor_class, log_stats,
384
+ executor_fail_callback)
385
+
386
+ self.step_fn = (self.step if self.batch_queue is None else
387
+ self.step_with_batch_queue)
388
+ self.engines_running = False
389
+
390
+ # Send ready message.
391
+ num_gpu_blocks = vllm_config.cache_config.num_gpu_blocks
392
+ input_socket.send(
393
+ msgspec.msgpack.encode({
394
+ "status": "READY",
395
+ "local": on_head_node,
396
+ "num_gpu_blocks": num_gpu_blocks,
397
+ }))
398
+
399
+ # Background Threads and Queues for IO. These enable us to
400
+ # overlap ZMQ socket IO with GPU since they release the GIL,
401
+ # and to overlap some serialization/deserialization with the
402
+ # model forward pass.
403
+ # Threads handle Socket <-> Queues and core_busy_loop uses Queue.
404
+ self.input_queue = input_queue
405
+ self.output_queue = queue.Queue[Union[EngineCoreOutputs, bytes]]()
406
+ threading.Thread(target=self.process_input_socket,
407
+ args=(input_socket, ),
408
+ daemon=True).start()
409
+ input_socket = None
410
+ self.output_thread = threading.Thread(
411
+ target=self.process_output_socket,
412
+ args=(output_address, engine_index),
413
+ daemon=True)
414
+ self.output_thread.start()
415
+ finally:
416
+ if input_socket is not None:
417
+ input_socket.close(linger=0)
418
+
419
+ @staticmethod
420
+ def startup_handshake(input_socket: zmq.Socket, on_head_node: bool,
421
+ parallel_config: ParallelConfig) -> str:
422
+
423
+ # Send registration message.
424
+ input_socket.send(
425
+ msgspec.msgpack.encode({
426
+ "status": "HELLO",
427
+ "local": on_head_node,
428
+ }))
429
+
430
+ # Receive initialization message.
431
+ logger.info("Waiting for init message from front-end.")
432
+ if not input_socket.poll(timeout=HANDSHAKE_TIMEOUT_MINS * 60 * 1000):
433
+ raise RuntimeError("Did not receive response from front-end "
434
+ f"process within {HANDSHAKE_TIMEOUT_MINS} "
435
+ f"minutes")
436
+ init_bytes = input_socket.recv()
437
+ init_message = msgspec.msgpack.decode(init_bytes)
438
+ logger.debug("Received init message: %s", init_message)
439
+
440
+ output_socket_address = init_message["output_socket_address"]
441
+ #TBD(nick) maybe replace IP with configured head node address
442
+
443
+ received_parallel_config = init_message["parallel_config"]
444
+ for key, value in received_parallel_config.items():
445
+ setattr(parallel_config, key, value)
446
+
447
+ return output_socket_address
448
449
@staticmethod
450
def run_engine_core(*args,
@@ -412,7 +475,7 @@ class EngineCoreProc(EngineCore):
475
try:
476
parallel_config: ParallelConfig = kwargs[
477
"vllm_config"].parallel_config
415
- if parallel_config.data_parallel_size > 1:
478
+ if parallel_config.data_parallel_size > 1 or dp_rank > 0:
479
# Set data parallel rank for this engine process.
480
parallel_config.data_parallel_rank = dp_rank
481
parallel_config.data_parallel_rank_local = local_dp_rank
@@ -436,6 +499,9 @@ class EngineCoreProc(EngineCore):
499
if engine_core is not None:
500
engine_core.shutdown()
501
502
+ def _init_data_parallel(self, vllm_config: VllmConfig):
503
+ pass
504
+
505
def run_busy_loop(self):
506
"""Core busy loop of the EngineCore."""
507
@@ -527,40 +593,25 @@ class EngineCoreProc(EngineCore):
593
logger.fatal("vLLM shutdown signal from EngineCore failed "
594
"to send. Please report this issue.")
595
530
- def process_input_socket(self, input_path: str, engine_index: int):
596
+ def process_input_socket(self, input_socket: zmq.Socket):
597
"""Input socket IO thread."""
598
599
# Msgpack serialization decoding.
600
add_request_decoder = MsgpackDecoder(EngineCoreRequest)
601
generic_decoder = MsgpackDecoder()
536
- identity = engine_index.to_bytes(length=2, byteorder="little")
602
538
- with zmq_socket_ctx(input_path,
539
- zmq.DEALER,
540
- identity=identity,
541
- bind=False) as socket:
542
-
543
- # Send ready message to front-end once input socket is connected.
544
- message_dict = {
545
- 'type': 'READY',
546
- 'num_gpu_blocks': self.vllm_config.cache_config.num_gpu_blocks,
547
- }
548
- message = json.dumps(message_dict).encode('utf-8')
549
- socket.send(message)
550
-
551
- while True:
552
- # (RequestType, RequestData)
553
- type_frame, *data_frames = socket.recv_multipart(copy=False)
554
- request_type = EngineCoreRequestType(bytes(type_frame.buffer))
603
+ while True:
604
+ # (RequestType, RequestData)
605
+ type_frame, *data_frames = input_socket.recv_multipart(copy=False)
606
+ request_type = EngineCoreRequestType(bytes(type_frame.buffer))
607
556
- # Deserialize the request data.
557
- decoder = add_request_decoder if (
558
- request_type
559
- == EngineCoreRequestType.ADD) else generic_decoder
560
- request = decoder.decode(data_frames)
608
+ # Deserialize the request data.
609
+ decoder = add_request_decoder if (
610
+ request_type == EngineCoreRequestType.ADD) else generic_decoder
611
+ request = decoder.decode(data_frames)
612
562
- # Push to input queue for core busy loop.
563
- self.input_queue.put_nowait((request_type, request))
613
+ # Push to input queue for core busy loop.
614
+ self.input_queue.put_nowait((request_type, request))
615
616
def process_output_socket(self, output_path: str, engine_index: int):
617
"""Output socket IO thread."""
@@ -609,9 +660,9 @@ class DPEngineCoreProc(EngineCoreProc):
660
661
def __init__(
662
self,
612
- input_path: str,
613
- output_path: str,
663
vllm_config: VllmConfig,
664
+ on_head_node: bool,
665
+ input_address: str,
666
executor_class: type[Executor],
667
log_stats: bool,
668
):
@@ -623,8 +674,20 @@ class DPEngineCoreProc(EngineCoreProc):
674
_add_prefix(sys.stdout, process_name, pid)
675
_add_prefix(sys.stderr, process_name, pid)
676
626
- dp_size = vllm_config.parallel_config.data_parallel_size
677
+ # Counts forward-passes of the model so that we can synchronize
678
+ # finished with DP peers every N steps.
679
+ self.counter = 0
680
+
681
+ # Initialize the engine.
682
+ dp_rank = vllm_config.parallel_config.data_parallel_rank
683
+ super().__init__(vllm_config, on_head_node, input_address,
684
+ executor_class, log_stats, dp_rank)
685
+
686
+ def _init_data_parallel(self, vllm_config: VllmConfig):
687
+
688
+ # Configure GPUs and stateless process group for data parallel.
689
dp_rank = vllm_config.parallel_config.data_parallel_rank
690
+ dp_size = vllm_config.parallel_config.data_parallel_size
691
local_dp_rank = vllm_config.parallel_config.data_parallel_rank_local
692
693
assert dp_size > 1
@@ -632,24 +695,16 @@ class DPEngineCoreProc(EngineCoreProc):
695
696
from vllm.platforms import current_platform
697
device_control_env_var = current_platform.device_control_env_var
635
- tp_size = vllm_config.parallel_config.tensor_parallel_size
698
+ world_size = vllm_config.parallel_config.world_size
699
os.environ[device_control_env_var] = ",".join(
700
str(current_platform.device_id_to_physical_device_id(i))
638
- for i in range(local_dp_rank * tp_size, (local_dp_rank + 1) *
639
- tp_size))
701
+ for i in range(local_dp_rank * world_size, (local_dp_rank + 1) *
702
+ world_size))
703
704
self.local_dp_rank = local_dp_rank
705
self.dp_group = vllm_config.parallel_config.stateless_init_dp_group()
706
self.current_wave = 0
707
645
- # Initialize the engine after setting up environment.
646
- super().__init__(input_path, output_path, vllm_config, executor_class,
647
- log_stats, dp_rank)
648
-
649
- # Counts forward-passes of the model so that we can synchronize
650
- # finished with DP peers every N steps.
651
- self.counter = 0
652
-
708
def shutdown(self):
709
super().shutdown()
710
if dp_group := getattr(self, "dp_group", None):
vllm/v1/engine/core_client.py
+174
-119
@@ -1,7 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
import asyncio
3
import contextlib
4
-import json
4
import queue
5
import uuid
6
import weakref
@@ -9,25 +8,27 @@ from abc import ABC, abstractmethod
8
from collections import deque
9
from collections.abc import Awaitable, Sequence
10
from concurrent.futures import Future
12
-from dataclasses import dataclass, field
11
+from dataclasses import dataclass
12
+from enum import Enum, auto
13
from threading import Thread
14
from typing import Any, Callable, Optional, TypeVar, Union
15
16
+import msgspec
17
import zmq
18
import zmq.asyncio
19
19
-from vllm.config import VllmConfig
20
+from vllm.config import ParallelConfig, VllmConfig
21
from vllm.logger import init_logger
22
from vllm.lora.request import LoRARequest
22
-from vllm.utils import (get_open_zmq_inproc_path, get_open_zmq_ipc_path,
23
- make_zmq_socket)
23
+from vllm.utils import (get_open_port, get_open_zmq_inproc_path,
24
+ get_open_zmq_ipc_path, get_tcp_uri, make_zmq_socket)
25
from vllm.v1.engine import (EngineCoreOutputs, EngineCoreRequest,
26
EngineCoreRequestType, UtilityOutput)
27
from vllm.v1.engine.core import EngineCore, EngineCoreProc
28
from vllm.v1.engine.exceptions import EngineDeadError
29
from vllm.v1.executor.abstract import Executor
30
from vllm.v1.serial_utils import MsgpackDecoder, MsgpackEncoder, bytestr
30
-from vllm.v1.utils import BackgroundProcHandle
31
+from vllm.v1.utils import CoreEngineProcManager
32
33
logger = init_logger(__name__)
34
@@ -264,45 +265,22 @@ class InprocClient(EngineCoreClient):
265
return self.engine_core.collective_rpc(method, timeout, args, kwargs)
266
267
268
+class CoreEngineState(Enum):
269
+ NEW = auto()
270
+ CONNECTED = auto()
271
+ READY = auto()
272
+
273
+
274
class CoreEngine:
275
"""One per data parallel rank."""
276
270
- def __init__(
271
- self,
272
- vllm_config: VllmConfig,
273
- executor_class: type[Executor],
274
- log_stats: bool,
275
- input_path: str,
276
- output_path: str,
277
- index: int = 0,
278
- local_dp_rank: int = 0,
279
- ):
277
+ def __init__(self, index: int = 0, local: bool = True):
278
+ self.local = local
279
self.index = index
280
self.identity = index.to_bytes(length=2, byteorder="little")
282
- try:
283
- # Start EngineCore in background process.
284
- self.proc_handle = BackgroundProcHandle(
285
- input_path=input_path,
286
- output_path=output_path,
287
- process_name=f"EngineCore_{index}",
288
- target_fn=EngineCoreProc.run_engine_core,
289
- process_kwargs={
290
- "vllm_config": vllm_config,
291
- "dp_rank": index,
292
- "local_dp_rank": local_dp_rank,
293
- "executor_class": executor_class,
294
- "log_stats": log_stats,
295
- })
296
-
297
- self.num_reqs_in_flight = 0
298
- finally:
299
- if not hasattr(self, "num_reqs_in_flight"):
300
- # Ensure socket is closed if process fails to start.
301
- self.close()
281
303
- def close(self):
304
- if proc_handle := getattr(self, "proc_handle", None):
305
- proc_handle.shutdown()
282
+ self.state = CoreEngineState.NEW
283
+ self.num_reqs_in_flight = 0
284
285
286
@dataclass
@@ -311,7 +289,7 @@ class BackgroundResources:
289
circular reference back to the client object."""
290
291
ctx: Union[zmq.Context]
314
- core_engines: list[CoreEngine] = field(default_factory=list)
292
+ local_engine_manager: Optional[CoreEngineProcManager] = None
293
output_socket: Optional[Union[zmq.Socket, zmq.asyncio.Socket]] = None
294
input_socket: Optional[Union[zmq.Socket, zmq.asyncio.Socket]] = None
295
output_queue_task: Optional[asyncio.Task] = None
@@ -325,8 +303,8 @@ class BackgroundResources:
303
"""Clean up background resources."""
304
305
self.engine_dead = True
328
- for core_engine in self.core_engines:
329
- core_engine.close()
306
+ if self.local_engine_manager is not None:
307
+ self.local_engine_manager.close()
308
309
if self.output_queue_task is not None:
310
self.output_queue_task.cancel()
@@ -388,25 +366,56 @@ class MPClient(EngineCoreClient):
366
self._finalizer = weakref.finalize(self, self.resources)
367
success = False
368
try:
391
- # Paths and sockets for IPC.
392
- self.output_path = get_open_zmq_ipc_path()
393
- input_path = get_open_zmq_ipc_path()
394
- self.input_socket = make_zmq_socket(self.ctx,
395
- input_path,
396
- zmq.ROUTER,
397
- bind=True)
398
- self.resources.input_socket = self.input_socket
399
-
400
- new_core_engine = lambda index, local_dp_rank=None: CoreEngine(
401
- vllm_config, executor_class, log_stats, input_path, self.
402
- output_path, index, local_dp_rank)
403
-
404
- # Start engine core process(es).
405
- self._init_core_engines(vllm_config, new_core_engine,
406
- self.resources.core_engines)
369
+ parallel_config = vllm_config.parallel_config
370
+ local_engine_count = parallel_config.data_parallel_size_local
371
+ start_index = parallel_config.data_parallel_rank
372
+ local_start_index = parallel_config.data_parallel_rank_local
373
+
374
+ # SPMD mode is where there is an LLM instance per DP rank and
375
+ # one core engine per LLM, see
376
+ # examples/offline_inference/data_parallel.py.
377
+ spmd_mode = local_start_index is not None
378
+ if spmd_mode:
379
+ assert local_engine_count == 1
380
+ self.core_engines = [
381
+ CoreEngine(index=local_start_index, local=True)
382
+ ]
383
+ else:
384
+ assert start_index == 0
385
+ local_start_index = 0
386
+ self.core_engines = [
387
+ CoreEngine(index=i, local=(i < local_engine_count))
388
+ for i in range(parallel_config.data_parallel_size)
389
+ ]
390
+
391
+ input_address, output_address = self._get_zmq_addresses(
392
+ parallel_config, spmd_mode)
393
+
394
+ # Create input and output sockets.
395
+ self.input_socket = self.resources.input_socket = make_zmq_socket(
396
+ self.ctx, input_address, zmq.ROUTER, bind=True)
397
+
398
+ self.resources.output_socket = make_zmq_socket(
399
+ self.ctx, output_address, zmq.constants.PULL)
400
+ # Start local engines.
401
+ if local_engine_count:
402
+ # In server mode, start_index and local_start_index will
403
+ # both be 0.
404
+ self.resources.local_engine_manager = CoreEngineProcManager(
405
+ EngineCoreProc.run_engine_core,
406
+ vllm_config=vllm_config,
407
+ executor_class=executor_class,
408
+ log_stats=log_stats,
409
+ input_address=input_address,
410
+ on_head_node=True,
411
+ local_engine_count=local_engine_count,
412
+ start_index=start_index,
413
+ local_start_index=local_start_index)
414
+
415
+ self.core_engine = self.core_engines[0]
416
417
# Wait for engine core process(es) to start.
409
- self._wait_for_engine_startup()
418
+ self._wait_for_engine_startup(output_address, parallel_config)
419
420
self.utility_results: dict[int, AnyFuture] = {}
421
@@ -420,56 +429,116 @@ class MPClient(EngineCoreClient):
429
if not success:
430
self._finalizer()
431
423
- def _wait_for_engine_startup(self):
432
+ @staticmethod
433
+ def _get_zmq_addresses(parallel_config: ParallelConfig,
434
+ spmd_mode: bool) -> tuple[str, str]:
435
+ """Returns (input_address, output_address)."""
436
+ dp_size = parallel_config.data_parallel_size
437
+ local_engine_count = parallel_config.data_parallel_size_local
438
+
439
+ if local_engine_count == dp_size or spmd_mode:
440
+ input_address = get_open_zmq_ipc_path()
441
+ output_address = get_open_zmq_ipc_path()
442
+ else:
443
+ host = parallel_config.data_parallel_master_ip
444
+ input_port = parallel_config.data_parallel_rpc_port
445
+ output_port = get_open_port()
446
+ input_address = get_tcp_uri(host, input_port)
447
+ output_address = get_tcp_uri(host, output_port)
448
+
449
+ return input_address, output_address
450
+
451
+ def _wait_for_engine_startup(self, output_address: str,
452
+ parallel_config: ParallelConfig):
453
# Get a sync handle to the socket which can be sync or async.
454
sync_input_socket = zmq.Socket.shadow(self.input_socket)
455
456
# Wait for engine core process(es) to send ready messages.
428
- identities = set(eng.index for eng in self.resources.core_engines)
457
+ local_count = parallel_config.data_parallel_size_local
458
+ remote_count = len(self.core_engines) - local_count
459
+ # [local, remote] counts
460
+ conn_pending, start_pending = [local_count, remote_count], [0, 0]
461
+
462
poller = zmq.Poller()
463
poller.register(sync_input_socket, zmq.POLLIN)
431
- for eng in self.resources.core_engines:
432
- poller.register(eng.proc_handle, zmq.POLLIN)
433
- while identities:
464
+ proc_manager = self.resources.local_engine_manager
465
+ if proc_manager is not None:
466
+ for sentinel in proc_manager.sentinels():
467
+ poller.register(sentinel, zmq.POLLIN)
468
+ while any(conn_pending) or any(start_pending):
469
events = poller.poll(STARTUP_POLL_PERIOD_MS)
470
if not events:
436
- logger.debug("Waiting for %d core engine proc(s) to start: %s",
437
- len(identities), identities)
471
+ if any(conn_pending):
472
+ logger.debug(
473
+ "Waiting for %d local, %d remote core engine proc(s) "
474
+ "to connect.", *conn_pending)
475
+ if any(start_pending):
476
+ logger.debug(
477
+ "Waiting for %d local, %d remote core engine proc(s) "
478
+ "to start.", *start_pending)
479
continue
480
if len(events) > 1 or events[0][0] != sync_input_socket:
440
- # One of the core processes exited.
481
+ # One of the local core processes exited.
482
+ finished = proc_manager.finished_procs(
483
+ ) if proc_manager else {}
484
raise RuntimeError("Engine core initialization failed. "
442
- "See root cause above.")
443
-
444
- eng_id_bytes, data = sync_input_socket.recv_multipart()
445
- eng_id = int.from_bytes(eng_id_bytes, byteorder="little")
446
- if eng_id not in identities:
447
- raise RuntimeError(f"Unexpected or duplicate engine: {eng_id}")
448
- message_dict = json.loads(data.decode('utf-8'))
449
- if message_dict['type'] != 'READY':
450
- raise RuntimeError(f"Engine {eng_id} failed: {data.decode()}")
451
- logger.info("Core engine process %d ready.", eng_id)
452
- identities.discard(eng_id)
453
- # Setup KV cache config with initialization state from
454
- # engine core process. Sum values from all engines in DP case.
455
- num_gpu_blocks = self.vllm_config.cache_config.num_gpu_blocks or 0
456
- num_gpu_blocks += message_dict['num_gpu_blocks']
457
- self.vllm_config.cache_config.num_gpu_blocks = num_gpu_blocks
458
-
459
- def _init_core_engines(
460
- self,
461
- vllm_config: VllmConfig,
462
- new_core_engine: Callable[[int, Optional[int]], CoreEngine],
463
- core_engines: list[CoreEngine],
464
- ) -> None:
465
-
466
- # Default case - single core engine.
467
- core_engine = new_core_engine(
468
- vllm_config.parallel_config.data_parallel_rank,
469
- vllm_config.parallel_config.data_parallel_rank_local,
470
- )
471
- core_engines.append(core_engine)
472
- self.core_engine = core_engine
485
+ "See root cause above. "
486
+ f"Failed core proc(s): {finished}")
487
+
488
+ # Receive HELLO and READY messages from the input socket.
489
+ eng_identity, ready_msg_bytes = sync_input_socket.recv_multipart()
490
+ eng_index = int.from_bytes(eng_identity, byteorder="little")
491
+ engine = next(
492
+ (e for e in self.core_engines if e.identity == eng_identity),
493
+ None)
494
+ if engine is None:
495
+ raise RuntimeError(f"Message from engine with unexpected data "
496
+ f"parallel rank: {eng_index}")
497
+ msg = msgspec.msgpack.decode(ready_msg_bytes)
498
+ status, local = msg["status"], msg["local"]
499
+ if local != engine.local:
500
+ raise RuntimeError(f"{status} message from "
501
+ f"{'local' if local else 'remote'} "
502
+ f"engine {eng_index}, expected it to be "
503
+ f"{'local' if engine.local else 'remote'}")
504
+
505
+ if status == "HELLO" and engine.state == CoreEngineState.NEW:
506
+
507
+ # Send init message with DP config info.
508
+ init_message = self.encoder.encode({
509
+ "output_socket_address": output_address,
510
+ "parallel_config": {
511
+ "data_parallel_master_ip":
512
+ parallel_config.data_parallel_master_ip,
513
+ "data_parallel_master_port":
514
+ parallel_config.data_parallel_master_port,
515
+ "data_parallel_size":
516
+ parallel_config.data_parallel_size,
517
+ },
518
+ })
519
+ sync_input_socket.send_multipart((eng_identity, *init_message),
520
+ copy=False)
521
+ conn_pending[0 if local else 1] -= 1
522
+ start_pending[0 if local else 1] += 1
523
+ engine.state = CoreEngineState.CONNECTED
524
+ elif status == "READY" and (engine.state
525
+ == CoreEngineState.CONNECTED):
526
+ # Setup KV cache config with initialization state from
527
+ # engine core process. Sum values from all engines in DP case.
528
+ cache_config = self.vllm_config.cache_config
529
+ num_gpu_blocks = cache_config.num_gpu_blocks or 0
530
+ num_gpu_blocks += msg['num_gpu_blocks']
531
+ cache_config.num_gpu_blocks = num_gpu_blocks
532
+
533
+ start_pending[0 if local else 1] -= 1
534
+ engine.state = CoreEngineState.READY
535
+ else:
536
+ raise RuntimeError(f"Unexpected {status} message for "
537
+ f"{'local' if local else 'remote'} engine "
538
+ f"{eng_index} in {engine.state} state.")
539
+
540
+ logger.debug("%s from %s core engine process %s.", status,
541
+ "local" if local else "remote", eng_index)
542
543
def shutdown(self):
544
# Terminate background resources.
@@ -520,7 +589,8 @@ class SyncMPClient(MPClient):
589
# Ensure that the outputs socket processing thread does not have
590
# a ref to the client which prevents gc.
591
ctx = self.ctx
523
- output_path = self.output_path
592
+ out_socket = self.resources.output_socket
593
+ assert out_socket is not None
594
decoder = self.decoder
595
utility_results = self.utility_results
596
outputs_queue = self.outputs_queue
@@ -531,7 +601,6 @@ class SyncMPClient(MPClient):
601
602
def process_outputs_socket():
603
shutdown_socket = ctx.socket(zmq.PAIR)
534
- out_socket = make_zmq_socket(ctx, output_path, zmq.constants.PULL)
604
try:
605
shutdown_socket.bind(shutdown_path)
606
poller = zmq.Poller()
@@ -566,6 +635,9 @@ class SyncMPClient(MPClient):
635
daemon=True)
636
self.output_queue_thread.start()
637
638
+ # The thread takes on responsibility for closing the socket.
639
+ self.resources.output_socket = None
640
+
641
def get_output(self) -> EngineCoreOutputs:
642
# If an exception arises in process_outputs_socket task,
643
# it is forwarded to the outputs_queue so we can raise it
@@ -693,10 +765,8 @@ class AsyncMPClient(MPClient):
765
self.__class__,
766
"process_engine_outputs", None)
767
_self_ref = weakref.ref(self) if output_handler else None
696
- output_path = self.output_path
697
- output_socket = make_zmq_socket(self.ctx, output_path,
698
- zmq.constants.PULL)
699
- resources.output_socket = output_socket
768
+ output_socket = resources.output_socket
769
+ assert output_socket is not None
770
771
async def process_outputs_socket():
772
try:
@@ -861,21 +931,6 @@ class DPAsyncMPClient(AsyncMPClient):
931
932
assert len(self.core_engines) > 1
933
864
- def _init_core_engines(
865
- self,
866
- vllm_config: VllmConfig,
867
- new_core_engine: Callable[[int, Optional[int]], CoreEngine],
868
- core_engines: list[CoreEngine],
869
- ) -> None:
870
-
871
- # Launch a core engine for each data parallel rank.
872
- dp_size = vllm_config.parallel_config.data_parallel_size
873
- for i in range(dp_size):
874
- # Multi-node not yet supported so local_dp_rank == dp_rank.
875
- core_engines.append(new_core_engine(i, i))
876
-
877
- self.core_engines = core_engines
878
-
934
async def call_utility_async(self, method: str, *args) -> Any:
935
# Only the result from the first engine is returned.
936
return (await asyncio.gather(*[
vllm/v1/utils.py
+78
-33
@@ -1,20 +1,23 @@
1
# SPDX-License-Identifier: Apache-2.0
2
3
import os
4
+import time
5
import weakref
6
from collections import defaultdict
7
from collections.abc import Sequence
7
-from multiprocessing import Process
8
-from typing import (TYPE_CHECKING, Any, Callable, Generic, Optional, TypeVar,
9
- Union, overload)
8
+from multiprocessing import Process, connection
9
+from typing import (TYPE_CHECKING, Callable, Generic, Optional, TypeVar, Union,
10
+ overload)
11
12
import torch
13
14
+from vllm.config import VllmConfig
15
from vllm.logger import init_logger
16
from vllm.model_executor.models.utils import extract_layer_index
17
from vllm.usage.usage_lib import (UsageContext, is_usage_stats_enabled,
18
usage_message)
19
from vllm.utils import get_mp_context, kill_process_tree
20
+from vllm.v1.executor.abstract import Executor
21
22
if TYPE_CHECKING:
23
from vllm.attention.layer import Attention
@@ -92,7 +95,7 @@ class ConstantList(Generic[T], Sequence):
95
return f"ConstantList({self._x})"
96
97
95
-class BackgroundProcHandle:
98
+class CoreEngineProcManager:
99
"""
100
Utility class to handle creation, readiness, and shutdown
101
of background processes used by the AsyncLLM and LLMEngine.
@@ -100,49 +103,91 @@ class BackgroundProcHandle:
103
104
def __init__(
105
self,
103
- input_path: str,
104
- output_path: str,
105
- process_name: str,
106
target_fn: Callable,
107
- process_kwargs: dict[Any, Any],
107
+ local_engine_count: int,
108
+ start_index: int,
109
+ local_start_index: int,
110
+ vllm_config: VllmConfig,
111
+ on_head_node: bool,
112
+ input_address: str,
113
+ executor_class: type[Executor],
114
+ log_stats: bool,
115
):
116
context = get_mp_context()
117
+ common_kwargs = {
118
+ "vllm_config": vllm_config,
119
+ "on_head_node": on_head_node,
120
+ "input_address": input_address,
121
+ "executor_class": executor_class,
122
+ "log_stats": log_stats,
123
+ }
124
+
125
+ self.processes: list[Process] = []
126
+ for index in range(local_engine_count):
127
+ local_index = local_start_index + index
128
+ global_index = start_index + index
129
+ # Start EngineCore in background process.
130
+ self.processes.append(
131
+ context.Process(target=target_fn,
132
+ name=f"EngineCore_{global_index}",
133
+ kwargs=common_kwargs | {
134
+ "dp_rank": global_index,
135
+ "local_dp_rank": local_index,
136
+ }))
137
+
138
+ self._finalizer = weakref.finalize(self, shutdown, self.processes,
139
+ input_address)
140
+ try:
141
+ for proc in self.processes:
142
+ proc.start()
143
+ finally:
144
+ # Kill other procs if not all are running.
145
+ if self.finished_procs():
146
+ self.close()
147
+
148
+ def close(self):
149
+ """Shutdown all procs."""
150
+ self._finalizer()
151
111
- assert ("input_path" not in process_kwargs
112
- and "output_path" not in process_kwargs)
113
- process_kwargs["input_path"] = input_path
114
- process_kwargs["output_path"] = output_path
115
-
116
- # Run busy loop in background process.
117
- self.proc: Process = context.Process(target=target_fn,
118
- kwargs=process_kwargs,
119
- name=process_name)
120
- self._finalizer = weakref.finalize(self, shutdown, self.proc,
121
- input_path, output_path)
122
- self.proc.start()
152
+ def join_first(self):
153
+ """Wait for any process to exit."""
154
+ connection.wait(proc.sentinel for proc in self.processes)
155
124
- def fileno(self):
125
- return self.proc.sentinel
156
+ def sentinels(self) -> list:
157
+ return [proc.sentinel for proc in self.processes]
158
127
- def shutdown(self):
128
- self._finalizer()
159
+ def finished_procs(self) -> dict[str, int]:
160
+ """Returns dict of proc name -> exit code for any finished procs."""
161
+ return {
162
+ proc.name: proc.exitcode
163
+ for proc in self.processes if proc.exitcode is not None
164
+ }
165
166
167
# Note(rob): shutdown function cannot be a bound method,
132
-# else the gc cannot collect the object.
133
-def shutdown(proc: Process, input_path: str, output_path: str):
168
+# else the gc cannot collect the objedecoupct.
169
+def shutdown(procs: list[Process], input_address: str):
170
# Shutdown the process.
135
- if proc.is_alive():
136
- proc.terminate()
137
- proc.join(5)
138
-
171
+ for proc in procs:
172
+ if proc.is_alive():
173
+ proc.terminate()
174
+
175
+ # Allow 5 seconds for remaining procs to terminate.
176
+ deadline = time.monotonic() + 5
177
+ for proc in procs:
178
+ remaining = deadline - time.monotonic()
179
+ if remaining <= 0:
180
+ break
181
+ if proc.is_alive():
182
+ proc.join(remaining)
183
+
184
+ for proc in procs:
185
if proc.is_alive() and (pid := proc.pid) is not None:
186
kill_process_tree(pid)
187
188
# Remove zmq ipc socket files.
143
- ipc_sockets = [output_path, input_path]
144
- for ipc_socket in ipc_sockets:
145
- socket_file = ipc_socket.replace("ipc://", "")
189
+ if input_address.startswith("ipc://"):
190
+ socket_file = input_address[len("ipc://"):]
191
if os and os.path.exists(socket_file):
192
os.remove(socket_file)
193