lmcache-cli 0.5.6rc4.dev27__py3-none-any.whl → 0.5.7.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. lmcache/_version.py +3 -3
  2. lmcache/cli/commands/bench/l2_adapter_bench/data.py +1 -1
  3. lmcache/cli/commands/query/_request.py +22 -6
  4. lmcache/integration/sglang/multi_process_adapter.py +1 -1
  5. lmcache/integration/sglang/unified_lmcache_mp_connector.py +28 -31
  6. lmcache/integration/vllm/lmcache_mp_connector.py +49 -36
  7. lmcache/integration/vllm/vllm_multi_process_adapter.py +233 -23
  8. lmcache/utils.py +11 -0
  9. lmcache/v1/distributed/l1_manager.py +2 -1
  10. lmcache/v1/internal_api_server/vllm/cache_api.py +1 -1
  11. lmcache/v1/memory_management.py +1 -1
  12. lmcache/v1/mp_observability/event.py +4 -1
  13. lmcache/v1/mp_observability/event_bus.py +28 -0
  14. lmcache/v1/multiprocess/config.py +11 -0
  15. lmcache/v1/multiprocess/custom_types.py +33 -0
  16. lmcache/v1/multiprocess/futures.py +63 -0
  17. lmcache/v1/multiprocess/modules/kv_events.py +302 -0
  18. lmcache/v1/multiprocess/modules/lmcache_driven_transfer.py +17 -2
  19. lmcache/v1/multiprocess/modules/management.py +3 -9
  20. lmcache/v1/multiprocess/request_handler.py +6 -3
  21. lmcache/v1/multiprocess/rpc.py +16 -4
  22. lmcache/v1/multiprocess/server.py +10 -3
  23. lmcache/v1/multiprocess/transfer_context/async_engine_driven.py +3 -2
  24. lmcache/v1/multiprocess/transfer_context/base.py +9 -3
  25. lmcache/v1/multiprocess/transfer_context/worker_transfer.py +53 -50
  26. lmcache/v1/multiprocess/transfer_mode.py +30 -0
  27. lmcache/v1/multiprocess/transport/base.py +7 -1
  28. lmcache/v1/multiprocess/transport/grpc_impl/client.py +26 -6
  29. lmcache/v1/multiprocess/transport/grpc_impl/descriptors.py +1 -1
  30. lmcache/v1/multiprocess/transport/grpc_impl/method_registry.py +6 -0
  31. lmcache/v1/multiprocess/transport/grpc_impl/protos/observability_service.proto +26 -0
  32. lmcache/v1/multiprocess/transport/grpc_impl/server.py +42 -1
  33. lmcache/v1/multiprocess/transport/zmq_impl/client.py +4 -4
  34. lmcache/v1/multiprocess/transport/zmq_impl/mq.py +163 -10
  35. lmcache/v1/platform/base/device_spec.py +33 -4
  36. lmcache/v1/platform/base/ipc_wire.py +195 -0
  37. lmcache/v1/platform/base/ipc_wrapper.py +71 -23
  38. lmcache/v1/platform/devices/cpu/__init__.py +15 -0
  39. lmcache/v1/platform/devices/cpu/shm.py +23 -1
  40. lmcache/v1/platform/devices/cuda/__init__.py +4 -0
  41. lmcache/v1/platform/devices/cuda/ipc_wrapper.py +82 -7
  42. lmcache/v1/platform/devices/musa/__init__.py +6 -1
  43. lmcache/v1/platform/devices/musa/ipc_wrapper.py +16 -0
  44. lmcache/v1/platform/devices/neuron/__init__.py +1 -1
  45. lmcache/v1/platform/devices/npu/__init__.py +4 -0
  46. lmcache/v1/platform/devices/npu/ipc_wrapper.py +47 -0
  47. lmcache/v1/platform/devices/rbln/__init__.py +3 -4
  48. lmcache/v1/platform/torch_ops/_kv_format.py +1 -49
  49. lmcache/v1/platform/torch_ops/mem_kernels.py +4 -3
  50. lmcache/v1/platform/torch_ops/mp_mem_kernels.py +48 -61
  51. {lmcache_cli-0.5.6rc4.dev27.dist-info → lmcache_cli-0.5.7.dev0.dist-info}/METADATA +1 -1
  52. {lmcache_cli-0.5.6rc4.dev27.dist-info → lmcache_cli-0.5.7.dev0.dist-info}/RECORD +56 -53
  53. {lmcache_cli-0.5.6rc4.dev27.dist-info → lmcache_cli-0.5.7.dev0.dist-info}/WHEEL +0 -0
  54. {lmcache_cli-0.5.6rc4.dev27.dist-info → lmcache_cli-0.5.7.dev0.dist-info}/entry_points.txt +0 -0
  55. {lmcache_cli-0.5.6rc4.dev27.dist-info → lmcache_cli-0.5.7.dev0.dist-info}/licenses/LICENSE +0 -0
  56. {lmcache_cli-0.5.6rc4.dev27.dist-info → lmcache_cli-0.5.7.dev0.dist-info}/top_level.txt +0 -0
