1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+#
4
+# The transfer-thread scaffolding (KVTransferThread, KVCacheStoreSendingThread,
5
+# KVCacheStoreRecvingThread) is adapted from vllm-project/vllm-ascend
6
+# (vllm_ascend/distributed/kv_transfer/kv_pool/ascend_store/).
7
+"""Worker-side logic for MooncakeStoreConnector.
8
+
9
+Includes the store worker, transfer threads, lookup server,
10
+and MooncakeDistributedStore integration.
11
+"""
12
+
13
+import json
14
+import os
15
+import queue
16
+import threading
17
+from collections import defaultdict
18
+from dataclasses import dataclass
19
+from typing import Any
20
+
21
+import regex as re
22
+import torch
23
+import zmq
24
+
25
+import vllm.envs as envs
26
+from vllm.config import VllmConfig
27
+from vllm.distributed import (
28
+ get_dcp_group,
29
+ get_pcp_group,
30
+ get_tensor_model_parallel_rank,
31
+ get_tensor_model_parallel_world_size,
32
+)
33
+from vllm.distributed.kv_events import BlockStored
34
+from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_utils import (
35
+ get_mooncake_dp_engine_index,
36
+)
37
+from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store.data import ( # noqa: E501
38
+ ChunkedTokenDatabase,
39
+ KeyMetadata,
40
+ MooncakeStoreConnectorMetadata,
41
+ ReqMeta,
42
+)
43
+from vllm.logger import init_logger
44
+from vllm.utils.network_utils import get_ip, make_zmq_socket
45
+from vllm.v1.core.kv_cache_utils import BlockHash, maybe_convert_block_hash
46
+from vllm.v1.serial_utils import MsgpackDecoder, MsgpackEncoder
47
+
48
+logger = init_logger(__name__)
49
+
50
+DEFAULT_GLOBAL_SEGMENT_SIZE = 4 * 1024 * 1024 * 1024 # 4 GiB
51
+DEFAULT_LOCAL_BUFFER_SIZE = 4 * 1024 * 1024 * 1024 # 4 GiB
52
+MOONCAKE_NO_AVAILABLE_HANDLE = -200
53
+
54
+
55
+@dataclass
56
+class MooncakeStoreConfig:
57
+ """Configuration for MooncakeDistributedStore."""
58
+
59
+ metadata_server: str
60
+ global_segment_size: int
61
+ local_buffer_size: int
62
+ protocol: str
63
+ device_name: str
64
+ master_server_address: str
65
+
66
+ @staticmethod
67
+ def from_file(file_path: str) -> "MooncakeStoreConfig":
68
+ with open(file_path) as file:
69
+ config = json.load(file)
70
+ return MooncakeStoreConfig(
71
+ metadata_server=config.get("metadata_server", ""),
72
+ global_segment_size=_parse_size(
73
+ config.get("global_segment_size", DEFAULT_GLOBAL_SEGMENT_SIZE)
74
+ ),
75
+ local_buffer_size=_parse_size(
76
+ config.get("local_buffer_size", DEFAULT_LOCAL_BUFFER_SIZE)
77
+ ),
78
+ protocol=config.get("protocol", "rdma"),
79
+ device_name=config.get("device_name", ""),
80
+ master_server_address=config.get("master_server_address", ""),
81
+ )
82
+
83
+ @staticmethod
84
+ def load_from_env() -> "MooncakeStoreConfig":
85
+ config_path = os.getenv("MOONCAKE_CONFIG_PATH")
86
+ if not config_path:
87
+ raise ValueError(
88
+ "The environment variable 'MOONCAKE_CONFIG_PATH' is not set."
89
+ )
90
+ return MooncakeStoreConfig.from_file(config_path)
91
+
92
+
93
+def _parse_size(value: Any) -> int:
94
+ """Parse storage size strings with units: GB, MB, KB, B."""
95
+ if isinstance(value, int):
96
+ return value
97
+ if not isinstance(value, str):
98
+ try:
99
+ return int(value)
100
+ except (TypeError, ValueError) as e:
101
+ raise TypeError(f"Unsupported type for size: {type(value)}") from e
102
+
103
+ cleaned = value.strip().lower()
104
+ if not cleaned:
105
+ raise ValueError("Size cannot be empty.")
106
+
107
+ unit_multipliers = {
108
+ "gb": 1024**3,
109
+ "mb": 1024**2,
110
+ "kb": 1024,
111
+ "b": 1,
112
+ }
113
+ match = re.match(r"^\s*([\d.]+)\s*(gb|mb|kb|b)?\s*$", cleaned)
114
+ if not match:
115
+ raise ValueError(f"Invalid format: '{value}'")
116
+
117
+ number_str = match.group(1)
118
+ unit = match.group(2) or "b"
119
+ multiplier = unit_multipliers[unit]
120
+
121
+ try:
122
+ numeric_value = float(number_str)
123
+ except ValueError as exc:
124
+ raise ValueError(f"Invalid numeric value '{number_str}' in: '{value}'") from exc
125
+ return int(numeric_value * multiplier)
126
+
127
+
128
+# ============================================================
129
+# Transfer Threads
130
+# ============================================================
131
+
132
+
133
+class KVTransferThread(threading.Thread):
134
+ """Base class for async KV cache transfer threads."""
135
+
136
+ def __init__(
137
+ self,
138
+ store: Any,
139
+ token_database: ChunkedTokenDatabase,
140
+ block_size: int,
141
+ tp_rank: int,
142
+ ready_event: threading.Event,
143
+ name: str,
144
+ ):
145
+ super().__init__(daemon=True, name=name)
146
+ self.store = store
147
+ self.ready_event = ready_event
148
+ self.block_size = block_size
149
+ self.tp_rank = tp_rank
150
+ self.token_database = token_database
151
+ self.done_task_lock = threading.Lock()
152
+ self.request_queue: queue.Queue[Any] = queue.Queue()
153
+ self.finished_requests: set[str] = set()
154
+ self.kv_event_lock = threading.Lock()
155
+ self.kv_events: list[BlockStored] = []
156
+
157
+ def add_request(self, request: ReqMeta) -> None:
158
+ self.request_queue.put(request)
159
+
160
+ def get_and_clear_finished_requests(self) -> set[str]:
161
+ with self.done_task_lock:
162
+ finished = self.finished_requests.copy()
163
+ self.finished_requests.clear()
164
+ return finished
165
+
166
+ def set_finished_request(self, req_id: str):
167
+ with self.done_task_lock:
168
+ self.finished_requests.add(req_id)
169
+
170
+ def run(self):
171
+ self.ready_event.set()
172
+ while True:
173
+ try:
174
+ request_data = self.request_queue.get()
175
+ if request_data is None:
176
+ logger.warning("Received a None request!")
177
+ self.request_queue.task_done()
178
+ continue
179
+ self._handle_request(request_data)
180
+ except Exception as e:
181
+ logger.error("Error in %s: %s", self.name, e)
182
+
183
+ def _handle_request(self, req_meta: Any):
184
+ pass
185
+
186
+ def update_kv_event(self, events: list[BlockStored]):
187
+ with self.kv_event_lock:
188
+ self.kv_events.extend(events)
189
+
190
+ def get_kv_events(self) -> list[BlockStored]:
191
+ with self.kv_event_lock:
192
+ events = self.kv_events.copy()
193
+ self.kv_events.clear()
194
+ return events
195
+
196
+
197
+class KVCacheStoreSendingThread(KVTransferThread):
198
+ """Background thread for storing KV cache blocks to the store."""
199
+
200
+ def __init__(
201
+ self,
202
+ store: Any,
203
+ token_database: ChunkedTokenDatabase,
204
+ block_size: int,
205
+ tp_rank: int,
206
+ put_step: int,
207
+ kv_role: str,
208
+ ready_event: threading.Event,
209
+ enable_kv_event: bool = False,
210
+ ):
211
+ super().__init__(
212
+ store,
213
+ token_database,
214
+ block_size,
215
+ tp_rank,
216
+ ready_event,
217
+ name="KVCacheStoreSendingThread",
218
+ )
219
+ self.put_step = put_step
220
+ self.kv_role = kv_role
221
+ self.stored_requests: defaultdict[str, int] = defaultdict(int)
222
+ self.enable_kv_event = enable_kv_event
223
+
224
+ # Pause store requests when CPU offloading is under pressure.
225
+ self._store_pressure_active = False
226
+ self._skip_store_requests: set[str] = set()
227
+
228
+ def add_stored_request(self, req_id: str):
229
+ with self.done_task_lock:
230
+ self.stored_requests[req_id] += 1
231
+
232
+ def dec_stored_request(self, req_id: str):
233
+ with self.done_task_lock:
234
+ if req_id in self.stored_requests:
235
+ self.stored_requests[req_id] -= 1
236
+
237
+ def delete_finished_stored_request(self, req_id: str):
238
+ with self.done_task_lock:
239
+ if req_id in self.stored_requests:
240
+ del self.stored_requests[req_id]
241
+ self._skip_store_requests.discard(req_id)
242
+
243
+ def _should_skip_request(self, req_id: str) -> bool:
244
+ with self.done_task_lock:
245
+ return self._store_pressure_active and req_id in self._skip_store_requests
246
+
247
+ def _mark_request_skipped_for_pressure(self, req_id: str) -> bool:
248
+ with self.done_task_lock:
249
+ already_skipped = req_id in self._skip_store_requests
250
+ self._store_pressure_active = True
251
+ self._skip_store_requests.add(req_id)
252
+ return already_skipped
253
+
254
+ def _clear_store_pressure(self) -> bool:
255
+ with self.done_task_lock:
256
+ if not self._store_pressure_active and not self._skip_store_requests:
257
+ return False
258
+ self._store_pressure_active = False
259
+ self._skip_store_requests.clear()
260
+ return True
261
+
262
+ def _handle_request(self, req_meta: ReqMeta):
263
+ token_len = req_meta.token_len_chunk
264
+ block_ids = req_meta.block_ids
265
+ req_id = req_meta.req_id
266
+ current_event = req_meta.current_event
267
+
268
+ if req_id not in self.stored_requests:
269
+ self.request_queue.task_done()
270
+ return
271
+ if self._should_skip_request(req_id):
272
+ logger.debug(
273
+ "Skipping Mooncake store for request %s while CPU offloading "
274
+ "is under pressure",
275
+ req_id,
276
+ )
277
+ self.dec_stored_request(req_id)
278
+ self.request_queue.task_done()
279
+ return
280
+
281
+ starts = []
282
+ ends = []
283
+ keys = []
284
+ block_hashes: list[BlockHash] = []
285
+ for index, (start, end, key) in enumerate(
286
+ self.token_database.process_tokens(token_len, req_meta.block_hashes)
287
+ ):
288
+ starts.append(start)
289
+ ends.append(end)
290
+ keys.append(key.to_string())
291
+ block_hashes.append(req_meta.block_hashes[index])
292
+
293
+ # Apply put_step striding for TP
294
+ starts = starts[self.tp_rank % self.put_step :: self.put_step]
295
+ ends = ends[self.tp_rank % self.put_step :: self.put_step]
296
+ keys = keys[self.tp_rank % self.put_step :: self.put_step]
297
+ block_hashes = block_hashes[self.tp_rank % self.put_step :: self.put_step]
298
+
299
+ if not keys:
300
+ self.dec_stored_request(req_id)
301
+ return
302
+
303
+ # Check which blocks already exist (dedup)
304
+ exists_states = self.store.batch_is_exist(keys)
305
+ missing_indices = [i for i, exists in enumerate(exists_states) if exists != 1]
306
+
307
+ if not missing_indices:
308
+ self.dec_stored_request(req_id)
309
+ return
310
+
311
+ starts = [starts[i] for i in missing_indices]
312
+ ends = [ends[i] for i in missing_indices]
313
+ keys = [keys[i] for i in missing_indices]
314
+ block_hashes = [block_hashes[i] for i in missing_indices]
315
+
316
+ logger.debug(
317
+ "Storing KV cache for %d out of %d blocks "
318
+ "(missing_count=%d) for request %s",
319
+ len(keys),
320
+ token_len // self.block_size,
321
+ len(missing_indices),
322
+ req_id,
323
+ )
324
+
325
+ addrs = []
326
+ sizes = []
327
+ stored_events: list[BlockStored] = []
328
+ prev_key = None
329
+ new_block_hashes = [maybe_convert_block_hash(bh) for bh in block_hashes]
330
+
331
+ for index, start in enumerate(starts):
332
+ addr, size, _ = self.token_database.prepare_value(
333
+ start, ends[index], block_ids
334
+ )
335
+ addrs.append(addr)
336
+ sizes.append(size)
337
+
338
+ if self.enable_kv_event:
339
+ token_ids = (
340
+ req_meta.token_ids[start : ends[index]]
341
+ if req_meta.token_ids is not None
342
+ else None
343
+ )
344
+ stored_event = BlockStored(
345
+ block_hashes=[new_block_hashes[index]],
346
+ parent_block_hash=prev_key,
347
+ token_ids=token_ids,
348
+ block_size=req_meta.original_block_size,
349
+ lora_id=None,
350
+ medium="cpu",
351
+ lora_name=None,
352
+ )
353
+ stored_events.append(stored_event)
354
+ prev_key = new_block_hashes[index]
355
+
356
+ if current_event is not None:
357
+ current_event.synchronize()
358
+
359
+ try:
360
+ res = self.store.batch_put_from_multi_buffers(keys, addrs, sizes)
361
+ failed = [i for i, v in enumerate(res) if v < 0]
362
+ if failed:
363
+ # Compute total bytes attempted for this batch
364
+ total_bytes = sum(sum(s) if isinstance(s, list) else s for s in sizes)
365
+ failed_codes = set(res[i] for i in failed)
366
+ logger.warning(
367
+ "batch_put failed: %d/%d keys failed "
368
+ "(codes=%s, batch_bytes=%d, num_keys=%d), "
369
+ "first_key=%s",
370
+ len(failed),
371
+ len(keys),
372
+ failed_codes,
373
+ total_bytes,
374
+ len(keys),
375
+ keys[0] if keys else "N/A",
376
+ )
377
+ if (
378
+ MOONCAKE_NO_AVAILABLE_HANDLE in failed_codes
379
+ and not self._mark_request_skipped_for_pressure(req_id)
380
+ ):
381
+ logger.warning(
382
+ "Detected Mooncake CPU offloading pressure "
383
+ "(NO_AVAILABLE_HANDLE); skipping future store "
384
+ "batches for request %s until a later store "
385
+ "batch succeeds",
386
+ req_id,
387
+ )
388
+ elif self._clear_store_pressure():
389
+ logger.info(
390
+ "Mooncake CPU offloading pressure cleared after a "
391
+ "successful store batch"
392
+ )
393
+ except Exception as e:
394
+ logger.error("Failed to put key %s, error: %s", keys, e)
395
+
396
+ if self.enable_kv_event and stored_events:
397
+ self.update_kv_event(stored_events)
398
+
399
+ self.dec_stored_request(req_id)
400
+ self.request_queue.task_done()
401
+
402
+
403
+class KVCacheStoreRecvingThread(KVTransferThread):
404
+ """Background thread for loading KV cache blocks from the store."""
405
+
406
+ def __init__(
407
+ self,
408
+ store: Any,
409
+ token_database: ChunkedTokenDatabase,
410
+ block_size: int,
411
+ tp_rank: int,
412
+ ready_event: threading.Event,
413
+ ):
414
+ super().__init__(
415
+ store,
416
+ token_database,
417
+ block_size,
418
+ tp_rank,
419
+ ready_event,
420
+ name="KVCacheStoreRecvingThread",
421
+ )
422
+
423
+ def _handle_request(self, req_meta: ReqMeta):
424
+ token_len = req_meta.load_spec.token_len # type: ignore[union-attr]
425
+ req_id = req_meta.req_id
426
+ mask_num = (
427
+ req_meta.load_spec.vllm_cached_tokens # type: ignore[union-attr]
428
+ // self.block_size
429
+ * self.block_size
430
+ )
431
+
432
+ addr_list = []
433
+ size_list = []
434
+ key_list = []
435
+ for start, end, key in self.token_database.process_tokens(
436
+ token_len, req_meta.block_hashes, mask_num
437
+ ):
438
+ addr, size, _ = self.token_database.prepare_value(
439
+ start, end, req_meta.block_ids
440
+ )
441
+ key_list.append(key.to_string())
442
+ addr_list.append(addr)
443
+ size_list.append(size)
444
+
445
+ # Rotate lists by tp_rank for load balancing
446
+ key_list_c = (
447
+ key_list[self.tp_rank % len(key_list) :]
448
+ + key_list[: self.tp_rank % len(key_list)]
449
+ )
450
+ addr_list_c = (
451
+ addr_list[self.tp_rank % len(addr_list) :]
452
+ + addr_list[: self.tp_rank % len(addr_list)]
453
+ )
454
+ size_list_c = (
455
+ size_list[self.tp_rank % len(size_list) :]
456
+ + size_list[: self.tp_rank % len(size_list)]
457
+ )
458
+
459
+ try:
460
+ res = self.store.batch_get_into_multi_buffers(
461
+ key_list_c, addr_list_c, size_list_c
462
+ )
463
+ failed = [
464
+ (key, value)
465
+ for key, value in zip(key_list_c, res, strict=True)
466
+ if value < 0
467
+ ]
468
+ if failed:
469
+ logger.warning(
470
+ "Failed to get %d Mooncake keys (batch_keys=%d, first_failures=%s)",
471
+ len(failed),
472
+ len(key_list_c),
473
+ failed[:3],
474
+ )
475
+ except Exception as e:
476
+ logger.warning(
477
+ "Failed to get Mooncake batch %s, error: %s",
478
+ key_list_c[:3],
479
+ e,
480
+ )
481
+
482
+ self.set_finished_request(req_id)
483
+ self.request_queue.task_done()
484
+
485
+
486
+# ============================================================
487
+# Store Worker
488
+# ============================================================
489
+
490
+
491
+class MooncakeStoreWorker:
492
+ """Worker-side component for MooncakeStoreConnector."""
493
+
494
+ def __init__(self, vllm_config: VllmConfig):
495
+ try:
496
+ from mooncake.store import MooncakeDistributedStore # type: ignore
497
+ except ImportError as e:
498
+ raise ImportError(
499
+ "Please install mooncake by following the instructions at "
500
+ "https://github.com/kvcache-ai/Mooncake/blob/main/doc/"
501
+ "en/build.md to run vLLM with MooncakeStoreConnector."
502
+ ) from e
503
+
504
+ model_config = vllm_config.model_config
505
+ parallel_config = vllm_config.parallel_config
506
+
507
+ self.dp_rank = get_mooncake_dp_engine_index(parallel_config)
508
+ self.tp_rank = get_tensor_model_parallel_rank()
509
+ self.tp_size = get_tensor_model_parallel_world_size()
510
+ self.pp_size = parallel_config.pipeline_parallel_size
511
+ self.pp_rank = (parallel_config.rank // self.tp_size) % self.pp_size
512
+
513
+ self.pcp_size = get_pcp_group().world_size
514
+ self.pcp_rank = get_pcp_group().rank_in_group if self.pcp_size > 1 else 0
515
+ self.dcp_size = get_dcp_group().world_size
516
+ self.dcp_rank = get_dcp_group().rank_in_group if self.dcp_size > 1 else 0
517
+
518
+ assert vllm_config.kv_transfer_config is not None
519
+ self.kv_role = vllm_config.kv_transfer_config.kv_role
520
+ self.load_async = vllm_config.kv_transfer_config.kv_connector_extra_config.get(
521
+ "load_async", True
522
+ )
523
+ self.cache_config = vllm_config.cache_config
524
+ self.original_block_size = self.cache_config.block_size
525
+ self.block_size = self.cache_config.block_size
526
+ if self.pcp_size > 1:
527
+ self.block_size *= self.pcp_size
528
+ if self.dcp_size > 1:
529
+ self.block_size *= self.dcp_size
530
+ self.num_layers = model_config.get_num_layers(parallel_config)
531
+
532
+ self.use_mla = False
533
+ if (
534
+ hasattr(model_config, "use_mla")
535
+ and isinstance(model_config.use_mla, bool)
536
+ and model_config.use_mla
537
+ ):
538
+ self.use_mla = True
539
+
540
+ if self.use_mla:
541
+ self.num_kv_head = 1
542
+ else:
543
+ self.num_kv_head = model_config.get_total_num_kv_heads()
544
+
545
+ if self.num_kv_head < self.tp_size:
546
+ self.put_step = self.tp_size // self.num_kv_head
547
+ self.head_or_tp_rank = self.tp_rank // self.put_step
548
+ else:
549
+ self.head_or_tp_rank = self.tp_rank
550
+ self.put_step = 1
551
+
552
+ self.metadata = KeyMetadata(
553
+ model_name=model_config.model.rstrip("/").split("/")[-1],
554
+ tp_rank=self.head_or_tp_rank,
555
+ pcp_rank=self.pcp_rank,
556
+ dcp_rank=self.dcp_rank,
557
+ pp_rank=self.pp_rank,
558
+ )
559
+
560
+ self.token_database = ChunkedTokenDatabase(self.metadata, self.block_size)
561
+
562
+ # Initialize MooncakeDistributedStore with its own TransferEngine
563
+ store_config = MooncakeStoreConfig.load_from_env()
564
+ self.store = MooncakeDistributedStore()
565
+
566
+ local_seg = get_ip()
567
+ config_dict = {
568
+ "local_hostname": local_seg,
569
+ "metadata_server": store_config.metadata_server,
570
+ "global_segment_size": str(store_config.global_segment_size),
571
+ "local_buffer_size": str(store_config.local_buffer_size),
572
+ "protocol": store_config.protocol,
573
+ "rdma_devices": store_config.device_name,
574
+ "master_server_addr": store_config.master_server_address,
575
+ }
576
+ ret = self.store.setup(config_dict)
577
+ if ret != 0:
578
+ msg = "Initialize MooncakeDistributedStore failed."
579
+ logger.error(msg)
580
+ raise RuntimeError(msg)
581
+
582
+ kv_event_config = vllm_config.kv_events_config
583
+ self.enable_kv_events = False
584
+ if kv_event_config and kv_event_config.enable_kv_cache_events:
585
+ self.enable_kv_events = True
586
+
587
+ self.kv_send_thread: KVCacheStoreSendingThread | None = None
588
+ self.kv_recv_thread: KVCacheStoreRecvingThread | None = None
589
+ self.finished_store_req: set[str] = set()
590
+
591
+ # Start lookup server on rank 0 for scheduler-side prefix queries
592
+ self.lookup_server: LookupKeyServer | None = None
593
+ if vllm_config.parallel_config.rank == 0:
594
+ self.lookup_server = LookupKeyServer(self, vllm_config)
595
+
596
+ def register_cross_layers_kv_caches(self, kv_cache: torch.Tensor) -> None:
597
+ """Register a cross-layers KV cache tensor.
598
+
599
+ Wraps the unified tensor in a single-entry dict so that the
600
+ existing stride-based logic in register_kv_caches() produces
601
+ the correct single-segment result (block_len = page_size * num_layers).
602
+ """
603
+ self.register_kv_caches({"__cross_layer__": kv_cache})
604
+
605
+ def register_kv_caches(self, kv_caches: dict[str, torch.Tensor]):
606
+ """Register KV cache tensors and start transfer threads."""
607
+ # TODO(yifan): we haven't supported HMA yet.
608
+ first_kv_cache = next(iter(kv_caches.values()))
609
+
610
+ # num_blocks from cache_config is authoritative (set after
611
+ # profiling, before KV cache allocation).
612
+ assert self.cache_config.num_gpu_blocks is not None
613
+ self.num_blocks = self.cache_config.num_gpu_blocks
614
+
615
+ # Detect the KV cache memory layout using the stride-based
616
+ # approach from simple_kv_offload/worker.py.
617
+ #
618
+ # The physical layout varies across attention backends:
619
+ # FlashAttn/ROCm : (2, num_blocks, ...) → K/V outermost
620
+ # FlashInfer/MLA : (num_blocks, ...) → blocks outermost
621
+ #
622
+ # We derive page_size_bytes = storage.nbytes() // num_blocks,
623
+ # then classify dims: any dim whose byte-stride exceeds
624
+ # page_size_bytes must be an outer segment dim (e.g. the K/V
625
+ # dim of size 2). For those backends we register each segment
626
+ # (K, V) as a separate base-address so that the per-block
627
+ # offset arithmetic in prepare_value() stays correct.
628
+ storage = first_kv_cache.untyped_storage()
629
+ el = first_kv_cache.element_size()
630
+ page_size_bytes = storage.nbytes() // self.num_blocks
631
+ outer_dims = [
632
+ d
633
+ for d in range(first_kv_cache.ndim)
634
+ if first_kv_cache.stride(d) * el > page_size_bytes
635
+ ]
636
+
637
+ # Register buffers with the store (deduplicate shared storages)
638
+ # and record per-segment base addresses for every layer.
639
+ seen_ptrs: set[int] = set()
640
+ self.kv_caches_base_addr: list[int] = []
641
+ self.block_len: list[int] = []
642
+
643
+ for cache in kv_caches.values():
644
+ cache_storage = cache.untyped_storage()
645
+ base_addr = cache_storage.data_ptr()
646
+ region_len = cache_storage.nbytes()
647
+
648
+ if base_addr not in seen_ptrs:
649
+ seen_ptrs.add(base_addr)
650
+ ret = self.store.register_buffer(base_addr, region_len)
651
+ if ret != 0:
652
+ logger.error(
653
+ "register_buffer failed for addr %#x len %d: %d",
654
+ base_addr,
655
+ region_len,
656
+ ret,
657
+ )
658
+
659
+ if not outer_dims:
660
+ # Blocks-first layout (FlashInfer / MLA): one segment.
661
+ self.kv_caches_base_addr.append(base_addr)
662
+ self.block_len.append(page_size_bytes)
663
+ else:
664
+ # K/V-first layout (FlashAttn / ROCm): split segments.
665
+ seg_stride = cache.stride(outer_dims[0]) * el
666
+ for idx in range(cache.shape[outer_dims[0]]):
667
+ self.kv_caches_base_addr.append(base_addr + idx * seg_stride)
668
+ self.block_len.append(seg_stride // self.num_blocks)
669
+
670
+ logger.info(
671
+ "Registering KV_Caches. use_mla: %s, shape %s, "
672
+ "num_blocks: %d, block_len: %s, "
673
+ "per_key_bytes: %d, "
674
+ "num_segments: %d",
675
+ self.use_mla,
676
+ first_kv_cache.shape,
677
+ self.num_blocks,
678
+ list(set(self.block_len)),
679
+ sum(self.block_len),
680
+ len(self.kv_caches_base_addr),
681
+ )
682
+
683
+ self.token_database.set_kv_caches_base_addr(self.kv_caches_base_addr)
684
+ self.token_database.set_block_len(self.block_len)
685
+
686
+ # Start transfer threads
687
+ if self.kv_role in ["kv_producer", "kv_both"]:
688
+ ready_event_sending = threading.Event()
689
+ self.kv_send_thread = KVCacheStoreSendingThread(
690
+ self.store,
691
+ self.token_database,
692
+ self.block_size,
693
+ self.tp_rank,
694
+ self.put_step,
695
+ self.kv_role,
696
+ ready_event_sending,
697
+ self.enable_kv_events,
698
+ )
699
+ self.kv_send_thread.start()
700
+
701
+ ready_event_recving = threading.Event()
702
+ self.kv_recv_thread = KVCacheStoreRecvingThread(
703
+ self.store,
704
+ self.token_database,
705
+ self.block_size,
706
+ self.tp_rank,
707
+ ready_event_recving,
708
+ )
709
+ self.kv_recv_thread.start()
710
+ ready_event_recving.wait()
711
+
712
+ def start_load_kv(
713
+ self,
714
+ metadata: MooncakeStoreConnectorMetadata,
715
+ ):
716
+ """No-op: loads are issued in get_finished() for overlap."""
717
+ pass
718
+
719
+ def wait_for_save(
720
+ self,
721
+ metadata: MooncakeStoreConnectorMetadata,
722
+ ):
723
+ """No-op: stores are issued in get_finished() for overlap."""
724
+ pass
725
+
726
+ def get_finished(
727
+ self,
728
+ finished_req_ids: set[str],
729
+ meta: MooncakeStoreConnectorMetadata,
730
+ ) -> tuple[set[str], set[str]]:
731
+ """Issue all I/O and get completed send/recv request IDs.
732
+
733
+ All load and store I/O requests are issued here (after model
734
+ compute is launched on the compute stream) for better
735
+ compute-I/O overlap.
736
+ """
737
+ # Issue async loads
738
+ for request in meta.requests:
739
+ load_spec = request.load_spec
740
+ if load_spec is None or not load_spec.can_load:
741
+ continue
742
+
743
+ token_len = request.token_len_chunk
744
+ if (load_spec.kvpool_cached_tokens % self.block_size != 0) and (
745
+ load_spec.kvpool_cached_tokens == token_len - 1
746
+ ):
747
+ token_len = load_spec.kvpool_cached_tokens + 1
748
+ else:
749
+ token_len = load_spec.kvpool_cached_tokens
750
+ load_spec.token_len = token_len
751
+
752
+ assert self.kv_recv_thread is not None
753
+ self.kv_recv_thread.add_request(request)
754
+
755
+ assert self.load_async, "load_async must be True for better performance."
756
+ # Issue stores with CUDA event synchronization
757
+ if self.kv_role in ["kv_producer", "kv_both"]:
758
+ current_event = None
759
+ for request in meta.requests:
760
+ if request.can_save:
761
+ current_event = torch.cuda.Event()
762
+ current_event.record()
763
+ break
764
+
765
+ for request in meta.requests:
766
+ if not request.can_save:
767
+ continue
768
+ request.current_event = current_event
769
+ assert self.kv_send_thread is not None
770
+ self.kv_send_thread.add_stored_request(request.req_id)
771
+ self.kv_send_thread.add_request(request)
772
+
773
+ # Check completion of previously queued transfers
774
+ done_sending = (
775
+ self._get_and_clear_finished_sending(finished_req_ids, meta)
776
+ if self.kv_role in ["kv_producer", "kv_both"]
777
+ else set()
778
+ )
779
+
780
+ done_recving = (
781
+ self.kv_recv_thread.get_and_clear_finished_requests()
782
+ if self.load_async and self.kv_recv_thread is not None
783
+ else set()
784
+ )
785
+
786
+ logger.debug(
787
+ "Completed send: %d, recv: %d, tp_rank: %d",
788
+ len(done_sending),
789
+ len(done_recving),
790
+ self.tp_rank,
791
+ )
792
+ return done_sending, done_recving
793
+
794
+ def _get_and_clear_finished_sending(
795
+ self,
796
+ finished_req_ids: set[str],
797
+ meta: MooncakeStoreConnectorMetadata,
798
+ ) -> set[str]:
799
+ assert self.kv_send_thread is not None
800
+ finished_sending: set[str] = set()
801
+
802
+ for req_id in meta.preempted_req_ids:
803
+ self.kv_send_thread.delete_finished_stored_request(req_id)
804
+
805
+ for req_id in self.kv_send_thread.stored_requests.copy():
806
+ if (
807
+ self.kv_send_thread.stored_requests[req_id] == 0
808
+ and req_id in self.finished_store_req
809
+ ):
810
+ self.finished_store_req.remove(req_id)
811
+ finished_sending.add(req_id)
812
+ self.kv_send_thread.delete_finished_stored_request(req_id)
813
+
814
+ for req_id in finished_req_ids:
815
+ req_remain_jobs = self.kv_send_thread.stored_requests.get(req_id)
816
+ if req_remain_jobs == 0:
817
+ finished_sending.add(req_id)
818
+ self.kv_send_thread.delete_finished_stored_request(req_id)
819
+ elif req_remain_jobs is not None:
820
+ self.finished_store_req.add(req_id)
821
+
822
+ return finished_sending
823
+
824
+ def lookup(
825
+ self,
826
+ token_len: int,
827
+ block_hashes: list[BlockHash],
828
+ ) -> int:
829
+ """Check how many prefix tokens exist in the store.
830
+
831
+ Checks across all TP ranks and PP ranks.
832
+ """
833
+ end = 0
834
+ keys: list[str] = []
835
+ try:
836
+ starts: list[int] = []
837
+ for start, end, key in self.token_database.process_tokens(
838
+ token_len, block_hashes
839
+ ):
840
+ keys.append(key.to_string())
841
+ starts.append(start)
842
+
843
+ # Expand keys for all TP ranks
844
+ multi_tp_keys = keys[:]
845
+ for i in range(1, min(self.tp_size, self.num_kv_head)):
846
+ for item in keys:
847
+ new_str = item.replace("@tp_rank:0", f"@tp_rank:{i}", 1)
848
+ multi_tp_keys.append(new_str)
849
+
850
+ # Expand keys for all PP ranks
851
+ pp_base_keys = multi_tp_keys.copy()
852
+ for i in range(1, self.pp_size):
853
+ for item in pp_base_keys:
854
+ new_str = item.replace("@pp_rank:0", f"@pp_rank:{i}", 1)
855
+ multi_tp_keys.append(new_str)
856
+
857
+ res = self.store.batch_is_exist(multi_tp_keys)
858
+
859
+ num_block = len(keys)
860
+ multi_tp_values = [
861
+ res[i * num_block : (i + 1) * num_block]
862
+ for i in range(min(self.tp_size, self.num_kv_head) * self.pp_size)
863
+ ]
864
+ index = self._find_min_first_non_one_index(multi_tp_values)
865
+ if index != -1:
866
+ return starts[index]
867
+ except Exception as e:
868
+ logger.error("Remote connection failed in lookup: %s", e)
869
+ return 0
870
+ return end
871
+
872
+ @staticmethod
873
+ def _find_min_first_non_one_index(
874
+ arr: list[list[int]],
875
+ ) -> int:
876
+ try:
877
+ return min(idx for row in arr for idx, val in enumerate(row) if val != 1)
878
+ except ValueError:
879
+ return -1
880
+
881
+ def get_kv_events(self) -> list[BlockStored]:
882
+ if self.enable_kv_events and self.kv_send_thread is not None:
883
+ return self.kv_send_thread.get_kv_events()
884
+ return []
885
+
886
+
887
+# ============================================================
888
+# Lookup Key Server
889
+# ============================================================
890
+
891
+
892
+class LookupKeyServer:
893
+ """ZMQ server on worker rank 0 for handling prefix lookup queries."""
894
+
895
+ def __init__(
896
+ self,
897
+ store_worker: MooncakeStoreWorker,
898
+ vllm_config: VllmConfig,
899
+ ):
900
+ self.decoder = MsgpackDecoder()
901
+ self.ctx = zmq.Context() # type: ignore[attr-defined]
902
+ socket_path = get_zmq_rpc_path_lookup(vllm_config)
903
+ self._ipc_path = socket_path.removeprefix("ipc://")
904
+ if os.path.exists(self._ipc_path):
905
+ os.unlink(self._ipc_path)
906
+ self.socket = make_zmq_socket(
907
+ self.ctx,
908
+ socket_path,
909
+ zmq.REP, # type: ignore[attr-defined]
910
+ bind=True,
911
+ )
912
+
913
+ self.store_worker = store_worker
914
+ self.running = True
915
+
916
+ def process_request():
917
+ while self.running:
918
+ all_frames = self.socket.recv_multipart(copy=False)
919
+ token_len = int.from_bytes(all_frames[0], byteorder="big")
920
+ hash_frames = all_frames[1:]
921
+ hashes_str = self.decoder.decode(hash_frames)
922
+ result = self.store_worker.lookup(token_len, hashes_str)
923
+ response = result.to_bytes(4, "big")
924
+ self.socket.send(response)
925
+
926
+ self.thread = threading.Thread(target=process_request, daemon=True)
927
+ self.thread.start()
928
+
929
+ def close(self):
930
+ self.socket.close(linger=0)
931
+ if os.path.exists(self._ipc_path):
932
+ os.unlink(self._ipc_path)
933
+
934
+
935
+# ============================================================
936
+# Lookup Key Client
937
+# ============================================================
938
+
939
+
940
+class LookupKeyClient:
941
+ """ZMQ client for querying prefix cache hits from worker."""
942
+
943
+ def __init__(self, vllm_config: VllmConfig):
944
+ self.encoder = MsgpackEncoder()
945
+ self.ctx = zmq.Context() # type: ignore[attr-defined]
946
+ socket_path = get_zmq_rpc_path_lookup(vllm_config)
947
+ self.socket = make_zmq_socket(
948
+ self.ctx,
949
+ socket_path,
950
+ zmq.REQ, # type: ignore[attr-defined]
951
+ bind=False,
952
+ )
953
+
954
+ def lookup(self, token_len: int, block_hashes: list[BlockHash]) -> int:
955
+ hash_strs = [h.hex() for h in block_hashes]
956
+ hash_frames = self.encoder.encode(hash_strs)
957
+ token_len_bytes = token_len.to_bytes(4, byteorder="big")
958
+ all_frames = [token_len_bytes] + list(hash_frames)
959
+ self.socket.send_multipart(all_frames, copy=False)
960
+ resp = self.socket.recv()
961
+ result = int.from_bytes(resp, "big")
962
+ return result
963
+
964
+ def close(self):
965
+ self.socket.close(linger=0)
966
+
967
+
968
+def get_zmq_rpc_path_lookup(vllm_config: VllmConfig) -> str:
969
+ """Construct IPC path for ZMQ lookup socket."""
970
+ dp_rank = get_mooncake_dp_engine_index(vllm_config.parallel_config)
971
+ base_url = envs.VLLM_RPC_BASE_PATH
972
+ rpc_port = 0
973
+ assert vllm_config.kv_transfer_config is not None
974
+ extra_config = vllm_config.kv_transfer_config.kv_connector_extra_config
975
+ if "lookup_rpc_port" in extra_config:
976
+ rpc_port = extra_config["lookup_rpc_port"]
977
+ uid = os.getuid()
978
+ logger.debug("Base URL: %s, RPC Port: %s, UID: %s", base_url, rpc_port, uid)
979
+ return f"ipc://{base_url}/lookup_rpc_port_{rpc_port}_uid{uid}_dp_rank{dp_rank}"