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, 05:30 UTC
[NIXL] Terminate handshake listener thread in shutdown (#26404)
Signed-off-by: Mark McLoughlin <markmc@redhat.com>
Mark McLoughlin committed
Oct 22, 2025 at 15:59 UTC
4ca13a8667c7c177f99ed15ed3f6996dbb081b8a
2 files changed
+52
-11
tests/v1/kv_connector/unit/test_nixl_connector.py
+22
-7
@@ -938,6 +938,13 @@ def test_abort_timeout_on_prefiller(monkeypatch, distributed_executor_backend):
938
monkeypatch.setenv("VLLM_ENABLE_V1_MULTIPROCESSING", "0")
939
monkeypatch.setenv("VLLM_NIXL_ABORT_REQUEST_TIMEOUT", str(timeout))
940
941
+ def run_test_and_cleanup():
942
+ llm = LLM(**llm_kwargs)
943
+ try:
944
+ _run_abort_timeout_test(llm, timeout)
945
+ finally:
946
+ llm.llm_engine.engine_core.shutdown()
947
+
948
# Build runtime_env only if we're using Ray
949
if distributed_executor_backend == "ray":
950
with _make_fake_nixl_pkg() as working_dir:
@@ -950,15 +957,16 @@ def test_abort_timeout_on_prefiller(monkeypatch, distributed_executor_backend):
957
},
958
}
959
ray.init(runtime_env=runtime_env)
953
-
954
- _run_abort_timeout_test(llm_kwargs, timeout)
960
+ try:
961
+ run_test_and_cleanup()
962
+ finally:
963
+ ray.shutdown()
964
else:
956
- _run_abort_timeout_test(llm_kwargs, timeout)
965
+ run_test_and_cleanup()
966
967
959
-def _run_abort_timeout_test(llm_kwargs: dict, timeout: int):
968
+def _run_abort_timeout_test(llm: LLM, timeout: int):
969
"""Helper function to run the abort timeout test logic."""
961
- llm = LLM(**llm_kwargs)
970
remote_prefill_opts = {
971
"do_remote_decode": True,
972
"do_remote_prefill": False,
@@ -1042,7 +1050,7 @@ def test_register_kv_caches(dist_init):
1050
),
1051
patch(
1052
"vllm.distributed.kv_transfer.kv_connector.v1.nixl_connector.threading.Thread"
1045
- ),
1053
+ ) as mock_thread,
1054
): # noqa: E501
1055
# Create connector
1056
connector = NixlConnector(vllm_config, KVConnectorRole.WORKER)
@@ -1054,6 +1062,9 @@ def test_register_kv_caches(dist_init):
1062
mock_wrapper_instance = mock_nixl_wrapper.return_value
1063
connector.connector_worker.nixl_wrapper = mock_wrapper_instance
1064
1065
+ # Reassure the shutdown() check that the thread is terminated
1066
+ mock_thread.return_value.is_alive.return_value = False
1067
+
1068
# Execute register_kv_caches
1069
connector.register_kv_caches(kv_caches)
1070
@@ -1171,6 +1182,7 @@ def test_shutdown_cleans_up_resources(dist_init):
1182
with (
1183
patch.object(worker, "_handshake_initiation_executor") as mock_exec,
1184
patch.object(worker, "_nixl_handshake_listener_t") as mock_listener,
1185
+ patch.object(worker, "_nixl_handshake_listener_stop_event") as mock_event,
1186
patch.object(nixl_wrapper, "release_xfer_handle") as mock_rel_xfer,
1187
patch.object(nixl_wrapper, "release_dlist_handle") as mock_rel_dlist,
1188
patch.object(nixl_wrapper, "remove_remote_agent") as mock_rem_agent,
@@ -1182,6 +1194,8 @@ def test_shutdown_cleans_up_resources(dist_init):
1194
worker._remote_agents = {"engine1": {0: "agent1"}}
1195
worker._registered_descs = ["desc1", "desc2"]
1196
1197
+ mock_listener.is_alive.return_value = False
1198
+
1199
worker.shutdown()
1200
1201
# Test idempotency
@@ -1189,7 +1203,8 @@ def test_shutdown_cleans_up_resources(dist_init):
1203
worker.shutdown()
1204
1205
mock_exec.shutdown.assert_called_with(wait=False)
1192
- mock_listener.join.assert_called_once_with(timeout=0)
1206
+ mock_event.set.assert_called_once()
1207
+ mock_listener.join.assert_called_once_with(timeout=1.0)
1208
1209
mock_rel_xfer.assert_called_once_with(123)
1210
assert mock_rel_dlist.call_count == 2
vllm/distributed/kv_transfer/kv_connector/v1/nixl_connector.py
+30
-4
@@ -520,6 +520,8 @@ class NixlConnectorScheduler:
520
class NixlConnectorWorker:
521
"""Implementation of Worker side methods"""
522
523
+ _POLL_TIMEOUT = 0.1 # Handshake thread polls for stop event every 100ms
524
+
525
@dataclass
526
class TpKVTopology:
527
"""
@@ -719,6 +721,7 @@ class NixlConnectorWorker:
721
722
# Background thread for handling new handshake requests.
723
self._nixl_handshake_listener_t: threading.Thread | None = None
724
+ self._nixl_handshake_listener_stop_event: threading.Event | None = None
725
# Background thread for initializing new NIXL handshakes.
726
self._handshake_initiation_executor = ThreadPoolExecutor(
727
# NIXL is not guaranteed to be thread-safe, limit 1 worker.
@@ -773,6 +776,7 @@ class NixlConnectorWorker:
776
def _nixl_handshake_listener(
777
metadata: NixlAgentMetadata,
778
ready_event: threading.Event,
779
+ stop_event: threading.Event,
780
base_port: int,
781
tp_rank: int,
782
):
@@ -791,7 +795,14 @@ class NixlConnectorWorker:
795
logger.debug("Starting listening on path: %s", path)
796
with zmq_ctx(zmq.ROUTER, path) as sock:
797
ready_event.set()
794
- while True:
798
+ poller = zmq.Poller()
799
+ poller.register(sock, zmq.POLLIN)
800
+ while not stop_event.is_set():
801
+ events = dict(
802
+ poller.poll(timeout=NixlConnectorWorker._POLL_TIMEOUT * 1000)
803
+ )
804
+ if sock not in events:
805
+ continue
806
identity, _, msg = sock.recv_multipart()
807
if msg != GET_META_MSG:
808
logger.warning("Connection listener got unexpected message %s", msg)
@@ -1101,14 +1112,21 @@ class NixlConnectorWorker:
1112
attn_backend_name=self.backend_name,
1113
kv_cache_layout=self.kv_cache_layout,
1114
)
1104
- ready_event = threading.Event()
1115
+ ready_event, stop_event = threading.Event(), threading.Event()
1116
self._nixl_handshake_listener_t = threading.Thread(
1117
target=self._nixl_handshake_listener,
1107
- args=(metadata, ready_event, self.side_channel_port, self.tp_rank),
1118
+ args=(
1119
+ metadata,
1120
+ ready_event,
1121
+ stop_event,
1122
+ self.side_channel_port,
1123
+ self.tp_rank,
1124
+ ),
1125
daemon=True,
1126
name="nixl_handshake_listener",
1127
)
1128
self._nixl_handshake_listener_t.start()
1129
+ self._nixl_handshake_listener_stop_event = stop_event
1130
ready_event.wait() # Wait for listener ZMQ socket to be ready.
1131
1132
def add_remote_agent(
@@ -1782,11 +1800,19 @@ class NixlConnectorWorker:
1800
self._invalid_block_ids = set()
1801
return result
1802
1803
+ def __del__(self):
1804
+ self.shutdown()
1805
+
1806
def shutdown(self):
1807
"""Shutdown the connector worker."""
1808
self._handshake_initiation_executor.shutdown(wait=False)
1809
+ if self._nixl_handshake_listener_stop_event is not None:
1810
+ self._nixl_handshake_listener_stop_event.set()
1811
+ self._nixl_handshake_listener_stop_event = None
1812
if self._nixl_handshake_listener_t is not None:
1789
- self._nixl_handshake_listener_t.join(timeout=0)
1813
+ # Generous timeout to allow the thread to exit
1814
+ self._nixl_handshake_listener_t.join(timeout=self._POLL_TIMEOUT * 10)
1815
+ assert not self._nixl_handshake_listener_t.is_alive()
1816
self._nixl_handshake_listener_t = None
1817
for handles in self._recving_transfers.values():
1818
for handle, _ in handles: