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,1352 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Standard
3
+ from collections import OrderedDict
4
+ from concurrent.futures import Future
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Coroutine,
9
+ Dict,
10
+ Generator,
11
+ List,
12
+ Optional,
13
+ Sequence,
14
+ Tuple,
15
+ Union,
16
+ cast,
17
+ )
18
+ import asyncio
19
+ import functools
20
+ import threading
21
+
22
+ # Third Party
23
+ import torch
24
+
25
+ # First Party
26
+ from lmcache.logging import init_logger
27
+ from lmcache.observability import PrometheusLogger
28
+ from lmcache.utils import (
29
+ CacheEngineKey,
30
+ _lmcache_nvtx_annotate,
31
+ start_loop_in_thread_with_exceptions,
32
+ )
33
+ from lmcache.v1.config import LMCacheEngineConfig
34
+ from lmcache.v1.event_manager import EventManager, EventStatus, EventType
35
+ from lmcache.v1.memory_management import (
36
+ MemoryFormat,
37
+ MemoryObj,
38
+ )
39
+ from lmcache.v1.metadata import LMCacheMetadata
40
+ from lmcache.v1.storage_backend import CreateStorageBackends, is_cuda_worker
41
+ from lmcache.v1.storage_backend.abstract_backend import (
42
+ AllocatorBackendInterface,
43
+ StorageBackendInterface,
44
+ )
45
+ from lmcache.v1.storage_backend.local_cpu_backend import LocalCPUBackend
46
+
47
+ if TYPE_CHECKING:
48
+ # First Party
49
+ from lmcache.v1.cache_controller.worker import LMCacheWorker
50
+ from lmcache.v1.lookup_client.lmcache_async_lookup_client import (
51
+ LMCacheAsyncLookupServer,
52
+ )
53
+
54
+ logger = init_logger(__name__)
55
+
56
+
57
+ # Helper function to get the class name of the backend
58
+ def get_backend_cname(backend: StorageBackendInterface) -> str:
59
+ return backend.__class__.__name__
60
+
61
+
62
+ # Helper function to allocate and copy memory objects between D and H
63
+ def allocate_and_copy_objects(
64
+ allocator_backend: AllocatorBackendInterface,
65
+ keys: Sequence[CacheEngineKey],
66
+ src_memory_objs: list[MemoryObj],
67
+ stream: torch.cuda.Stream,
68
+ ) -> tuple[Sequence[CacheEngineKey], list[MemoryObj]]:
69
+ """
70
+ Allocate the memory objects and copy the data from src_memory_objs to
71
+ the newly allocated memory objects
72
+
73
+ Args:
74
+ allocator_backend: the allocator backend to allocate the new memory
75
+ objects
76
+ keys: the cache engine keys corresponding to the memory objects
77
+ src_memory_objs: the memory objects to copy from
78
+ stream: the cuda stream to run the copy in
79
+
80
+ Returns:
81
+ - list of cache engine keys that corresponds to the memory objects
82
+ that has been successfully allocated
83
+ - list of the memory objects that has been successfully allocated
84
+ """
85
+ allocated_objects = []
86
+ for key, src_memory_obj in zip(keys, src_memory_objs, strict=False):
87
+ if allocator_backend.contains(key):
88
+ continue
89
+ memory_obj = allocator_backend.allocate(
90
+ src_memory_obj.get_shape(),
91
+ src_memory_obj.get_dtype(),
92
+ fmt=src_memory_obj.meta.fmt,
93
+ eviction=True,
94
+ busy_loop=False,
95
+ )
96
+
97
+ if memory_obj is None:
98
+ break
99
+
100
+ if memory_obj.tensor is None:
101
+ # This should not happen with current implementation,
102
+ # but handle it defensively to avoid memory leak
103
+ logger.warning(
104
+ "Allocated MemoryObj has None tensor, this is unexpected. "
105
+ "Releasing the memory object."
106
+ )
107
+ memory_obj.ref_count_down()
108
+ break
109
+
110
+ with torch.cuda.stream(stream):
111
+ memory_obj.tensor.copy_(src_memory_obj.tensor, non_blocking=True)
112
+ allocated_objects.append(memory_obj)
113
+
114
+ if stream is not None:
115
+ stream.synchronize()
116
+ return keys[: len(allocated_objects)], allocated_objects
117
+
118
+
119
+ class WeightedSemaphore:
120
+ def __init__(self, chunk_budget: int):
121
+ # it is physically impossible to have more fragmentation than 50%
122
+ # when all of the chunks are of the same size (save_unfull_chunk=False)
123
+ # so we can safely allocate half of the chunk budget for concurrent requests
124
+ self._concurrent_budget_cap = chunk_budget // 2
125
+ self._chunk_budget_cap = chunk_budget
126
+ self._current_chunks = self._concurrent_budget_cap
127
+ self._cond = asyncio.Condition()
128
+
129
+ async def acquire(self, n: int = 1) -> None:
130
+ if n > self._chunk_budget_cap:
131
+ raise ValueError(
132
+ f"Trying to acquire {n} chunks, "
133
+ f"Cannot acquire more than {self._chunk_budget_cap} chunks"
134
+ "Please set the max local cpu size to a larger value"
135
+ )
136
+
137
+ async with self._cond:
138
+ logger.debug(f"WeightedSemaphore: Attempting to acquire {n} chunks")
139
+ if n <= self._concurrent_budget_cap:
140
+ await self._cond.wait_for(lambda: self._current_chunks >= n)
141
+ self._current_chunks -= n
142
+ else:
143
+ # Oversized case: require exclusive access
144
+ await self._cond.wait_for(
145
+ lambda: self._current_chunks == self._concurrent_budget_cap
146
+ )
147
+ # Reserve everything
148
+ self._current_chunks = 0
149
+ logger.debug(
150
+ f"WeightedSemaphore: Acquired {n} chunks, "
151
+ f"remaining chunks: {self._current_chunks}"
152
+ )
153
+
154
+ async def release(self, n: int = 1) -> None:
155
+ async with self._cond:
156
+ if n <= self._concurrent_budget_cap:
157
+ self._current_chunks += n
158
+ else:
159
+ self._current_chunks = self._concurrent_budget_cap
160
+ self._cond.notify_all()
161
+
162
+
163
+ class AsyncMultiSerializer:
164
+ """
165
+ Prevent race conditions where multiple batched_get's cause the local CPU
166
+ backend to allocate memory objects in parallel and get deadlocked.
167
+ Make the assumption that the save_unfull_chunk is False so that we
168
+ can assume that we can always use 50% of the given memory
169
+ """
170
+
171
+ def __init__(
172
+ self,
173
+ allocator_backend: AllocatorBackendInterface,
174
+ loop: asyncio.AbstractEventLoop,
175
+ ):
176
+ self.chunk_budget = allocator_backend.calculate_chunk_budget()
177
+ self._sem = WeightedSemaphore(self.chunk_budget)
178
+ self.loop = loop
179
+
180
+ async def run(
181
+ self,
182
+ coro_fn: Coroutine[Any, Any, Any],
183
+ num_chunks: int,
184
+ ) -> Any:
185
+ await self._sem.acquire(num_chunks)
186
+ try:
187
+ return await coro_fn
188
+ finally:
189
+ await self._sem.release(num_chunks)
190
+
191
+
192
+ class AsyncSingleSerializer:
193
+ """
194
+ Prevent race conditions in a naive way by forcing each request that
195
+ is passed through to be serialized
196
+ """
197
+
198
+ def __init__(self, loop: asyncio.AbstractEventLoop):
199
+ self.loop = loop
200
+ # lazy init in run
201
+ self.lock: Optional[asyncio.Lock] = None
202
+
203
+ async def run(self, coro_fn: Coroutine[Any, Any, Any], *args, **kwargs) -> Any:
204
+ # we need to lazily initialize the lock to
205
+ # place it on the calling event loop
206
+ if self.lock is None:
207
+ self.lock = asyncio.Lock()
208
+ async with self.lock: # type: ignore
209
+ return await coro_fn
210
+
211
+
212
+ AsyncSerializer = Union[AsyncSingleSerializer, AsyncMultiSerializer]
213
+
214
+
215
+ # TODO: extend this class to implement caching policies and eviction policies
216
+ class StorageManager:
217
+ """
218
+ The StorageManager is responsible for managing the storage backends.
219
+ """
220
+
221
+ def __init__(
222
+ self,
223
+ config: LMCacheEngineConfig,
224
+ metadata: LMCacheMetadata,
225
+ event_manager: EventManager,
226
+ lmcache_worker: Optional["LMCacheWorker"] = None,
227
+ async_lookup_server: Optional["LMCacheAsyncLookupServer"] = None,
228
+ ):
229
+ self.config = config
230
+ self.metadata = metadata
231
+ self.loop = asyncio.new_event_loop()
232
+
233
+ self.thread = threading.Thread(
234
+ target=start_loop_in_thread_with_exceptions,
235
+ args=(self.loop,),
236
+ name="storage-manger-event-loop",
237
+ )
238
+ self.thread.start()
239
+
240
+ self.storage_backends: OrderedDict[str, StorageBackendInterface] = OrderedDict()
241
+ self.manager_lock = threading.Lock()
242
+ self.lmcache_worker = lmcache_worker
243
+
244
+ # Use the unified create path so that init and
245
+ # dynamic creation share the same logic.
246
+ self.create_backends()
247
+
248
+ # the backend used for actual storage
249
+ self.non_allocator_backends = self.get_non_allocator_backends()
250
+
251
+ self.enable_pd = config.enable_pd
252
+
253
+ self.allocator_backend = None
254
+ if metadata.role != "scheduler":
255
+ self.allocator_backend = self._get_allocator_backend(config)
256
+
257
+ self.local_cpu_backend = self.storage_backends.get("LocalCPUBackend", None)
258
+
259
+ self.instance_id = config.lmcache_instance_id
260
+ self.worker_id = metadata.worker_id
261
+
262
+ self.event_manager = event_manager
263
+
264
+ self.async_lookup_server: Optional["LMCacheAsyncLookupServer"] = (
265
+ async_lookup_server
266
+ )
267
+ self.async_serializer: Optional[AsyncSerializer] = None
268
+
269
+ # The cuda stream for internal copies during put
270
+ if is_cuda_worker(metadata):
271
+ self.internal_copy_stream = torch.cuda.Stream()
272
+ else:
273
+ self.internal_copy_stream = None
274
+
275
+ # freeze mode: only use local_cpu backend for retrieval
276
+ self._freeze = False
277
+ self._freeze_lock = threading.RLock()
278
+
279
+ # Backend bypass mode: skip specific backends during health check failures
280
+ self._bypassed_backends: set[str] = set()
281
+ self._bypass_lock = threading.RLock()
282
+
283
+ if not self.enable_pd and self.config.enable_async_loading:
284
+ assert self.allocator_backend is not None
285
+ self.async_serializer = AsyncSingleSerializer(self.loop)
286
+
287
+ self._setup_metrics()
288
+
289
+ def _setup_metrics(self) -> None:
290
+ prometheus_logger = PrometheusLogger.GetInstanceOrNone()
291
+ if prometheus_logger is None:
292
+ logger.warning(
293
+ "PrometheusLogger is not initialized, "
294
+ "event metrics will not be collected"
295
+ )
296
+ return
297
+
298
+ metric_map = {
299
+ "storage_events_ongoing_count": EventStatus.ONGOING,
300
+ "storage_events_done_count": EventStatus.DONE,
301
+ "storage_events_not_found_count": EventStatus.NOT_FOUND,
302
+ }
303
+
304
+ for metric_name, status in metric_map.items():
305
+ metric = getattr(prometheus_logger, metric_name)
306
+ metric.set_function(
307
+ lambda s=status: self.event_manager.get_events_count_by_status(
308
+ EventType.LOADING, s
309
+ )
310
+ )
311
+
312
+ def _get_allocator_backend(
313
+ self, config: LMCacheEngineConfig
314
+ ) -> AllocatorBackendInterface:
315
+ if self.enable_pd:
316
+ allocator_backend = self.storage_backends["PDBackend"]
317
+ elif "MaruBackend" in self.storage_backends:
318
+ if "LocalCPUBackend" in self.storage_backends:
319
+ allocator_backend = self.storage_backends["LocalCPUBackend"]
320
+ else:
321
+ allocator_backend = self.storage_backends["MaruBackend"]
322
+ else:
323
+ allocator_backend = self.storage_backends["LocalCPUBackend"]
324
+ assert isinstance(allocator_backend, AllocatorBackendInterface)
325
+ return allocator_backend
326
+
327
+ @_lmcache_nvtx_annotate
328
+ def allocate(
329
+ self,
330
+ shapes: Union[torch.Size, list[torch.Size]],
331
+ dtypes: Union[torch.dtype, list[torch.dtype]],
332
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
333
+ eviction=True,
334
+ busy_loop=True,
335
+ ) -> Optional[MemoryObj]:
336
+ """
337
+ Allocate memory object with memory allocator.
338
+ Use LRU evictor if eviction is enabled.
339
+ """
340
+ # TODO (Jiayi): We might need to pre-allocate and management
341
+ # disk in a similar way as CPU.
342
+ assert self.allocator_backend is not None
343
+ return self.allocator_backend.allocate(
344
+ shapes, dtypes, fmt, eviction=eviction, busy_loop=busy_loop
345
+ )
346
+
347
+ @_lmcache_nvtx_annotate
348
+ def batched_allocate(
349
+ self,
350
+ shapes: Union[torch.Size, list[torch.Size]],
351
+ dtypes: Union[torch.dtype, list[torch.dtype]],
352
+ batch_size: int,
353
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
354
+ eviction=True,
355
+ busy_loop=True,
356
+ ) -> Optional[list[MemoryObj]]:
357
+ """
358
+ Batched allocate memory object with memory allocator.
359
+ Use LRU evictor if eviction is enabled.
360
+ """
361
+ # TODO (Jiayi): We might need to pre-allocate and management
362
+ # disk in a similar way as CPU.
363
+ if self.allocator_backend is None:
364
+ raise RuntimeError("Allocator backend not available for scheduler role")
365
+ return self.allocator_backend.batched_allocate(
366
+ shapes, dtypes, batch_size, fmt, eviction=eviction, busy_loop=busy_loop
367
+ )
368
+
369
+ def put(
370
+ self,
371
+ key: CacheEngineKey,
372
+ memory_obj: MemoryObj,
373
+ location: Optional[str] = None,
374
+ ) -> None:
375
+ """
376
+ Non-blocking function to put the memory object into the storages.
377
+ Do not store if the same object is being stored (handled here by
378
+ storage manager) or has been stored (handled by storage backend).
379
+ """
380
+ raise RuntimeError(
381
+ "StorageManager.put is deprecated and should not be called anymore"
382
+ )
383
+
384
+ def batched_put(
385
+ self,
386
+ keys: Sequence[CacheEngineKey],
387
+ memory_objs: List[MemoryObj],
388
+ transfer_spec=None,
389
+ location: Optional[str] = None,
390
+ ) -> None:
391
+ """
392
+ Non-blocking function to batched put the memory objects into the
393
+ storage backends.
394
+ Do not store if the same object is being stored (handled here by
395
+ storage manager) or has been stored (handled by storage backend).
396
+ """
397
+ # The dictionary from backend cname to objects and keys
398
+ obj_dict: dict[
399
+ str,
400
+ tuple[Sequence[CacheEngineKey], list[MemoryObj]],
401
+ ] = {}
402
+ if self.allocator_backend is None:
403
+ # For scheduler role, no allocator backend available
404
+ raise RuntimeError("Batched put not available for scheduler role")
405
+ obj_dict[get_backend_cname(self.allocator_backend)] = (
406
+ keys,
407
+ memory_objs,
408
+ )
409
+
410
+ for backend_name, backend in self.storage_backends.items():
411
+ if location and backend_name != location:
412
+ continue
413
+ # Skip bypassed backends
414
+ with self._bypass_lock:
415
+ if backend_name in self._bypassed_backends:
416
+ continue
417
+
418
+ allocator_backend = backend.get_allocator_backend()
419
+ cname = get_backend_cname(allocator_backend)
420
+ if cname not in obj_dict:
421
+ new_keys, new_objs = allocate_and_copy_objects(
422
+ allocator_backend, keys, memory_objs, self.internal_copy_stream
423
+ )
424
+ obj_dict[cname] = (new_keys, new_objs)
425
+
426
+ # NOTE: the handling of exists_in_put_tasks
427
+ # is done in the backend
428
+ ks, objs = obj_dict[cname]
429
+ backend.batched_submit_put_task(ks, objs, transfer_spec=transfer_spec)
430
+
431
+ for cname, (ks, objs) in obj_dict.items():
432
+ for memory_obj in objs:
433
+ memory_obj.ref_count_down()
434
+
435
+ def get(
436
+ self,
437
+ key: CacheEngineKey,
438
+ location: Optional[str] = None,
439
+ ) -> Optional[MemoryObj]:
440
+ """
441
+ Blocking function to get the memory object from the storages.
442
+ """
443
+
444
+ # Search all backends for blocking get
445
+ for backend_name, backend in self.get_active_storage_backends(location):
446
+ # TODO(Jiayi): need to make sure all memory_objs returned
447
+ # are allocated by the allocator backend.
448
+ memory_obj = backend.get_blocking(key)
449
+ if memory_obj:
450
+ if (
451
+ backend_name not in ["LocalCPUBackend", "PDBackend", "MaruBackend"]
452
+ and "LocalCPUBackend" in self.storage_backends
453
+ ):
454
+ local_cpu_backend = self.storage_backends["LocalCPUBackend"]
455
+ assert isinstance(local_cpu_backend, LocalCPUBackend)
456
+ local_cpu_backend.submit_put_task(key, memory_obj)
457
+ return memory_obj
458
+
459
+ return None
460
+
461
+ def get_non_blocking(
462
+ self,
463
+ key: CacheEngineKey,
464
+ location: Optional[str] = None,
465
+ ) -> Optional[Future]:
466
+ """
467
+ Non-blocking function to get the memory object from the storages.
468
+ """
469
+ # TODO (Jiayi): incorporate prefetching here
470
+
471
+ # Search all backends for non-blocking get
472
+ for backend_name, backend in self.get_active_storage_backends(location):
473
+ # NOTE(Jiayi): bypass the allocator for now
474
+ task = backend.get_non_blocking(key)
475
+ if task:
476
+ # TODO (Jiayi): add write-back logic here
477
+ return task
478
+ return None
479
+
480
+ def batched_get(
481
+ self,
482
+ keys: List[CacheEngineKey],
483
+ location: Optional[str] = None,
484
+ ) -> List[Optional[MemoryObj]]:
485
+ """
486
+ Blocking function to get the memory objects from the storages.
487
+ """
488
+ # TODO (ApostaC): remove the nested optional here
489
+ for backend_name, storage_backend in self.get_active_storage_backends(location):
490
+ memory_objs = storage_backend.batched_get_blocking(keys)
491
+ if memory_objs:
492
+ # Align with single-key `get()` logic:
493
+ # auto-write remote data to local CPU cache
494
+ if (
495
+ backend_name not in ["LocalCPUBackend", "PDBackend", "MaruBackend"]
496
+ and "LocalCPUBackend" in self.storage_backends
497
+ and None not in memory_objs
498
+ ):
499
+ logger.debug(
500
+ "Storing %s objects from %s to LocalCPUBackend",
501
+ len(keys),
502
+ backend_name,
503
+ )
504
+ local_cpu_backend = self.storage_backends["LocalCPUBackend"]
505
+ assert isinstance(local_cpu_backend, LocalCPUBackend)
506
+ # Type cast: Safe (we verified no Nones above)
507
+ # `batched_submit_put_task` expects list[MemoryObj]
508
+ # TODO (lisiG9): Refactor this write-back logic into caching
509
+ # policy module
510
+ memory_objs_no_none = cast(List[MemoryObj], memory_objs)
511
+ local_cpu_backend.batched_submit_put_task(keys, memory_objs_no_none)
512
+ return memory_objs
513
+ return [None] * len(keys)
514
+
515
+ def layerwise_batched_get(
516
+ self,
517
+ keys: List[List[CacheEngineKey]],
518
+ location: Optional[str] = None,
519
+ ) -> Generator[Future, None, None]:
520
+ """
521
+ Non-blocking function to get the memory objects into the storages
522
+ in a layerwise manner.
523
+ Do not store if the same object is being stored (handled here by
524
+ storage manager) or has been stored (handled by storage backend).
525
+
526
+ :param List[List[CacheEngineKey]] keys: The keys to get. The first
527
+ dimension corresponds to the number of layers, and the second
528
+ dimension corresponds to the number of chunks.
529
+
530
+ :return: A generator that yields a future for each layer.
531
+ """
532
+ if location is None:
533
+ location = "LocalCPUBackend"
534
+ for keys_multi_chunk in keys:
535
+ # Retrieve all chunks for one layer
536
+ backend = self.storage_backends[location]
537
+ # TODO(Jiayi): need to make async loading and layerwise compatible
538
+ coro = backend.batched_get_non_blocking("fake_lookup_id", keys_multi_chunk)
539
+ task = asyncio.run_coroutine_threadsafe(coro, self.loop)
540
+ yield task
541
+
542
+ def prefetch_single_done_callback(
543
+ self,
544
+ future: asyncio.Future,
545
+ keys: list[CacheEngineKey],
546
+ backend_name: str,
547
+ ) -> None:
548
+ """
549
+ Callback function when a single prefetch task
550
+ (i.e., prefetching from a single backend) is done.
551
+ """
552
+ # TODO(Jiayi): support write-back policy here
553
+ pass
554
+
555
+ def prefetch_all_done_callback(
556
+ self,
557
+ task: asyncio.Future,
558
+ lookup_id: str,
559
+ cum_chunk_lengths_total: list[int],
560
+ tier_expected_chunks: list[int],
561
+ ) -> None:
562
+ """
563
+ Callback function when all prefetch tasks
564
+ (i.e., prefetching from all backends for the entire request) are done.
565
+ """
566
+ assert self.async_lookup_server is not None
567
+ self.event_manager.update_event_status(
568
+ EventType.LOADING, lookup_id, status=EventStatus.DONE
569
+ )
570
+ res = task.result()
571
+
572
+ # Calculate total retrieved chunks across all tiers based on actual results
573
+ # from batched_get_non_blocking, not the batched_async_contains results.
574
+ # This handles the case where chunks may be evicted between contains check
575
+ # and actual retrieval.
576
+ #
577
+ # Example: chunk_size=256, 7 chunks total (1792 tokens) across 3 tiers
578
+ # cum_chunk_lengths_total = [0, 256, 512, 768, 1024, 1280, 1536, 1792]
579
+ # tier_expected_chunks = [3, 2, 2] # Tier 0: 3, Tier 1: 2, Tier 2: 2
580
+ #
581
+ # Chunks:
582
+ # [0 1 2 3 4 5 6]
583
+ # |-----| <--- stored in Tier0, tier_expected_chunks[0]==3
584
+ # |---| <--- stored in Tier1, tier_expected_chunks[1]==2
585
+ # |---| <--- stored in Tier2, tier_expected_chunks[2]==2
586
+ #
587
+ # Case 1: All chunks retrieved successfully
588
+ # [0 1 2 3 4 5 6]
589
+ # |-----| <--- Tier0: retrieved 3 chunks (obj0, obj1, obj2)
590
+ # |---| <--- Tier1: retrieved 2 chunks (obj3, obj4)
591
+ # |---| <--- Tier2: retrieved 2 chunks (obj5, obj6)
592
+ # res = [[obj0, obj1, obj2], [obj3, obj4], [obj5, obj6]]
593
+ # total_retrieved_chunks = 7
594
+ # retrieved_length = cum_chunk_lengths_total[7] = 1792
595
+ #
596
+ # Case 2: Tier 1 only got 1 chunk (eviction), Tier 2 got all 2 chunks
597
+ # [0 1 2 3 4 5 6]
598
+ # |-----| <--- Tier0: retrieved 3 chunks (obj0, obj1, obj2)
599
+ # |-|X| <--- Tier1: retrieved 1 chunk (obj3), missing obj4
600
+ # |---| <--- Tier2: retrieved 2 chunks (obj5, obj6) - IGNORED
601
+ # res = [[obj0, obj1, obj2], [obj3], [obj5, obj6]]
602
+ # total_retrieved_chunks = 4 (stop at tier 1, tier 2 chunks ignored)
603
+ # retrieved_length = cum_chunk_lengths_total[4] = 1024
604
+ # Note: Even though tier 2 successfully retrieved 2 chunks, they are
605
+ # not counted because tier 1 has a gap, breaking prefix continuity.
606
+ #
607
+ # Case 3: Tier 0 only got 2 chunks (eviction), other tiers got all
608
+ # [0 1 2 3 4 5 6]
609
+ # |---|X| <--- Tier0: retrieved 2 chunks (obj0, obj1), missing obj2
610
+ # |---| <--- Tier1: retrieved 2 chunks (obj3, obj4) - IGNORED
611
+ # |---| <--- Tier2: retrieved 2 chunks (obj5, obj6) - IGNORED
612
+ # res = [[obj0, obj1], [obj3, obj4], [obj5, obj6]]
613
+ # total_retrieved_chunks = 2 (stop at tier 0, all subsequent ignored)
614
+ # retrieved_length = cum_chunk_lengths_total[2] = 512
615
+ total_retrieved_chunks = 0
616
+ for tier_idx, tier_result in enumerate(res):
617
+ actual_chunks = len(tier_result)
618
+ expected_chunks = tier_expected_chunks[tier_idx]
619
+ total_retrieved_chunks += actual_chunks
620
+
621
+ # If a tier retrieved fewer chunks than expected, we stop counting
622
+ # because subsequent chunks are not contiguous
623
+ if actual_chunks < expected_chunks:
624
+ # Release all chunks in subsequent tiers since they won't be used
625
+ for subsequent_tier in res[tier_idx + 1 :]:
626
+ for mem_obj in subsequent_tier:
627
+ mem_obj.ref_count_down()
628
+ break
629
+
630
+ retrieved_length = cum_chunk_lengths_total[total_retrieved_chunks]
631
+ logger.info(
632
+ f"Responding to scheduler for lookup id {lookup_id}"
633
+ f" with retrieved length {retrieved_length}"
634
+ )
635
+ self.async_lookup_server.send_response_to_scheduler(lookup_id, retrieved_length)
636
+
637
+ async def async_lookup_and_prefetch(
638
+ self,
639
+ lookup_id: str,
640
+ keys: list[CacheEngineKey],
641
+ cum_chunk_lengths: list[int],
642
+ search_range: Optional[list[str]] = None,
643
+ pin: bool = False,
644
+ ) -> None:
645
+ """
646
+ Perform asynchronous lookup and prefetching across all storage backends.
647
+
648
+ :param str lookup_id: The unique id (e.g., request id) for the request.
649
+ :param list[CacheEngineKey] keys: The keys to lookup and prefetch.
650
+ :param list[int] cum_chunk_lengths: The cumulative token lengths of the chunks.
651
+ This is a list where cum_chunk_lengths[i] represents the total number of
652
+ tokens from chunk 0 to chunk i-1 (inclusive).
653
+ Example: If chunk_size=256 and we have 3 chunks:
654
+ - chunk 0: 256 tokens (tokens 0-255)
655
+ - chunk 1: 256 tokens (tokens 256-511)
656
+ - chunk 2: 128 tokens (tokens 512-639)
657
+ Then cum_chunk_lengths = [0, 256, 512, 640]
658
+ Note: len(cum_chunk_lengths) = len(keys) + 1
659
+ :param Optional[list[str]] search_range: The range of storage backends
660
+ to search in. Should be a subset of ["LocalCPUBackend",
661
+ "LocalDiskBackend"] for now. If None, search in all backends.
662
+ :param bool pin: Whether to pin the keys.
663
+ """
664
+
665
+ # NOTE(Jiayi): Currently, the retrieval pattern is always
666
+ # prefix-based. That is, we retrieve 0-t1 tokens from backend 1
667
+ # and retrieve t1-t2 tokens from backend 2, etc. The assumption
668
+ # here is that the suffix chunks are more likely to be evicted
669
+ # than the prefix chunks.
670
+ # TODO(Jiayi): We need to change/optimize this for non-prefix
671
+ # based retrieval patterns or cases where middle chunks are missing.
672
+
673
+ # NOTE(Jiayi): We can tolerate the last tier to have fewer loaded
674
+ # chunks than its lookup result indicated. This is especially helpful
675
+ # for P2PBackend.
676
+
677
+ num_total_chunks = len(keys)
678
+ num_total_hit_chunks = 0
679
+ # cum_chunk_lengths_total: A copy of the original cumulative chunk lengths
680
+ # for all chunks. This is preserved to calculate the final token count
681
+ # based on the actual retrieved chunks.
682
+ # Example: If chunk_size=256 and we have 3 chunks with total 640 tokens:
683
+ # cum_chunk_lengths_total = [0, 256, 512, 640]
684
+ # If we retrieve 2 chunks, the retrieved token count is:
685
+ # cum_chunk_lengths_total[2] = 512 tokens
686
+ cum_chunk_lengths_total = cum_chunk_lengths[:]
687
+ loading_tasks = []
688
+ tier_expected_chunks = []
689
+ # we also keep track of the keys for each tier and each chunk
690
+ loading_task_keys: list[list[CacheEngineKey]] = []
691
+ for backend_name, backend in self.get_active_storage_backends(
692
+ search_range=search_range
693
+ ):
694
+ num_hit_chunks = await backend.batched_async_contains(lookup_id, keys, pin)
695
+
696
+ if num_hit_chunks == 0:
697
+ continue
698
+
699
+ num_total_hit_chunks += num_hit_chunks
700
+ tier_expected_chunks.append(num_hit_chunks)
701
+
702
+ backend_keys = keys[:num_hit_chunks]
703
+ loading_task_keys.append(backend_keys)
704
+
705
+ assert self.async_serializer is not None, (
706
+ "Async serializer must be initialized via post_init before using "
707
+ "async_lookup_and_prefetch."
708
+ )
709
+ # num_hit_chunks is only used for the multi serializer
710
+ get_coro = self.async_serializer.run(
711
+ backend.batched_get_non_blocking(
712
+ lookup_id,
713
+ backend_keys,
714
+ {"cum_chunk_lengths": cum_chunk_lengths[: num_hit_chunks + 1]},
715
+ ),
716
+ num_hit_chunks,
717
+ )
718
+ loading_task = asyncio.create_task(get_coro)
719
+ loading_task.add_done_callback(
720
+ functools.partial(
721
+ self.prefetch_single_done_callback,
722
+ keys=keys,
723
+ backend_name=backend_name,
724
+ )
725
+ )
726
+
727
+ loading_tasks.append(loading_task)
728
+
729
+ cum_chunk_lengths = cum_chunk_lengths[num_hit_chunks:]
730
+
731
+ if num_total_hit_chunks == num_total_chunks:
732
+ break
733
+ keys = keys[num_hit_chunks:]
734
+
735
+ # If no chunks were hit across all backends, respond immediately and return.
736
+ if num_total_hit_chunks == 0:
737
+ if self.async_lookup_server is not None:
738
+ self.async_lookup_server.send_response_to_scheduler(lookup_id, 0)
739
+ return
740
+
741
+ # gather_with_keys() here make a pair of (key, memory_obj) for each chunk
742
+ # in each tier. The all_done result's layout is like following and
743
+ # will be processed in _async_process_tokens_internal()
744
+ # Tier 0:
745
+ # Tuple(loading_task_keys[0][0] : MemoryObj0)
746
+ # Tuple(loading_task_keys[0][1] : MemoryObj1)
747
+ # Tier 1:
748
+ # Tuple(loading_task_keys[1][0] : MemoryObj2)
749
+ # Tuple(loading_task_keys[1][1] : MemoryObj3)
750
+ async def gather_with_keys() -> list[list[tuple[CacheEngineKey, MemoryObj]]]:
751
+ loading_results = await asyncio.gather(*loading_tasks)
752
+ return [
753
+ list(zip(keys, results, strict=False))
754
+ for keys, results in zip(
755
+ loading_task_keys, loading_results, strict=False
756
+ )
757
+ ]
758
+
759
+ all_done = asyncio.create_task(gather_with_keys())
760
+ # Register the event before adding the callback to avoid race conditions
761
+ self.event_manager.add_event(
762
+ EventType.LOADING,
763
+ lookup_id,
764
+ all_done,
765
+ )
766
+
767
+ all_done.add_done_callback(
768
+ lambda future: self.prefetch_all_done_callback(
769
+ future,
770
+ lookup_id,
771
+ cum_chunk_lengths_total,
772
+ tier_expected_chunks,
773
+ )
774
+ )
775
+
776
+ def set_hot_cache(self, enabled: bool) -> None:
777
+ """
778
+ Dynamically enable or disable the hot cache on LocalCPUBackend.
779
+
780
+ When disabled, the existing hot cache entries will be cleared
781
+ and no new data will be written to the hot cache.
782
+
783
+ Args:
784
+ enabled: True to enable hot cache, False to disable
785
+ """
786
+ backend = self.local_cpu_backend
787
+ if not isinstance(backend, LocalCPUBackend):
788
+ logger.warning("Cannot set hot_cache: LocalCPUBackend not available")
789
+ return
790
+
791
+ if not enabled:
792
+ backend.clear()
793
+ backend.use_hot = enabled
794
+ logger.info("LocalCPUBackend hot_cache set to %s", enabled)
795
+
796
+ def is_hot_cache_enabled(self) -> bool:
797
+ """
798
+ Get the current hot cache status of LocalCPUBackend.
799
+
800
+ Returns:
801
+ bool: True if hot cache is enabled, False otherwise
802
+ """
803
+ backend = self.local_cpu_backend
804
+ if not isinstance(backend, LocalCPUBackend):
805
+ return False
806
+ return backend.use_hot
807
+
808
+ def set_freeze(self, enabled: bool) -> None:
809
+ """
810
+ Set freeze mode.
811
+
812
+ When enabled, only local_cpu backend will be used for retrieval.
813
+ """
814
+ with self._freeze_lock:
815
+ self._freeze = enabled
816
+ logger.info("StorageManager freeze mode set to %s", enabled)
817
+
818
+ def is_frozen(self) -> bool:
819
+ """
820
+ Get freeze mode status.
821
+
822
+ Returns:
823
+ bool: True if freeze mode is enabled, False otherwise
824
+ """
825
+ with self._freeze_lock:
826
+ return self._freeze
827
+
828
+ def set_backend_bypass(self, backend_name: str, bypassed: bool) -> None:
829
+ """
830
+ Set bypass mode for a specific backend.
831
+
832
+ When a backend is bypassed:
833
+ - It will be skipped during contains/put/get operations
834
+ - This is typically used when a health check fails with LOCAL_CPU fallback
835
+
836
+ Args:
837
+ backend_name: The name of the backend to bypass (e.g., "RemoteBackend")
838
+ bypassed: True to bypass, False to restore normal operation
839
+ """
840
+ with self._bypass_lock:
841
+ if bypassed:
842
+ self._bypassed_backends.add(backend_name)
843
+ logger.info(f"StorageManager: Backend {backend_name} is now bypassed")
844
+ else:
845
+ self._bypassed_backends.discard(backend_name)
846
+ logger.info(
847
+ f"StorageManager: Backend {backend_name} bypass removed, "
848
+ "restored to normal operation"
849
+ )
850
+
851
+ def is_backend_bypassed(self, backend_name: str) -> bool:
852
+ """
853
+ Check if a backend is currently bypassed.
854
+
855
+ Args:
856
+ backend_name: The name of the backend to check
857
+
858
+ Returns:
859
+ bool: True if the backend is bypassed, False otherwise
860
+ """
861
+ with self._bypass_lock:
862
+ return backend_name in self._bypassed_backends
863
+
864
+ def get_bypassed_backends(self) -> List[str]:
865
+ """
866
+ Get the list of currently bypassed backend names.
867
+
868
+ Returns:
869
+ List[str]: List of bypassed backend names
870
+ """
871
+ with self._bypass_lock:
872
+ return list(self._bypassed_backends)
873
+
874
+ def get_all_backend_names(self) -> List[str]:
875
+ """
876
+ Get the list of all registered backend names.
877
+
878
+ Returns:
879
+ List[str]: List of all backend names
880
+ """
881
+ return list(self.storage_backends.keys())
882
+
883
+ def contains(
884
+ self,
885
+ key: CacheEngineKey,
886
+ search_range: Optional[List[str]] = None,
887
+ pin: bool = False,
888
+ ) -> Optional[str]:
889
+ """
890
+ Check whether the key exists in the storage backend.
891
+
892
+ :param CacheEngineKey key: The key to check.
893
+
894
+ :param Optional[List[str]] search_range: The range of storage backends
895
+ to search in. Should be a subset of ["LocalCPUBackend",
896
+ "LocalDiskBackend"] for now.
897
+ If None, search in all backends.
898
+
899
+ :param bool pin: Whether to pin the key.
900
+
901
+ return: True if the key exists in the specified storage backends.
902
+ """
903
+
904
+ for backend_name, backend in self.get_active_storage_backends(
905
+ search_range=search_range
906
+ ):
907
+ # NOTE(Jiayi): We do not pin for PDBackend
908
+ pin_in_backend = pin if backend_name != "PDBackend" else False
909
+
910
+ if backend.contains(key, pin_in_backend):
911
+ return backend_name
912
+
913
+ return None
914
+
915
+ def batched_contains(
916
+ self,
917
+ keys: List[CacheEngineKey],
918
+ search_range: Optional[List[str]] = None,
919
+ pin: bool = False,
920
+ ) -> tuple[int, dict]:
921
+ """
922
+ Check whether the key exists in the storage backend.
923
+
924
+ :param List[CacheEngineKey] keys: The keys to check.
925
+
926
+ :param Optional[List[str]] search_range: The range of storage backends
927
+ to search in. Should be a subset of ["LocalCPUBackend",
928
+ "LocalDiskBackend"] for now.
929
+ If None, search in all backends.
930
+
931
+ :param bool pin: Whether to pin the key.
932
+
933
+ return: Return hit chunks and block mapping by prefix match.
934
+ """
935
+ total_keys = len(keys)
936
+ total_hit_chunks = 0
937
+ block_mapping = {}
938
+ for backend_name, backend in self.get_active_storage_backends(
939
+ search_range=search_range
940
+ ):
941
+ # NOTE(Jiayi): We do not pin for PDBackend
942
+ pin_in_backend = pin if backend_name != "PDBackend" else False
943
+
944
+ hit_chunks = backend.batched_contains(keys, pin_in_backend)
945
+ if hit_chunks == 0:
946
+ continue
947
+ block_mapping[backend_name] = keys[:hit_chunks]
948
+ total_hit_chunks += hit_chunks
949
+ if total_hit_chunks == total_keys:
950
+ break
951
+ keys = keys[hit_chunks:]
952
+
953
+ return total_hit_chunks, block_mapping
954
+
955
+ def get_block_mapping(
956
+ self, chunk_infos: List[Tuple[CacheEngineKey, int, int]]
957
+ ) -> Dict[str, List[Tuple[CacheEngineKey, int, int]]]:
958
+ """
959
+ Get block mapping for the given chunk infos, works by prefix match.
960
+
961
+ :param List[Tuple[CacheEngineKey, int, int]] chunk_infos:
962
+ List of chunk infos, each tuple contains (key, begin, end)
963
+
964
+ :return: Dict[str, List[Tuple[CacheEngineKey, int, int]]]:
965
+ Block mapping for the given chunk infos, each key is the backend name,
966
+ each value is a list of chunk infos in the backend.
967
+ """
968
+ keys = [chunk_info[0] for chunk_info in chunk_infos]
969
+ total_keys = len(keys)
970
+ block_mapping = {}
971
+ total_hit_chunks = 0
972
+ for backend_name, backend in self.get_active_storage_backends():
973
+ hit_chunks = backend.batched_contains(keys)
974
+ if hit_chunks == 0:
975
+ continue
976
+ block_mapping[backend_name] = chunk_infos[
977
+ total_hit_chunks : total_hit_chunks + hit_chunks
978
+ ]
979
+ total_hit_chunks += hit_chunks
980
+ if total_hit_chunks == total_keys:
981
+ break
982
+ keys = keys[hit_chunks:]
983
+ return block_mapping
984
+
985
+ def touch_cache(self) -> None:
986
+ for backend_name, backend in self.storage_backends.items():
987
+ if backend_name == "LocalCPUBackend" or backend_name == "LocalDiskBackend":
988
+ backend.touch_cache()
989
+
990
+ def remove(
991
+ self,
992
+ key: CacheEngineKey,
993
+ locations: Optional[List[str]] = None,
994
+ ) -> int:
995
+ """
996
+ Remove the key and the corresponding cache in the specified
997
+ locations.
998
+
999
+ :param CacheEngineKey key: The key to remove.
1000
+
1001
+ :param Optional[List[str]] locations: The range of storage backends
1002
+ to perform `remove` in.
1003
+ Should be a subset of ["LocalCPUBackend", "LocalDiskBackend"] for now.
1004
+ If None, perform `remove` in all backends.
1005
+
1006
+ return: Total number of removed caches in the specified
1007
+ storage backends.
1008
+ """
1009
+
1010
+ num_removed = 0
1011
+ for backend_name, backend in self.storage_backends.items():
1012
+ # TODO(Jiayi): need to handle remove in non-cpu backends
1013
+ if locations is None or backend_name in locations:
1014
+ num_removed += backend.remove(key)
1015
+
1016
+ return num_removed
1017
+
1018
+ def batched_remove(
1019
+ self,
1020
+ keys: List[CacheEngineKey],
1021
+ locations: Optional[List[str]] = None,
1022
+ ) -> int:
1023
+ """
1024
+ Batched remove the keys and the corresponding cache in the specified
1025
+ locations.
1026
+
1027
+ :param List[CacheEngineKey] keys: The keys to remove.
1028
+
1029
+ :param Optional[List[str]] locations: The range of storage backends
1030
+ to perform `remove` in.
1031
+ Should be a subset of ["LocalCPUBackend", "LocalDiskBackend"] for now.
1032
+ If None, perform `remove` in all backends.
1033
+
1034
+ return: Total number of removed caches in the specified
1035
+ storage backends.
1036
+ """
1037
+ num_removed = 0
1038
+ for backend_name, backend in self.storage_backends.items():
1039
+ if locations is None or backend_name in locations:
1040
+ num_removed += backend.batched_remove(keys)
1041
+
1042
+ return num_removed
1043
+
1044
+ def batched_unpin(
1045
+ self,
1046
+ keys: List[CacheEngineKey],
1047
+ locations: Optional[List[str]] = None,
1048
+ ) -> None:
1049
+ """
1050
+ Unpin the keys in the specified locations.
1051
+
1052
+ :param List[CacheEngineKey] keys: The keys to unpin.
1053
+
1054
+ :param Optional[List[str]] locations: The range of storage backends
1055
+ to perform `unpin` in.
1056
+ Should be a subset of ["LocalCPUBackend", "LocalDiskBackend"] for now.
1057
+ If None, perform `unpin` in all backends.
1058
+ """
1059
+ for backend_name, backend in self.storage_backends.items():
1060
+ if locations is None or backend_name in locations:
1061
+ for key in keys:
1062
+ backend.unpin(key)
1063
+
1064
+ def clear(
1065
+ self,
1066
+ locations: Optional[List[str]] = None,
1067
+ ) -> int:
1068
+ """
1069
+ Clear all caches in the specified locations.
1070
+
1071
+ :param Optional[List[str]] locations: The range of storage backends
1072
+ to perform `clear` in.
1073
+ Should be a subset of ["LocalCPUBackend", "LocalDiskBackend"] for now.
1074
+ If None, perform `clear` in all backends.
1075
+
1076
+ return: Total number of cleared tokens in the specified
1077
+ storage backends.
1078
+ """
1079
+
1080
+ num_cleared_tokens = 0
1081
+ for backend_name, backend in self.storage_backends.items():
1082
+ # TODO(Jiayi): need to handle remove in non-cpu backends
1083
+ if locations is None or backend_name in locations:
1084
+ if hasattr(backend, "clear"):
1085
+ num_cleared_tokens += backend.clear()
1086
+ else:
1087
+ logger.warning(
1088
+ f"Storage backend {backend_name} does not support "
1089
+ "clear operation. Skipping."
1090
+ )
1091
+
1092
+ return num_cleared_tokens
1093
+
1094
+ def memcheck(self) -> bool:
1095
+ """
1096
+ Check the integrity of the underlying storage backend's
1097
+ memory allocators
1098
+
1099
+ Returns:
1100
+ True if everything is good otherwise False
1101
+ """
1102
+ for backend in self.storage_backends.values():
1103
+ if not isinstance(backend, AllocatorBackendInterface):
1104
+ continue
1105
+ if not backend.get_memory_allocator().memcheck():
1106
+ return False
1107
+ return True
1108
+
1109
+ def get_active_storage_backends(
1110
+ self,
1111
+ location: Optional[str] = None,
1112
+ search_range: Optional[List[str]] = None,
1113
+ ) -> Generator[Tuple[str, StorageBackendInterface], None, None]:
1114
+ """
1115
+ Get the active storage backends based on freeze mode, bypass mode, and filters.
1116
+
1117
+ :param Optional[str] location: If specified, only yield backends
1118
+ matching this exact name.
1119
+ :param Optional[List[str]] search_range: If specified, only yield
1120
+ backends whose names are in this list.
1121
+
1122
+ :return: Generator of (backend_name, backend) tuples.
1123
+ """
1124
+ for backend_name, backend in self.storage_backends.items():
1125
+ # In freeze mode, only use local_cpu backend
1126
+ with self._freeze_lock:
1127
+ if self._freeze and backend_name != "LocalCPUBackend":
1128
+ continue
1129
+ # Skip bypassed backends
1130
+ with self._bypass_lock:
1131
+ if backend_name in self._bypassed_backends:
1132
+ continue
1133
+ if location and backend_name != location:
1134
+ continue
1135
+ if search_range and backend_name not in search_range:
1136
+ continue
1137
+ yield backend_name, backend
1138
+
1139
+ def get_non_allocator_backends(self) -> List[str]:
1140
+ """
1141
+ Get the names of the actual storage backends. Some backends,
1142
+ such as LocalCPUBackend and PDBackend, in some cases, only
1143
+ serve as a backend for allocation.
1144
+ """
1145
+ storage_names = []
1146
+ for backend_name, backend in self.storage_backends.items():
1147
+ if "LocalCPUBackend" == backend_name and not self.config.local_cpu:
1148
+ # if local_cpu is False, means LocalCPUBackend is only a allocator
1149
+ continue
1150
+ if "PDBackend" == backend_name and backend.pd_config.role == "sender": # type: ignore
1151
+ # if pd_config.role is sender, means PDBackend is only a allocator
1152
+ continue
1153
+ storage_names.append(backend_name)
1154
+ return storage_names
1155
+
1156
+ def list_backends(self) -> Dict[str, str]:
1157
+ """
1158
+ List all active storage backends.
1159
+
1160
+ Returns:
1161
+ Dict mapping backend name to its class name.
1162
+ """
1163
+ with self.manager_lock:
1164
+ return {
1165
+ name: type(backend).__name__
1166
+ for name, backend in self.storage_backends.items()
1167
+ }
1168
+
1169
+ def close_backend(self, backend_name: str) -> bool:
1170
+ """
1171
+ Close and remove a specific storage backend by name.
1172
+
1173
+ The backend will be closed and removed from the internal
1174
+ dict so that no stale references remain.
1175
+
1176
+ Args:
1177
+ backend_name: The name of the backend to close.
1178
+
1179
+ Returns:
1180
+ True if the backend was found and closed, False
1181
+ otherwise.
1182
+ """
1183
+ with self.manager_lock:
1184
+ backend = self.storage_backends.get(backend_name)
1185
+ if backend is None:
1186
+ logger.warning(
1187
+ "Backend %s not found, cannot close",
1188
+ backend_name,
1189
+ )
1190
+ return False
1191
+
1192
+ try:
1193
+ logger.info("Closing backend: %s", backend_name)
1194
+ backend.close()
1195
+ except Exception:
1196
+ logger.exception("Error closing backend %s", backend_name)
1197
+
1198
+ del self.storage_backends[backend_name]
1199
+
1200
+ # Update derived references
1201
+ self.non_allocator_backends = self.get_non_allocator_backends()
1202
+ if backend_name == "LocalCPUBackend":
1203
+ self.local_cpu_backend = None
1204
+ logger.info("Backend %s closed and removed", backend_name)
1205
+ return True
1206
+
1207
+ def create_backends(self) -> Dict[str, str]:
1208
+ """
1209
+ Create new storage backends based on current config.
1210
+
1211
+ Backends that are already present will be skipped
1212
+ **before** instantiation so that no unnecessary
1213
+ resources are allocated. This allows callers to close
1214
+ a subset of backends, update config via ``/conf``,
1215
+ and then call this method to bring up only the missing
1216
+ backends.
1217
+
1218
+ Returns:
1219
+ Dict mapping newly created backend name to its
1220
+ class name.
1221
+ """
1222
+ with self.manager_lock:
1223
+ existing_names = set(self.storage_backends)
1224
+ new_backends = CreateStorageBackends(
1225
+ self.config,
1226
+ self.metadata,
1227
+ self.loop,
1228
+ dst_device=("cuda" if is_cuda_worker(self.metadata) else "cpu"),
1229
+ lmcache_worker=self.lmcache_worker,
1230
+ skip_backends=existing_names,
1231
+ existing_backends=self.storage_backends,
1232
+ )
1233
+
1234
+ created: Dict[str, str] = {}
1235
+ for name, backend in new_backends.items():
1236
+ self.storage_backends[name] = backend
1237
+ created[name] = type(backend).__name__
1238
+ logger.info(
1239
+ "Created backend: %s (%s)",
1240
+ name,
1241
+ created[name],
1242
+ )
1243
+
1244
+ # Refresh derived references
1245
+ self.non_allocator_backends = self.get_non_allocator_backends()
1246
+ cpu = self.storage_backends.get("LocalCPUBackend")
1247
+ if cpu is not None:
1248
+ self.local_cpu_backend = cpu
1249
+
1250
+ return created
1251
+
1252
+ def recreate_backend(self, backend_name: str) -> Dict[str, str]:
1253
+ """
1254
+ Close a backend and recreate it from current config.
1255
+
1256
+ This is an atomic close-then-create operation that
1257
+ combines :meth:`close_backend` and :meth:`create_backends`
1258
+ into a single step.
1259
+
1260
+ Args:
1261
+ backend_name: Name of the backend to recreate
1262
+ (e.g. ``RemoteBackend``).
1263
+
1264
+ Returns:
1265
+ Dict mapping newly created backend name to its
1266
+ class name.
1267
+
1268
+ Raises:
1269
+ KeyError: If *backend_name* does not exist.
1270
+ """
1271
+ with self.manager_lock:
1272
+ backend = self.storage_backends.get(backend_name)
1273
+ if backend is None:
1274
+ raise KeyError("Backend %s not found" % backend_name)
1275
+
1276
+ # --- close ---
1277
+ try:
1278
+ logger.info("Closing backend: %s", backend_name)
1279
+ backend.close()
1280
+ except Exception:
1281
+ logger.exception("Error closing backend %s", backend_name)
1282
+ del self.storage_backends[backend_name]
1283
+
1284
+ # --- create ---
1285
+ existing_names = set(self.storage_backends)
1286
+ new_backends = CreateStorageBackends(
1287
+ self.config,
1288
+ self.metadata,
1289
+ self.loop,
1290
+ dst_device=("cuda" if is_cuda_worker(self.metadata) else "cpu"),
1291
+ lmcache_worker=self.lmcache_worker,
1292
+ skip_backends=existing_names,
1293
+ existing_backends=self.storage_backends,
1294
+ )
1295
+
1296
+ created: Dict[str, str] = {}
1297
+ for name, be in new_backends.items():
1298
+ self.storage_backends[name] = be
1299
+ created[name] = type(be).__name__
1300
+ logger.info(
1301
+ "Recreated backend: %s (%s)",
1302
+ name,
1303
+ created[name],
1304
+ )
1305
+
1306
+ # Refresh derived references
1307
+ self.non_allocator_backends = self.get_non_allocator_backends()
1308
+ cpu = self.storage_backends.get("LocalCPUBackend")
1309
+ if cpu is not None:
1310
+ self.local_cpu_backend = cpu
1311
+ elif backend_name == "LocalCPUBackend":
1312
+ self.local_cpu_backend = None
1313
+
1314
+ return created
1315
+
1316
+ def close(self):
1317
+ logger.info("Closing StorageManager...")
1318
+
1319
+ # Close all backends
1320
+ for name, backend in self.storage_backends.items():
1321
+ try:
1322
+ logger.info(f"Closing storage backend: {name}")
1323
+ backend.close()
1324
+ logger.info(f"Storage backend {name} closed successfully")
1325
+ except Exception as e:
1326
+ logger.error(f"Error closing backend {name}: {e}")
1327
+
1328
+ # Stop event loop
1329
+ try:
1330
+ if self.loop.is_running():
1331
+ logger.info("Stopping event loop...")
1332
+ self.loop.call_soon_threadsafe(self.loop.stop)
1333
+ logger.info("Event loop stop signaled")
1334
+ except Exception as e:
1335
+ logger.error(f"Error stopping event loop: {e}")
1336
+
1337
+ # Wait for thread with timeout
1338
+ if self.thread.is_alive():
1339
+ logger.info("Waiting for storage manager thread to finish...")
1340
+ self.thread.join(timeout=10.0)
1341
+
1342
+ if self.thread.is_alive():
1343
+ logger.warning(
1344
+ "Storage manager thread did not terminate within 10s timeout. "
1345
+ "Proceeding with shutdown anyway."
1346
+ )
1347
+ else:
1348
+ logger.info("Storage manager thread terminated successfully")
1349
+ else:
1350
+ logger.info("Storage manager thread already stopped")
1351
+
1352
+ logger.info("Storage manager closed.")