[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)