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 6, 12:00 UTC
elastic_ep: Fix stateless group port races (#36330)
Signed-off-by: Itay Alroy <ialroy@nvidia.com>
Itay Alroy committed
Mar 18, 2026 at 16:36 UTC
de1a86b7dea68dffdfbeda77c8407c587ee90542
12 files changed
+224
-225
.buildkite/test_areas/expert_parallelism.yaml
+1
-2
@@ -24,8 +24,7 @@ steps:
24
25
- label: Elastic EP Scaling Test
26
timeout_in_minutes: 20
27
- device: b200
28
- optional: true
27
+ device: h100
28
working_dir: "/vllm-workspace/tests"
29
num_devices: 4
30
source_file_dependencies:
vllm/config/parallel.py
+31
-85
@@ -2,6 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
import os
5
+import socket
6
from collections.abc import Callable
7
from typing import TYPE_CHECKING, Any, Literal, overload
8
@@ -266,33 +267,9 @@ class ParallelConfig:
267
Set to be private as it's not intended to be configured by users.
268
"""
269
269
- _stateless_dp_group_port_list: list[list[int]] = Field(default_factory=list)
270
- """List of open ports for stateless DP groups when enable_elastic_ep is True.
271
- Set to be private as it's not intended to be configured by users.
272
- It is a list of list[int], with each inner list contains a set of 3 ports
273
- to be used for setting up the stateless CPU/device/TCPStore groups
274
- in StatelessGroupCoordinator. The number of inner lists is equal to
275
- the number of DP groups,
276
- i.e., len(self._stateless_dp_group_port_list) == world_size_across_dp // dp_size,
277
- and len(self._stateless_dp_group_port_list[i]) == 3 for all i.
278
- """
279
-
280
- _stateless_ep_group_port_list: list[list[int]] = Field(default_factory=list)
281
- """List of open ports for stateless EP groups when enable_elastic_ep is True.
282
- Set to be private as it's not intended to be configured by users.
283
- len(self._stateless_ep_group_port_list) == world_size_across_dp // ep_size,
284
- """
285
-
286
- _stateless_eplb_group_port_list: list[list[int]] = Field(default_factory=list)
287
- """List of open ports for stateless EPLB groups when enable_elastic_ep is True.
288
- Same topology as EP but separate NCCL communicator to avoid deadlocks.
289
- """
290
-
291
- _stateless_world_group_port_list: list[list[int]] = Field(default_factory=list)
292
- """List of open ports for stateless world group when enable_elastic_ep is True.
293
- Set to be private as it's not intended to be configured by users.
294
- len(self._stateless_world_group_port_list) == 1,
295
- """
270
+ _coord_store_port: int = 0
271
+ """Port of the coordination TCPStore. Can be set by the API server; workers
272
+ connect as clients to exchange self-picked group ports at runtime."""
273
274
decode_context_parallel_size: int = 1
275
"""Number of decode context parallel groups, because the world size does
@@ -465,65 +442,32 @@ class ParallelConfig:
442
443
return answer
444
468
- def allocate_elastic_ep_ports(self) -> None:
469
- """Allocate all ports for elastic EP (stateless groups + DP master).
445
+ def _pick_stateless_dp_port(self) -> tuple[int, socket.socket | None]:
446
+ """Return ``(port, listen_socket)`` for DP group init.
447
471
- Must be called AFTER ray.init() so that ports claimed by Ray's
472
- idle worker pool are already in use and won't be returned by
473
- get_open_ports_list().
448
+ With a coord store, rank 0 binds a socket and publishes the port;
449
+ others read it. Without one, pops a pre-allocated port and
450
+ returns ``listen_socket=None``.
451
"""
475
- if not self.enable_elastic_ep:
476
- return
477
- if self._stateless_world_group_port_list:
478
- return
479
-
480
- num_world_groups = 1
481
- dp_size = self.data_parallel_size
482
- ep_size = self.data_parallel_size * self.world_size_across_dp
483
- num_dp_groups = max(1, self.world_size_across_dp // dp_size)
484
- num_ep_groups = max(1, self.world_size_across_dp // ep_size)
485
- num_eplb_groups = num_ep_groups
486
- total_stateless_ports = (
487
- num_world_groups + num_dp_groups + num_ep_groups + num_eplb_groups
488
- ) * 3
489
- num_dp_master_ports = 5
490
-
491
- all_ports = get_open_ports_list(total_stateless_ports + num_dp_master_ports)
492
-
493
- self._data_parallel_master_port_list = all_ports[-num_dp_master_ports:]
494
- self.data_parallel_master_port = self._data_parallel_master_port_list.pop()
495
- all_ports = all_ports[:-num_dp_master_ports]
496
-
497
- self._stateless_world_group_port_list = [
498
- all_ports[i : i + 3] for i in range(0, num_world_groups * 3, 3)
499
- ]
500
- start_idx = num_world_groups * 3
501
- self._stateless_dp_group_port_list = [
502
- all_ports[i : i + 3]
503
- for i in range(start_idx, start_idx + num_dp_groups * 3, 3)
504
- ]
505
- start_idx += num_dp_groups * 3
506
- self._stateless_ep_group_port_list = [
507
- all_ports[i : i + 3]
508
- for i in range(start_idx, start_idx + num_ep_groups * 3, 3)
509
- ]
510
- start_idx += num_ep_groups * 3
511
- self._stateless_eplb_group_port_list = [
512
- all_ports[i : i + 3]
513
- for i in range(start_idx, start_idx + num_eplb_groups * 3, 3)
514
- ]
515
-
516
- def get_next_stateless_world_group_port(self) -> list[int]:
517
- return self._stateless_world_group_port_list.pop()
518
-
519
- def get_next_stateless_dp_group_port(self) -> list[int]:
520
- return self._stateless_dp_group_port_list.pop()
521
-
522
- def get_next_stateless_ep_group_port(self) -> list[int]:
523
- return self._stateless_ep_group_port_list.pop()
524
-
525
- def get_next_stateless_eplb_group_port(self) -> list[int]:
526
- return self._stateless_eplb_group_port_list.pop()
452
+ if not self._coord_store_port:
453
+ return self.get_next_dp_init_port(), None
454
+
455
+ from vllm.distributed.utils import get_cached_tcp_store_client
456
+
457
+ store = get_cached_tcp_store_client(
458
+ self.data_parallel_master_ip, self._coord_store_port
459
+ )
460
+
461
+ key = "dp_master_port"
462
+ if self.data_parallel_rank == 0:
463
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
464
+ s.bind((self.data_parallel_master_ip, 0))
465
+ s.listen()
466
+ port = s.getsockname()[1]
467
+ store.set(key, str(port).encode())
468
+ return port, s
469
+ else:
470
+ return int(store.get(key).decode()), None
471
472
@overload
473
def stateless_init_dp_group(
@@ -553,14 +497,16 @@ class ParallelConfig:
497
last_exc: Exception | None = None
498
for _ in range(max_retries):
499
try:
500
+ port, listen_socket = self._pick_stateless_dp_port()
501
# use gloo since the engine process might not have cuda device
502
return stateless_init_torch_distributed_process_group(
503
self.data_parallel_master_ip,
559
- self.get_next_dp_init_port(),
504
+ port,
505
self.data_parallel_rank,
506
self.data_parallel_size,
507
backend="gloo",
508
return_store=return_store,
509
+ listen_socket=listen_socket,
510
)
511
except DistNetworkError as e:
512
# We only want to retry when the root cause is EADDRINUSE.
vllm/distributed/elastic_ep/elastic_execute.py
+2
-4
@@ -162,10 +162,8 @@ class ElasticEPScalingExecutor:
162
new_dp_size=new_dp_size,
163
new_world_size_across_dp=new_world_size_across_dp,
164
master_ip=reconfig_request.new_data_parallel_master_ip,
165
- world_group_ports=reconfig_request.new_stateless_world_group_port_list,
166
- dp_group_ports=reconfig_request.new_stateless_dp_group_port_list,
167
- ep_group_ports=reconfig_request.new_stateless_ep_group_port_list,
168
- eplb_group_ports=reconfig_request.new_stateless_eplb_group_port_list,
165
+ coord_store_port=reconfig_request.coord_store_port,
166
+ enable_eplb=updated_config.parallel_config.enable_eplb,
167
)
168
self.worker.model_runner.eep_eplb_suppressed = True
169
standby_ep_group = get_standby_ep_group()
vllm/distributed/elastic_ep/elastic_state.py
+1
-12
@@ -563,15 +563,4 @@ class ElasticEPScalingState:
563
parallel_config._data_parallel_master_port_list = (
564
reconfig_request.new_data_parallel_master_port_list
565
)
566
- parallel_config._stateless_world_group_port_list = (
567
- reconfig_request.new_stateless_world_group_port_list
568
- )
569
- parallel_config._stateless_dp_group_port_list = (
570
- reconfig_request.new_stateless_dp_group_port_list
571
- )
572
- parallel_config._stateless_ep_group_port_list = (
573
- reconfig_request.new_stateless_ep_group_port_list
574
- )
575
- parallel_config._stateless_eplb_group_port_list = (
576
- reconfig_request.new_stateless_eplb_group_port_list
577
- )
566
+ parallel_config._coord_store_port = reconfig_request.coord_store_port
vllm/distributed/elastic_ep/standby_state.py
+15
-9
@@ -38,10 +38,8 @@ def create_standby_groups(
38
new_dp_size: int,
39
new_world_size_across_dp: int,
40
master_ip: str,
41
- world_group_ports: list[list[int]],
42
- dp_group_ports: list[list[int]],
43
- ep_group_ports: list[list[int]],
44
- eplb_group_ports: list[list[int]] | None = None,
41
+ coord_store_port: int,
42
+ enable_eplb: bool = True,
43
backend: str | None = None,
44
) -> None:
45
global \
@@ -51,19 +49,23 @@ def create_standby_groups(
49
_STANDBY_EP, \
50
_STANDBY_EPLB
51
52
+ from vllm.distributed.utils import get_cached_tcp_store_client
53
+
54
assert new_world_size_across_dp == torch.distributed.get_world_size() * new_dp_size
55
world_group = get_world_group()
56
assert isinstance(world_group, StatelessGroupCoordinator)
57
backend = backend or world_group.backend
58
59
+ coord_store = get_cached_tcp_store_client(master_ip, coord_store_port)
60
+
61
standby_world_ranks = [list(range(new_world_size_across_dp))]
62
_STANDBY_WORLD = _init_stateless_group(
63
standby_world_ranks,
64
"world",
63
- world_group_ports,
65
master_ip,
66
backend,
67
use_device_communicator=False,
68
+ coord_store=coord_store,
69
)
70
_STANDBY_WORLD_NODE_COUNT = _node_count(_STANDBY_WORLD.tcp_store_group)
71
@@ -76,7 +78,7 @@ def create_standby_groups(
78
standby_dp_ranks = all_ranks.transpose(1, 3).reshape(-1, new_dp_size).unbind(0)
79
standby_dp_ranks = [x.tolist() for x in standby_dp_ranks]
80
_STANDBY_DP = _init_stateless_group(
79
- standby_dp_ranks, "dp", dp_group_ports, master_ip, backend
81
+ standby_dp_ranks, "dp", master_ip, backend, coord_store=coord_store
82
)
83
84
standby_ep_ranks = (
@@ -84,12 +86,16 @@ def create_standby_groups(
86
)
87
standby_ep_ranks = [x.tolist() for x in standby_ep_ranks]
88
_STANDBY_EP = _init_stateless_group(
87
- standby_ep_ranks, "ep", ep_group_ports, master_ip, backend
89
+ standby_ep_ranks, "ep", master_ip, backend, coord_store=coord_store
90
)
91
90
- if eplb_group_ports is not None:
92
+ if enable_eplb:
93
_STANDBY_EPLB = _init_stateless_group(
92
- standby_ep_ranks, "eplb", eplb_group_ports, master_ip, backend
94
+ standby_ep_ranks,
95
+ "eplb",
96
+ master_ip,
97
+ backend,
98
+ coord_store=coord_store,
99
)
100
101
vllm/distributed/parallel_state.py
+20
-21
@@ -40,13 +40,16 @@ import torch
40
import torch.distributed
41
import torch.distributed._functional_collectives as funcol
42
import torch.distributed._symmetric_memory
43
-from torch.distributed import Backend, ProcessGroup
43
+from torch.distributed import Backend, ProcessGroup, Store
44
45
import vllm.envs as envs
46
from vllm.distributed.device_communicators.base_device_communicator import (
47
DeviceCommunicatorBase,
48
)
49
-from vllm.distributed.utils import StatelessProcessGroup
49
+from vllm.distributed.utils import (
50
+ StatelessProcessGroup,
51
+ get_cached_tcp_store_client,
52
+)
53
from vllm.logger import init_logger
54
from vllm.utils.import_utils import resolve_obj_by_qualname
55
from vllm.utils.network_utils import get_distributed_init_method
@@ -1164,9 +1167,9 @@ def init_model_parallel_group(
1167
def _init_stateless_group(
1168
group_ranks: list[list[int]],
1169
group_name: str,
1167
- group_ports: list[list[int]],
1170
host: str,
1171
backend: str,
1172
+ coord_store: Store,
1173
use_device_communicator: bool = True,
1174
) -> "StatelessGroupCoordinator":
1175
"""Create a StatelessGroupCoordinator with the given parameters."""
@@ -1180,7 +1183,7 @@ def _init_stateless_group(
1183
use_device_communicator=use_device_communicator,
1184
group_name=group_name,
1185
host=host,
1183
- group_ports=group_ports,
1186
+ coord_store=coord_store,
1187
global_rank=world.rank,
1188
global_world_size=world.world_size,
1189
)
@@ -1321,7 +1324,9 @@ def _init_elastic_ep_world(
1324
group_ranks = [all_ranks[i : i + 1] for i in range(global_world_size)]
1325
if global_rank in all_ranks:
1326
group_ranks = [all_ranks]
1324
- group_ports = [parallel_config.get_next_stateless_world_group_port()]
1327
+ coord_store = get_cached_tcp_store_client(
1328
+ parallel_config.data_parallel_master_ip, parallel_config._coord_store_port
1329
+ )
1330
world = StatelessGroupCoordinator(
1331
group_ranks=group_ranks,
1332
local_rank=local_rank,
@@ -1329,7 +1334,7 @@ def _init_elastic_ep_world(
1334
use_device_communicator=False,
1335
group_name="world",
1336
host=parallel_config.data_parallel_master_ip,
1332
- group_ports=group_ports,
1337
+ coord_store=coord_store,
1338
global_rank=global_rank,
1339
global_world_size=global_world_size,
1340
)
@@ -1513,7 +1518,13 @@ def initialize_model_parallel(
1518
config = get_current_vllm_config()
1519
data_parallel_size = config.parallel_config.data_parallel_size
1520
enable_elastic_ep = config.parallel_config.enable_elastic_ep
1521
+ parallel_config = config.parallel_config
1522
+ coord_store: Store | None = None
1523
if enable_elastic_ep:
1524
+ coord_store = get_cached_tcp_store_client(
1525
+ parallel_config.data_parallel_master_ip,
1526
+ parallel_config._coord_store_port,
1527
+ )
1528
# Use stateless world group for global information
1529
world_size = get_world_group().world_size
1530
rank = get_world_group().rank
@@ -1633,16 +1644,12 @@ def initialize_model_parallel(
1644
group_ranks = all_ranks.transpose(1, 4).reshape(-1, data_parallel_size).unbind(0)
1645
group_ranks = [x.tolist() for x in group_ranks]
1646
if enable_elastic_ep:
1636
- parallel_config = config.parallel_config
1637
- dp_ports = [
1638
- parallel_config.get_next_stateless_dp_group_port() for _ in group_ranks
1639
- ]
1647
_DP = _init_stateless_group(
1648
group_ranks,
1649
"dp",
1643
- dp_ports,
1650
parallel_config.data_parallel_master_ip,
1651
backend,
1652
+ coord_store=coord_store,
1653
)
1654
else:
1655
_DP = init_model_parallel_group(
@@ -1665,16 +1672,12 @@ def initialize_model_parallel(
1672
)
1673
group_ranks = [x.tolist() for x in group_ranks]
1674
if enable_elastic_ep:
1668
- parallel_config = config.parallel_config
1669
- ep_ports = [
1670
- parallel_config.get_next_stateless_ep_group_port() for _ in group_ranks
1671
- ]
1675
_EP = _init_stateless_group(
1676
group_ranks,
1677
"ep",
1675
- ep_ports,
1678
parallel_config.data_parallel_master_ip,
1679
backend,
1680
+ coord_store=coord_store,
1681
)
1682
else:
1683
_EP = init_model_parallel_group(
@@ -1693,16 +1696,12 @@ def initialize_model_parallel(
1696
and config.parallel_config.enable_eplb
1697
):
1698
if enable_elastic_ep:
1696
- eplb_ports = [
1697
- parallel_config.get_next_stateless_eplb_group_port()
1698
- for _ in group_ranks
1699
- ]
1699
_EPLB = _init_stateless_group(
1700
group_ranks,
1701
"eplb",
1703
- eplb_ports,
1702
parallel_config.data_parallel_master_ip,
1703
backend,
1704
+ coord_store=coord_store,
1705
)
1706
else:
1707
_EPLB = init_model_parallel_group(
vllm/distributed/stateless_coordinator.py
+50
-7
@@ -1,9 +1,11 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import socket
4
+import struct
5
from typing import Any, Optional
6
7
import torch
6
-from torch.distributed import Backend, ProcessGroup
8
+from torch.distributed import Backend, ProcessGroup, Store
9
10
from vllm.distributed.device_communicators.cuda_communicator import CudaCommunicator
11
from vllm.distributed.parallel_state import (
@@ -23,6 +25,38 @@ from vllm.utils.import_utils import resolve_obj_by_qualname
25
26
logger = init_logger(__name__)
27
28
+_PORTS_FMT = "!3I"
29
+
30
+
31
+def _allocate_group_ports(
32
+ key: str,
33
+ host: str,
34
+ coord_store: Store,
35
+) -> tuple[list[int], list[socket.socket]]:
36
+ """Bind 3 sockets and publish the ports to *coord_store*.
37
+
38
+ Called by rank 0 only. Returns ``(ports, sockets)`` with the
39
+ sockets still open.
40
+ """
41
+ socks: list[socket.socket] = []
42
+ ports: list[int] = []
43
+ for _ in range(3):
44
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
45
+ s.bind((host, 0))
46
+ s.listen()
47
+ socks.append(s)
48
+ ports.append(s.getsockname()[1])
49
+ coord_store.set(key, struct.pack(_PORTS_FMT, *ports))
50
+ return ports, socks
51
+
52
+
53
+def _fetch_group_ports(key: str, coord_store: Store) -> list[int]:
54
+ """Read 3 ports published by rank 0 from *coord_store*.
55
+
56
+ Blocks until the key is available.
57
+ """
58
+ return list(struct.unpack(_PORTS_FMT, coord_store.get(key)))
59
+
60
61
class StatelessGroupCoordinator(GroupCoordinator):
62
"""
@@ -39,10 +73,10 @@ class StatelessGroupCoordinator(GroupCoordinator):
73
local_rank: int,
74
torch_distributed_backend: str | Backend,
75
use_device_communicator: bool,
76
+ coord_store: Store,
77
use_message_queue_broadcaster: bool = False,
78
group_name: str | None = None,
79
host: str = "127.0.0.1",
45
- group_ports: list[list[int]] | None = None,
80
global_rank: int = 0,
81
global_world_size: int = 1,
82
):
@@ -61,17 +95,23 @@ class StatelessGroupCoordinator(GroupCoordinator):
95
96
backend = str(torch_distributed_backend)
97
self.backend = backend
64
- assert group_ports is not None, "group_ports is not provided"
98
for idx, ranks in enumerate(group_ranks):
99
if self.rank in ranks:
100
self.ranks = ranks
101
self.world_size = len(ranks)
102
self.rank_in_group = ranks.index(self.rank)
103
71
- ports = group_ports[idx]
72
- device_port = ports[0]
73
- cpu_port = ports[1]
74
- tcp_store_port = ports[2]
104
+ key = f"{group_name}_{idx}"
105
+ if self.rank_in_group == 0:
106
+ ports, socks = _allocate_group_ports(
107
+ key,
108
+ host,
109
+ coord_store,
110
+ )
111
+ else:
112
+ ports = _fetch_group_ports(key, coord_store)
113
+ socks = []
114
+ device_port, cpu_port, tcp_store_port = ports
115
116
device_group = stateless_init_torch_distributed_process_group(
117
host=host,
@@ -80,6 +120,7 @@ class StatelessGroupCoordinator(GroupCoordinator):
120
world_size=self.world_size,
121
backend=backend,
122
group_name=f"{self.unique_name}_device",
123
+ listen_socket=socks[0] if socks else None,
124
)
125
cpu_group = stateless_init_torch_distributed_process_group(
126
host=host,
@@ -88,12 +129,14 @@ class StatelessGroupCoordinator(GroupCoordinator):
129
world_size=self.world_size,
130
backend="gloo",
131
group_name=f"{self.unique_name}_cpu",
132
+ listen_socket=socks[1] if socks else None,
133
)
134
tcp_store_group = StatelessProcessGroup.create(
135
host=host,
136
port=tcp_store_port,
137
rank=self.rank_in_group,
138
world_size=self.world_size,
139
+ listen_socket=socks[2] if socks else None,
140
)
141
142
self_device_group = device_group
vllm/distributed/utils.py
+60
-18
@@ -6,6 +6,7 @@
6
# https://github.com/NVIDIA/Megatron-LM/blob/main/megatron/core/tensor_parallel/utils.py
7
# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
8
import dataclasses
9
+import functools
10
import os
11
import pickle
12
import socket
@@ -139,6 +140,29 @@ def get_pp_indices(
140
return (start_layer, end_layer)
141
142
143
+def create_tcp_store(
144
+ host: str,
145
+ port: int,
146
+ listen_socket: socket.socket | None = None,
147
+ **kwargs: Any,
148
+) -> TCPStore:
149
+ """Create a TCPStore, optionally taking ownership of ``listen_socket``."""
150
+ if listen_socket is None:
151
+ return TCPStore(host_name=host, port=port, **kwargs)
152
+
153
+ listen_fd = listen_socket.detach()
154
+ try:
155
+ return TCPStore(
156
+ host_name=host,
157
+ port=port,
158
+ master_listen_fd=listen_fd,
159
+ **kwargs,
160
+ )
161
+ except Exception:
162
+ socket.close(listen_fd)
163
+ raise
164
+
165
+
166
@dataclasses.dataclass
167
class StatelessProcessGroup:
168
"""A dataclass to hold a metadata store, and the rank, world_size of the
@@ -150,9 +174,6 @@ class StatelessProcessGroup:
174
world_size: int
175
store: torch._C._distributed_c10d.Store
176
153
- # stores a reference to the socket so that the file descriptor stays alive
154
- socket: socket.socket | None
155
-
177
data_expiration_seconds: int = 3600 # 1 hour
178
179
# dst rank -> counter
@@ -419,6 +440,7 @@ class StatelessProcessGroup:
440
world_size: int,
441
data_expiration_seconds: int = 3600,
442
store_timeout: int = 300,
443
+ listen_socket: socket.socket | None = None,
444
) -> "StatelessProcessGroup":
445
"""A replacement for `torch.distributed.init_process_group` that does not
446
pollute the global state.
@@ -436,36 +458,39 @@ class StatelessProcessGroup:
458
C, and D can call `StatelessProcessGroup.create` to form another group.
459
""" # noqa
460
launch_server = rank == 0
439
- if launch_server:
440
- # listen on the specified interface (instead of 0.0.0.0)
461
+ if launch_server and listen_socket is None:
462
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
463
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
464
listen_socket.bind((host, port))
465
listen_socket.listen()
445
- listen_fd = listen_socket.fileno()
446
- else:
447
- listen_socket = None
448
- listen_fd = None
449
-
450
- store = TCPStore(
451
- host_name=host,
452
- port=port,
466
+ store = create_tcp_store(
467
+ host,
468
+ port,
469
+ listen_socket=listen_socket,
470
world_size=world_size,
471
is_master=launch_server,
472
timeout=timedelta(seconds=store_timeout),
473
use_libuv=False, # for now: github.com/pytorch/pytorch/pull/150215
457
- master_listen_fd=listen_fd,
474
)
475
476
return StatelessProcessGroup(
477
rank=rank,
478
world_size=world_size,
479
store=store,
464
- socket=listen_socket,
480
data_expiration_seconds=data_expiration_seconds,
481
)
482
483
484
+@functools.lru_cache(maxsize=1)
485
+def get_cached_tcp_store_client(host: str, port: int) -> TCPStore:
486
+ """Return a cached TCPStore client.
487
+
488
+ Cached so that every call with the same ``(host, port)`` reuses the
489
+ same connection. A new ``(host, port)`` evicts the old entry.
490
+ """
491
+ return TCPStore(host, port, is_master=False, wait_for_workers=False)
492
+
493
+
494
def init_gloo_process_group(
495
prefix_store: PrefixStore,
496
group_rank: int,
@@ -504,6 +529,7 @@ def stateless_init_torch_distributed_process_group(
529
backend: str,
530
group_name: str | None = None,
531
return_store: bool = False,
532
+ listen_socket: socket.socket | None = None,
533
) -> ProcessGroup | tuple[ProcessGroup, Store]:
534
"""
535
A replacement for `torch.distributed.init_process_group` that does not
@@ -535,14 +561,30 @@ def stateless_init_torch_distributed_process_group(
561
are the same as process 1 and 5, the main communication channel is
562
always formed with process 1, 2, ..., 8, and the additional communication
563
channel is formed with process 9 and 10.
564
+
565
+ When *listen_socket* is provided, the rendezvous step
566
+ is skipped and a ``TCPStore`` server is created directly using the
567
+ pre-bound socket. This is useful for eliminating TOCTOU races
568
+ between port allocation and binding.
569
"""
570
init_method = get_tcp_uri(host, port)
571
backend = Backend(backend) # it is basically string
572
timeout = _get_default_timeout(backend)
573
543
- store, rank, world_size = next(
544
- rendezvous(init_method, rank, world_size, timeout=timeout)
545
- )
574
+ if listen_socket is not None:
575
+ store = create_tcp_store(
576
+ host,
577
+ port,
578
+ listen_socket=listen_socket,
579
+ world_size=world_size,
580
+ is_master=True,
581
+ timeout=timeout,
582
+ multi_tenant=True,
583
+ )
584
+ else:
585
+ store, rank, world_size = next(
586
+ rendezvous(init_method, rank, world_size, timeout=timeout)
587
+ )
588
store.set_timeout(timeout)
589
590
group_rank = rank
vllm/v1/engine/__init__.py
+1
-4
@@ -237,10 +237,7 @@ class ReconfigureDistributedRequest(msgspec.Struct):
237
new_data_parallel_master_ip: str
238
new_data_parallel_master_port: int
239
new_data_parallel_master_port_list: list[int]
240
- new_stateless_world_group_port_list: list[list[int]]
241
- new_stateless_dp_group_port_list: list[list[int]]
242
- new_stateless_ep_group_port_list: list[list[int]]
243
- new_stateless_eplb_group_port_list: list[list[int]]
240
+ coord_store_port: int
241
242
243
class ReconfigureRankType(enum.IntEnum):
vllm/v1/engine/core.py
+1
@@ -1767,6 +1767,7 @@ class DPEngineCoreProc(EngineCoreProc):
1767
new_parallel_config._data_parallel_master_port_list = (
1768
reconfig_request.new_data_parallel_master_port_list
1769
)
1770
+ new_parallel_config._coord_store_port = reconfig_request.coord_store_port
1771
1772
is_scale_down = reconfig_request.new_data_parallel_size < old_dp_size
1773
is_shutdown = (
vllm/v1/engine/core_client.py
+28
-62
@@ -455,56 +455,6 @@ class ElasticScalingCache:
455
pending_notifications: dict[EEPNotificationType, set[int]]
456
457
458
-def allocate_stateless_group_ports(parallel_config, new_data_parallel_size: int):
459
- """
460
- Allocate stateless group ports for elastic EP.
461
- """
462
- from vllm.utils.network_utils import get_open_ports_list
463
-
464
- assert parallel_config.enable_elastic_ep, "Elastic EP must be enabled"
465
- world_size = parallel_config.world_size
466
- new_world_size_across_dp = world_size * new_data_parallel_size
467
- num_world_groups = 1
468
- num_dp_groups = max(1, new_world_size_across_dp // new_data_parallel_size)
469
- num_ep_groups = max(
470
- 1,
471
- new_world_size_across_dp
472
- // (new_data_parallel_size * parallel_config.tensor_parallel_size),
473
- )
474
- num_eplb_groups = num_ep_groups
475
- total_ports_needed = (
476
- num_world_groups + num_dp_groups + num_ep_groups + num_eplb_groups
477
- ) * 3 + 5
478
- all_ports = get_open_ports_list(total_ports_needed)
479
- new_data_parallel_master_port_list = all_ports[-5:]
480
- all_ports = all_ports[:-5]
481
- new_stateless_world_group_port_list = [
482
- all_ports[i : i + 3] for i in range(0, num_world_groups * 3, 3)
483
- ]
484
- start_idx = num_world_groups * 3
485
- new_stateless_dp_group_port_list = [
486
- all_ports[i : i + 3] for i in range(start_idx, start_idx + num_dp_groups * 3, 3)
487
- ]
488
- start_idx += num_dp_groups * 3
489
- new_stateless_ep_group_port_list = [
490
- all_ports[i : i + 3] for i in range(start_idx, start_idx + num_ep_groups * 3, 3)
491
- ]
492
- start_idx += num_ep_groups * 3
493
- new_stateless_eplb_group_port_list = [
494
- all_ports[i : i + 3]
495
- for i in range(start_idx, start_idx + num_eplb_groups * 3, 3)
496
- ]
497
-
498
- parallel_config._stateless_world_group_port_list = (
499
- new_stateless_world_group_port_list
500
- )
501
- parallel_config._stateless_dp_group_port_list = new_stateless_dp_group_port_list
502
- parallel_config._stateless_ep_group_port_list = new_stateless_ep_group_port_list
503
- parallel_config._stateless_eplb_group_port_list = new_stateless_eplb_group_port_list
504
- parallel_config.data_parallel_master_port = new_data_parallel_master_port_list.pop()
505
- parallel_config._data_parallel_master_port_list = new_data_parallel_master_port_list
506
-
507
-
458
class MPClient(EngineCoreClient):
459
"""
460
MPClient: base client for multi-proc EngineCore.
@@ -1541,6 +1491,28 @@ class DPLBAsyncMPClient(DPAsyncMPClient):
1491
self._ensure_output_queue_task()
1492
await future
1493
1494
+ def _setup_elastic_ep_reconfig_bootstrap(self) -> tuple[str, int]:
1495
+ from vllm.distributed.utils import create_tcp_store
1496
+ from vllm.utils.network_utils import get_open_ports_list
1497
+
1498
+ parallel_config = self.vllm_config.parallel_config
1499
+ parallel_config._data_parallel_master_port_list = get_open_ports_list(5)
1500
+ parallel_config.data_parallel_master_port = (
1501
+ parallel_config._data_parallel_master_port_list.pop()
1502
+ )
1503
+
1504
+ ip = parallel_config.data_parallel_master_ip
1505
+ store = create_tcp_store(
1506
+ ip,
1507
+ 0,
1508
+ is_master=True,
1509
+ world_size=-1,
1510
+ wait_for_workers=False,
1511
+ )
1512
+ parallel_config._coord_store_port = store.port
1513
+ self._coord_store = store
1514
+ return ip, store.port
1515
+
1516
async def _scale_up_elastic_ep(
1517
self, cur_data_parallel_size: int, new_data_parallel_size: int
1518
) -> None:
@@ -1555,7 +1527,7 @@ class DPLBAsyncMPClient(DPAsyncMPClient):
1527
)
1528
1529
parallel_config = self.vllm_config.parallel_config
1558
- allocate_stateless_group_ports(parallel_config, new_data_parallel_size)
1530
+ ip, coord_store_port = self._setup_elastic_ep_reconfig_bootstrap()
1531
1532
# Phase 1: Send reconfig messages to existing engines
1533
reconfig_futures = []
@@ -1564,13 +1536,10 @@ class DPLBAsyncMPClient(DPAsyncMPClient):
1536
new_data_parallel_size=new_data_parallel_size,
1537
new_data_parallel_rank=ReconfigureRankType.KEEP_CURRENT_RANK,
1538
new_data_parallel_rank_local=ReconfigureRankType.KEEP_CURRENT_RANK,
1567
- new_data_parallel_master_ip=parallel_config.data_parallel_master_ip,
1539
+ new_data_parallel_master_ip=ip,
1540
new_data_parallel_master_port=parallel_config.data_parallel_master_port,
1541
new_data_parallel_master_port_list=parallel_config._data_parallel_master_port_list,
1570
- new_stateless_world_group_port_list=parallel_config._stateless_world_group_port_list,
1571
- new_stateless_dp_group_port_list=parallel_config._stateless_dp_group_port_list,
1572
- new_stateless_ep_group_port_list=parallel_config._stateless_ep_group_port_list,
1573
- new_stateless_eplb_group_port_list=parallel_config._stateless_eplb_group_port_list,
1542
+ coord_store_port=coord_store_port,
1543
)
1544
coro = self._call_utility_async(
1545
"reinitialize_distributed", reconfig_request, engine=engine
@@ -1650,7 +1619,7 @@ class DPLBAsyncMPClient(DPAsyncMPClient):
1619
)
1620
1621
parallel_config = self.vllm_config.parallel_config
1653
- allocate_stateless_group_ports(parallel_config, new_data_parallel_size)
1622
+ ip, coord_store_port = self._setup_elastic_ep_reconfig_bootstrap()
1623
1624
reconfig_futures = []
1625
for cur_dp_rank, engine in enumerate(self.core_engines):
@@ -1658,13 +1627,10 @@ class DPLBAsyncMPClient(DPAsyncMPClient):
1627
new_data_parallel_size=new_data_parallel_size,
1628
new_data_parallel_rank=ReconfigureRankType.KEEP_CURRENT_RANK,
1629
new_data_parallel_rank_local=ReconfigureRankType.KEEP_CURRENT_RANK,
1661
- new_data_parallel_master_ip=parallel_config.data_parallel_master_ip,
1630
+ new_data_parallel_master_ip=ip,
1631
new_data_parallel_master_port=parallel_config.data_parallel_master_port,
1632
new_data_parallel_master_port_list=parallel_config._data_parallel_master_port_list,
1664
- new_stateless_world_group_port_list=parallel_config._stateless_world_group_port_list,
1665
- new_stateless_dp_group_port_list=parallel_config._stateless_dp_group_port_list,
1666
- new_stateless_ep_group_port_list=parallel_config._stateless_ep_group_port_list,
1667
- new_stateless_eplb_group_port_list=parallel_config._stateless_eplb_group_port_list,
1633
+ coord_store_port=coord_store_port,
1634
)
1635
if cur_dp_rank >= new_data_parallel_size:
1636
reconfig_request.new_data_parallel_rank = (
vllm/v1/engine/utils.py
+14
-1
@@ -301,7 +301,20 @@ class CoreEngineActorManager:
301
else:
302
ray.init()
303
304
- vllm_config.parallel_config.allocate_elastic_ep_ports()
304
+ parallel_config = vllm_config.parallel_config
305
+ if parallel_config.enable_elastic_ep:
306
+ from vllm.distributed.utils import create_tcp_store
307
+
308
+ ip = parallel_config.data_parallel_master_ip
309
+ store = create_tcp_store(
310
+ ip,
311
+ 0,
312
+ is_master=True,
313
+ world_size=-1,
314
+ wait_for_workers=False,
315
+ )
316
+ parallel_config._coord_store_port = store.port
317
+ self._coord_store = store
318
319
if placement_groups is not None:
320
assert local_dp_ranks is not None, (