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 2, 20:45 UTC
[KV Connector] Propagate MooncakeStore load failures (#42788)
Signed-off-by: Dao Le <Dao007forever@gmail.com>
Dao007forever committed
May 25, 2026 at 22:12 UTC
c2a4005c70e8155624e9687de5e83b395f2a2c83
4 files changed
+143
-15
tests/v1/kv_connector/unit/test_mooncake_store_connector.py
+4
@@ -103,14 +103,18 @@ def test_worker_methods_delegate_to_store_worker():
103
104
worker = mock_worker_cls.return_value
105
worker.get_finished.return_value = ({"req-1"}, {"req-2"})
106
+ worker.get_block_ids_with_load_errors.return_value = {3, 4}
107
connector.bind_connector_metadata(metadata)
108
109
connector.register_kv_caches(kv_caches)
110
result = connector.get_finished(finished_req_ids)
111
+ invalid_block_ids = connector.get_block_ids_with_load_errors()
112
113
worker.register_kv_caches.assert_called_once_with(kv_caches)
114
worker.get_finished.assert_called_once_with(finished_req_ids, metadata)
115
assert result == ({"req-1"}, {"req-2"})
116
+ worker.get_block_ids_with_load_errors.assert_called_once_with()
117
+ assert invalid_block_ids == {3, 4}
118
119
120
def test_get_kv_connector_kv_cache_events_returns_none_when_empty():
tests/v1/kv_connector/unit/test_mooncake_store_worker.py
+71
-3
@@ -77,6 +77,7 @@ def _make_store_sending_thread(
77
def _make_store_recving_thread(
78
store: MagicMock,
79
*,
80
+ tp_rank: int = 0,
81
disk_offload_buffer_budget_bytes: int | None = None,
82
) -> mooncake_store_worker.KVCacheStoreRecvingThread:
83
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheGroupSpec
@@ -96,7 +97,7 @@ def _make_store_recving_thread(
97
store=store,
98
token_databases=[token_database],
99
block_size=16,
99
- tp_rank=0,
100
+ tp_rank=tp_rank,
101
ready_event=threading.Event(),
102
coord=coord,
103
disk_offload_buffer_budget_bytes=disk_offload_buffer_budget_bytes,
@@ -460,6 +461,67 @@ def test_store_sending_thread_only_skips_on_no_available_handle():
461
assert store.batch_put_from_multi_buffers.call_count == 2
462
463
464
+def test_store_recving_thread_reports_failed_block_ids():
465
+ store = MagicMock()
466
+ store.batch_get_into_multi_buffers.return_value = [256, -5, -7]
467
+ thread = _make_store_recving_thread(store)
468
+
469
+ thread._handle_request(
470
+ _make_load_req(
471
+ "req-a",
472
+ [b"a0", b"a1", b"a2"],
473
+ token_len=48,
474
+ )
475
+ )
476
+
477
+ assert thread.get_and_clear_finished_requests() == {"req-a"}
478
+ assert thread.get_and_clear_block_ids_with_load_errors() == {1, 2}
479
+ assert thread.get_and_clear_block_ids_with_load_errors() == set()
480
+
481
+
482
+def test_store_recving_thread_reports_failed_block_ids_after_rotation():
483
+ store = MagicMock()
484
+ store.batch_get_into_multi_buffers.return_value = [256, -5, 256]
485
+ thread = _make_store_recving_thread(store, tp_rank=1)
486
+
487
+ thread._handle_request(
488
+ _make_load_req(
489
+ "req-a",
490
+ [b"a0", b"a1", b"a2"],
491
+ token_len=48,
492
+ )
493
+ )
494
+
495
+ assert thread.get_and_clear_block_ids_with_load_errors() == {2}
496
+
497
+
498
+def test_store_recving_thread_reports_all_attempted_blocks_on_exception():
499
+ store = MagicMock()
500
+ store.batch_get_into_multi_buffers.side_effect = RuntimeError("boom")
501
+ thread = _make_store_recving_thread(store)
502
+
503
+ thread._handle_request(
504
+ _make_load_req(
505
+ "req-a",
506
+ [b"a0", b"a1", b"a2"],
507
+ token_len=48,
508
+ )
509
+ )
510
+
511
+ assert thread.get_and_clear_finished_requests() == {"req-a"}
512
+ assert thread.get_and_clear_block_ids_with_load_errors() == {0, 1, 2}
513
+
514
+
515
+def test_store_worker_get_block_ids_with_load_errors_delegates_to_recv_thread():
516
+ recv_thread = MagicMock()
517
+ recv_thread.get_and_clear_block_ids_with_load_errors.return_value = {3, 4}
518
+ w = _make_bare_worker()
519
+ w.kv_recv_thread = recv_thread
520
+
521
+ assert w.get_block_ids_with_load_errors() == {3, 4}
522
+ recv_thread.get_and_clear_block_ids_with_load_errors.assert_called_once_with()
523
+
524
+
525
def test_store_sending_thread_passes_replicate_config_when_preferred_segment_set():
526
store = MagicMock()
527
store.batch_is_exist.side_effect = lambda keys: [0] * len(keys)
@@ -682,6 +744,7 @@ def test_recv_thread_stops_after_first_failing_disk_offload_sub_batch():
744
thread._handle_request(req)
745
746
assert store.batch_get_into_multi_buffers.call_count == 1
747
+ assert thread.get_and_clear_block_ids_with_load_errors() == {0, 1}
748
749
750
def test_recv_thread_skips_split_when_budget_holds_all_keys():
@@ -711,21 +774,26 @@ def test_recv_thread_skips_split_when_budget_holds_all_keys():
774
775
776
def test_recv_thread_reports_unsplittable_key_larger_than_budget():
777
+ # Non-zero tp_rank exercises the rotation: the oversized key may not sit
778
+ # at the original request's first block. Every block must still be marked
779
+ # invalid, since none are loaded.
780
store = MagicMock()
781
thread = _make_store_recving_thread(
782
store,
783
+ tp_rank=2,
784
disk_offload_buffer_budget_bytes=_DISK_OFFLOAD_BUDGET_TOO_SMALL,
785
)
786
787
req = _make_load_req(
788
"req-a",
722
- [b"a0"],
723
- token_len=16,
789
+ [b"a0", b"a1", b"a2"],
790
+ token_len=48,
791
)
792
793
thread._handle_request(req)
794
795
assert store.batch_get_into_multi_buffers.call_count == 0
796
+ assert thread.get_and_clear_block_ids_with_load_errors() == {0, 1, 2}
797
798
799
def test_requester_worker_init_uses_positional_setup(tmp_path, monkeypatch):
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/connector.py
+4
@@ -270,6 +270,10 @@ class MooncakeStoreConnector(KVConnectorBase_V1, SupportsHMA):
270
assert isinstance(metadata, MooncakeStoreConnectorMetadata)
271
return self.connector_worker.get_finished(finished_req_ids, metadata)
272
273
+ def get_block_ids_with_load_errors(self) -> set[int]:
274
+ assert self.connector_worker is not None
275
+ return self.connector_worker.get_block_ids_with_load_errors()
276
+
277
def get_kv_connector_kv_cache_events(
278
self,
279
) -> MooncakeStoreKVEvents | None:
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/worker.py
+64
-12
@@ -20,7 +20,7 @@ import time
20
from collections import defaultdict
21
from collections.abc import Callable
22
from dataclasses import dataclass
23
-from typing import Any, Literal
23
+from typing import Any, Literal, TypeVar
24
25
import regex as re
26
import torch
@@ -68,6 +68,12 @@ DEFAULT_GLOBAL_SEGMENT_SIZE = 4 * 1024 * 1024 * 1024 # 4 GiB
68
DEFAULT_LOCAL_BUFFER_SIZE = 4 * 1024 * 1024 * 1024 # 4 GiB
69
70
MOONCAKE_NO_AVAILABLE_HANDLE = -200
71
+_T = TypeVar("_T")
72
+
73
+
74
+def _rotate_list(values: list[_T], offset: int) -> list[_T]:
75
+ return values[offset:] + values[:offset]
76
+
77
78
# Mirrors FileStorageConfig::local_buffer_size in Mooncake C++.
79
DEFAULT_MOONCAKE_DISK_STAGING_BUFFER_BYTES = 1280 * 1024 * 1024
@@ -711,6 +717,9 @@ class KVCacheStoreRecvingThread(KVTransferThread):
717
name="KVCacheStoreRecvingThread",
718
record_operation=record_operation,
719
)
720
+ # _invalid_block_ids can be access by both the Worker and RecvingThread
721
+ self._invalid_block_ids_lock = threading.Lock()
722
+ self._invalid_block_ids: set[int] = set()
723
self.disk_offload_buffer_budget_bytes = disk_offload_buffer_budget_bytes
724
self.usable_disk_offload_buffer_budget_bytes = (
725
None
@@ -721,6 +730,16 @@ class KVCacheStoreRecvingThread(KVTransferThread):
730
)
731
self.coord = coord
732
733
+ def _add_load_error_block_ids(self, block_ids: list[int]) -> None:
734
+ with self._invalid_block_ids_lock:
735
+ self._invalid_block_ids.update(block_ids)
736
+
737
+ def get_and_clear_block_ids_with_load_errors(self) -> set[int]:
738
+ with self._invalid_block_ids_lock:
739
+ invalid_block_ids = self._invalid_block_ids.copy()
740
+ self._invalid_block_ids.clear()
741
+ return invalid_block_ids
742
+
743
def _handle_request(self, req_meta: ReqMeta):
744
token_len = req_meta.load_spec.token_len # type: ignore[union-attr]
745
req_id = req_meta.req_id
@@ -737,6 +756,7 @@ class KVCacheStoreRecvingThread(KVTransferThread):
756
addr_list: list[list[int]] = []
757
size_list: list[list[int]] = []
758
key_list: list[str] = []
759
+ block_id_list: list[int] = []
760
for g_idx, db in enumerate(self.token_databases):
761
mask = load_mask_per_group[g_idx]
762
for start, end, key in db.process_tokens(
@@ -745,25 +765,29 @@ class KVCacheStoreRecvingThread(KVTransferThread):
765
chunk_idx = start // db.block_size
766
if chunk_idx >= len(mask) or not mask[chunk_idx]:
767
continue
748
- addr, size, _ = db.prepare_value(start, end, req_meta.block_ids[g_idx])
768
+ addr, size, block_id = db.prepare_value(
769
+ start, end, req_meta.block_ids[g_idx]
770
+ )
771
key_list.append(key.to_string())
772
addr_list.append(addr)
773
size_list.append(size)
774
+ block_id_list.append(block_id)
775
753
- # Rotate lists by tp_rank for load balancing
776
+ # Rotate aligned lists by tp_rank for load balancing.
777
rotation = self.tp_rank % len(key_list)
755
- key_list_c = key_list[rotation:] + key_list[:rotation]
756
- addr_list_c = addr_list[rotation:] + addr_list[:rotation]
757
- size_list_c = size_list[rotation:] + size_list[:rotation]
778
+ key_list_c = _rotate_list(key_list, rotation)
779
+ addr_list_c = _rotate_list(addr_list, rotation)
780
+ size_list_c = _rotate_list(size_list, rotation)
781
+ block_id_list_c = _rotate_list(block_id_list, rotation)
782
759
- load_batches = [(key_list_c, addr_list_c, size_list_c)]
783
+ load_batches = [(key_list_c, addr_list_c, size_list_c, block_id_list_c)]
784
if self.usable_disk_offload_buffer_budget_bytes is not None:
785
total_staging_bytes = sum(
786
_estimate_disk_offload_staging_bytes(size) for size in size_list_c
787
)
788
if total_staging_bytes > self.usable_disk_offload_buffer_budget_bytes:
789
assert self.disk_offload_buffer_budget_bytes is not None
766
- load_batches, oversized_key = _split_disk_offload_load_batches(
790
+ split_batches, oversized_key = _split_disk_offload_load_batches(
791
key_list_c,
792
addr_list_c,
793
size_list_c,
@@ -772,6 +796,10 @@ class KVCacheStoreRecvingThread(KVTransferThread):
796
)
797
if oversized_key is not None:
798
oversized_key_index = key_list_c.index(oversized_key)
799
+ # Mark every block: we skip the whole request, and the
800
+ # tp_rank rotation means oversized_key isn't necessarily
801
+ # the first block in the request's original order.
802
+ self._add_load_error_block_ids(block_id_list_c)
803
oversized_key_bytes = _estimate_disk_offload_staging_bytes(
804
size_list_c[oversized_key_index]
805
)
@@ -786,12 +814,25 @@ class KVCacheStoreRecvingThread(KVTransferThread):
814
self.set_finished_request(req_id)
815
self.request_queue.task_done()
816
return
817
+ load_batches = []
818
+ block_id_offset = 0
819
+ for batch_keys, batch_addrs, batch_sizes in split_batches:
820
+ next_block_id_offset = block_id_offset + len(batch_keys)
821
+ batch_block_ids = block_id_list_c[
822
+ block_id_offset:next_block_id_offset
823
+ ]
824
+ load_batches.append(
825
+ (batch_keys, batch_addrs, batch_sizes, batch_block_ids)
826
+ )
827
+ block_id_offset = next_block_id_offset
828
829
current_batch_keys: list[str] = key_list_c
830
+ current_batch_block_ids: list[int] = block_id_list_c
831
batch_bytes = 0
832
try:
793
- for batch_keys, batch_addrs, batch_sizes in load_batches:
833
+ for batch_keys, batch_addrs, batch_sizes, batch_block_ids in load_batches:
834
current_batch_keys = batch_keys
835
+ current_batch_block_ids = batch_block_ids
836
batch_bytes = _sum_batch_bytes(batch_sizes)
837
tiers_by_key: dict[str, str] | None = None
838
if envs.VLLM_MOONCAKE_STORE_TIER_LOG:
@@ -806,8 +847,10 @@ class KVCacheStoreRecvingThread(KVTransferThread):
847
req_id, batch_keys, res, tiers_by_key
848
)
849
failed = [
809
- (key, value)
810
- for key, value in zip(batch_keys, res, strict=True)
850
+ (key, value, block_id)
851
+ for key, value, block_id in zip(
852
+ batch_keys, res, batch_block_ids, strict=True
853
+ )
854
if value < 0
855
]
856
self._record_operation(
@@ -819,15 +862,19 @@ class KVCacheStoreRecvingThread(KVTransferThread):
862
num_failed_keys=len(failed),
863
)
864
if failed:
865
+ self._add_load_error_block_ids(
866
+ [block_id for _, _, block_id in failed]
867
+ )
868
logger.warning(
869
"Failed to get %d Mooncake keys from sub-batch "
870
"(batch_keys=%d, first_failures=%s)",
871
len(failed),
872
len(batch_keys),
827
- failed[:3],
873
+ [(key, value) for key, value, _ in failed[:3]],
874
)
875
break
876
except Exception as e:
877
+ self._add_load_error_block_ids(current_batch_block_ids)
878
self._record_operation(
879
"load_get",
880
load_get_start,
@@ -1239,6 +1286,11 @@ class MooncakeStoreWorker:
1286
)
1287
return done_sending, done_recving
1288
1289
+ def get_block_ids_with_load_errors(self) -> set[int]:
1290
+ if self.kv_recv_thread is None:
1291
+ return set()
1292
+ return self.kv_recv_thread.get_and_clear_block_ids_with_load_errors()
1293
+
1294
def _record_kv_connector_operation(
1295
self,
1296
operation: str,