lmcache/_version.py CHANGED
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.5.6rc4.dev27'
22
- __version_tuple__ = version_tuple = (0, 5, 6, 'rc4', 'dev27')
21
+ __version__ = version = '0.5.7.dev0'
22
+ __version_tuple__ = version_tuple = (0, 5, 7, 'dev0')
23
23
 
24
- __commit_id__ = commit_id = 'g7d7ca4704'
24
+ __commit_id__ = commit_id = 'g3e11e1081'
@@ -140,7 +140,7 @@ def make_memory_objects(
140
140
  metadata = MemoryObjMetadata(
141
141
  shape=torch.Size([data_size]),
142
142
  dtype=torch.uint8,
143
- address=raw_tensor.data_ptr(),
143
+ address=start,
144
144
  phy_size=data_size * raw_tensor.element_size(),
145
145
  fmt=MemoryFormat.KV_2LTD,
146
146
  ref_count=1,
@@ -130,8 +130,23 @@ def _stream(
130
130
  chat: bool,
131
131
  max_tokens: int,
132
132
  ) -> dict[str, Any]:
133
- """POST with ``stream: true``; parse SSE; return the completion text plus
134
- TTFT/TPOT and token metrics."""
133
+ """POST with ``stream: true`` and collect text and token/latency metrics.
134
+
135
+ Args:
136
+ url: Endpoint URL for the streaming request.
137
+ body: OpenAI-compatible request payload.
138
+ timeout: HTTP request timeout in seconds.
139
+ chat: Whether the endpoint uses chat completion chunks.
140
+ max_tokens: Output-token estimate when the server omits its count.
141
+
142
+ Returns:
143
+ Completion text, server token counts, and measured latency metrics.
144
+ A reported completion count of zero is preserved.
145
+
146
+ Raises:
147
+ RuntimeError: If the HTTP request fails, the server reports an error,
148
+ or the stream contains neither text nor usage.
149
+ """
135
150
  payload = {
136
151
  **body,
137
152
  "stream": True,
@@ -199,9 +214,8 @@ def _stream(
199
214
 
200
215
  u = usage or {}
201
216
  prompt_tokens = int(u.get("prompt_tokens") or 0)
202
- num_completion = int(u.get("completion_tokens") or 0)
203
- # Match V2RequestSender: server count if present, else max_tokens cap.
204
- num_generated = num_completion if num_completion > 0 else max_tokens
217
+ num_completion = u.get("completion_tokens")
218
+ num_generated = int(num_completion) if num_completion is not None else max_tokens
205
219
  if first_token_t is None:
206
220
  # Use total round-trip as a conservative TTFT approximation.
207
221
  ttft_s = t1 - t0
@@ -294,7 +308,9 @@ class Request:
294
308
  Returns:
295
309
  A ``(answer, metrics)`` tuple where ``answer`` is the model's
296
310
  completion text and ``metrics`` is a :data:`MetricMap` of token
297
- and latency stats keyed by metric id.
311
+ and latency stats keyed by metric id. Output tokens use the
312
+ server's completion count, including zero, or ``max_tokens``
313
+ when the count is absent.
298
314
  """
299
315
  request_data = self.build_request(prompt)
300
316
  result = self._query_with_fallback(request_data)
@@ -101,7 +101,7 @@ def _wrap_sglang_kv_caches(
101
101
  device = _validate_sglang_kv_pools(k_pool, v_pool)
102
102
  tensors = [*k_pool, *v_pool]
103
103
  device_spec = get_device_spec(device.type)
104
- if device_spec is None or not device_spec.is_handle_transfer_available():
104
+ if device_spec is None or not device_spec.is_lmcache_driven_available():
105
105
  raise ValueError(
106
106
  "SGLang MP handle transfer is unavailable for device type "
107
107
  f"{device.type!r}: required memory IPC, event IPC, cache context, "
@@ -4,7 +4,7 @@
4
4
  This module deliberately talks to LMCache's engine-neutral MP protocol. It
5
5
  does not use LMCache's legacy SGLang integration and it never constructs an
6
6
  in-process LMCache engine. The registered SGLang GPU KV tensors remain owned
7
- by SGLang; LMCache accesses them through device-memory and event IPC handles.
7
+ by SGLang; the selected transfer context moves their KV data.
8
8
  """
9
9
 
10
10
  # Future
@@ -62,7 +62,7 @@ class _ImmediateFuture:
62
62
 
63
63
 
64
64
  class UnifiedLMCacheMPConnector:
65
- """Asynchronous, CUDA-IPC connector to a standalone LMCache server."""
65
+ """Asynchronous device-IPC connector to a standalone LMCache server."""
66
66
 
67
67
  def __init__(
68
68
  self,
@@ -113,8 +113,6 @@ class UnifiedLMCacheMPConnector:
113
113
  if not kv_groups or any(not group.kv_tensors for group in kv_groups):
114
114
  raise ValueError("LMCache KV group registration cannot be empty")
115
115
  kv_tensors = [tensor for group in kv_groups for tensor in group.kv_tensors]
116
- if any(t.device.type != "cuda" for t in kv_tensors):
117
- raise NotImplementedError("LMCache MP currently requires CUDA KV tensors")
118
116
  if any(t.device != kv_tensors[0].device for t in kv_tensors):
119
117
  raise ValueError("All LMCache-registered KV tensors must share one device")
120
118
  if any(t.dim() < 2 for t in kv_tensors):
@@ -240,6 +238,9 @@ class UnifiedLMCacheMPConnector:
240
238
  _DEFAULT_HEARTBEAT_INTERVAL_SECONDS,
241
239
  )
242
240
  )
241
+ self._mp_transfer_mode = config.get_extra_config_value( # type: ignore[attr-defined]
242
+ "lmcache.mp.mp_transfer_mode", None
243
+ )
243
244
  if any(
244
245
  group.recurrent_state or group.sliding_window_size >= 0
245
246
  for group in wire_groups
@@ -302,7 +303,6 @@ class UnifiedLMCacheMPConnector:
302
303
  ) = self._build_engine_group_info_specs()
303
304
  self._req_client = RequestClientFactory.create(self.server_url)
304
305
  self._transfer_ctx: Optional["TransferContext"] = None
305
- self._event_backend: Any = None
306
306
  self._registered = False
307
307
  self._closed = False
308
308
  self._lookups: dict[str, LMCacheLookupOperation] = {}
@@ -321,9 +321,10 @@ class UnifiedLMCacheMPConnector:
321
321
  f"LMCache chunk size {self.chunk_size} must be a positive "
322
322
  "multiple of every SGLang group tokens_per_block"
323
323
  )
324
- # LMCache-driven MP ignores this compatibility argument; per-group
325
- # block counts are derived from EngineGroupInfo.tokens_per_block.
326
- self.blocks_in_chunk = self.chunk_size
324
+ # Engine-driven currently accepts one transfer group and needs its
325
+ # physical block count. LMCache-driven derives per-group counts from
326
+ # EngineGroupInfo and ignores this compatibility argument.
327
+ self.blocks_in_chunk = self.chunk_size // self._kv_groups[0].tokens_per_block
327
328
  self.register_kv_cache()
328
329
  logger.info("UnifiedLMCacheMPConnector initialized succeed.")
329
330
 
@@ -525,22 +526,19 @@ class UnifiedLMCacheMPConnector:
525
526
  dist.all_reduce(tensor, op=op, group=self.pp_group)
526
527
 
527
528
  def register_kv_cache(self) -> None:
528
- """Export the SGLang GPU tensors to the LMCache MP server once."""
529
+ """Register SGLang KV tensors with the selected transfer context."""
529
530
  # First Party
530
531
  from lmcache.utils import EngineType
531
532
  from lmcache.v1.multiprocess.group_view import EngineGroupInfo
532
533
  from lmcache.v1.multiprocess.transfer_context import create_transfer_context
533
- from lmcache.v1.platform.base.event_ipc import get_event_ipc_backend
534
534
 
535
535
  if self._registered:
536
536
  raise RuntimeError("LMCache KV tensors are already registered")
537
- self._event_backend = get_event_ipc_backend(self.device)
538
- self._event_backend.check_event_support(self.device)
539
537
  transfer_ctx = create_transfer_context(
540
538
  self._kv_caches,
541
539
  instance_id=self.instance_id,
542
540
  req_client=self._req_client,
543
- mode="lmcache_driven",
541
+ mode=self._mp_transfer_mode,
544
542
  )
545
543
  self._transfer_ctx = transfer_ctx
546
544
  engine_group_infos = [
@@ -599,13 +597,6 @@ class UnifiedLMCacheMPConnector:
599
597
  self._heartbeat_thread = heartbeat_thread
600
598
  logger.info("LMCache MP heartbeat thread started")
601
599
 
602
- def _new_event(self, stream: Any = None) -> Any:
603
- event = self._event_backend.create_event(self.device)
604
- if stream is None:
605
- stream = torch.get_device_module(self.device).current_stream()
606
- self._event_backend.record_event(event, stream)
607
- return event
608
-
609
600
  def _create_key(
610
601
  self,
611
602
  operation: LMCacheLookupOperation,
@@ -886,11 +877,7 @@ class UnifiedLMCacheMPConnector:
886
877
  transfer_ctx = self._transfer_ctx
887
878
  if transfer_ctx is None:
888
879
  raise RuntimeError("LMCache KV tensors are not registered")
889
- event = (
890
- self._new_event()
891
- if producer_stream is None
892
- else self._new_event(producer_stream)
893
- )
880
+ event = self._create_recorded_event(transfer_ctx, producer_stream)
894
881
  future = transfer_ctx.submit_retrieve(
895
882
  operation.request_id,
896
883
  key,
@@ -941,11 +928,11 @@ class UnifiedLMCacheMPConnector:
941
928
  otherwise ``False``.
942
929
 
943
930
  Notes:
944
- Waiting for the raw MQ response imports the LMCache server's
945
- completion event. Device completion is then ordered with a stream
946
- wait, so the CPU does not wait for H2D. On a cross-rank failure the
947
- successful ranks synchronize their local work before the caller
948
- releases destination slots.
931
+ LMCache-driven retrieval imports the server's completion event and
932
+ orders it on ``stream`` without a CPU device synchronization.
933
+ Engine-driven retrieval completes its worker-side scatter before
934
+ returning its plain future. On a cross-rank failure, successful
935
+ ranks synchronize local work before destination slots are released.
949
936
  """
950
937
  local_success = False
951
938
  try:
@@ -1121,7 +1108,7 @@ class UnifiedLMCacheMPConnector:
1121
1108
  key = self._create_key(
1122
1109
  lookup, start=start, end=aligned_end, worker_id=self.kv_worker_id
1123
1110
  )
1124
- event = self._new_event()
1111
+ event = transfer_ctx.create_recorded_event()
1125
1112
  future = transfer_ctx.submit_store(
1126
1113
  request_id,
1127
1114
  key,
@@ -1260,3 +1247,13 @@ class UnifiedLMCacheMPConnector:
1260
1247
  transfer_ctx.close()
1261
1248
  self._transfer_ctx = None
1262
1249
  self._req_client.close()
1250
+
1251
+ def _create_recorded_event(
1252
+ self, transfer_ctx: "TransferContext", stream: Any = None
1253
+ ) -> Any:
1254
+ """Create a context-owned event on an optional SGLang producer stream."""
1255
+ if stream is None:
1256
+ return transfer_ctx.create_recorded_event()
1257
+ device_module = torch.get_device_module(self.device)
1258
+ with device_module.stream(stream):
1259
+ return transfer_ctx.create_recorded_event()
@@ -10,6 +10,7 @@ import time
10
10
  # Third Party
11
11
  from vllm.config import VllmConfig
12
12
  from vllm.distributed.kv_events import (
13
+ BlockRemoved,
13
14
  BlockStored,
14
15
  KVCacheEvent,
15
16
  KVConnectorKVEvents,
@@ -81,6 +82,7 @@ from lmcache.integration.vllm.utils import (
81
82
  mla_only,
82
83
  vllm_layout_hints,
83
84
  )
85
+ from lmcache.utils import CacheRemoveEvent
84
86
  from lmcache.utils import init_logger as lmcache_init_logger
85
87
 
86
88
  try:
@@ -146,16 +148,23 @@ class LMCacheMPKVEvents(KVConnectorKVEvents):
146
148
 
147
149
  def __init__(self, num_workers: int) -> None:
148
150
  self._aggregator = KVEventAggregator(num_workers)
151
+ self._batches: list[list[KVCacheEvent]] = []
149
152
 
150
153
  def add_events(self, events: list[KVCacheEvent]) -> None:
151
154
  """Add events from one worker."""
152
155
  self._aggregator.add_events(events)
156
+ self._batches.append(list(events))
153
157
 
154
158
  def aggregate(self) -> "LMCacheMPKVEvents":
155
- """Retain only events seen from every contributing worker."""
156
- common_events = self._aggregator.get_common_events()
159
+ """Keep all pollers' events, preserving transitions within each batch."""
160
+ remaining = set(self._aggregator.get_all_events())
161
+ merged: list[KVCacheEvent] = []
162
+ for events in self._batches:
163
+ merged.extend(event for event in events if event in remaining)
164
+ remaining.difference_update(events)
165
+ self._batches = [merged] if merged else []
157
166
  self._aggregator.clear_events()
158
- self._aggregator.add_events(common_events)
167
+ self._aggregator.add_events(merged)
159
168
  self._aggregator.reset_workers()
160
169
  return self
161
170
 
@@ -164,8 +173,8 @@ class LMCacheMPKVEvents(KVConnectorKVEvents):
164
173
  self._aggregator.increment_workers(count)
165
174
 
166
175
  def get_all_events(self) -> list[KVCacheEvent]:
167
- """Return all buffered events."""
168
- return self._aggregator.get_all_events()
176
+ """Return all buffered events in worker order."""
177
+ return [event for batch in self._batches for event in batch]
169
178
 
170
179
  def get_number_of_workers(self) -> int:
171
180
  """Return the number of contributing workers."""
@@ -175,6 +184,7 @@ class LMCacheMPKVEvents(KVConnectorKVEvents):
175
184
  """Clear buffered events and reset the worker count."""
176
185
  self._aggregator.clear_events()
177
186
  self._aggregator.reset_workers()
187
+ self._batches.clear()
178
188
 
179
189
 
180
190
  # Helper functions
@@ -727,7 +737,8 @@ class LMCacheMPConnector(KVConnectorBase_V1, SupportsHMA):
727
737
  extra_config=vllm_config.kv_transfer_config.kv_connector_extra_config,
728
738
  )
729
739
  self.request_trackers: dict[str, LMCacheMPRequestTracker] = {}
730
- self._kv_cache_events: LMCacheMPKVEvents | None = None
740
+ # Worker events aggregated per step, awaiting take_events().
741
+ self._kv_cache_events: list[KVCacheEvent] = []
731
742
 
732
743
  # Align-mode Mamba keeps one speculative block per draft token at
733
744
  # the tail of a request's block list.
@@ -1124,8 +1135,10 @@ class LMCacheMPConnector(KVConnectorBase_V1, SupportsHMA):
1124
1135
  """Return worker-side KV cache events collected since the last step.
1125
1136
 
1126
1137
  Returns:
1127
- A vLLM KV event container with completed LMCache store events, or
1128
- None when disabled or when no store completed.
1138
+ A vLLM KV event container with ``BlockStored`` events for LMCache
1139
+ stores (own completed stores and stores learned from the MP
1140
+ server) and ``BlockRemoved`` events for CPU evictions, or None
1141
+ when disabled or when nothing happened.
1129
1142
  """
1130
1143
  if not self._enable_kv_events:
1131
1144
  return None
@@ -1133,21 +1146,27 @@ class LMCacheMPConnector(KVConnectorBase_V1, SupportsHMA):
1133
1146
  if not events:
1134
1147
  return None
1135
1148
 
1136
- blocks = [
1137
- BlockStored(
1138
- block_hashes=[
1139
- _convert_kv_event_hash(block_hash)
1140
- for block_hash in event.block_hashes
1141
- ],
1142
- parent_block_hash=_convert_kv_event_hash(event.parent_block_hash),
1143
- token_ids=event.token_ids,
1144
- lora_id=event.lora_id,
1145
- block_size=event.block_size,
1146
- medium=event.medium,
1147
- lora_name=event.lora_name,
1149
+ blocks: list[KVCacheEvent] = []
1150
+ for event in events:
1151
+ block_hashes = [
1152
+ _convert_kv_event_hash(block_hash) for block_hash in event.block_hashes
1153
+ ]
1154
+ if isinstance(event, CacheRemoveEvent):
1155
+ blocks.append(
1156
+ BlockRemoved(block_hashes=block_hashes, medium=event.medium)
1157
+ )
1158
+ continue
1159
+ blocks.append(
1160
+ BlockStored(
1161
+ block_hashes=block_hashes,
1162
+ parent_block_hash=_convert_kv_event_hash(event.parent_block_hash),
1163
+ token_ids=event.token_ids,
1164
+ lora_id=event.lora_id,
1165
+ block_size=event.block_size,
1166
+ medium=event.medium,
1167
+ lora_name=event.lora_name,
1168
+ )
1148
1169
  )
1149
- for event in events
1150
- ]
1151
1170
  kv_events = LMCacheMPKVEvents(num_workers=1)
1152
1171
  kv_events.add_events(blocks)
1153
1172
  return kv_events
@@ -1487,13 +1506,8 @@ class LMCacheMPConnector(KVConnectorBase_V1, SupportsHMA):
1487
1506
  """
1488
1507
  kv_cache_events = connector_output.kv_cache_events
1489
1508
  if kv_cache_events and isinstance(kv_cache_events, LMCacheMPKVEvents):
1490
- if self._kv_cache_events is None:
1491
- self._kv_cache_events = kv_cache_events
1492
- else:
1493
- self._kv_cache_events.add_events(kv_cache_events.get_all_events())
1494
- self._kv_cache_events.increment_workers(
1495
- kv_cache_events.get_number_of_workers()
1496
- )
1509
+ # One aggregated batch per step keeps the workers' event order.
1510
+ self._kv_cache_events.extend(kv_cache_events.aggregate().get_all_events())
1497
1511
 
1498
1512
  if not self.lazy_offload:
1499
1513
  return
@@ -1577,14 +1591,13 @@ class LMCacheMPConnector(KVConnectorBase_V1, SupportsHMA):
1577
1591
  """
1578
1592
  Take the KV cache events from the connector.
1579
1593
 
1580
- Yields:
1581
- New KV cache events since the last call.
1594
+ Returns:
1595
+ The KV cache events collected since the last call, in the order
1596
+ the workers reported them. The buffer is cleared.
1582
1597
  """
1583
- if self._kv_cache_events is not None:
1584
- self._kv_cache_events.aggregate()
1585
- yield from self._kv_cache_events.get_all_events()
1586
- self._kv_cache_events.clear_events()
1587
- self._kv_cache_events = None
1598
+ events = self._kv_cache_events
1599
+ self._kv_cache_events = []
1600
+ return events
1588
1601
 
1589
1602
  def has_pending_push_work(self) -> bool:
1590
1603
  """Return whether vLLM should keep stepping for pending push work.