lmcache-cli 0.4.5.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 (399) hide show
  1. lmcache/__init__.py +84 -0
  2. lmcache/_version.py +24 -0
  3. lmcache/cli/__init__.py +1 -0
  4. lmcache/cli/commands/__init__.py +34 -0
  5. lmcache/cli/commands/base.py +157 -0
  6. lmcache/cli/commands/bench/__init__.py +557 -0
  7. lmcache/cli/commands/bench/engine_bench/__init__.py +1 -0
  8. lmcache/cli/commands/bench/engine_bench/config.py +245 -0
  9. lmcache/cli/commands/bench/engine_bench/interactive/__init__.py +274 -0
  10. lmcache/cli/commands/bench/engine_bench/interactive/config.json +10 -0
  11. lmcache/cli/commands/bench/engine_bench/interactive/schema.py +352 -0
  12. lmcache/cli/commands/bench/engine_bench/interactive/state.py +327 -0
  13. lmcache/cli/commands/bench/engine_bench/interactive/terminal.py +291 -0
  14. lmcache/cli/commands/bench/engine_bench/progress.py +145 -0
  15. lmcache/cli/commands/bench/engine_bench/request_sender.py +232 -0
  16. lmcache/cli/commands/bench/engine_bench/stats.py +275 -0
  17. lmcache/cli/commands/bench/engine_bench/workloads/__init__.py +153 -0
  18. lmcache/cli/commands/bench/engine_bench/workloads/base.py +122 -0
  19. lmcache/cli/commands/bench/engine_bench/workloads/long_doc_permutator.py +435 -0
  20. lmcache/cli/commands/bench/engine_bench/workloads/long_doc_qa.py +281 -0
  21. lmcache/cli/commands/bench/engine_bench/workloads/multi_round_chat.py +337 -0
  22. lmcache/cli/commands/bench/engine_bench/workloads/random_prefill.py +178 -0
  23. lmcache/cli/commands/describe.py +310 -0
  24. lmcache/cli/commands/kvcache.py +133 -0
  25. lmcache/cli/commands/mock.py +75 -0
  26. lmcache/cli/commands/ping.py +113 -0
  27. lmcache/cli/commands/query/__init__.py +155 -0
  28. lmcache/cli/commands/query/prompt.py +134 -0
  29. lmcache/cli/commands/query/request.py +357 -0
  30. lmcache/cli/commands/server.py +99 -0
  31. lmcache/cli/commands/tool/__init__.py +63 -0
  32. lmcache/cli/commands/tool/cache_simulator.py +113 -0
  33. lmcache/cli/commands/trace/__init__.py +505 -0
  34. lmcache/cli/commands/trace/dispatch.py +249 -0
  35. lmcache/cli/commands/trace/driver.py +372 -0
  36. lmcache/cli/commands/trace/stats.py +289 -0
  37. lmcache/cli/documents/lmcache.txt +11 -0
  38. lmcache/cli/main.py +42 -0
  39. lmcache/cli/metrics/__init__.py +29 -0
  40. lmcache/cli/metrics/formatter.py +171 -0
  41. lmcache/cli/metrics/handler.py +94 -0
  42. lmcache/cli/metrics/metrics.py +161 -0
  43. lmcache/cli/metrics/section.py +77 -0
  44. lmcache/connections.py +173 -0
  45. lmcache/integration/__init__.py +2 -0
  46. lmcache/integration/base_service_factory.py +165 -0
  47. lmcache/integration/request_telemetry/__init__.py +1 -0
  48. lmcache/integration/request_telemetry/base.py +51 -0
  49. lmcache/integration/request_telemetry/factory.py +113 -0
  50. lmcache/integration/request_telemetry/fastapi.py +109 -0
  51. lmcache/integration/request_telemetry/noop.py +35 -0
  52. lmcache/integration/sglang/__init__.py +2 -0
  53. lmcache/integration/sglang/sglang_adapter.py +326 -0
  54. lmcache/integration/sglang/utils.py +39 -0
  55. lmcache/integration/vllm/__init__.py +1 -0
  56. lmcache/integration/vllm/lmcache_connector_v1.py +213 -0
  57. lmcache/integration/vllm/lmcache_connector_v1_085.py +150 -0
  58. lmcache/integration/vllm/lmcache_mp_connector_0180.py +1072 -0
  59. lmcache/integration/vllm/tests/test_mm_hash_utils.py +112 -0
  60. lmcache/integration/vllm/utils.py +433 -0
  61. lmcache/integration/vllm/vllm_multi_process_adapter.py +1090 -0
  62. lmcache/integration/vllm/vllm_service_factory.py +339 -0
  63. lmcache/integration/vllm/vllm_v1_adapter.py +1713 -0
  64. lmcache/logging.py +107 -0
  65. lmcache/native_storage_ops.pyi +230 -0
  66. lmcache/non_cuda_equivalents.py +1424 -0
  67. lmcache/observability.py +1958 -0
  68. lmcache/storage_backend/serde/__init__.py +1 -0
  69. lmcache/storage_backend/serde/cachegen_basics.py +210 -0
  70. lmcache/storage_backend/serde/cachegen_decoder.py +207 -0
  71. lmcache/storage_backend/serde/cachegen_encoder.py +394 -0
  72. lmcache/storage_backend/serde/serde.py +75 -0
  73. lmcache/tools/__init__.py +1 -0
  74. lmcache/tools/cache_simulator/README.md +392 -0
  75. lmcache/tools/cache_simulator/__init__.py +1 -0
  76. lmcache/tools/cache_simulator/docs/simulate_example.png +0 -0
  77. lmcache/tools/cache_simulator/docs/sweep_example.png +0 -0
  78. lmcache/tools/cache_simulator/gen_bench_dataset.py +360 -0
  79. lmcache/tools/cache_simulator/lru_cache.py +124 -0
  80. lmcache/tools/cache_simulator/plot_hit_rate.py +231 -0
  81. lmcache/tools/cache_simulator/simulator.py +795 -0
  82. lmcache/tools/controller_benchmark/README.md +161 -0
  83. lmcache/tools/controller_benchmark/__init__.py +1 -0
  84. lmcache/tools/controller_benchmark/__main__.py +331 -0
  85. lmcache/tools/controller_benchmark/benchmark.py +660 -0
  86. lmcache/tools/controller_benchmark/config.py +44 -0
  87. lmcache/tools/controller_benchmark/constants.py +10 -0
  88. lmcache/tools/controller_benchmark/handlers/__init__.py +46 -0
  89. lmcache/tools/controller_benchmark/handlers/admit.py +52 -0
  90. lmcache/tools/controller_benchmark/handlers/base.py +47 -0
  91. lmcache/tools/controller_benchmark/handlers/deregister.py +49 -0
  92. lmcache/tools/controller_benchmark/handlers/evict.py +52 -0
  93. lmcache/tools/controller_benchmark/handlers/heartbeat.py +56 -0
  94. lmcache/tools/controller_benchmark/handlers/p2p_lookup.py +47 -0
  95. lmcache/tools/controller_benchmark/handlers/register.py +56 -0
  96. lmcache/tools/mp_status_viewer/__init__.py +1 -0
  97. lmcache/tools/mp_status_viewer/__main__.py +95 -0
  98. lmcache/usage_context.py +417 -0
  99. lmcache/utils.py +665 -0
  100. lmcache/v1/__init__.py +2 -0
  101. lmcache/v1/api_server/__init__.py +2 -0
  102. lmcache/v1/api_server/__main__.py +537 -0
  103. lmcache/v1/basic_check.py +112 -0
  104. lmcache/v1/cache_controller/__init__.py +9 -0
  105. lmcache/v1/cache_controller/commands/__init__.py +15 -0
  106. lmcache/v1/cache_controller/commands/base.py +35 -0
  107. lmcache/v1/cache_controller/commands/full_sync.py +49 -0
  108. lmcache/v1/cache_controller/config.py +176 -0
  109. lmcache/v1/cache_controller/controller_manager.py +535 -0
  110. lmcache/v1/cache_controller/controllers/__init__.py +11 -0
  111. lmcache/v1/cache_controller/controllers/full_sync_tracker.py +473 -0
  112. lmcache/v1/cache_controller/controllers/kv_controller.py +439 -0
  113. lmcache/v1/cache_controller/controllers/registration_controller.py +282 -0
  114. lmcache/v1/cache_controller/executor.py +463 -0
  115. lmcache/v1/cache_controller/frontend/static/css/style.css +201 -0
  116. lmcache/v1/cache_controller/frontend/static/img/logo.png +0 -0
  117. lmcache/v1/cache_controller/frontend/static/index.html +234 -0
  118. lmcache/v1/cache_controller/frontend/static/js/controller_app.js +660 -0
  119. lmcache/v1/cache_controller/full_sync_sender.py +475 -0
  120. lmcache/v1/cache_controller/locks.py +149 -0
  121. lmcache/v1/cache_controller/message.py +828 -0
  122. lmcache/v1/cache_controller/observability.py +208 -0
  123. lmcache/v1/cache_controller/utils.py +679 -0
  124. lmcache/v1/cache_controller/worker.py +665 -0
  125. lmcache/v1/cache_engine.py +2058 -0
  126. lmcache/v1/cache_interface.py +19 -0
  127. lmcache/v1/check/__init__.py +74 -0
  128. lmcache/v1/check/check_mode_gen.py +86 -0
  129. lmcache/v1/check/check_mode_test_l2_adapter.py +284 -0
  130. lmcache/v1/check/check_mode_test_remote.py +155 -0
  131. lmcache/v1/check/check_mode_test_storage_manager.py +142 -0
  132. lmcache/v1/check/utils.py +571 -0
  133. lmcache/v1/compute/__init__.py +2 -0
  134. lmcache/v1/compute/attention/__init__.py +0 -0
  135. lmcache/v1/compute/attention/abstract.py +39 -0
  136. lmcache/v1/compute/attention/flash_attn.py +129 -0
  137. lmcache/v1/compute/attention/flash_infer_sparse.py +284 -0
  138. lmcache/v1/compute/attention/metadata.py +85 -0
  139. lmcache/v1/compute/attention/utils.py +14 -0
  140. lmcache/v1/compute/blend/__init__.py +7 -0
  141. lmcache/v1/compute/blend/blender.py +168 -0
  142. lmcache/v1/compute/blend/metadata.py +34 -0
  143. lmcache/v1/compute/blend/utils.py +63 -0
  144. lmcache/v1/compute/models/__init__.py +0 -0
  145. lmcache/v1/compute/models/base.py +141 -0
  146. lmcache/v1/compute/models/llama.py +9 -0
  147. lmcache/v1/compute/models/qwen3.py +24 -0
  148. lmcache/v1/compute/models/utils.py +68 -0
  149. lmcache/v1/compute/positional_encoding.py +199 -0
  150. lmcache/v1/config.py +848 -0
  151. lmcache/v1/config_base.py +848 -0
  152. lmcache/v1/distributed/api.py +248 -0
  153. lmcache/v1/distributed/config.py +321 -0
  154. lmcache/v1/distributed/error.py +64 -0
  155. lmcache/v1/distributed/eviction.py +192 -0
  156. lmcache/v1/distributed/eviction_policy/__init__.py +21 -0
  157. lmcache/v1/distributed/eviction_policy/factory.py +27 -0
  158. lmcache/v1/distributed/eviction_policy/lru.py +244 -0
  159. lmcache/v1/distributed/eviction_policy/noop.py +50 -0
  160. lmcache/v1/distributed/internal_api.py +170 -0
  161. lmcache/v1/distributed/l1_manager.py +835 -0
  162. lmcache/v1/distributed/l2_adapters/__init__.py +67 -0
  163. lmcache/v1/distributed/l2_adapters/base.py +360 -0
  164. lmcache/v1/distributed/l2_adapters/config.py +385 -0
  165. lmcache/v1/distributed/l2_adapters/factory.py +205 -0
  166. lmcache/v1/distributed/l2_adapters/fs_l2_adapter.py +747 -0
  167. lmcache/v1/distributed/l2_adapters/fs_native_l2_adapter.py +167 -0
  168. lmcache/v1/distributed/l2_adapters/mock_l2_adapter.py +516 -0
  169. lmcache/v1/distributed/l2_adapters/mooncake_store_l2_adapter.py +135 -0
  170. lmcache/v1/distributed/l2_adapters/native_connector_l2_adapter.py +468 -0
  171. lmcache/v1/distributed/l2_adapters/native_plugin_l2_adapter.py +199 -0
  172. lmcache/v1/distributed/l2_adapters/nixl_store_dynamic_l2_adapter.py +831 -0
  173. lmcache/v1/distributed/l2_adapters/nixl_store_l2_adapter.py +983 -0
  174. lmcache/v1/distributed/l2_adapters/plugin_l2_adapter.py +210 -0
  175. lmcache/v1/distributed/l2_adapters/resp_l2_adapter.py +176 -0
  176. lmcache/v1/distributed/memory_manager.py +179 -0
  177. lmcache/v1/distributed/storage_controller.py +39 -0
  178. lmcache/v1/distributed/storage_controllers/__init__.py +43 -0
  179. lmcache/v1/distributed/storage_controllers/eviction_controller.py +242 -0
  180. lmcache/v1/distributed/storage_controllers/prefetch_controller.py +830 -0
  181. lmcache/v1/distributed/storage_controllers/prefetch_policy.py +193 -0
  182. lmcache/v1/distributed/storage_controllers/store_controller.py +452 -0
  183. lmcache/v1/distributed/storage_controllers/store_policy.py +213 -0
  184. lmcache/v1/distributed/storage_manager.py +532 -0
  185. lmcache/v1/event_manager.py +145 -0
  186. lmcache/v1/exceptions/__init__.py +16 -0
  187. lmcache/v1/gpu_connector/__init__.py +126 -0
  188. lmcache/v1/gpu_connector/gpu_connectors.py +1906 -0
  189. lmcache/v1/gpu_connector/gpu_ops.py +85 -0
  190. lmcache/v1/gpu_connector/hpu_connector.py +326 -0
  191. lmcache/v1/gpu_connector/mock_gpu_connector.py +67 -0
  192. lmcache/v1/gpu_connector/utils.py +890 -0
  193. lmcache/v1/gpu_connector/xpu_connectors.py +916 -0
  194. lmcache/v1/health_monitor/__init__.py +1 -0
  195. lmcache/v1/health_monitor/base.py +587 -0
  196. lmcache/v1/health_monitor/checks/__init__.py +1 -0
  197. lmcache/v1/health_monitor/checks/remote_backend_check.py +304 -0
  198. lmcache/v1/health_monitor/constants.py +36 -0
  199. lmcache/v1/internal_api_server/__init__.py +0 -0
  200. lmcache/v1/internal_api_server/api_registry.py +59 -0
  201. lmcache/v1/internal_api_server/api_server.py +120 -0
  202. lmcache/v1/internal_api_server/common/__init__.py +1 -0
  203. lmcache/v1/internal_api_server/common/env_api.py +22 -0
  204. lmcache/v1/internal_api_server/common/loglevel_api.py +57 -0
  205. lmcache/v1/internal_api_server/common/metrics_api.py +29 -0
  206. lmcache/v1/internal_api_server/common/periodic_thread_api.py +138 -0
  207. lmcache/v1/internal_api_server/common/run_script_api.py +73 -0
  208. lmcache/v1/internal_api_server/common/thread_api.py +63 -0
  209. lmcache/v1/internal_api_server/controller/__init__.py +1 -0
  210. lmcache/v1/internal_api_server/controller/key_stats_api.py +81 -0
  211. lmcache/v1/internal_api_server/controller/worker_info_api.py +136 -0
  212. lmcache/v1/internal_api_server/utils.py +43 -0
  213. lmcache/v1/internal_api_server/vllm/__init__.py +1 -0
  214. lmcache/v1/internal_api_server/vllm/backend_api.py +221 -0
  215. lmcache/v1/internal_api_server/vllm/bypass_api.py +204 -0
  216. lmcache/v1/internal_api_server/vllm/cache_api.py +895 -0
  217. lmcache/v1/internal_api_server/vllm/chunk_statistics_api.py +141 -0
  218. lmcache/v1/internal_api_server/vllm/conf_api.py +147 -0
  219. lmcache/v1/internal_api_server/vllm/freeze_api.py +172 -0
  220. lmcache/v1/internal_api_server/vllm/hot_cache_api.py +184 -0
  221. lmcache/v1/internal_api_server/vllm/inference_api.py +65 -0
  222. lmcache/v1/internal_api_server/vllm/load_fs_chunks_api.py +320 -0
  223. lmcache/v1/internal_api_server/vllm/lookup_api.py +145 -0
  224. lmcache/v1/internal_api_server/vllm/version_api.py +25 -0
  225. lmcache/v1/kv_layer_groups.py +267 -0
  226. lmcache/v1/lazy_memory_allocator.py +284 -0
  227. lmcache/v1/lookup_client/__init__.py +25 -0
  228. lmcache/v1/lookup_client/abstract_client.py +77 -0
  229. lmcache/v1/lookup_client/async_lookup_message.py +50 -0
  230. lmcache/v1/lookup_client/chunk_statistics_lookup_client.py +200 -0
  231. lmcache/v1/lookup_client/factory.py +251 -0
  232. lmcache/v1/lookup_client/hit_limit_lookup_client.py +86 -0
  233. lmcache/v1/lookup_client/lmcache_async_lookup_client.py +407 -0
  234. lmcache/v1/lookup_client/lmcache_lookup_client.py +285 -0
  235. lmcache/v1/lookup_client/lmcache_lookup_client_bypass.py +99 -0
  236. lmcache/v1/lookup_client/mooncake_lookup_client.py +87 -0
  237. lmcache/v1/lookup_client/record_strategies/__init__.py +77 -0
  238. lmcache/v1/lookup_client/record_strategies/base.py +327 -0
  239. lmcache/v1/lookup_client/record_strategies/file_hash.py +130 -0
  240. lmcache/v1/lookup_client/record_strategies/memory_bloom_filter.py +81 -0
  241. lmcache/v1/manager.py +539 -0
  242. lmcache/v1/memory_management.py +2619 -0
  243. lmcache/v1/metadata.py +114 -0
  244. lmcache/v1/mp_observability/AGENTS.override.md +21 -0
  245. lmcache/v1/mp_observability/README.md +204 -0
  246. lmcache/v1/mp_observability/config.py +340 -0
  247. lmcache/v1/mp_observability/event.py +100 -0
  248. lmcache/v1/mp_observability/event_bus.py +313 -0
  249. lmcache/v1/mp_observability/otel_init.py +145 -0
  250. lmcache/v1/mp_observability/subscribers/__init__.py +28 -0
  251. lmcache/v1/mp_observability/subscribers/logging/__init__.py +19 -0
  252. lmcache/v1/mp_observability/subscribers/logging/l1.py +56 -0
  253. lmcache/v1/mp_observability/subscribers/logging/l2.py +73 -0
  254. lmcache/v1/mp_observability/subscribers/logging/lookup_hash.py +209 -0
  255. lmcache/v1/mp_observability/subscribers/logging/mp_server.py +90 -0
  256. lmcache/v1/mp_observability/subscribers/logging/sm.py +59 -0
  257. lmcache/v1/mp_observability/subscribers/metrics/__init__.py +20 -0
  258. lmcache/v1/mp_observability/subscribers/metrics/l0_lifecycle.py +290 -0
  259. lmcache/v1/mp_observability/subscribers/metrics/l1.py +55 -0
  260. lmcache/v1/mp_observability/subscribers/metrics/l1_lifecycle.py +166 -0
  261. lmcache/v1/mp_observability/subscribers/metrics/l2.py +121 -0
  262. lmcache/v1/mp_observability/subscribers/metrics/sm.py +69 -0
  263. lmcache/v1/mp_observability/subscribers/tracing/__init__.py +12 -0
  264. lmcache/v1/mp_observability/subscribers/tracing/mp_server.py +333 -0
  265. lmcache/v1/mp_observability/subscribers/tracing/span_registry.py +148 -0
  266. lmcache/v1/mp_observability/trace/__init__.py +50 -0
  267. lmcache/v1/mp_observability/trace/codecs.py +255 -0
  268. lmcache/v1/mp_observability/trace/decorator.py +147 -0
  269. lmcache/v1/mp_observability/trace/format.py +132 -0
  270. lmcache/v1/mp_observability/trace/lifecycle.py +83 -0
  271. lmcache/v1/mp_observability/trace/reader.py +167 -0
  272. lmcache/v1/mp_observability/trace/recorder.py +300 -0
  273. lmcache/v1/multiprocess/__init__.py +0 -0
  274. lmcache/v1/multiprocess/affinity_pool.py +102 -0
  275. lmcache/v1/multiprocess/blend_server_v2.py +891 -0
  276. lmcache/v1/multiprocess/config.py +253 -0
  277. lmcache/v1/multiprocess/custom_types.py +281 -0
  278. lmcache/v1/multiprocess/futures.py +194 -0
  279. lmcache/v1/multiprocess/gpu_context.py +511 -0
  280. lmcache/v1/multiprocess/http_server.py +235 -0
  281. lmcache/v1/multiprocess/mp_runtime_plugin_launcher.py +130 -0
  282. lmcache/v1/multiprocess/mq.py +732 -0
  283. lmcache/v1/multiprocess/protocol.py +86 -0
  284. lmcache/v1/multiprocess/protocols/README.md +213 -0
  285. lmcache/v1/multiprocess/protocols/__init__.py +127 -0
  286. lmcache/v1/multiprocess/protocols/base.py +89 -0
  287. lmcache/v1/multiprocess/protocols/blend.py +109 -0
  288. lmcache/v1/multiprocess/protocols/blend_v2.py +57 -0
  289. lmcache/v1/multiprocess/protocols/controller.py +53 -0
  290. lmcache/v1/multiprocess/protocols/debug.py +34 -0
  291. lmcache/v1/multiprocess/protocols/engine.py +146 -0
  292. lmcache/v1/multiprocess/protocols/observability.py +39 -0
  293. lmcache/v1/multiprocess/server.py +1134 -0
  294. lmcache/v1/multiprocess/session.py +190 -0
  295. lmcache/v1/multiprocess/token_hasher.py +441 -0
  296. lmcache/v1/offload_server/__init__.py +17 -0
  297. lmcache/v1/offload_server/abstract_server.py +37 -0
  298. lmcache/v1/offload_server/message.py +30 -0
  299. lmcache/v1/offload_server/zmq_server.py +122 -0
  300. lmcache/v1/periodic_thread.py +579 -0
  301. lmcache/v1/pin_monitor.py +246 -0
  302. lmcache/v1/plugin/__init__.py +0 -0
  303. lmcache/v1/plugin/runtime_plugin_launcher.py +211 -0
  304. lmcache/v1/protocol.py +317 -0
  305. lmcache/v1/rpc/__init__.py +17 -0
  306. lmcache/v1/rpc/transport.py +105 -0
  307. lmcache/v1/rpc/zmq_transport.py +213 -0
  308. lmcache/v1/rpc_utils.py +165 -0
  309. lmcache/v1/server/__init__.py +2 -0
  310. lmcache/v1/server/__main__.py +170 -0
  311. lmcache/v1/server/storage_backend/__init__.py +21 -0
  312. lmcache/v1/server/storage_backend/abstract_backend.py +80 -0
  313. lmcache/v1/server/storage_backend/local_backend.py +75 -0
  314. lmcache/v1/server/utils.py +21 -0
  315. lmcache/v1/standalone/__init__.py +1 -0
  316. lmcache/v1/standalone/__main__.py +583 -0
  317. lmcache/v1/standalone/manager.py +80 -0
  318. lmcache/v1/standalone/standalone_service_factory.py +86 -0
  319. lmcache/v1/storage_backend/__init__.py +313 -0
  320. lmcache/v1/storage_backend/abstract_backend.py +445 -0
  321. lmcache/v1/storage_backend/audit_backend.py +233 -0
  322. lmcache/v1/storage_backend/batched_message_sender.py +222 -0
  323. lmcache/v1/storage_backend/cache_policy/__init__.py +45 -0
  324. lmcache/v1/storage_backend/cache_policy/base_policy.py +87 -0
  325. lmcache/v1/storage_backend/cache_policy/fifo.py +58 -0
  326. lmcache/v1/storage_backend/cache_policy/lfu.py +105 -0
  327. lmcache/v1/storage_backend/cache_policy/lru.py +81 -0
  328. lmcache/v1/storage_backend/cache_policy/mru.py +61 -0
  329. lmcache/v1/storage_backend/connector/__init__.py +443 -0
  330. lmcache/v1/storage_backend/connector/audit_adapter.py +77 -0
  331. lmcache/v1/storage_backend/connector/audit_connector.py +320 -0
  332. lmcache/v1/storage_backend/connector/base_connector.py +379 -0
  333. lmcache/v1/storage_backend/connector/blackhole_adapter.py +21 -0
  334. lmcache/v1/storage_backend/connector/blackhole_connector.py +37 -0
  335. lmcache/v1/storage_backend/connector/eic_adapter.py +31 -0
  336. lmcache/v1/storage_backend/connector/eic_connector.py +757 -0
  337. lmcache/v1/storage_backend/connector/external_adapter.py +79 -0
  338. lmcache/v1/storage_backend/connector/fs_adapter.py +51 -0
  339. lmcache/v1/storage_backend/connector/fs_connector.py +403 -0
  340. lmcache/v1/storage_backend/connector/infinistore_adapter.py +56 -0
  341. lmcache/v1/storage_backend/connector/infinistore_connector.py +177 -0
  342. lmcache/v1/storage_backend/connector/instrumented_connector.py +219 -0
  343. lmcache/v1/storage_backend/connector/lm_adapter.py +31 -0
  344. lmcache/v1/storage_backend/connector/lm_connector.py +176 -0
  345. lmcache/v1/storage_backend/connector/mock_adapter.py +57 -0
  346. lmcache/v1/storage_backend/connector/mock_connector.py +349 -0
  347. lmcache/v1/storage_backend/connector/mooncakestore_adapter.py +43 -0
  348. lmcache/v1/storage_backend/connector/mooncakestore_connector.py +614 -0
  349. lmcache/v1/storage_backend/connector/redis_adapter.py +181 -0
  350. lmcache/v1/storage_backend/connector/redis_connector.py +828 -0
  351. lmcache/v1/storage_backend/connector/s3_adapter.py +59 -0
  352. lmcache/v1/storage_backend/connector/s3_connector.py +699 -0
  353. lmcache/v1/storage_backend/connector/sagemaker_hyperpod_adapter.py +233 -0
  354. lmcache/v1/storage_backend/connector/sagemaker_hyperpod_connector.py +987 -0
  355. lmcache/v1/storage_backend/connector/valkey_adapter.py +114 -0
  356. lmcache/v1/storage_backend/connector/valkey_connector.py +627 -0
  357. lmcache/v1/storage_backend/gds_backend.py +1199 -0
  358. lmcache/v1/storage_backend/job_executor/__init__.py +0 -0
  359. lmcache/v1/storage_backend/job_executor/base_executor.py +34 -0
  360. lmcache/v1/storage_backend/job_executor/pq_executor.py +235 -0
  361. lmcache/v1/storage_backend/local_cpu_backend.py +810 -0
  362. lmcache/v1/storage_backend/local_disk_backend.py +656 -0
  363. lmcache/v1/storage_backend/maru_backend.py +734 -0
  364. lmcache/v1/storage_backend/naive_serde/__init__.py +50 -0
  365. lmcache/v1/storage_backend/naive_serde/cachegen_basics.py +133 -0
  366. lmcache/v1/storage_backend/naive_serde/cachegen_decoder.py +135 -0
  367. lmcache/v1/storage_backend/naive_serde/cachegen_encoder.py +83 -0
  368. lmcache/v1/storage_backend/naive_serde/kivi_serde.py +22 -0
  369. lmcache/v1/storage_backend/naive_serde/naive_serde.py +18 -0
  370. lmcache/v1/storage_backend/naive_serde/serde.py +37 -0
  371. lmcache/v1/storage_backend/native_clients/connector_client_base.py +165 -0
  372. lmcache/v1/storage_backend/native_clients/resp_client.py +35 -0
  373. lmcache/v1/storage_backend/nixl_storage_backend.py +1400 -0
  374. lmcache/v1/storage_backend/p2p_backend.py +788 -0
  375. lmcache/v1/storage_backend/path_sharder.py +117 -0
  376. lmcache/v1/storage_backend/pd_backend.py +646 -0
  377. lmcache/v1/storage_backend/plugins/dax_backend.py +1443 -0
  378. lmcache/v1/storage_backend/plugins/rust_raw_block_backend.py +1361 -0
  379. lmcache/v1/storage_backend/remote_backend.py +624 -0
  380. lmcache/v1/storage_backend/resp_client.py +227 -0
  381. lmcache/v1/storage_backend/storage_backend_listener.py +19 -0
  382. lmcache/v1/storage_backend/storage_manager.py +1352 -0
  383. lmcache/v1/system_detection.py +110 -0
  384. lmcache/v1/token_database.py +551 -0
  385. lmcache/v1/transfer_channel/__init__.py +83 -0
  386. lmcache/v1/transfer_channel/abstract.py +285 -0
  387. lmcache/v1/transfer_channel/mock_memory_channel.py +156 -0
  388. lmcache/v1/transfer_channel/nixl_channel.py +639 -0
  389. lmcache/v1/transfer_channel/py_socket_channel.py +260 -0
  390. lmcache/v1/transfer_channel/transfer_utils.py +63 -0
  391. lmcache/v1/utils/__init__.py +1 -0
  392. lmcache/v1/utils/bloom_filter.py +109 -0
  393. lmcache/v1/utils/cache_utils.py +125 -0
  394. lmcache_cli-0.4.5.dev0.dist-info/METADATA +185 -0
  395. lmcache_cli-0.4.5.dev0.dist-info/RECORD +399 -0
  396. lmcache_cli-0.4.5.dev0.dist-info/WHEEL +5 -0
  397. lmcache_cli-0.4.5.dev0.dist-info/entry_points.txt +2 -0
  398. lmcache_cli-0.4.5.dev0.dist-info/licenses/LICENSE +201 -0
  399. lmcache_cli-0.4.5.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2058 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Standard
