[Perf][KVConnector][Mooncake] Parallelize KV load with a receive-thread pool (#45971)

Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

Yifan Qiao committed Jun 24, 2026 at 18:25 UTC 9e88e969c08e11035793e939ea76e9b7e526bc0f
3 files changed +54 -27
tests/v1/kv_connector/unit/test_mooncake_store_worker.py
+5 -2
@@ -4,6 +4,7 @@
4 import json
5 import logging
6 import math
7 +import queue
8 import sys
9 import threading
10 import types
@@ -760,7 +761,7 @@ def test_store_worker_get_block_ids_with_load_errors_delegates_to_recv_thread():
761 recv_thread = MagicMock()
762 recv_thread.get_and_clear_block_ids_with_load_errors.return_value = {3, 4}
763 w = _make_bare_worker()
763 - w.kv_recv_thread = recv_thread
764 + w.kv_recv_threads = [recv_thread]
765
766 assert w.get_block_ids_with_load_errors() == {3, 4}
767 recv_thread.get_and_clear_block_ids_with_load_errors.assert_called_once_with()
@@ -1574,7 +1575,9 @@ def _make_bare_worker(
1575 worker.put_step = 1
1576 worker.enable_kv_events = False
1577 worker.kv_send_thread = None
1577 - worker.kv_recv_thread = None
1578 + worker.kv_recv_threads = []
1579 + worker.num_recv_threads = 1
1580 + worker.recv_request_queue = queue.Queue()
1581 worker.tp_size = 1
1582 worker.num_kv_head = 1
1583 worker.pp_size = 1
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/worker.py
+40 -25
@@ -359,6 +359,7 @@ class KVTransferThread(threading.Thread):
359 ready_event: threading.Event,
360 name: str,
361 record_operation: Callable[..., None] | None = None,
362 + request_queue: queue.Queue[Any] | None = None,
363 ):
364 super().__init__(daemon=True, name=name)
365 self.store = store
@@ -368,7 +369,7 @@ class KVTransferThread(threading.Thread):
369 self.token_databases = token_databases
370 self._record_operation_cb = record_operation
371 self.done_task_lock = threading.Lock()
371 - self.request_queue: queue.Queue[Any] = queue.Queue()
372 + self.request_queue: queue.Queue[Any] = request_queue or queue.Queue()
373 self.finished_requests: set[str] = set()
374 self.kv_event_lock = threading.Lock()
375 self.kv_events: list[BlockStored] = []
@@ -749,6 +750,7 @@ class KVCacheStoreRecvingThread(KVTransferThread):
750 ready_event: threading.Event,
751 disk_offload_buffer_budget_bytes: int | None = None,
752 record_operation: Callable[..., None] | None = None,
753 + request_queue: queue.Queue[Any] | None = None,
754 ):
755 super().__init__(
756 store,
@@ -758,6 +760,7 @@ class KVCacheStoreRecvingThread(KVTransferThread):
760 ready_event,
761 name="KVCacheStoreRecvingThread",
762 record_operation=record_operation,
763 + request_queue=request_queue,
764 )
765 # _invalid_block_ids can be access by both the Worker and RecvingThread
766 self._invalid_block_ids_lock = threading.Lock()
@@ -1101,7 +1104,10 @@ class MooncakeStoreWorker:
1104 self.enable_kv_events = True
1105
1106 self.kv_send_thread: KVCacheStoreSendingThread | None = None
1104 - self.kv_recv_thread: KVCacheStoreRecvingThread | None = None
1107 + # Pool of load-receive threads
1108 + self.kv_recv_threads: list[KVCacheStoreRecvingThread] = []
1109 + self.num_recv_threads = max(1, envs.VLLM_MOONCAKE_LOAD_RECV_THREADS)
1110 + self.recv_request_queue: queue.Queue[ReqMeta] = queue.Queue()
1111 self.finished_store_req: set[str] = set()
1112 self._kv_connector_stats_lock = threading.Lock()
1113 self.kv_connector_stats = MooncakeStoreConnectorStats()
@@ -1260,19 +1266,30 @@ class MooncakeStoreWorker:
1266 )
1267 self.kv_send_thread.start()
1268
1263 - ready_event_recving = threading.Event()
1264 - self.kv_recv_thread = KVCacheStoreRecvingThread(
1265 - self.store,
1266 - self.coord,
1267 - self.token_dbs,
1268 - self.block_size,
1269 - self.tp_rank,
1270 - ready_event_recving,
1271 - disk_offload_buffer_budget_bytes=self.disk_offload_buffer_budget_bytes,
1272 - record_operation=self._record_kv_connector_operation,
1269 + self.kv_recv_threads = []
1270 + ready_events_recving = []
1271 + for i in range(self.num_recv_threads):
1272 + ready_event_recving = threading.Event()
1273 + recv_thread = KVCacheStoreRecvingThread(
1274 + self.store,
1275 + self.coord,
1276 + self.token_dbs,
1277 + self.block_size,
1278 + self.tp_rank,
1279 + ready_event_recving,
1280 + disk_offload_buffer_budget_bytes=self.disk_offload_buffer_budget_bytes,
1281 + record_operation=self._record_kv_connector_operation,
1282 + request_queue=self.recv_request_queue,
1283 + )
1284 + recv_thread.name = f"KVCacheStoreRecvingThread-{i}"
1285 + recv_thread.start()
1286 + self.kv_recv_threads.append(recv_thread)
1287 + ready_events_recving.append(ready_event_recving)
1288 + for ready_event_recving in ready_events_recving:
1289 + ready_event_recving.wait()
1290 + logger.info(
1291 + "Started %d Mooncake KV-load receive thread(s)", self.num_recv_threads
1292 )
1274 - self.kv_recv_thread.start()
1275 - ready_event_recving.wait()
1293
1294 def start_load_kv(
1295 self,
@@ -1306,9 +1323,7 @@ class MooncakeStoreWorker:
1323 continue
1324
1325 load_spec.token_len = load_spec.kvpool_cached_tokens
1309 -
1310 - assert self.kv_recv_thread is not None
1311 - self.kv_recv_thread.add_request(request)
1326 + self.recv_request_queue.put(request)
1327
1328 assert self.load_async, "load_async must be True for better performance."
1329 # Issue stores with CUDA event synchronization
@@ -1335,11 +1350,10 @@ class MooncakeStoreWorker:
1350 else set()
1351 )
1352
1338 - done_recving = (
1339 - self.kv_recv_thread.get_and_clear_finished_requests()
1340 - if self.load_async and self.kv_recv_thread is not None
1341 - else set()
1342 - )
1353 + done_recving: set[str] = set()
1354 + if self.load_async:
1355 + for recv_thread in self.kv_recv_threads:
1356 + done_recving |= recv_thread.get_and_clear_finished_requests()
1357
1358 logger.debug(
1359 "Completed send: %d, recv: %d, tp_rank: %d",
@@ -1350,9 +1364,10 @@ class MooncakeStoreWorker:
1364 return done_sending, done_recving
1365
1366 def get_block_ids_with_load_errors(self) -> set[int]:
1353 - if self.kv_recv_thread is None:
1354 - return set()
1355 - return self.kv_recv_thread.get_and_clear_block_ids_with_load_errors()
1367 + block_ids: set[int] = set()
1368 + for recv_thread in self.kv_recv_threads:
1369 + block_ids |= recv_thread.get_and_clear_block_ids_with_load_errors()
1370 + return block_ids
1371
1372 def _record_kv_connector_operation(
1373 self,
vllm/envs.py
+9
@@ -200,6 +200,7 @@ if TYPE_CHECKING:
200 VLLM_NIXL_SIDE_CHANNEL_PORT: int = 5600
201 VLLM_MOONCAKE_BOOTSTRAP_PORT: int = 8998
202 VLLM_MOONCAKE_STORE_TIER_LOG: bool = False
203 + VLLM_MOONCAKE_LOAD_RECV_THREADS: int = 1
204 VLLM_MOONCAKE_DISK_STAGING_USABLE_RATIO: float = 0.9
205 MOONCAKE_PREFERRED_SEGMENT: str | None = None
206 MOONCAKE_REQUESTER_LOCAL_HOSTNAME: str | None = None
@@ -1531,6 +1532,14 @@ environment_variables: dict[str, Callable[[], Any]] = {
1532 "VLLM_MOONCAKE_STORE_TIER_LOG": lambda: (
1533 os.getenv("VLLM_MOONCAKE_STORE_TIER_LOG", "False").lower() in ("true", "1")
1534 ),
1535 + # Number of parallel KV-load receive threads per worker rank. Lets the
1536 + # per-request control overhead (Python prep + master key lookup) of one
1537 + # request overlap with the RDMA transfer of another, keeping the transfer
1538 + # engine's queue pairs busy. Helps when that overhead is significant or
1539 + # per-request batches are too small to saturate the link on their own.
1540 + "VLLM_MOONCAKE_LOAD_RECV_THREADS": lambda: int(
1541 + os.getenv("VLLM_MOONCAKE_LOAD_RECV_THREADS", "1")
1542 + ),
1543 # Fraction of the owner's DirectIO staging buffer to fill per GET batch.
1544 "VLLM_MOONCAKE_DISK_STAGING_USABLE_RATIO": lambda: float(
1545 os.getenv("VLLM_MOONCAKE_DISK_STAGING_USABLE_RATIO", "0.9")