[Bugfix] Fix get_open_port() livelock on DP-reserved ports and cover get_open_ports_list (#50965)
Signed-off-by: Chenglun Hu <chenglunhu@gmail.com> Signed-off-by: aoshen02 <aoshen02@users.noreply.github.com> Co-authored-by: Chenglun Hu <chenglunhu@gmail.com> Co-authored-by: ferkans-amir <amir.rezaei@tu-berlin.de> Co-authored-by: aoshen02 <aoshen02@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
aoshen02 committed
Aug 7, 2026 at 22:39 UTC
448344c0e29383adfe606a5c7ede72dd74705321
2 files changed
+115
-8
tests/utils_/test_network_utils.py
+97
@@ -1,10 +1,12 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import socket
4
+import threading
5
6
import pytest
7
import zmq
8
9
+from vllm.utils import network_utils
10
from vllm.utils.network_utils import (
11
get_open_port,
12
get_open_ports_list,
@@ -17,6 +19,26 @@ from vllm.utils.network_utils import (
19
)
20
21
22
+def _call_with_timeout(func, timeout: float = 10.0):
23
+ """Run func in a daemon thread so a livelock regression fails the test
24
+ quickly instead of hanging the suite."""
25
+ result: dict = {}
26
+
27
+ def target():
28
+ try:
29
+ result["value"] = func()
30
+ except BaseException as e:
31
+ result["error"] = e
32
+
33
+ thread = threading.Thread(target=target, daemon=True)
34
+ thread.start()
35
+ thread.join(timeout)
36
+ assert not thread.is_alive(), f"call did not finish within {timeout}s"
37
+ if "error" in result:
38
+ raise result["error"]
39
+ return result["value"]
40
+
41
+
42
def test_get_open_port(monkeypatch: pytest.MonkeyPatch):
43
with monkeypatch.context() as m:
44
m.setenv("VLLM_PORT", "5678")
@@ -29,6 +51,20 @@ def test_get_open_port(monkeypatch: pytest.MonkeyPatch):
51
s3.bind(("localhost", get_open_port()))
52
53
54
+def test_get_open_port_vllm_port_in_dp_reserved_range(
55
+ monkeypatch: pytest.MonkeyPatch,
56
+):
57
+ # VLLM_PORT falling inside the data-parallel reserved window used to make
58
+ # get_open_port() loop forever (issue #50024). It must instead return a
59
+ # port outside the reserved range.
60
+ with monkeypatch.context() as m:
61
+ m.setenv("VLLM_DP_MASTER_PORT", "5680")
62
+ # 5682 is inside [5680, 5690).
63
+ m.setenv("VLLM_PORT", "5682")
64
+ port = _call_with_timeout(get_open_port)
65
+ assert port not in range(5680, 5690)
66
+
67
+
68
def test_get_open_ports_list_with_vllm_port(monkeypatch: pytest.MonkeyPatch):
69
with monkeypatch.context() as m:
70
m.setenv("VLLM_PORT", "5678")
@@ -48,6 +84,67 @@ def test_get_open_ports_list_with_vllm_port(monkeypatch: pytest.MonkeyPatch):
84
s.close()
85
86
87
+def test_get_open_port_skips_reserved_dp_master_ports(
88
+ monkeypatch: pytest.MonkeyPatch,
89
+):
90
+ """VLLM_PORT == VLLM_DP_MASTER_PORT must not livelock by returning the
91
+ same free-but-reserved port forever (upstream issue #50024)."""
92
+ base = 42123
93
+ with monkeypatch.context() as m:
94
+ m.setenv("VLLM_PORT", str(base))
95
+ m.setenv("VLLM_DP_MASTER_PORT", str(base))
96
+ port = _call_with_timeout(get_open_port)
97
+ assert port >= base + 10
98
+
99
+
100
+def test_get_open_ports_list_skips_reserved_dp_master_ports(
101
+ monkeypatch: pytest.MonkeyPatch,
102
+):
103
+ """The VLLM_PORT band scan must not hand out ports inside the window
104
+ reserved for the data parallel master process."""
105
+ base = 42223
106
+ reserved = range(base, base + 10)
107
+ with monkeypatch.context() as m:
108
+ m.setenv("VLLM_PORT", str(base))
109
+ m.setenv("VLLM_DP_MASTER_PORT", str(base))
110
+ ports = _call_with_timeout(lambda: get_open_ports_list(5))
111
+ assert len(ports) == 5
112
+ assert len(set(ports)) == 5, "ports must be unique"
113
+ assert all(p not in reserved for p in ports)
114
+
115
+
116
+def test_get_open_port_ephemeral_skips_reserved_range(
117
+ monkeypatch: pytest.MonkeyPatch,
118
+):
119
+ """An ephemeral port that happens to fall in the reserved range is
120
+ replaced by a rescan starting past the range."""
121
+ base = 42323
122
+ with monkeypatch.context() as m:
123
+ m.delenv("VLLM_PORT", raising=False)
124
+ m.setenv("VLLM_DP_MASTER_PORT", str(base))
125
+ m.setattr(
126
+ network_utils,
127
+ "_get_open_port",
128
+ lambda start_port=None, max_attempts=None: (
129
+ base if start_port is None else start_port
130
+ ),
131
+ )
132
+ assert _call_with_timeout(get_open_port) == base + 10
133
+
134
+
135
+def test_get_open_port_ephemeral_without_dp_master_port(
136
+ monkeypatch: pytest.MonkeyPatch,
137
+):
138
+ """Without VLLM_PORT and VLLM_DP_MASTER_PORT, an ephemeral port is
139
+ returned as before."""
140
+ with monkeypatch.context() as m:
141
+ m.delenv("VLLM_PORT", raising=False)
142
+ m.delenv("VLLM_DP_MASTER_PORT", raising=False)
143
+ port = _call_with_timeout(get_open_port)
144
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
145
+ s.bind(("localhost", port))
146
+
147
+
148
@pytest.mark.parametrize(
149
"path,expected",
150
[
vllm/utils/network_utils.py
+18
-8
@@ -147,6 +147,14 @@ def get_open_zmq_inproc_path() -> str:
147
return f"inproc://{uuid4()}"
148
149
150
+def _get_reserved_port_range() -> range:
151
+ """Ports reserved for the data parallel master process (empty if unset)."""
152
+ if "VLLM_DP_MASTER_PORT" not in os.environ:
153
+ return range(0)
154
+ dp_master_port = envs.VLLM_DP_MASTER_PORT
155
+ return range(dp_master_port, dp_master_port + 10)
156
+
157
+
158
def get_open_port() -> int:
159
"""
160
Get an open port for the vLLM process to listen on.
@@ -156,14 +164,11 @@ def get_open_port() -> int:
164
Right now we reserve 10 ports for the data parallel master
165
process. Currently it uses 2 ports.
166
"""
159
- if "VLLM_DP_MASTER_PORT" in os.environ:
160
- dp_master_port = envs.VLLM_DP_MASTER_PORT
161
- reserved_port_range = range(dp_master_port, dp_master_port + 10)
162
- while True:
163
- candidate_port = _get_open_port()
164
- if candidate_port not in reserved_port_range:
165
- return candidate_port
166
- return _get_open_port()
167
+ reserved_port_range = _get_reserved_port_range()
168
+ port = _get_open_port()
169
+ if port in reserved_port_range:
170
+ port = _get_open_port(start_port=reserved_port_range.stop, max_attempts=1000)
171
+ return port
172
173
174
def get_open_ports_list(count: int = 5) -> list[int]:
@@ -174,9 +179,14 @@ def get_open_ports_list(count: int = 5) -> list[int]:
179
"""
180
ports_set = set[int]()
181
if envs.VLLM_PORT is not None:
182
+ reserved_port_range = _get_reserved_port_range()
183
next_port = envs.VLLM_PORT
184
for _ in range(count):
185
port = _get_open_port(start_port=next_port, max_attempts=1000)
186
+ if port in reserved_port_range:
187
+ port = _get_open_port(
188
+ start_port=reserved_port_range.stop, max_attempts=1000
189
+ )
190
ports_set.add(port)
191
next_port = port + 1
192
return list(ports_set)