3
+ from collections import defaultdict
4
+ from collections.abc import Iterable
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Callable,
9
+ Dict,
10
+ Generator,
11
+ List,
12
+ Optional,
13
+ Tuple,
14
+ Union,
15
+ )
16
+
17
+ if TYPE_CHECKING:
18
+ # First Party
19
+ from lmcache.v1.health_monitor.base import HealthMonitor
20
+
21
+ # Standard
22
+ import asyncio
23
+ import gc
24
+ import multiprocessing
25
+ import time
26
+
27
+ # Third Party
28
+ import torch
29
+
30
+ # First Party
31
+ from lmcache.logging import init_logger
32
+ from lmcache.observability import LMCacheStatsLogger, LMCStatsMonitor
33
+ from lmcache.usage_context import InitializeUsageContext
34
+ from lmcache.utils import (
35
+ CacheEngineKey,
36
+ CacheStoreEvent,
37
+ _lmcache_nvtx_annotate,
38
+ compress_slot_mapping,
39
+ convert_tokens_to_list,
40
+ )
41
+ from lmcache.v1.config import LMCacheEngineConfig
42
+ from lmcache.v1.event_manager import EventManager, EventStatus, EventType
43
+ from lmcache.v1.gpu_connector.gpu_connectors import GPUConnectorInterface
44
+ from lmcache.v1.gpu_connector.utils import assert_layerwise_gpu_connector
45
+ from lmcache.v1.memory_management import CuFileMemoryAllocator # noqa: E501
46
+ from lmcache.v1.memory_management import ( # noqa: E501
47
+ MemoryAllocatorInterface,
48
+ MemoryFormat,
49
+ MemoryObj,
50
+ MemoryObjMetadata,
51
+ MixedMemoryAllocator,
52
+ PagedTensorMemoryAllocator,
53
+ TensorMemoryObj,
54
+ )
55
+ from lmcache.v1.metadata import LMCacheMetadata
56
+ from lmcache.v1.pin_monitor import PinMonitor
57
+ from lmcache.v1.storage_backend.storage_manager import StorageManager
58
+ from lmcache.v1.system_detection import NUMADetector, NUMAMapping
59
+ from lmcache.v1.token_database import (
60
+ ChunkedTokenDatabase,
61
+ SegmentTokenDatabase,
62
+ TokenDatabase,
63
+ )
64
+
65
+ logger = init_logger(__name__)
66
+
67
+ # Type aliases for processed chunks
68
+ # (cache_key, memory_obj, start_index, end_index)
69
+ ProcessedChunk = Tuple[CacheEngineKey, MemoryObj, int, int]
70
+ # (list of processed chunks, total kv size)
71
+ ProcessTokensInternalResult = Tuple[List[ProcessedChunk], int]
72
+
73
+
74
+ class CacheEngineEndSignal:
75
+ pass
76
+
77
+
78
+ class LMCacheEngine:
79
+ """The main class for the cache engine.
80
+
81
+ When storing the KV caches into the cache engine, it takes GPU KV
82
+ caches from the serving engine and convert them into MemoryObjs that
83
+ resides in the CPU. The MemoryObjs are then being stored into the
84
+ StorageBackends in an asynchronous manner.
85
+
86
+ When retrieving the KV caches from the cache engine, it fetches the
87
+ MemoryObjs from the StorageBackends and convert them into GPU KV caches
88
+ by GPUConnectors specialized for the serving engine.
89
+
90
+ It also supports prefetching the KV caches from the StorageBackends.
91
+ It relies on the StorageBackends to manage the requests of prefetching
92
+ and real retrieval and avoid the conflicts.
93
+ """
94
+
95
+ def __init__(
96
+ self,
97
+ config: LMCacheEngineConfig,
98
+ metadata: LMCacheMetadata,
99
+ token_database: TokenDatabase,
100
+ gpu_connector: Optional[GPUConnectorInterface],
101
+ broadcast_fn: Callable[[torch.Tensor, int], None],
102
+ broadcast_object_fn: Callable[[Any, int], Any],
103
+ ):
104
+ logger.info(f"Creating LMCacheEngine with config: {config}")
105
+ self.config = config
106
+ self.metadata = metadata
107
+ self.token_database = token_database
108
+ self.gpu_connector = gpu_connector
109
+ self.broadcast_fn = broadcast_fn
110
+ self.broadcast_object_fn = broadcast_object_fn
111
+ # save_only_first_rank only works when use mla
112
+ self.save_only_first_rank = (
113
+ self.config.get_extra_config_value("save_only_first_rank", metadata.use_mla)
114
+ and metadata.use_mla
115
+ )
116
+
117
+ if self.save_only_first_rank and self.gpu_connector is not None:
118
+ self.broadcast_stream = (
119
+ self.gpu_connector.load_stream
120
+ if hasattr(self.gpu_connector, "load_stream")
121
+ else torch.cuda.Stream()
122
+ )
123
+
124
+ self.enable_controller = config.enable_controller
125
+
126
+ # NOTE: Unix systems use fork by default
127
+ multiprocessing.set_start_method("spawn", force=True)
128
+
129
+ # avoid circular import
130
+ # First Party
131
+ from lmcache.v1.cache_controller import LMCacheWorker
132
+
133
+ self.lmcache_worker: Optional[LMCacheWorker] = None
134
+ lmcache_worker_ids = config.get_lmcache_worker_ids(
135
+ metadata.use_mla, metadata.world_size
136
+ )
137
+ # lmcache_worker_ids is empty means start on all workers
138
+ if (
139
+ self.enable_controller
140
+ and self.metadata.role != "scheduler"
141
+ and (not lmcache_worker_ids or metadata.worker_id in lmcache_worker_ids)
142
+ ):
143
+ self.lmcache_worker = LMCacheWorker(config, metadata, self)
144
+ else:
145
+ self.lmcache_worker = None
146
+ logger.info(
147
+ "LMCacheWorker is not initialized (related configs: "
148
+ "enable_controller: %s, role: %s, worker_id: %s, worker_ids: %s).",
149
+ self.enable_controller,
150
+ self.metadata.role,
151
+ self.metadata.worker_id,
152
+ lmcache_worker_ids,
153
+ )
154
+
155
+ self.async_loading = config.enable_async_loading
156
+ self.event_manager = EventManager()
157
+
158
+ self.use_layerwise = config.use_layerwise
159
+
160
+ # TODO: support save_only_first_rank when use layerwise
161
+ # if use_layerwise is True, all ranks will initialize the storage_manager
162
+ # if save_only_first_rank is False, all ranks will initialize
163
+ # the storage_manager
164
+ # if save_only_first_rank is True, only the first rank and
165
+ # lookup server workers will initialize the storage_manager
166
+ self.storage_manager: Optional[StorageManager] = None
167
+
168
+ # KV events
169
+ self.kv_events_enabled = False
170
+ self.kv_events_enabled = config.enable_kv_events
171
+ if self.kv_events_enabled:
172
+ self.kv_events: List[CacheStoreEvent] = []
173
+ logger.info("KV events are enabled.")
174
+ else:
175
+ logger.info("KV events are disabled.")
176
+
177
+ # HACK: remove this in the future
178
+ # NOTE (Jiayi): This is currently used to support
179
+ # dropping the kv cache from the buffer in PD backend
180
+ # at decoder.
181
+ self.remove_after_retrieve = config.enable_pd and config.pd_role == "receiver"
182
+
183
+ # asymmetric store/retrieve location can be specified
184
+ # this is typically used (but not limited) in PD system
185
+ self.store_location = config.store_location
186
+ self.retrieve_locations = config.retrieve_locations
187
+
188
+ self.num_layers = metadata.kv_shape[0]
189
+ self.fmt = None
190
+ if self.use_layerwise:
191
+ if metadata.use_mla:
192
+ self.fmt = MemoryFormat.KV_MLA_FMT
193
+ elif config.enable_blending:
194
+ self.fmt = MemoryFormat.KV_2TD
195
+ else:
196
+ self.fmt = MemoryFormat.KV_T2D
197
+ if metadata.use_mla:
198
+ self.fmt = MemoryFormat.KV_MLA_FMT
199
+
200
+ # NOTE(ApostaC): we haven't support lookup-cache yet
201
+ self.lookup_cache: dict[CacheEngineKey, Any] = {}
202
+
203
+ # lookup_id -> {location -> [pinned keys]}
204
+ self.lookup_pins: dict[str, dict[str, list]] = defaultdict(
205
+ lambda: defaultdict(list)
206
+ )
207
+
208
+ InitializeUsageContext(config, metadata)
209
+ self.stats_monitor = LMCStatsMonitor.GetOrCreate()
210
+ # Initialize PinMonitor singleton with config
211
+ PinMonitor.GetOrCreate(config)
212
+
213
+ self.post_inited = False
214
+
215
+ # Flag to control KVCache Check logging (can be toggled via API)
216
+ self.kvcache_check_log_enabled = False
217
+
218
+ gc.collect()
219
+ if not config.py_enable_gc:
220
+ gc.disable()
221
+
222
+ # Health monitor reference (injected by LMCacheManager)
223
+ self._health_monitor: Optional["HealthMonitor"] = None
224
+
225
+ # Flag to indicate if initialization failed (irrecoverable error)
226
+ self._init_failed = False
227
+
228
+ def set_health_monitor(self, health_monitor: "HealthMonitor") -> None:
229
+ """
230
+ Set the health monitor reference.
231
+
232
+ This is called by LMCacheManager after creating the HealthMonitor
233
+ to inject the reference into the engine.
234
+
235
+ Args:
236
+ health_monitor: The HealthMonitor instance from LMCacheManager
237
+ """
238
+ self._health_monitor = health_monitor
239
+
240
+ def is_healthy(self) -> bool:
241
+ """
242
+ Check if the LMCache system is healthy.
243
+
244
+ This method returns False if:
245
+ - Initialization failed (irrecoverable error)
246
+ - HealthMonitor reports unhealthy
247
+
248
+ If no health monitor is set and initialization succeeded,
249
+ it returns True (assume healthy).
250
+
251
+ Returns:
252
+ bool: True if healthy, False otherwise
253
+ """
254
+ if self._init_failed:
255
+ return False
256
+ if self._health_monitor is not None:
257
+ return self._health_monitor.is_healthy()
258
+ return True
259
+
260
+ def _get_req_id(self, kwargs: dict) -> str:
261
+ """Extracts request ID from kwargs for logging."""
262
+ return kwargs.get("req_id", "unspecified")
263
+
264
+ def mark_init_failed(self, reason: str = "") -> None:
265
+ """
266
+ Mark the engine as having failed initialization.
267
+
268
+ This is called by LMCacheManager when an irrecoverable error occurs
269
+ during initialization or post_init. Once marked, is_healthy() will
270
+ always return False, causing the system to fall back to recomputation.
271
+
272
+ Args:
273
+ reason: Optional reason string for logging
274
+ """
275
+ self._init_failed = True
276
+ if reason:
277
+ logger.error("LMCacheEngine marked as init failed: %s", reason)
278
+ else:
279
+ logger.error("LMCacheEngine marked as init failed")
280
+
281
+ def post_init(self, **kwargs) -> None:
282
+ if not self.post_inited:
283
+ logger.info("Post initializing LMCacheEngine")
284
+ lookup_server_worker_ids = self.config.get_lookup_server_worker_ids(
285
+ self.metadata.use_mla, self.metadata.world_size
286
+ )
287
+ if (
288
+ self.lmcache_worker is not None
289
+ or self.use_layerwise
290
+ or not self.save_only_first_rank
291
+ or self.metadata.is_first_rank()
292
+ or len(lookup_server_worker_ids) == 0
293
+ or self.metadata.worker_id in lookup_server_worker_ids
294
+ ):
295
+ logger.info(
296
+ f"Initialize storage manager on rank {self.metadata.worker_id}, "
297
+ f"use layerwise: {self.use_layerwise},"
298
+ f"save only first rank: {self.save_only_first_rank}"
299
+ )
300
+ async_lookup_server = kwargs.get("async_lookup_server", None)
301
+ self.storage_manager = StorageManager(
302
+ self.config,
303
+ self.metadata,
304
+ event_manager=self.event_manager,
305
+ lmcache_worker=self.lmcache_worker,
306
+ async_lookup_server=async_lookup_server,
307
+ )
308
+ self.post_inited = True
309
+
310
+ def freeze(self, enabled: bool) -> None:
311
+ """
312
+ Set the freeze mode for the cache engine.
313
+
314
+ When freeze mode is enabled:
315
+ - All store operations will be skipped (no new data stored)
316
+ - Only local_cpu backend will be used for retrieval
317
+ - No admit/evict messages will be generated
318
+ This protects the local_cpu hot cache from changes.
319
+
320
+ Args:
321
+ enabled (bool): Whether to enable freeze mode
322
+ """
323
+ if self.storage_manager is not None:
324
+ self.storage_manager.set_freeze(enabled)
325
+
326
+ def is_frozen(self) -> bool:
327
+ """
328
+ Get the current freeze mode status.
329
+
330
+ Returns:
331
+ bool: True if freeze mode is enabled, False otherwise
332
+ """
333
+ if self.storage_manager is not None:
334
+ return self.storage_manager.is_frozen()
335
+ return False
336
+
337
+ def set_hot_cache(self, enabled: bool) -> None:
338
+ """
339
+ Dynamically enable or disable the LocalCPUBackend hot cache.
340
+
341
+ When disabled, the existing hot cache entries will be cleared
342
+ and no new data will be written to the hot cache.
343
+
344
+ Args:
345
+ enabled (bool): Whether to enable hot cache
346
+ """
347
+ if self.storage_manager is not None:
348
+ self.storage_manager.set_hot_cache(enabled)
349
+
350
+ def is_hot_cache_enabled(self) -> bool:
351
+ """
352
+ Get the current hot cache status of LocalCPUBackend.
353
+
354
+ Returns:
355
+ bool: True if hot cache is enabled, False otherwise
356
+ """
357
+ if self.storage_manager is not None:
358
+ return self.storage_manager.is_hot_cache_enabled()
359
+ return False
360
+
361
+ @_lmcache_nvtx_annotate
362
+ @torch.inference_mode()
363
+ def store(
364
+ self,
365
+ tokens: Optional[Union[torch.Tensor, list[int]]] = None,
366
+ hashes: Optional[List[int]] = None,
367
+ offsets: Optional[List[int]] = None,
368
+ mask: Optional[torch.Tensor] = None,
369
+ **kwargs,
370
+ ) -> None:
371
+ """Store the tokens/hashes and mask into the cache engine.
372
+
373
+ :param Optional[torch.Tensor] tokens: The tokens of the corresponding KV caches.
374
+
375
+ :param Optional[List[int]] hashes: The hashes of the corresponding KV caches.
376
+
377
+ :param Optional[torch.Tensor] mask: The mask for the tokens. Should
378
+ have the same length as tokens. And the mask should ALWAYS be like
379
+ FFFFFTTTTTTT, where True means the tokens needs to be matched,
380
+ and the Falses will ALWAYS be at the PREFIX of the tensor.
381
+
382
+ :param **kwargs: The additional arguments for the storage backend which
383
+ will be passed into the gpu_connector.
384
+ Should include KV cache specific information (e.g., paged KV buffer
385
+ and the page tables).
386
+
387
+ :raises: ValueError if the number of Falses in the mask is not a
388
+ multiple of the chunk size.
389
+ """
390
+ # Health check: block operation if LMCache is unhealthy
391
+ if not self.is_healthy():
392
+ logger.warning("LMCache is unhealthy, skipping store operation")
393
+ return
394
+
395
+ assert self.gpu_connector is not None, (
396
+ "gpu_connector is required for store operation"
397
+ )
398
+
399
+ if self._is_passive():
400
+ logger.debug(f"rank={self.metadata.worker_id} ignore store")
401
+ return
402
+
403
+ assert self.storage_manager is not None
404
+
405
+ # Get req_id for logging
406
+ req_id = self._get_req_id(kwargs)
407
+
408
+ # Initialize num_to_store_tokens to avoid reference before assignment
409
+ num_to_store_tokens = 0
410
+
411
+ if mask is not None:
412
+ num_to_store_tokens = torch.sum(mask).item()
413
+ elif tokens is not None:
414
+ num_to_store_tokens = len(tokens)
415
+ elif hashes is not None:
416
+ assert offsets is not None, (
417
+ "Offsets should be set when hashes are provided during store"
418
+ )
419
+ num_to_store_tokens = sum(offsets)
420
+ kwargs["slot_mapping"] = torch.tensor(
421
+ kwargs["slot_mapping"], dtype=torch.long, device="cuda"
422
+ )
423
+
424
+ assert tokens is not None or hashes is not None, (
425
+ "Either 'tokens' or 'hashes' must be provided."
426
+ )
427
+
428
+ # KVCache Check logging
429
+ self._log_kvcache_for_check(
430
+ operation="Store",
431
+ kwargs=kwargs,
432
+ token_count=num_to_store_tokens,
433
+ require_req_id=False,
434
+ )
435
+
436
+ # Check if freeze mode is enabled
437
+ if self.is_frozen():
438
+ logger.debug(
439
+ "Freeze mode enabled, skipping store operation for %d tokens",
440
+ num_to_store_tokens,
441
+ )
442
+ return
443
+
444
+ store_stats = self.stats_monitor.on_store_request(num_to_store_tokens)
445
+
446
+ starts: List[int] = []
447
+ ends: List[int] = []
448
+ keys: List[CacheEngineKey] = []
449
+ memory_objs: List[MemoryObj] = []
450
+
451
+ tot_kv_size = 0
452
+ tot_token_num = 0
453
+
454
+ request_configs = kwargs.get("request_configs")
455
+ if request_configs is not None and len(request_configs) != 0:
456
+ assert isinstance(request_configs, dict)
457
+
458
+ with store_stats.profile_process_tokens():
459
+ prev_key = 0
460
+ for start, end, key in self.token_database.process_tokens(
461
+ tokens,
462
+ hashes,
463
+ offsets,
464
+ mask,
465
+ request_configs=request_configs,
466
+ ):
467
+ assert isinstance(key, CacheEngineKey)
468
+ # Allocate the memory object
469
+ num_tokens = end - start
470
+ kv_shapes = self.metadata.get_shapes(num_tokens)
471
+ kv_dtypes = self.metadata.get_dtypes()
472
+
473
+ # TODO (Jiayi): should be batched in the future
474
+ memory_obj = self.storage_manager.allocate(
475
+ kv_shapes,
476
+ kv_dtypes,
477
+ busy_loop=self.config.get_extra_config_value(
478
+ "force_store_wait", False
479
+ ),
480
+ fmt=self.fmt,
481
+ )
482
+ if memory_obj is None:
483
+ logger.warning(
484
+ "Local cpu memory under pressure so"
485
+ " choosing to store only "
486
+ f" {len(memory_objs)}"
487
+ " total chunks of KV cache."
488
+ )
489
+ break
490
+
491
+ starts.append(start)
492
+ ends.append(end)
493
+ keys.append(key)
494
+ memory_objs.append(memory_obj)
495
+ tot_kv_size += memory_obj.get_size()
496
+ tot_token_num += num_tokens
497
+
498
+ # Create KV event
499
+ if self.kv_events_enabled:
500
+ stored_event = CacheStoreEvent(
501
+ block_hashes=[key.chunk_hash],
502
+ parent_block_hash=None if start == 0 else prev_key,
503
+ token_ids=[],
504
+ block_size=num_tokens,
505
+ lora_id=None,
506
+ medium="cpu",
507
+ lora_name=None,
508
+ )
509
+ if tokens is not None:
510
+ stored_event.token_ids = convert_tokens_to_list(
511
+ tokens,
512
+ start,
513
+ end,
514
+ )
515
+ if isinstance(tokens, torch.Tensor):
516
+ stored_event.medium = tokens.device
517
+ elif hashes is not None:
518
+ stored_event.token_ids = hashes[start : end + 1]
519
+ logger.debug(
520
+ (
521
+ "Added kv cache event '%s' to kv cache events queue"
522
+ % stored_event
523
+ )
524
+ )
525
+ self.kv_events.append(stored_event)
526
+ prev_key = key.chunk_hash
527
+
528
+ # memory_objs might be empty, directly return to avoid sending tokens
529
+ if not memory_objs:
530
+ return
531
+
532
+ with store_stats.profile_from_gpu():
533
+ self.gpu_connector.batched_from_gpu(memory_objs, starts, ends, **kwargs)
534
+
535
+ with store_stats.profile_put():
536
+ transfer_spec = kwargs.get("transfer_spec", None)
537
+ # TODO: we implicitly rely on batched_put to call ref_count_down
538
+ # this management should be done in a cleaner way
539
+ self.storage_manager.batched_put(
540
+ keys,
541
+ memory_objs,
542
+ transfer_spec=transfer_spec,
543
+ location=self.store_location,
544
+ )
545
+
546
+ self.stats_monitor.on_store_finished(
547
+ store_stats,
548
+ tot_token_num,
549
+ )
550
+ tot_time = store_stats.time_to_store()
551
+
552
+ logger.info(
553
+ "[req_id=%s] Stored %d out of total %d tokens. "
554
+ "size: %.4f GB, cost %.4f ms, throughput: %.4f GB/s; "
555
+ "offload_time: %.4f ms, put_time: %.4f ms",
556
+ req_id,
557
+ tot_token_num,
558
+ num_to_store_tokens,
559
+ tot_kv_size / 1024**3,
560
+ tot_time * 1000,
561
+ tot_kv_size / tot_time / 1024**3 if tot_time > 0 else 0,
562
+ (store_stats.process_tokens_time + store_stats.from_gpu_time) * 1000,
563
+ store_stats.put_time * 1000,
564
+ )
565
+
566
+ @_lmcache_nvtx_annotate
567
+ @torch.inference_mode()
568
+ def store_layer(
569
+ self,
570
+ tokens: Union[torch.Tensor, list[int]],
571
+ mask: Optional[torch.Tensor] = None,
572
+ **kwargs,
573
+ ) -> Generator[None, None, None]:
574
+ """
575
+ Store the KV cache in a layerwise manner.
576
+
577
+ :param torch.Tensor tokens: The tokens of the corresponding KV caches.
578
+
579
+ :param Optional[torch.Tensor] mask: The mask for the tokens. Should
580
+ have the same length as tokens. And the mask should ALWAYS be like
581
+ FFFFFTTTTTTT, where True means the tokens needs to be matched.
582
+
583
+ :param **kwargs: The additional arguments for the storage backend which
584
+ will be passed into the gpu_connector.
585
+
586
+ return: A generator that yields None. In the first iteration, the
587
+ generator allocates the memory objects for all layers and moves
588
+ the KV cache of the first layer from GPU to CPU. In the next
589
+ iterations, it moves the KV cache of layer i from GPU to the memory
590
+ objects (on CPU) and puts the memory objects of layer i-1 to the
591
+ storage backends. In the last iteration, it puts the memory objects
592
+ of the last layer to the storage backends.
593
+ """
594
+ # Health check: block operation if LMCache is unhealthy
595
+ if not self.is_healthy():
596
+ logger.warning("LMCache is unhealthy, skipping store_layer operation")
597
+ return
598
+
599
+ assert self.storage_manager is not None
600
+ assert self.gpu_connector is not None, (
601
+ "gpu_connector is required for store_layer operation"
602
+ )
603
+
604
+ # Get req_id for logging
605
+ req_id = self._get_req_id(kwargs)
606
+
607
+ if mask is not None:
608
+ num_to_store_tokens = torch.sum(mask).item()
609
+ else:
610
+ num_to_store_tokens = len(tokens)
611
+
612
+ # KVCache Check logging
613
+ self._log_kvcache_for_check(
614
+ operation="Layerwise store",
615
+ kwargs=kwargs,
616
+ token_count=num_to_store_tokens,
617
+ require_req_id=True,
618
+ )
619
+
620
+ monitor_req_id = self.stats_monitor.on_store_request(num_to_store_tokens)
621
+
622
+ # Check if freeze mode is enabled
623
+ if self.is_frozen():
624
+ logger.debug(
625
+ "Freeze mode enabled, skipping store_layer for %d tokens",
626
+ num_to_store_tokens,
627
+ )
628
+ # Still need to yield to avoid StopIteration
629
+ for layer_id in range(self.num_layers):
630
+ yield
631
+ return
632
+
633
+ starts = []
634
+ ends = []
635
+ keys = []
636
+ memory_objs = []
637
+ tot_token_num = 0
638
+ kv_dtype = self.metadata.kv_dtype
639
+ request_configs = kwargs.get("request_configs")
640
+ if request_configs is not None and len(request_configs) != 0:
641
+ assert isinstance(request_configs, dict)
642
+
643
+ prev_key = 0
644
+ for start, end, key in self.token_database.process_tokens(
645
+ tokens=tokens, mask=mask, request_configs=request_configs
646
+ ):
647
+ assert isinstance(key, CacheEngineKey)
648
+
649
+ keys_multi_layer = key.split_layers(self.num_layers)
650
+ # Only check the first layer
651
+ if self.storage_manager.contains(
652
+ keys_multi_layer[0], self.retrieve_locations
653
+ ):
654
+ continue
655
+
656
+ # Allocate the memory object
657
+ num_tokens = end - start
658
+ kv_shape_single_layer = self.gpu_connector.get_shape(num_tokens)
659
+
660
+ memory_objs_multi_layer = self.storage_manager.batched_allocate(
661
+ kv_shape_single_layer,
662
+ kv_dtype,
663
+ batch_size=self.num_layers,
664
+ fmt=self.fmt,
665
+ busy_loop=self.config.get_extra_config_value("force_store_wait", False),
666
+ )
667
+
668
+ if memory_objs_multi_layer is None:
669
+ logger.warning(
670
+ "Local cpu memory under pressure so"
671
+ " choosing to not store the KV cache."
672
+ )
673
+ break
674
+
675
+ starts.append(start)
676
+ ends.append(end)
677
+ keys.append(keys_multi_layer)
678
+ memory_objs.append(memory_objs_multi_layer)
679
+ tot_token_num += num_tokens
680
+
681
+ # Create KV event
682
+ if self.kv_events_enabled and tokens is not None:
683
+ stored_event = CacheStoreEvent(
684
+ block_hashes=[key.chunk_hash],
685
+ parent_block_hash=None if start == 0 else prev_key,
686
+ token_ids=[],
687
+ block_size=num_tokens,
688
+ lora_id=None,
689
+ medium="cpu",
690
+ lora_name=None,
691
+ )
692
+ if tokens is not None:
693
+ stored_event.token_ids = convert_tokens_to_list(
694
+ tokens,
695
+ start,
696
+ end,
697
+ )
698
+ if isinstance(tokens, torch.Tensor):
699
+ stored_event.medium = tokens.device
700
+ logger.debug(
701
+ f"Added kv cache event '{stored_event}' to kv cache events queue"
702
+ )
703
+ self.kv_events.append(stored_event)
704
+ prev_key = key.chunk_hash
705
+
706
+ if keys:
707
+ # Transpose the keys and memory objects into layer major format
708
+ memory_objs = [list(row) for row in zip(*memory_objs, strict=False)]
709
+ keys = [list(row) for row in zip(*keys, strict=False)]
710
+
711
+ # Calculate total KV size for logging
712
+ tot_kv_size = sum(
713
+ mo.get_size() for layer_objs in memory_objs for mo in layer_objs
714
+ )
715
+
716
+ assert_layerwise_gpu_connector(self.gpu_connector)
717
+
718
+ t_start = time.perf_counter()
719
+ mem_obj_generator = self.gpu_connector.batched_from_gpu(
720
+ memory_objs, starts, ends, **kwargs
721
+ )
722
+
723
+ next(mem_obj_generator)
724
+
725
+ for layer_id in range(self.num_layers):
726
+ yield
727
+ next(mem_obj_generator)
728
+ self.storage_manager.batched_put(
729
+ keys[layer_id], memory_objs[layer_id], location=self.store_location
730
+ )
731
+
732
+ tot_time = time.perf_counter() - t_start
733
+ logger.info(
734
+ "[req_id=%s] Stored %d out of total %d tokens. "
735
+ "size: %.4f GB, cost %.4f ms, throughput: %.4f GB/s",
736
+ req_id,
737
+ tot_token_num,
738
+ len(tokens),
739
+ tot_kv_size / 1024**3,
740
+ tot_time * 1000,
741
+ tot_kv_size / tot_time / 1024**3 if tot_time > 0 else 0,
742
+ )
743
+ else:
744
+ # If no cache are found, we still need to yield to avoid
745
+ # `StopIteration`
746
+ for layer_id in range(self.num_layers):
747
+ yield
748
+
749
+ self.stats_monitor.on_store_finished(monitor_req_id, tot_token_num)
750
+ yield
751
+
752
+ @_lmcache_nvtx_annotate
753
+ @torch.inference_mode()
754
+ def retrieve(
755
+ self,
756
+ tokens: Union[torch.Tensor, list[int]],
757
+ mask: Optional[torch.Tensor] = None,
758
+ **kwargs,
759
+ ) -> torch.Tensor:
760
+ """Retrieve the KV caches from the cache engine. And put the retrieved
761
+ KV cache to the serving engine via the GPU connector.
762
+
763
+ :param torch.Tensor tokens: The tokens of the corresponding KV caches.
764
+
765
+ :param Optional[torch.Tensor] mask: The mask for the tokens. Should
766
+ have the same length as tokens. And the mask should ALWAYS be like
767
+ FFFFFTTTTTTT, where True means the tokens needs to be matched,
768
+ and the Falses will ALWAYS be at the PREFIX of the tensor.
769
+
770
+ :param **kwargs: The additional arguments for the storage backend which
771
+ will be passed into the gpu_connector.
772
+ Should include KV cache specific information (e.g., paged KV buffer
773
+ and the page tables).
774
+
775
+ :return: the boolean mask indicating which tokens are retrieved. The
776
+ length of the mask should be the same as the tokens. On CPU.
777
+
778
+ :raises: ValueError if the number of Falses in the mask is not a
779
+ multiple of the chunk size.
780
+ """
781
+ # Health check: block operation if LMCache is unhealthy
782
+ if not self.is_healthy():
783
+ logger.warning("LMCache is unhealthy, skipping retrieve operation")
784
+ return torch.zeros(len(tokens), dtype=torch.bool)
785
+
786
+ assert self.gpu_connector is not None, (
787
+ "gpu_connector is required for retrieve operation"
788
+ )
789
+
790
+ # Get req_id for logging
791
+ req_id = self._get_req_id(kwargs)
792
+
793
+ tot_kv_size = 0
794
+
795
+ if mask is not None:
796
+ num_required_tokens = torch.sum(mask).item()
797
+ else:
798
+ num_required_tokens = len(tokens)
799
+
800
+ # KVCache Check logging
801
+ self._log_kvcache_for_check(
802
+ operation="retrieve",
803
+ kwargs=kwargs,
804
+ token_count=num_required_tokens,
805
+ require_req_id=True,
806
+ )
807
+
808
+ retrieve_stats = self.stats_monitor.on_retrieve_request(num_required_tokens)
809
+
810
+ ret_mask = torch.zeros(len(tokens), dtype=torch.bool, device="cpu")
811
+
812
+ reordered_chunks: List[ProcessedChunk] = []
813
+ if not self._is_passive():
814
+ with retrieve_stats.profile_process_tokens():
815
+ if self.async_loading:
816
+ reordered_chunks, tot_kv_size = self._async_process_tokens_internal( # noqa: E501
817
+ tokens,
818
+ mask,
819
+ ret_mask,
820
+ **kwargs,
821
+ )
822
+ else:
823
+ reordered_chunks, tot_kv_size = self._process_tokens_internal(
824
+ tokens,
825
+ mask,
826
+ ret_mask,
827
+ **kwargs,
828
+ )
829
+
830
+ if self.save_only_first_rank:
831
+ with retrieve_stats.profile_broadcast():
832
+ with torch.cuda.stream(self.broadcast_stream):
833
+ self._broadcast_or_receive_memory_objs(
834
+ reordered_chunks,
835
+ ret_mask,
836
+ )
837
+
838
+ # if self.gpu_connector has load_stream, self.broadcast_stream is equals
839
+ # to self.gpu_connector.load_stream, the broadcast and to_gpu operation
840
+ # will execute sequentially within the stream.
841
+ # if self.gpu_connector does not have load_stream, self.broadcast_stream
842
+ # is created by torch.cuda.Stream(), we need to synchronize broadcast
843
+ # operation, and then process to_cpu operation.
844
+ if not hasattr(self.gpu_connector, "load_stream"):
845
+ self.broadcast_stream.synchronize()
846
+
847
+ # NOTE(Jiayi): memory_obj doesn't have to be a pinned
848
+ # cpu tensor for the sake of performance.
849
+ # For example, disk->gpu is faster than disk->cpu->gpu.
850
+ # RDMA is another example.
851
+ if len(reordered_chunks) > 0:
852
+ with retrieve_stats.profile_to_gpu():
853
+ _, memory_objs, starts, ends = zip(*reordered_chunks, strict=False)
854
+ self.gpu_connector.batched_to_gpu(
855
+ list(memory_objs), list(starts), list(ends), **kwargs
856
+ )
857
+
858
+ # TODO(Jiayi): Remove the following for loop with batched operations
859
+ # TODO(Jiayi): Need to refactor the `remove_after_retrieve` logic.
860
+ for key, memory_obj, _, _ in reordered_chunks:
861
+ if self.remove_after_retrieve and not self._is_passive():
862
+ assert self.storage_manager is not None
863
+ self.storage_manager.remove(key, self.retrieve_locations)
864
+ if not self.async_loading:
865
+ memory_obj.ref_count_down()
866
+
867
+ retrieved_tokens = torch.sum(ret_mask)
868
+ self.stats_monitor.on_retrieve_finished(
869
+ retrieve_stats,
870
+ retrieved_tokens,
871
+ )
872
+ onload_time = retrieve_stats.time_to_retrieve()
873
+ # The retrieved may be larger than the need_to_load
874
+ # Example (page_size=16, chunk_size=256):
875
+ #
876
+ # chunks: [0..255] [256..511]
877
+ # pages: [0..15]...[240..255] [256..271][272..287] ...
878
+ #
879
+ # num_computed_tokens = 288 => vLLM already has [0..287] (18 pages)
880
+ # LMCache hit_prefix_tokens = 512 => cache covers [0..511] (2 chunks)
881
+ #
882
+ # Skip chunk 1, retrieve chunk 2, overwrite [256..287] (32-token overlap)
883
+ # need_to_load: 512 - 288 = 224 tokens
884
+ # retrieved: 256 tokens
885
+ if not self._is_passive():
886
+ logger.info(
887
+ "[req_id=%s] Retrieved %d out of %d required tokens "
888
+ "(from %d total tokens). size: %.4f gb, "
889
+ "cost %.4f ms, throughput: %.4f GB/s;",
890
+ req_id,
891
+ retrieved_tokens,
892
+ num_required_tokens,
893
+ len(tokens),
894
+ tot_kv_size / 1024**3,
895
+ onload_time * 1000,
896
+ tot_kv_size / onload_time / 1024**3 if onload_time > 0 else 0,
897
+ )
898
+ return ret_mask
899
+
900
+ @_lmcache_nvtx_annotate
901
+ @torch.inference_mode()
902
+ def retrieve_layer(
903
+ self,
904
+ tokens: Union[torch.Tensor, list[int]],
905
+ mask: Optional[torch.Tensor] = None,
906
+ **kwargs,
907
+ ) -> Generator[Optional[torch.Tensor], None, None]:
908
+ """
909
+ Retrieve the KV cache in a layerwise manner.
910
+
911
+ :param torch.Tensor tokens: The tokens of the corresponding KV caches.
912
+
913
+ :param Optional[torch.Tensor] mask: The mask for the tokens. Should
914
+ have the same length as tokens. And the mask should ALWAYS be like
915
+ FFFFFTTTTTTT, where True means the tokens needs to be matched.
916
+
917
+ :param **kwargs: The additional arguments for the storage backend which
918
+ will be passed into the gpu_connector.
919
+
920
+ return: A generator that yields Optional[torch.Tensor]. The tensor will
921
+ be the boolean mask indicating which tokens are retrieved and will
922
+ only be returned in the last iteration. In the first iteration,
923
+ the generator retrieve the memory objects of the first layer from
924
+ the storage backends. In the next iterations, it moves the KV cache
925
+ of layer i from the memory objects (on CPU) to GPU and retrieves
926
+ the memory objects of layer i+1 from the storage backends. In the
927
+ last iteration, it moves the memory objects of the last layer to
928
+ the GPU.
929
+ """
930
+ # Health check: block operation if LMCache is unhealthy
931
+ if not self.is_healthy():
932
+ logger.warning("LMCache is unhealthy, skipping retrieve_layer operation")
933
+ yield torch.zeros(len(tokens), dtype=torch.bool)
934
+ return
935
+
936
+ assert self.storage_manager is not None
937
+ assert self.gpu_connector is not None, (
938
+ "gpu_connector is required for retrieve_layer operation"
939
+ )
940
+
941
+ # Get req_id for logging
942
+ req_id = self._get_req_id(kwargs)
943
+
944
+ if mask is not None:
945
+ num_required_tokens = torch.sum(mask).item()
946
+ else:
947
+ num_required_tokens = len(tokens)
948
+ monitor_req_id = self.stats_monitor.on_retrieve_request(num_required_tokens)
949
+
950
+ ret_mask = torch.zeros(len(tokens), dtype=torch.bool, device="cpu")
951
+
952
+ starts = []
953
+ ends = []
954
+ keys = []
955
+
956
+ request_configs = kwargs.get("request_configs")
957
+ if request_configs is not None and len(request_configs) != 0:
958
+ assert isinstance(request_configs, dict)
959
+
960
+ location = None
961
+ for start, end, key in self.token_database.process_tokens(
962
+ tokens=tokens,
963
+ mask=mask,
964
+ request_configs=request_configs,
965
+ ):
966
+ assert isinstance(key, CacheEngineKey)
967
+
968
+ keys_multi_layer = key.split_layers(self.num_layers)
969
+
970
+ # NOTE: Only check the first layer
971
+ if current_location := self.storage_manager.contains(
972
+ keys_multi_layer[0], self.retrieve_locations
973
+ ):
974
+ if location is None:
975
+ location = current_location
976
+ else:
977
+ # TODO(Jiayi): Support multi-location retrieval in the future
978
+ assert location == current_location, (
979
+ "All retrieved keys should be from the same location "
980
+ "when use layerwise retrieval."
981
+ "Please support multi-location retrieval in the future."
982
+ )
983
+ else:
984
+ break
985
+
986
+ starts.append(start)
987
+ ends.append(end)
988
+ keys.append(keys_multi_layer)
989
+
990
+ ret_mask[start:end] = True
991
+
992
+ if keys:
993
+ # Transpose the keys into layer major format
994
+ keys_layer_major = [list(row) for row in zip(*keys, strict=False)]
995
+
996
+ get_generator = self.storage_manager.layerwise_batched_get(
997
+ keys_layer_major,
998
+ location=location,
999
+ )
1000
+
1001
+ assert_layerwise_gpu_connector(self.gpu_connector)
1002
+
1003
+ mem_obj_consumer = self.gpu_connector.batched_to_gpu(starts, ends, **kwargs)
1004
+ next(mem_obj_consumer)
1005
+
1006
+ to_count_down = []
1007
+ for layer_id in range(self.num_layers):
1008
+ task = next(get_generator)
1009
+
1010
+ assert task is not None
1011
+
1012
+ if layer_id == 0:
1013
+ # NOTE(Yuwei): For sglang integration we need to provide retrieved
1014
+ # tokens number in the first layer loading since there is no lookup
1015
+ yield torch.sum(ret_mask)
1016
+ else:
1017
+ yield None
1018
+
1019
+ mem_objs_layer = task.result()
1020
+ mem_obj_consumer.send(mem_objs_layer)
1021
+ to_count_down.extend(mem_objs_layer)
1022
+
1023
+ for mem_obj in to_count_down:
1024
+ mem_obj.ref_count_down()
1025
+ else:
1026
+ # If no cache are found, we still need to yield to avoid
1027
+ # `StopIteration`
1028
+ for layer_id in range(self.num_layers):
1029
+ yield None
1030
+
1031
+ yield None
1032
+
1033
+ # synchronize the last layer
1034
+ next(mem_obj_consumer)
1035
+
1036
+ # Unpin any disk-loaded staging objects now that the device-side sync
1037
+ # has been enqueued (mem_obj_consumer advanced past its sync point).
1038
+ # Without this, pin_count stays at 1 forever and the CPU staging pool
1039
+ # fills up, causing the next retrieve to deadlock inside allocate().
1040
+ for mem_obj in to_count_down:
1041
+ if mem_obj.is_pinned:
1042
+ mem_obj.unpin()
1043
+
1044
+ retrieved_tokens = torch.sum(ret_mask)
1045
+ self.stats_monitor.on_retrieve_finished(monitor_req_id, retrieved_tokens)
1046
+ if not self._is_passive():
1047
+ logger.info(
1048
+ "[req_id=%s] Retrieved %d out of %d out of total %d tokens",
1049
+ req_id,
1050
+ retrieved_tokens,
1051
+ num_required_tokens,
1052
+ len(tokens),
1053
+ )
1054
+
1055
+ yield ret_mask
1056
+
1057
+ @_lmcache_nvtx_annotate
1058
+ def lookup(
1059
+ self,
1060
+ tokens: Optional[Union[torch.Tensor, List[int]]] = None,
1061
+ hashes: Optional[List[int]] = None,
1062
+ offsets: Optional[List[int]] = None,
1063
+ search_range: Optional[List[str]] = None,
1064
+ lookup_id: Optional[str] = None,
1065
+ pin: bool = False,
1066
+ request_configs: Optional[dict] = None,
1067
+ ) -> int:
1068
+ """
1069
+ Checks the existence of KV cache of the tokens from the cache engine.
1070
+
1071
+ :param Optional[Union[torch.Tensor, List[int]]] tokens: the input tokens,
1072
+ with shape [seq_len]
1073
+
1074
+ :param Optional[List[int]] hashes: the input hashes, with length [num_chunks]
1075
+ :param Optional[List[int]] offsets: the offsets of each chunk,
1076
+ with length [num_chunks]
1077
+
1078
+ :param Optional[List[str]] search_range: The range of storage backends
1079
+ to search in. Should be a subset of
1080
+ ["LocalCPUBackend", "LocalDiskBackend"] for now.
1081
+ If None, search in all backends.
1082
+
1083
+ :param Optional[str] lookup_id: The lookup ID to
1084
+ associate with the lookup. When pin is true, this argument is
1085
+ required to be not None.
1086
+
1087
+ :param bool pin: If True, pin the KV cache in the storage.
1088
+
1089
+ :param Optional[dict] request_configs: the configs of the request.
1090
+
1091
+ :return: An int indicating how many prefix tokens exist inside LMCache.
1092
+ """
1093
+ # Health check: block operation if LMCache is unhealthy
1094
+ if not self.is_healthy():
1095
+ logger.warning("LMCache is unhealthy, skipping lookup operation")
1096
+ return 0
1097
+
1098
+ assert self.storage_manager is not None
1099
+
1100
+ if tokens is not None:
1101
+ lookup_stats = self.stats_monitor.on_lookup_request(len(tokens))
1102
+ else:
1103
+ assert offsets is not None
1104
+ assert hashes is not None
1105
+ lookup_stats = self.stats_monitor.on_lookup_request(sum(offsets))
1106
+
1107
+ if search_range is None:
1108
+ search_range = self.retrieve_locations
1109
+
1110
+ res = 0
1111
+ try:
1112
+ chunk_info_iterator = self.token_database.process_tokens(
1113
+ tokens=tokens,
1114
+ hashes=hashes,
1115
+ offsets=offsets,
1116
+ request_configs=request_configs,
1117
+ )
1118
+
1119
+ # TODO: support batched_contains when layerwise is enabled
1120
+ if self.use_layerwise:
1121
+ for start, end, key in chunk_info_iterator:
1122
+ assert isinstance(key, CacheEngineKey)
1123
+
1124
+ # TODO(Jiayi): Optimize by checking only the existence of the key
1125
+ # of one layer
1126
+ key_all_layers = key.split_layers(self.num_layers)
1127
+
1128
+ hit_chunks, block_mapping = self.storage_manager.batched_contains(
1129
+ key_all_layers, # type: ignore
1130
+ search_range,
1131
+ pin,
1132
+ )
1133
+ # Only all layers are hit and hit in one location,
1134
+ # we consider this key as a hit
1135
+ if hit_chunks == self.num_layers and len(block_mapping) == 1:
1136
+ if pin:
1137
+ assert lookup_id is not None, (
1138
+ "lookup_id is required when pin is True"
1139
+ )
1140
+ location = next(iter(block_mapping.keys()))
1141
+ self.lookup_pins[lookup_id][location].extend(key_all_layers)
1142
+ res = end
1143
+ continue
1144
+ return res
1145
+ else:
1146
+ chunk_info_list = []
1147
+ keys = []
1148
+ for chunk_info in chunk_info_iterator:
1149
+ assert isinstance(chunk_info[2], CacheEngineKey)
1150
+ start, end, _ = chunk_info
1151
+ chunk_info_list.append(chunk_info)
1152
+ # chunk_info contains (start, end, key)
1153
+ # chunk_info[2] is the key
1154
+ keys.append(chunk_info[2])
1155
+ # hit chunks by prefix matching
1156
+ hit_chunks, block_mapping = self.storage_manager.batched_contains(
1157
+ keys, search_range, pin
1158
+ )
1159
+ if pin and block_mapping:
1160
+ assert lookup_id is not None, (
1161
+ "lookup_id is required when pin is True"
1162
+ )
1163
+ self.lookup_pins[lookup_id] = block_mapping
1164
+ for idx, (start, end, key) in enumerate(chunk_info_list):
1165
+ if idx < hit_chunks:
1166
+ res = end
1167
+ continue
1168
+ return res
1169
+
1170
+ # all tokens where found, return the maximal end
1171
+ return res
1172
+ finally:
1173
+ self.stats_monitor.on_lookup_finished(lookup_stats, res)
1174
+ # vllm lookup sets pin to True
1175
+ if pin:
1176
+ # touch_cache is tightly coupled with batched_contains
1177
+ self.storage_manager.touch_cache()
1178
+
1179
+ @_lmcache_nvtx_annotate
1180
+ def move(
1181
+ self,
1182
+ tokens: Union[torch.Tensor, List[int]],
1183
+ old_position: str,
1184
+ new_position: tuple[str, str],
1185
+ event_id: str,
1186
+ do_copy: bool = True,
1187
+ ) -> int:
1188
+ """
1189
+ Perform cross-node move of the KV cache.
1190
+ """
1191
+ assert self.storage_manager is not None
1192
+
1193
+ num_tokens = self.lookup(
1194
+ tokens,
1195
+ search_range=[old_position],
1196
+ lookup_id=event_id,
1197
+ pin=True,
1198
+ )
1199
+
1200
+ if not num_tokens:
1201
+ logger.debug("Move is not performed as there are no tokens to move.")
1202
+ return 0
1203
+
1204
+ block_mapping = self.lookup_pins[event_id]
1205
+ assert len(block_mapping) == 1
1206
+ keys = block_mapping[old_position]
1207
+
1208
+ memory_objs = self.storage_manager.batched_get(
1209
+ keys=keys,
1210
+ location=old_position,
1211
+ )
1212
+ assert None not in memory_objs, "Failed to get memory objects to move"
1213
+ logger.debug(
1214
+ f"Trying to send {len(memory_objs)} memory objects to {new_position}"
1215
+ )
1216
+
1217
+ # TODO: reduce loops
1218
+ token_dim = memory_objs[0].meta.fmt.token_dim() # type: ignore
1219
+ offsets = [m.meta.shape[token_dim] for m in memory_objs] # type: ignore
1220
+
1221
+ transfer_spec = {
1222
+ "target_peer_init_url": new_position[0],
1223
+ "offsets": offsets,
1224
+ }
1225
+
1226
+ logger.info(self.storage_manager.storage_backends)
1227
+ p2p_backend = self.storage_manager.storage_backends["P2PBackend"]
1228
+
1229
+ future = asyncio.run_coroutine_threadsafe(
1230
+ p2p_backend.async_batched_submit_put_task(
1231
+ keys,
1232
+ memory_objs, # type: ignore
1233
+ transfer_spec=transfer_spec,
1234
+ ),
1235
+ self.storage_manager.loop,
1236
+ )
1237
+
1238
+ future.result()
1239
+
1240
+ if not do_copy:
1241
+ self.storage_manager.batched_remove(keys, locations=[old_position])
1242
+
1243
+ logger.debug(f"Moving {num_tokens} token from {old_position} to {new_position}")
1244
+ return num_tokens
1245
+
1246
+ # TODO(Jiayi): Add layerwise support.
1247
+ @_lmcache_nvtx_annotate
1248
+ def async_lookup_and_prefetch(
1249
+ self,
1250
+ lookup_id: str,
1251
+ tokens: Optional[Union[torch.Tensor, List[int]]] = None,
1252
+ hashes: Optional[List[int]] = None,
1253
+ offsets: Optional[List[int]] = None,
1254
+ search_range: Optional[List[str]] = None,
1255
+ pin: bool = False,
1256
+ request_configs: Optional[dict] = None,
1257
+ ) -> None:
1258
+ """
1259
+ An async version of lookup + prefetch.
1260
+
1261
+ There are three categories of backends:
1262
+ (1) sync lookup + sync retrieval (e.g., cpu)
1263
+ (2) sync lookup + async retrieval (e.g., disk)
1264
+ (3) async lookup + async retrieval (e.g., p2p)
1265
+ """
1266
+ assert self.storage_manager is not None
1267
+
1268
+ keys: list[CacheEngineKey] = []
1269
+ cum_chunk_lengths = [0]
1270
+
1271
+ if search_range is None:
1272
+ search_range = self.retrieve_locations
1273
+
1274
+ # TODO(Jiayi): make token database able to return list.
1275
+ for start, end, key in self.token_database.process_tokens(
1276
+ tokens=tokens,
1277
+ hashes=hashes,
1278
+ offsets=offsets,
1279
+ request_configs=request_configs,
1280
+ ):
1281
+ assert isinstance(key, CacheEngineKey)
1282
+ keys.append(key)
1283
+ cum_chunk_lengths.append(end)
1284
+
1285
+ asyncio.run_coroutine_threadsafe(
1286
+ self.storage_manager.async_lookup_and_prefetch(
1287
+ lookup_id, keys, cum_chunk_lengths, search_range, pin
1288
+ ),
1289
+ self.storage_manager.loop,
1290
+ )
1291
+
1292
+ def cleanup_memory_objs(self, lookup_id: str) -> None:
1293
+ """
1294
+ Cleanup memory objects allocated during prefetch for an aborted lookup.
1295
+
1296
+ Called by the scheduler when it determines that an aborted lookup
1297
+ has finished its prefetch tasks.
1298
+ """
1299
+ try:
1300
+ # Get the completed future from event_manager
1301
+ if (
1302
+ self.event_manager.get_event_status(EventType.LOADING, lookup_id)
1303
+ != EventStatus.DONE
1304
+ ):
1305
+ logger.debug(
1306
+ "No completed event found for lookup_id=%s to clean up.", lookup_id
1307
+ )
1308
+ return
1309
+ future = self.event_manager.pop_event(EventType.LOADING, lookup_id)
1310
+
1311
+ # Get memory objects from the future result
1312
+ memory_objs = future.result()
1313
+ # Flatten nested lists (each backend returns a list of chunks)
1314
+ memory_objs_flat = [mm for m in memory_objs for mm in m]
1315
+
1316
+ # Release each memory object
1317
+ for key, memory_obj in memory_objs_flat:
1318
+ try:
1319
+ logger.debug("Releasing memory object for lookup_id=%s", lookup_id)
1320
+ memory_obj.unpin()
1321
+ memory_obj.ref_count_down()
1322
+ except Exception as e:
1323
+ logger.error(f"Error releasing memory object: {e}")
1324
+ except Exception as e:
1325
+ logger.error(
1326
+ f"Error during cleanup_memory_objs for lookup_id={lookup_id}: {e}"
1327
+ )
1328
+
1329
+ # TODO(Jiayi): Need to handle the case where `tokens=None`.
1330
+ # In this case, we compress all tokens.
1331
+ # TODO(Jiayi): support other compression methods.
1332
+ @_lmcache_nvtx_annotate
1333
+ def compress(
1334
+ self,
1335
+ tokens: Union[torch.Tensor, List[int]],
1336
+ method: str,
1337
+ location: str,
1338
+ event_id: str,
1339
+ ) -> int:
1340
+ assert self.storage_manager is not None
1341
+ if method not in ["cachegen"]:
1342
+ logger.warning(f"Unsupported compression method: {method}.")
1343
+ return 0
1344
+
1345
+ # First Party
1346
+ from lmcache.v1.storage_backend.naive_serde import CreateSerde
1347
+
1348
+ serializer, _ = CreateSerde(method, self.metadata, self.config)
1349
+
1350
+ num_tokens = self.lookup(
1351
+ tokens,
1352
+ search_range=[location],
1353
+ lookup_id=event_id,
1354
+ pin=True,
1355
+ )
1356
+
1357
+ if not num_tokens:
1358
+ logger.debug("Move is not performed as there are no tokens to move.")
1359
+ return 0
1360
+
1361
+ block_mapping = self.lookup_pins[event_id]
1362
+ assert len(block_mapping) == 1
1363
+ keys = block_mapping[location]
1364
+
1365
+ memory_objs = self.storage_manager.batched_get(
1366
+ keys=keys,
1367
+ location=location,
1368
+ )
1369
+ assert None not in memory_objs, (
1370
+ "LMCacheEngine.compress: Failed to get memory objects to compress"
1371
+ )
1372
+
1373
+ compressed_memory_objs = []
1374
+ for memory_obj in memory_objs:
1375
+ assert memory_obj is not None
1376
+ compressed_memory_obj = serializer.serialize(memory_obj)
1377
+ memory_obj.unpin()
1378
+ compressed_memory_objs.append(compressed_memory_obj)
1379
+
1380
+ self.storage_manager.batched_remove(keys, locations=[location])
1381
+
1382
+ self.storage_manager.batched_put(
1383
+ keys=keys,
1384
+ memory_objs=compressed_memory_objs,
1385
+ location=location,
1386
+ )
1387
+
1388
+ return num_tokens
1389
+
1390
+ @_lmcache_nvtx_annotate
1391
+ def decompress(
1392
+ self,
1393
+ tokens: Union[torch.Tensor, List[int]],
1394
+ method: str,
1395
+ location: str,
1396
+ event_id: str,
1397
+ ) -> int:
1398
+ assert self.storage_manager is not None
1399
+ if method not in ["cachegen"]:
1400
+ logger.warning(f"Unsupported decompression method: {method}.")
1401
+ return 0
1402
+
1403
+ # First Party
1404
+ from lmcache.v1.storage_backend.naive_serde import CreateSerde
1405
+
1406
+ _, deserializer = CreateSerde(method, self.metadata, self.config)
1407
+
1408
+ num_tokens = self.lookup(
1409
+ tokens,
1410
+ search_range=[location],
1411
+ lookup_id=event_id,
1412
+ pin=True,
1413
+ )
1414
+
1415
+ if not num_tokens:
1416
+ logger.debug("there are no tokens to decompress.")
1417
+ return 0
1418
+
1419
+ block_mapping = self.lookup_pins[event_id]
1420
+ assert len(block_mapping) == 1
1421
+ keys = block_mapping[location]
1422
+
1423
+ compressed_memory_objs = self.storage_manager.batched_get(
1424
+ keys=keys,
1425
+ location=location,
1426
+ )
1427
+
1428
+ assert None not in compressed_memory_objs, (
1429
+ "LMCacheEngine.compress: Failed to get compressed "
1430
+ "memory objects to decompress"
1431
+ )
1432
+
1433
+ memory_objs = []
1434
+ for compressed_memory_obj in compressed_memory_objs:
1435
+ assert compressed_memory_obj is not None
1436
+ memory_obj = deserializer.deserialize(compressed_memory_obj)
1437
+ compressed_memory_obj.unpin()
1438
+ memory_objs.append(memory_obj)
1439
+
1440
+ self.storage_manager.batched_remove(keys, locations=[location])
1441
+
1442
+ self.storage_manager.batched_put(
1443
+ keys=keys,
1444
+ memory_objs=memory_objs,
1445
+ location=location,
1446
+ )
1447
+
1448
+ return num_tokens
1449
+
1450
+ @_lmcache_nvtx_annotate
1451
+ def lookup_unpin(self, lookup_id: str) -> None:
1452
+ if lookup_id in self.lookup_pins:
1453
+ assert self.storage_manager is not None
1454
+ for location, keys in self.lookup_pins.pop(lookup_id).items():
1455
+ self.storage_manager.batched_unpin(keys, [location])
1456
+
1457
+ elif (
1458
+ self.async_loading is not None
1459
+ and self.event_manager.get_event_status(EventType.LOADING, lookup_id)
1460
+ != EventStatus.NOT_FOUND
1461
+ ):
1462
+ self.cleanup_memory_objs(lookup_id)
1463
+
1464
+ @_lmcache_nvtx_annotate
1465
+ def clear(
1466
+ self,
1467
+ tokens: Optional[Union[torch.Tensor, List[int]]] = None,
1468
+ locations: Optional[List[str]] = None,
1469
+ request_configs: Optional[dict] = None,
1470
+ ) -> int:
1471
+ # TODO: need to clear by request_configs
1472
+ if self.save_only_first_rank:
1473
+ if self.metadata.is_first_rank():
1474
+ num_removed = self._clear(tokens, locations, request_configs)
1475
+ return num_removed
1476
+ else:
1477
+ return 0
1478
+ return self._clear(tokens, locations, request_configs)
1479
+
1480
+ @_lmcache_nvtx_annotate
1481
+ def get_kv_events(self) -> Iterable[CacheStoreEvent]:
1482
+ if self.kv_events_enabled and (events := self.kv_events):
1483
+ self.kv_events = []
1484
+ return events
1485
+ return []
1486
+
1487
+ def _clear(
1488
+ self,
1489
+ tokens: Optional[Union[torch.Tensor, List[int]]] = None,
1490
+ locations: Optional[List[str]] = None,
1491
+ request_configs: Optional[dict] = None,
1492
+ ) -> int:
1493
+ assert self.storage_manager is not None
1494
+ assert isinstance(self.storage_manager, StorageManager)
1495
+ # Clear all caches if tokens is None
1496
+ if tokens is None or len(tokens) == 0:
1497
+ num_cleared = self.storage_manager.clear(locations)
1498
+ return num_cleared
1499
+
1500
+ num_removed = 0
1501
+ # Only remove the caches for the given tokens
1502
+ for start, end, key in self.token_database.process_tokens(
1503
+ tokens=tokens, request_configs=request_configs
1504
+ ):
1505
+ assert isinstance(key, CacheEngineKey)
1506
+ removed = self.storage_manager.remove(key, locations)
1507
+ num_removed += removed
1508
+ return num_removed
1509
+
1510
+ @_lmcache_nvtx_annotate
1511
+ def health(
1512
+ self,
1513
+ ) -> int:
1514
+ """
1515
+ Check the health of the cache engine.
1516
+ return: 0 if healthy, otherwise the error code
1517
+ """
1518
+ assert self.storage_manager is not None
1519
+ return 0 if self.storage_manager.memcheck() else -1
1520
+
1521
+ def close(self) -> None:
1522
+ """Close the cache engine and free all the resources"""
1523
+ logger.info("Closing LMCacheEngine...")
1524
+
1525
+ if self.lmcache_worker is not None:
1526
+ try:
1527
+ logger.info("Closing lmcache_worker...")
1528
+ self.lmcache_worker.close()
1529
+ logger.info("lmcache_worker closed successfully")
1530
+ except Exception as e:
1531
+ logger.error(f"Error closing lmcache_worker: {e}")
1532
+
1533
+ try:
1534
+ logger.info("Closing storage_manager...")
1535
+ if self.storage_manager is not None:
1536
+ self.storage_manager.close()
1537
+ logger.info("storage_manager closed successfully")
1538
+ except Exception as e:
1539
+ logger.error(f"Error closing storage_manager: {e}")
1540
+
1541
+ logger.info("LMCacheEngine closed.")
1542
+
1543
+ def _async_process_tokens_internal(
1544
+ self,
1545
+ tokens,
1546
+ mask,
1547
+ ret_mask,
1548
+ **kwargs,
1549
+ ) -> ProcessTokensInternalResult:
1550
+ """
1551
+ This function is used to get the memory objects from the event manager.
1552
+
1553
+ Args:
1554
+ tokens: Input tokens to process
1555
+ mask: Mask indicating valid token positions
1556
+ ret_mask: Output mask updated with cache hit positions
1557
+ **kwargs: Additional keyword arguments
1558
+ """
1559
+ assert "req_id" in kwargs, "req_id is required for async loading"
1560
+ request_configs = kwargs.get("request_configs")
1561
+ if request_configs is not None and len(request_configs) != 0:
1562
+ assert isinstance(request_configs, dict)
1563
+
1564
+ tot_kv_size = 0
1565
+ chunks: List[ProcessedChunk] = []
1566
+ future = self.event_manager.get_event_future(
1567
+ EventType.LOADING, kwargs["req_id"]
1568
+ )
1569
+ # As mentioned in async_lookup_and_prefetch(), the future.result()
1570
+ # is key data pair for each chunk in each tier. So extract the key
1571
+ # and memory object pairs to memory_obj_map
1572
+ try:
1573
+ keyed_memory_objs = future.result()
1574
+ memory_obj_map: dict[CacheEngineKey, MemoryObj] = {}
1575
+ except Exception as e:
1576
+ logger.error(f"Error popping event for request {kwargs['req_id']}: {e}")
1577
+ return [], 0
1578
+
1579
+ for backend_results in keyed_memory_objs:
1580
+ for key, memory_obj in backend_results:
1581
+ memory_obj_map[key] = memory_obj
1582
+
1583
+ # TODO(Jiayi): hashing inside `process_tokens` can be skipped.
1584
+ used_keys: set[CacheEngineKey] = set()
1585
+ for start, end, key in self.token_database.process_tokens(
1586
+ tokens=tokens,
1587
+ mask=mask,
1588
+ request_configs=request_configs,
1589
+ ):
1590
+ assert isinstance(key, CacheEngineKey)
1591
+ memory_obj = memory_obj_map.get(key)
1592
+ if memory_obj is None:
1593
+ # returned chunks are expected to be contiguous.
1594
+ # break at the first missing chunk.
1595
+ break
1596
+ chunks.append((key, memory_obj, start, end))
1597
+ tot_kv_size += memory_obj.get_size()
1598
+ ret_mask[start:end] = True
1599
+ used_keys.add(key)
1600
+
1601
+ # NOTE: free the memory objects that are not hit.
1602
+ for key, mem_obj in memory_obj_map.items():
1603
+ if key not in used_keys:
1604
+ mem_obj.ref_count_down()
1605
+
1606
+ return chunks, tot_kv_size
1607
+
1608
+ def _process_tokens_internal(
1609
+ self,
1610
+ tokens,
1611
+ mask,
1612
+ ret_mask,
1613
+ **kwargs,
1614
+ ) -> ProcessTokensInternalResult:
1615
+ """Process tokens and populate the reordered lists.
1616
+
1617
+ This function is used to process tokens and populate the reordered lists.
1618
+
1619
+ Args:
1620
+ tokens: Input tokens to process
1621
+ mask: Mask indicating valid token positions
1622
+ ret_mask: Output mask updated with cache hit positions
1623
+ **kwargs: Additional keyword arguments
1624
+ """
1625
+ assert self.storage_manager is not None
1626
+
1627
+ tot_kv_size = 0
1628
+ reordered_chunks: List[ProcessedChunk] = []
1629
+ request_configs = kwargs.get("request_configs")
1630
+ if request_configs is not None and len(request_configs) != 0:
1631
+ assert isinstance(request_configs, dict)
1632
+
1633
+ chunk_infos = []
1634
+ for start, end, key in self.token_database.process_tokens(
1635
+ tokens=tokens,
1636
+ mask=mask,
1637
+ request_configs=request_configs,
1638
+ ):
1639
+ assert isinstance(key, CacheEngineKey)
1640
+ chunk_infos.append((key, start, end))
1641
+
1642
+ # block_mapping: location -> [(CacheEngineKey, start, end)]
1643
+ if (
1644
+ "req_id" in kwargs
1645
+ and kwargs["req_id"] in self.lookup_pins
1646
+ and len(self.lookup_pins[kwargs["req_id"]]) == 1
1647
+ ):
1648
+ location = next(iter(self.lookup_pins[kwargs["req_id"]].keys()))
1649
+ block_mapping = {location: chunk_infos}
1650
+ else:
1651
+ block_mapping = self.storage_manager.get_block_mapping(chunk_infos)
1652
+
1653
+ last_failed_block_start = None
1654
+ for location, blocks in block_mapping.items():
1655
+ keys = [key for key, _, _ in blocks]
1656
+ memory_objs = self.storage_manager.batched_get(
1657
+ keys=keys,
1658
+ location=location,
1659
+ )
1660
+
1661
+ used_keys: set[CacheEngineKey] = set()
1662
+ for (key, start, end), memory_obj in zip(blocks, memory_objs, strict=False):
1663
+ if memory_obj is None:
1664
+ logger.warning(
1665
+ "The cache block is in the storage, but it can't be retrieved"
1666
+ )
1667
+ if (
1668
+ last_failed_block_start is None
1669
+ # The minimum value should be taken here to ensure that
1670
+ # the prefix keys are all consecutive successful.
1671
+ or last_failed_block_start > start
1672
+ ):
1673
+ last_failed_block_start = start
1674
+ break
1675
+ reordered_chunks.append((key, memory_obj, start, end))
1676
+ tot_kv_size += memory_obj.get_size()
1677
+ ret_mask[start:end] = True
1678
+ used_keys.add(key)
1679
+
1680
+ for (key, _, _), memory_obj in zip(blocks, memory_objs, strict=False):
1681
+ if memory_obj is not None and key not in used_keys:
1682
+ logger.debug(
1683
+ "ref_count_down for %s of %s as the previous key failed",
1684
+ key,
1685
+ location,
1686
+ )
1687
+ memory_obj.ref_count_down()
1688
+
1689
+ if last_failed_block_start is not None:
1690
+ ret_mask[last_failed_block_start:] = False
1691
+
1692
+ for key, memory_obj, _, end in reordered_chunks:
1693
+ if end > last_failed_block_start:
1694
+ logger.debug(
1695
+ "ref_count_down for %s as it is truncated by "
1696
+ "last_failed_block_start %d",
1697
+ key,
1698
+ last_failed_block_start,
1699
+ )
1700
+ tot_kv_size -= memory_obj.get_size()
1701
+ memory_obj.ref_count_down()
1702
+
1703
+ reordered_chunks = [
1704
+ (key, memory_obj, start, end)
1705
+ for key, memory_obj, start, end in reordered_chunks
1706
+ if end <= last_failed_block_start
1707
+ ]
1708
+ return reordered_chunks, tot_kv_size
1709
+
1710
+ def _broadcast_or_receive_memory_objs(
1711
+ self,
1712
+ reordered_chunks,
1713
+ ret_mask,
1714
+ ):
1715
+ """
1716
+ Handles broadcasting or receiving memory objects in a distributed environment.
1717
+
1718
+ This function implements the communication logic where:
1719
+ - The first rank (coordinator) broadcasts memory objects and metadata to others
1720
+ - Other ranks receive and reconstruct the memory objects
1721
+
1722
+ Parameters:
1723
+ reordered_chunks: List of tuples containing [key, memory object, start, end]
1724
+ ret_mask: Boolean mask indicating which positions have been processed
1725
+
1726
+ Side Effects:
1727
+ - On first rank:
1728
+ * Broadcasts chunk count and each chunk's combined metadata
1729
+ * Broadcasts tensor data
1730
+ - On other ranks:
1731
+ * Receives chunk data and populates reordered_chunks
1732
+ * Updates ret_mask to mark received positions as True
1733
+ """
1734
+ if self.metadata.is_first_rank():
1735
+ # Broadcast total chunk count
1736
+ chunk_count = len(reordered_chunks)
1737
+ self.broadcast_object_fn(chunk_count, self.metadata.first_rank)
1738
+
1739
+ # Broadcast each chunk's data
1740
+ for key, memory_obj, start, end in reordered_chunks:
1741
+ # Combine (start, end) and metadata into single broadcast
1742
+ metadata_dict = memory_obj.metadata.to_dict()
1743
+ combined_metadata = (start, end, metadata_dict)
1744
+ self.broadcast_object_fn(combined_metadata, self.metadata.first_rank)
1745
+
1746
+ # Broadcast tensor data
1747
+ raw_tensor = memory_obj.raw_tensor
1748
+ assert raw_tensor is not None
1749
+ tensor_to_broadcast = raw_tensor.to(f"cuda:{self.metadata.worker_id}")
1750
+ self.broadcast_fn(tensor_to_broadcast, self.metadata.first_rank)
1751
+ else:
1752
+ # Receive total chunk count
1753
+ chunk_count = self.broadcast_object_fn(None, self.metadata.first_rank)
1754
+ if chunk_count is None:
1755
+ logger.warning(
1756
+ f"rank={self.metadata.worker_id} received None chunk_count"
1757
+ )
1758
+ return
1759
+
1760
+ # Fill reordered_chunks with received data
1761
+ for _ in range(chunk_count):
1762
+ # Receive combined metadata (start, end, metadata_dict)
1763
+ combined_metadata = self.broadcast_object_fn(
1764
+ None, self.metadata.first_rank
1765
+ )
1766
+ if combined_metadata is None:
1767
+ logger.warning(
1768
+ f"rank={self.metadata.worker_id} "
1769
+ "received None combined_metadata"
1770
+ )
1771
+ break
1772
+ start, end, metadata_dict = combined_metadata
1773
+ ret_mask[start:end] = True
1774
+
1775
+ # Create tensor and receive data
1776
+ metadata = MemoryObjMetadata.from_dict(metadata_dict)
1777
+ local_rank = self.metadata.worker_id % torch.cuda.device_count()
1778
+ raw_tensor = torch.empty(
1779
+ torch.Size([metadata.get_size()]),
1780
+ dtype=torch.uint8,
1781
+ device=f"cuda:{local_rank}",
1782
+ )
1783
+ self.broadcast_fn(raw_tensor, self.metadata.first_rank)
1784
+
1785
+ # Create temporary memory object (key not needed for other ranks)
1786
+ memory_obj = TensorMemoryObj(
1787
+ raw_data=raw_tensor, metadata=metadata, parent_allocator=None
1788
+ )
1789
+ reordered_chunks.append((None, memory_obj, start, end))
1790
+
1791
+ def _is_passive(self):
1792
+ """
1793
+ A 'passive' CacheEngine means that the node itself will not store/retrieve
1794
+ the data directly, but from the "active" worker (i.e., rank 0 in MLA)
1795
+ """
1796
+ return self.save_only_first_rank and not self.metadata.is_first_rank()
1797
+
1798
+ def _get_slot_mapping_list(
1799
+ self,
1800
+ slot_mapping: Optional[Union[torch.Tensor, List[int]]],
1801
+ ) -> Optional[List[int]]:
1802
+ """
1803
+ Convert slot_mapping to list if it's a tensor, otherwise return as is.
1804
+
1805
+ :param slot_mapping: The slot_mapping to convert,
1806
+ can be a torch.Tensor or List[int], or None
1807
+ :type slot_mapping: Optional[Union[torch.Tensor, List[int]]]
1808
+ :return: The slot_mapping as a List[int], or None if input is None
1809
+ :rtype: Optional[List[int]]
1810
+ """
1811
+ if slot_mapping is None:
1812
+ return None
1813
+ if isinstance(slot_mapping, torch.Tensor):
1814
+ return slot_mapping.tolist()
1815
+ # At this point, slot_mapping must be List[int]
1816
+ return slot_mapping
1817
+
1818
+ def _log_kvcache_for_check(
1819
+ self,
1820
+ operation: str,
1821
+ kwargs: dict,
1822
+ token_count: int,
1823
+ require_req_id: bool = False,
1824
+ ) -> None:
1825
+ """
1826
+ Helper method to log KVCache Check information.
1827
+
1828
+ This method centralizes the KVCache Check logging logic that was
1829
+ duplicated in multiple methods.
1830
+
1831
+ Args:
1832
+ operation: The operation being performed (e.g., "Store", "retrieve")
1833
+ kwargs: The keyword arguments containing slot_mapping and req_id
1834
+ token_count: The number of tokens involved in the operation
1835
+ require_req_id: Whether req_id must be present (default: False)
1836
+ """
1837
+ if not self.kvcache_check_log_enabled:
1838
+ return
1839
+
1840
+ slot_mapping = kwargs.get("slot_mapping")
1841
+ if slot_mapping is None:
1842
+ return
1843
+
1844
+ if require_req_id:
1845
+ req_id = kwargs.get("req_id")
1846
+ if req_id is None:
1847
+ return
1848
+ else:
1849
+ req_id = kwargs.get("req_id", "unspecified")
1850
+
1851
+ # Convert slot_mapping to list if it's a tensor
1852
+ slot_mapping_list = self._get_slot_mapping_list(slot_mapping)
1853
+ # slot_mapping_list should not be None when slot_mapping is not None
1854
+ assert slot_mapping_list is not None
1855
+
1856
+ logger.info(
1857
+ "[KVCache Check] %s request %s, tokens=%d, slot_mapping: %s",
1858
+ operation,
1859
+ req_id,
1860
+ token_count,
1861
+ compress_slot_mapping(slot_mapping_list),
1862
+ )
1863
+
1864
+
1865
+ class LMCacheEngineBuilder:
1866
+ _instances: Dict[str, LMCacheEngine] = {}
1867
+ _cfgs: Dict[str, LMCacheEngineConfig] = {}
1868
+ _metadatas: Dict[str, LMCacheMetadata] = {}
1869
+ _stat_loggers: Dict[str, LMCacheStatsLogger] = {}
1870
+
1871
+ # TODO(Jiayi): Please remove this helper function in the future.
1872
+ # Currently, it's only used for testing.
1873
+ @staticmethod
1874
+ def _Create_memory_allocator(
1875
+ config: LMCacheEngineConfig,
1876
+ metadata: LMCacheMetadata,
1877
+ numa_mapping: Optional[NUMAMapping] = None,
1878
+ ) -> MemoryAllocatorInterface:
1879
+ # NOTE: should remove this function after fixing the unit tests:
1880
+ # raise RuntimeError("_Create_memory_allocator is deprecated!")
1881
+ extra_config = config.extra_config
1882
+ enable_nixl_storage = extra_config is not None and extra_config.get(
1883
+ "enable_nixl_storage"
1884
+ )
1885
+
1886
+ if enable_nixl_storage:
1887
+ # TODO(Jiayi): weird to import from transfer utils.
1888
+ # First Party
1889
+ from lmcache.v1.transfer_channel.transfer_utils import (
1890
+ get_correct_device,
1891
+ )
1892
+
1893
+ corrected_device = get_correct_device(
1894
+ config.nixl_buffer_device,
1895
+ metadata.worker_id,
1896
+ )
1897
+
1898
+ buffer = torch.empty(
1899
+ config.nixl_buffer_size,
1900
+ dtype=torch.uint8,
1901
+ device=corrected_device,
1902
+ )
1903
+
1904
+ if corrected_device == "cpu":
1905
+ torch.cuda.cudart().cudaHostRegister(
1906
+ buffer.data_ptr(), config.nixl_buffer_size, 0
1907
+ )
1908
+ else:
1909
+ logger.info(f"Setting cuda device to {corrected_device} ")
1910
+ torch.cuda.set_device(corrected_device)
1911
+
1912
+ return PagedTensorMemoryAllocator(
1913
+ buffer,
1914
+ [torch.Size(metadata.kv_shape)],
1915
+ [metadata.kv_dtype],
1916
+ MemoryFormat.KV_2LTD,
1917
+ )
1918
+
1919
+ if config.gds_path is not None:
1920
+ assert config.gds_buffer_size is not None
1921
+ return CuFileMemoryAllocator(config.gds_buffer_size * 1024**2)
1922
+
1923
+ max_local_cpu_size = config.max_local_cpu_size
1924
+ # save_only_first_rank only works when use mla
1925
+ save_only_first_rank = (
1926
+ config.get_extra_config_value("save_only_first_rank", metadata.use_mla)
1927
+ and metadata.use_mla
1928
+ )
1929
+ if save_only_first_rank and metadata.is_first_rank():
1930
+ # Only the first rank will save the cache,
1931
+ # so we need to set it lager than other ranks
1932
+ first_rank_max_local_cpu_size = (
1933
+ config.extra_config.get(
1934
+ "first_rank_max_local_cpu_size", max_local_cpu_size
1935
+ )
1936
+ if config.extra_config
1937
+ else max_local_cpu_size
1938
+ )
1939
+ return MixedMemoryAllocator(
1940
+ int(first_rank_max_local_cpu_size * 1024**3),
1941
+ numa_mapping=numa_mapping,
1942
+ )
1943
+ return MixedMemoryAllocator(
1944
+ int(max_local_cpu_size * 1024**3),
1945
+ numa_mapping=numa_mapping,
1946
+ )
1947
+
1948
+ @staticmethod
1949
+ def _Create_token_database(
1950
+ config: LMCacheEngineConfig,
1951
+ metadata: LMCacheMetadata,
1952
+ ) -> TokenDatabase:
1953
+ if config.enable_blending:
1954
+ return SegmentTokenDatabase(config, metadata)
1955
+ return ChunkedTokenDatabase(config, metadata)
1956
+
1957
+ @classmethod
1958
+ def get_or_create(
1959
+ cls,
1960
+ instance_id: str,
1961
+ config: LMCacheEngineConfig,
1962
+ metadata: LMCacheMetadata,
1963
+ gpu_connector: Optional[GPUConnectorInterface],
1964
+ broadcast_fn: Callable[[torch.Tensor, int], None],
1965
+ broadcast_object_fn: Callable[[Any, int], Any],
1966
+ ) -> LMCacheEngine:
1967
+ """
1968
+ Builds a new LMCacheEngine instance if it doesn't already exist for the
1969
+ given ID.
1970
+
1971
+ raises: ValueError if the instance already exists with a different
1972
+ configuration.
1973
+ """
1974
+ logger.info(f"Creating LMCacheEngine instance {instance_id}")
1975
+ if instance_id not in cls._instances:
1976
+ numa_mapping = NUMADetector.get_numa_mapping(config)
1977
+ logger.info(f"NUMA mapping for instance {instance_id}: {numa_mapping}")
1978
+ token_database = cls._Create_token_database(config, metadata)
1979
+ stat_logger = LMCacheStatsLogger(
1980
+ metadata,
1981
+ log_interval=10,
1982
+ config=config,
1983
+ )
1984
+
1985
+ engine = LMCacheEngine(
1986
+ config,
1987
+ metadata,
1988
+ token_database,
1989
+ gpu_connector,
1990
+ broadcast_fn,
1991
+ broadcast_object_fn,
1992
+ )
1993
+
1994
+ cls._instances[instance_id] = engine
1995
+ cls._cfgs[instance_id] = config
1996
+ cls._metadatas[instance_id] = metadata
1997
+ cls._stat_loggers[instance_id] = stat_logger
1998
+ return engine
1999
+ else:
2000
+ if (
2001
+ cls._cfgs[instance_id] != config
2002
+ or cls._metadatas[instance_id] != metadata
2003
+ ):
2004
+ raise ValueError(
2005
+ f"Instance {instance_id} already exists with a different "
2006
+ f"configuration or metadata."
2007
+ )
2008
+ return cls._instances[instance_id]
2009
+
2010
+ @classmethod
2011
+ def get(cls, instance_id: str) -> Optional[LMCacheEngine]:
2012
+ """Returns the LMCacheEngine instance associated with the instance ID,
2013
+ or None if not found."""
2014
+ return cls._instances.get(instance_id)
2015
+
2016
+ @classmethod
2017
+ def destroy(cls, instance_id: str) -> None:
2018
+ """Close and delete the LMCacheEngine instance by the instance ID"""
2019
+ # TODO: unit test for this
2020
+ logger.info(f"Destroying LMCacheEngine instance: {instance_id}")
2021
+
2022
+ if instance_id in cls._instances:
2023
+ stat_logger = cls._stat_loggers[instance_id]
2024
+ try:
2025
+ logger.info("Shutting down stats logger...")
2026
+ stat_logger.shutdown()
2027
+ logger.info("Stats logger shut down successfully")
2028
+ except Exception as e:
2029
+ logger.error(f"Error shutting down stats logger: {e}")
2030
+
2031
+ engine = cls._instances[instance_id]
2032
+ try:
2033
+ logger.info("Closing cache engine...")
2034
+ engine.close()
2035
+ logger.info("Cache engine closed successfully")
2036
+ except Exception as e:
2037
+ logger.error(f"Error closing cache engine: {e}")
2038
+
2039
+ try:
2040
+ logger.info("Cleaning up instance dictionaries...")
2041
+ cls._instances.pop(instance_id, None)
2042
+ cls._cfgs.pop(instance_id, None)
2043
+ cls._metadatas.pop(instance_id, None)
2044
+ cls._stat_loggers.pop(instance_id, None)
2045
+ logger.info("Instance dictionaries cleaned up")
2046
+ except Exception as e:
2047
+ logger.error(f"Error cleaning up instances: {e}")
2048
+
2049
+ try:
2050
+ logger.info("Destroying stats monitor...")
2051
+ LMCStatsMonitor.DestroyInstance()
2052
+ logger.info("Stats monitor destroyed successfully")
2053
+ except Exception as e:
2054
+ logger.error(f"Error destroying stats monitor: {e}")
2055
+
2056
+ logger.info(f"LMCacheEngine instance {instance_id} destroyed")
2057
+ else:
2058
+ logger.warning(f"Instance {instance_id} not found for destruction")