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,1400 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2024-2025 LMCache Authors.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # Standard
17
+ from abc import ABC, abstractmethod
18
+ from dataclasses import dataclass
19
+ from typing import Any, Callable, List, Optional, Sequence, Set, Union, cast
20
+ from urllib.parse import quote as url_quote
21
+ import asyncio
22
+ import os
23
+ import threading
24
+ import time
25
+ import uuid
26
+
27
+ # Third Party
28
+ from nixl._api import nixl_agent as NixlAgent
29
+ from nixl._api import nixl_agent_config as NixlAgentConfig
30
+ from nixl._api import nixl_prepped_dlist_handle as NixlDlistHandle
31
+ from nixl._api import nixl_xfer_handle as NixlXferHandle
32
+ from nixl._api import (
33
+ nixlBind,
34
+ )
35
+ import torch
36
+
37
+ # First Party
38
+ from lmcache.logging import init_logger
39
+ from lmcache.utils import CacheEngineKey
40
+ from lmcache.v1.config import LMCacheEngineConfig
41
+ from lmcache.v1.memory_management import (
42
+ MemoryFormat,
43
+ MemoryObj,
44
+ MemoryObjMetadata,
45
+ PagedTensorMemoryAllocator,
46
+ _allocate_cpu_memory,
47
+ _allocate_gpu_memory,
48
+ _free_cpu_memory,
49
+ )
50
+ from lmcache.v1.metadata import LMCacheMetadata
51
+ from lmcache.v1.storage_backend.abstract_backend import AllocatorBackendInterface
52
+ from lmcache.v1.storage_backend.cache_policy import get_cache_policy
53
+ from lmcache.v1.transfer_channel.transfer_utils import get_correct_device
54
+
55
+ logger = init_logger(__name__)
56
+
57
+
58
+ @dataclass
59
+ class NixlStorageConfig:
60
+ buffer_size: int
61
+ pool_size: int
62
+ buffer_device: str
63
+ backend: str
64
+ backend_params: dict[str, str]
65
+ dynamic_storage: bool
66
+ enable_presence_cache: bool
67
+ enable_async_put: bool
68
+ use_direct_io: bool
69
+ path: str
70
+
71
+ @staticmethod
72
+ def validate_nixl_backend(dynamic_storage: bool, backend: str, device: str):
73
+ if dynamic_storage: # For now only supports OBJ backend
74
+ if backend in ("OBJ",):
75
+ return device == "cpu" or device == "cuda"
76
+ else:
77
+ return False
78
+ else:
79
+ if backend in ("GDS", "GDS_MT", "OBJ"):
80
+ return device == "cpu" or device == "cuda"
81
+ elif backend in ("POSIX", "HF3FS"):
82
+ return device == "cpu"
83
+ else:
84
+ return False
85
+
86
+ @staticmethod
87
+ def from_cache_engine_config(
88
+ config: LMCacheEngineConfig, metadata: LMCacheMetadata
89
+ ):
90
+ assert config.nixl_buffer_size is not None
91
+ assert config.nixl_buffer_device is not None
92
+
93
+ extra_config = config.extra_config
94
+ assert extra_config is not None
95
+ assert extra_config.get("enable_nixl_storage")
96
+
97
+ enable_presence_cache = extra_config.get("nixl_presence_cache", False)
98
+ enable_async_put = extra_config.get("nixl_async_put", False)
99
+ backend_params = extra_config.get("nixl_backend_params", {})
100
+ use_direct_io = extra_config.get("use_direct_io", False)
101
+ pool_size = extra_config.get("nixl_pool_size")
102
+ backend = extra_config.get("nixl_backend")
103
+ path = extra_config.get("nixl_path")
104
+
105
+ assert pool_size is not None
106
+ assert backend is not None
107
+ assert use_direct_io in [False, True]
108
+
109
+ dynamic_storage = pool_size == 0
110
+ if dynamic_storage:
111
+ assert not config.save_unfull_chunk, (
112
+ "save_unfull_chunk should be set to False when using dynamic storage"
113
+ )
114
+
115
+ corrected_device = get_correct_device(
116
+ config.nixl_buffer_device, metadata.worker_id
117
+ )
118
+
119
+ assert NixlStorageConfig.validate_nixl_backend(
120
+ dynamic_storage, backend, config.nixl_buffer_device
121
+ ), "Invalid NIXL backend & device combination"
122
+
123
+ return NixlStorageConfig(
124
+ buffer_size=config.nixl_buffer_size,
125
+ pool_size=pool_size,
126
+ buffer_device=corrected_device,
127
+ backend=backend,
128
+ backend_params=backend_params,
129
+ dynamic_storage=dynamic_storage,
130
+ enable_presence_cache=enable_presence_cache,
131
+ enable_async_put=enable_async_put,
132
+ use_direct_io=use_direct_io,
133
+ path=path,
134
+ )
135
+
136
+
137
+ class NixlDescPool(ABC):
138
+ def __init__(self, size: int):
139
+ self.lock = threading.Lock()
140
+ self.size: int = size
141
+ self.indices: List[int] = []
142
+ self.indices.extend(reversed(range(size)))
143
+
144
+ def get_num_available_descs(self) -> int:
145
+ with self.lock:
146
+ return len(self.indices)
147
+
148
+ def pop(self) -> int:
149
+ with self.lock:
150
+ assert len(self.indices) > 0
151
+ return self.indices.pop()
152
+
153
+ def push(self, index: int):
154
+ with self.lock:
155
+ assert len(self.indices) < self.size
156
+ self.indices.append(index)
157
+
158
+ @abstractmethod
159
+ def close(self):
160
+ pass
161
+
162
+
163
+ class NixlFilePool(NixlDescPool):
164
+ def __init__(self, size: int, path: str, use_direct_io: bool):
165
+ super().__init__(size)
166
+ self.fds: List[int] = []
167
+
168
+ assert path is not None
169
+
170
+ flags = os.O_CREAT | os.O_RDWR
171
+ if use_direct_io:
172
+ if hasattr(os, "O_DIRECT"):
173
+ flags |= os.O_DIRECT
174
+ else:
175
+ logger.warning(
176
+ "use_direct_io is True, but O_DIRECT is not available on "
177
+ "this system. Falling back to buffered I/O."
178
+ )
179
+ for i in reversed(range(size)):
180
+ filename = f"obj_{i}_{uuid.uuid4().hex[0:4]}.bin"
181
+ tmp_path = os.path.join(path, filename)
182
+ fd = os.open(tmp_path, flags)
183
+ self.fds.append(fd)
184
+
185
+ def close(self):
186
+ # TODO: do we need to delete the files?
187
+ with self.lock:
188
+ assert len(self.fds) == self.size
189
+ for fd in self.fds:
190
+ os.close(fd)
191
+
192
+
193
+ class NixlObjectPool(NixlDescPool):
194
+ def __init__(self, size: int):
195
+ super().__init__(size)
196
+ self.keys: List[str] = []
197
+
198
+ for i in reversed(range(size)):
199
+ key = f"obj_{i}_{uuid.uuid4().hex[0:4]}"
200
+ self.keys.append(key)
201
+
202
+ def close(self):
203
+ pass
204
+
205
+
206
+ class SetPresenceCache:
207
+ """Default presence cache using a thread-safe Python set."""
208
+
209
+ def __init__(self) -> None:
210
+ self._keys: set[int] = set()
211
+
212
+ def add(self, key: int) -> None:
213
+ self._keys.add(key)
214
+
215
+ def discard(self, key: int) -> None:
216
+ self._keys.discard(key)
217
+
218
+ def contains(self, key: int) -> bool:
219
+ return key in self._keys
220
+
221
+
222
+ PresenceCache = Union[SetPresenceCache]
223
+
224
+
225
+ @dataclass
226
+ class NixlDesc:
227
+ device_id: int
228
+ meta_info: str
229
+
230
+
231
+ @dataclass
232
+ class NixlKeyMetadata:
233
+ index: int
234
+ shape: Optional[torch.Size] = None
235
+ dtype: Optional[torch.dtype] = None
236
+ fmt: Optional[MemoryFormat] = None
237
+ pin_count: int = 0
238
+
239
+ def pin(self) -> bool:
240
+ self.pin_count += 1
241
+ return True
242
+
243
+ def unpin(self) -> bool:
244
+ self.pin_count -= 1
245
+ return True
246
+
247
+ @property
248
+ def is_pinned(self) -> bool:
249
+ return self.pin_count > 0
250
+
251
+ @property
252
+ def can_evict(self) -> bool:
253
+ """
254
+ Check if the related key can be evicted.
255
+ """
256
+ return not self.is_pinned
257
+
258
+
259
+ class NixlStorageAgent(ABC):
260
+ agent_name: str
261
+ nixl_agent: NixlAgent
262
+ mem_reg_descs: nixlBind.nixlRegDList
263
+ mem_xfer_handler: NixlDlistHandle
264
+
265
+ def __init__(
266
+ self,
267
+ allocator: PagedTensorMemoryAllocator,
268
+ device: str,
269
+ backend: str,
270
+ backend_params: dict[str, str],
271
+ ):
272
+ buffer_ptr = allocator.buffer_ptr
273
+ buffer_size = allocator.buffer_size
274
+ page_size = allocator.align_bytes
275
+
276
+ self.backend = backend
277
+ self.agent_name = "NixlAgent_" + str(uuid.uuid4())
278
+ nixl_conf = NixlAgentConfig(backends=[])
279
+ self.nixl_agent = NixlAgent(self.agent_name, nixl_conf)
280
+ self.nixl_agent.create_backend(backend, backend_params)
281
+
282
+ device_id = torch.cuda.current_device()
283
+ self.init_mem_handlers(device, buffer_ptr, buffer_size, page_size, device_id)
284
+
285
+ def init_mem_handlers(self, device, buffer_ptr, buffer_size, page_size, device_id):
286
+ # Break the registration into page size chunks to ensure the maximum buffer size
287
+ # of the underlying plugin is not exceeded
288
+ reg_list = [
289
+ (base_addr, page_size, device_id, "")
290
+ for base_addr in range(buffer_ptr, buffer_ptr + buffer_size, page_size)
291
+ ]
292
+ xfer_desc = [
293
+ (base_addr, page_size, device_id)
294
+ for base_addr in range(buffer_ptr, buffer_ptr + buffer_size, page_size)
295
+ ]
296
+
297
+ if device == "cpu":
298
+ mem_type = "DRAM"
299
+ else:
300
+ mem_type = "VRAM"
301
+
302
+ reg_descs = self.nixl_agent.register_memory(reg_list, mem_type=mem_type)
303
+ xfer_descs = self.nixl_agent.get_xfer_descs(xfer_desc, mem_type=mem_type)
304
+ xfer_handler = self.nixl_agent.prep_xfer_dlist(
305
+ "", xfer_descs, mem_type=mem_type
306
+ )
307
+
308
+ self.mem_reg_descs = reg_descs
309
+ self.mem_xfer_handler = xfer_handler
310
+
311
+ def get_mem_to_storage_handle(
312
+ self, mem_indices, storage_xfer_handler, storage_indices
313
+ ) -> NixlXferHandle:
314
+ return self.nixl_agent.make_prepped_xfer(
315
+ "WRITE",
316
+ self.mem_xfer_handler,
317
+ mem_indices,
318
+ storage_xfer_handler,
319
+ storage_indices,
320
+ )
321
+
322
+ def get_storage_to_mem_handle(
323
+ self, mem_indices, storage_xfer_handler, storage_indices
324
+ ) -> NixlXferHandle:
325
+ return self.nixl_agent.make_prepped_xfer(
326
+ "READ",
327
+ self.mem_xfer_handler,
328
+ mem_indices,
329
+ storage_xfer_handler,
330
+ storage_indices,
331
+ )
332
+
333
+ def post_blocking(self, handle: NixlXferHandle):
334
+ state = self.nixl_agent.transfer(handle)
335
+
336
+ while state != "DONE" and state != "ERR":
337
+ try:
338
+ state = self.nixl_agent.check_xfer_state(handle)
339
+ except nixlBind.nixlBackendError:
340
+ raise
341
+
342
+ if state == "ERR":
343
+ raise RuntimeError("NIXL transfer failed")
344
+
345
+ def release_handle(self, handle):
346
+ self.nixl_agent.release_xfer_handle(handle)
347
+
348
+ @abstractmethod
349
+ def close(self):
350
+ pass
351
+
352
+
353
+ class NixlStaticStorageAgent(NixlStorageAgent):
354
+ pool: NixlDescPool
355
+ storage_reg_descs: nixlBind.nixlRegDList
356
+ storage_xfer_descs: nixlBind.nixlXferDList
357
+ storage_xfer_handler: NixlDlistHandle
358
+
359
+ def __init__(
360
+ self,
361
+ allocator: PagedTensorMemoryAllocator,
362
+ pool: NixlDescPool,
363
+ device: str,
364
+ backend: str,
365
+ backend_params: dict[str, str],
366
+ ):
367
+ super().__init__(allocator, device, backend, backend_params)
368
+
369
+ page_size = allocator.align_bytes
370
+
371
+ if isinstance(pool, NixlFilePool):
372
+ self.init_storage_handlers_file(page_size, pool.fds)
373
+ elif isinstance(pool, NixlObjectPool):
374
+ self.init_storage_handlers_object(page_size, pool.keys)
375
+ else:
376
+ raise TypeError(f"Unsupported pool type: {type(pool).__name__}")
377
+
378
+ def init_storage_handlers_file(self, page_size, fds):
379
+ reg_list = []
380
+ xfer_desc = []
381
+ for fd in fds:
382
+ reg_list.append((0, page_size, fd, ""))
383
+ xfer_desc.append((0, page_size, fd))
384
+ reg_descs = self.nixl_agent.register_memory(reg_list, mem_type="FILE")
385
+ xfer_descs = self.nixl_agent.get_xfer_descs(xfer_desc, mem_type="FILE")
386
+ xfer_handler = self.nixl_agent.prep_xfer_dlist(
387
+ self.agent_name, xfer_desc, mem_type="FILE"
388
+ )
389
+
390
+ self.storage_reg_descs = reg_descs
391
+ self.storage_xfer_descs = xfer_descs
392
+ self.storage_xfer_handler = xfer_handler
393
+
394
+ def init_storage_handlers_object(self, page_size, keys):
395
+ reg_list = []
396
+ xfer_desc = []
397
+ for i, key in enumerate(keys):
398
+ reg_list.append((0, page_size, i, key))
399
+ xfer_desc.append((0, page_size, i))
400
+ reg_descs = self.nixl_agent.register_memory(reg_list, mem_type="OBJ")
401
+ xfer_descs = self.nixl_agent.get_xfer_descs(xfer_desc, mem_type="OBJ")
402
+ xfer_handler = self.nixl_agent.prep_xfer_dlist(
403
+ self.agent_name, xfer_desc, mem_type="OBJ"
404
+ )
405
+
406
+ self.storage_reg_descs = reg_descs
407
+ self.storage_xfer_descs = xfer_descs
408
+ self.storage_xfer_handler = xfer_handler
409
+
410
+ def get_mem_to_storage_handle(self, mem_indices, storage_indices) -> NixlXferHandle: # type: ignore[override]
411
+ return super().get_mem_to_storage_handle(
412
+ mem_indices, self.storage_xfer_handler, storage_indices
413
+ )
414
+
415
+ def get_storage_to_mem_handle(self, mem_indices, storage_indices) -> NixlXferHandle: # type: ignore[override]
416
+ return super().get_storage_to_mem_handle(
417
+ mem_indices, self.storage_xfer_handler, storage_indices
418
+ )
419
+
420
+ def close(self):
421
+ self.nixl_agent.release_dlist_handle(self.storage_xfer_handler)
422
+ self.nixl_agent.release_dlist_handle(self.mem_xfer_handler)
423
+ self.nixl_agent.deregister_memory(self.storage_reg_descs)
424
+ self.nixl_agent.deregister_memory(self.mem_reg_descs)
425
+
426
+
427
+ class NixlDynamicStorageAgent(NixlStorageAgent):
428
+ def __init__(
429
+ self,
430
+ allocator: PagedTensorMemoryAllocator,
431
+ device: str,
432
+ backend: str,
433
+ backend_params: dict[str, str],
434
+ ):
435
+ super().__init__(allocator, device, backend, backend_params)
436
+
437
+ if backend == "OBJ":
438
+ self.mem_type = "OBJ"
439
+ else:
440
+ # Already validated in validate_nixl_backend
441
+ raise ValueError(f"unexpected backend: {backend}")
442
+
443
+ def create_batched_storage_handler(self, descs: list[NixlDesc], page_size: int):
444
+ reg_list = []
445
+ xfer_desc = []
446
+
447
+ for i in range(len(descs)):
448
+ reg_list.append((0, page_size, descs[i].device_id, descs[i].meta_info))
449
+ xfer_desc.append((0, page_size, descs[i].device_id))
450
+
451
+ reg_descs = self.nixl_agent.register_memory(reg_list, self.mem_type)
452
+ xfer_descs = self.nixl_agent.get_xfer_descs(xfer_desc, self.mem_type)
453
+ xfer_handler = self.nixl_agent.prep_xfer_dlist(
454
+ self.agent_name, xfer_descs, mem_type=self.mem_type
455
+ )
456
+ return reg_descs, xfer_handler
457
+
458
+ def post_async(self, handle: NixlXferHandle):
459
+ """Non-blocking async post for WRITE operations."""
460
+ state = self.nixl_agent.transfer(handle)
461
+ return state
462
+
463
+ def release_storage_handler(self, reg_descs, xfer_handler):
464
+ """Release storage handler resources"""
465
+ self.nixl_agent.release_dlist_handle(xfer_handler)
466
+ self.nixl_agent.deregister_memory(reg_descs)
467
+
468
+ def nixl_desc_exists(self, meta_info: str) -> bool:
469
+ reg_list = [(0, 0, 0, meta_info)]
470
+ try:
471
+ resp = self.nixl_agent.query_memory(
472
+ reg_list, self.backend, mem_type=self.mem_type
473
+ )
474
+ # nixl api query_memory returns a list of nixlRegDesc
475
+ if resp[0] is None:
476
+ return False
477
+ return True
478
+ except Exception as exc:
479
+ logger.warning(f"NIXL Desc {meta_info} query failed: {exc}")
480
+ return False
481
+
482
+ def close(self):
483
+ self.nixl_agent.release_dlist_handle(self.mem_xfer_handler)
484
+ self.nixl_agent.deregister_memory(self.mem_reg_descs)
485
+
486
+
487
+ class NixlStorageBackend(AllocatorBackendInterface, ABC):
488
+ """
489
+ Implementation of the StorageBackendInterface for Nixl.
490
+
491
+ Currently, the put is synchronized and blocking, to simplify the
492
+ implementation.
493
+ """
494
+
495
+ def __init__(
496
+ self,
497
+ nixl_config: NixlStorageConfig,
498
+ config: LMCacheEngineConfig,
499
+ metadata: LMCacheMetadata,
500
+ loop: asyncio.AbstractEventLoop,
501
+ ):
502
+ """
503
+ Initialize the Nixl storage backend.
504
+
505
+ :param dst_device: the device where the blocking retrieved KV is stored,
506
+ could be either "cpu", "cuda", or "cuda:0", "cuda:1", etc.
507
+ """
508
+ super().__init__(dst_device=nixl_config.buffer_device)
509
+
510
+ self.loop = loop
511
+ self.key_lock = threading.RLock()
512
+
513
+ self.progress_lock = threading.RLock()
514
+ self.progress_set: Set[CacheEngineKey] = set()
515
+
516
+ self.memory_allocator = self.initialize_allocator(config, metadata)
517
+
518
+ def initialize_allocator(
519
+ self,
520
+ config: LMCacheEngineConfig,
521
+ metadata: LMCacheMetadata,
522
+ ) -> PagedTensorMemoryAllocator:
523
+ extra_config = config.extra_config
524
+ enable_nixl_storage = extra_config is not None and extra_config.get(
525
+ "enable_nixl_storage"
526
+ )
527
+ assert enable_nixl_storage
528
+ corrected_device = get_correct_device(
529
+ config.nixl_buffer_device,
530
+ metadata.worker_id,
531
+ )
532
+
533
+ if corrected_device == "cpu":
534
+ self.buffer = _allocate_cpu_memory(config.nixl_buffer_size)
535
+ self.free_pinned_buffer = True
536
+ else:
537
+ base_buffer, self.buffer = _allocate_gpu_memory(
538
+ config.nixl_buffer_size, corrected_device
539
+ )
540
+ torch.cuda.set_device(corrected_device)
541
+ self.base_buffer = base_buffer # Prevents early GC of the aligned tensor.
542
+ self.free_pinned_buffer = False
543
+
544
+ return PagedTensorMemoryAllocator(
545
+ self.buffer,
546
+ [torch.Size(metadata.kv_shape)],
547
+ [metadata.kv_dtype],
548
+ MemoryFormat.KV_2LTD,
549
+ )
550
+
551
+ def get_memory_allocator(self):
552
+ return self.memory_allocator
553
+
554
+ def allocate(
555
+ self,
556
+ shapes: Union[torch.Size, list[torch.Size]],
557
+ dtypes: Union[torch.dtype, list[torch.dtype]],
558
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
559
+ eviction: bool = True,
560
+ busy_loop: bool = True,
561
+ ) -> Optional[MemoryObj]:
562
+ if busy_loop:
563
+ logger.warning("NixlStorageBackend does not support busy loop for now")
564
+
565
+ return self.memory_allocator.allocate(shapes, dtypes, fmt)
566
+
567
+ def batched_allocate(
568
+ self,
569
+ shapes: Union[torch.Size, list[torch.Size]],
570
+ dtypes: Union[torch.dtype, list[torch.dtype]],
571
+ batch_size: int,
572
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
573
+ eviction: bool = True,
574
+ busy_loop: bool = True,
575
+ ) -> Optional[list[MemoryObj]]:
576
+ if busy_loop:
577
+ logger.warning("NixlStorageBackend does not support busy loop for now")
578
+
579
+ return self.memory_allocator.batched_allocate(shapes, dtypes, batch_size, fmt)
580
+
581
+ def get_allocator_backend(self):
582
+ return self
583
+
584
+ @abstractmethod
585
+ def contains(self, key: CacheEngineKey, pin: bool = False) -> bool:
586
+ pass
587
+
588
+ @abstractmethod
589
+ def exists_in_put_tasks(self, key: CacheEngineKey) -> bool:
590
+ pass
591
+
592
+ @abstractmethod
593
+ def batched_submit_put_task(
594
+ self,
595
+ keys: Sequence[CacheEngineKey],
596
+ memory_objs: List[MemoryObj],
597
+ transfer_spec: Any = None,
598
+ on_complete_callback: Optional[Callable[[CacheEngineKey], None]] = None,
599
+ ) -> None:
600
+ pass
601
+
602
+ @abstractmethod
603
+ def get_blocking(self, key: CacheEngineKey) -> Optional[MemoryObj]:
604
+ pass
605
+
606
+ @abstractmethod
607
+ async def batched_get_non_blocking(
608
+ self,
609
+ lookup_id: str,
610
+ keys: list[CacheEngineKey],
611
+ transfer_spec: Any = None,
612
+ ) -> list[MemoryObj]:
613
+ pass
614
+
615
+ @abstractmethod
616
+ def remove(self, key: CacheEngineKey, force: bool = True) -> bool:
617
+ pass
618
+
619
+ @abstractmethod
620
+ def pin(self, key: CacheEngineKey) -> bool:
621
+ pass
622
+
623
+ @abstractmethod
624
+ def unpin(self, key: CacheEngineKey) -> bool:
625
+ pass
626
+
627
+ @abstractmethod
628
+ def close(self) -> None:
629
+ pass
630
+
631
+ @staticmethod
632
+ def CreateNixlStorageBackend(
633
+ config: LMCacheEngineConfig,
634
+ loop: asyncio.AbstractEventLoop,
635
+ metadata: LMCacheMetadata,
636
+ ):
637
+ """
638
+ Create a Nixl backend with the given configuration.
639
+
640
+ :param nixl_config: The Nixl configuration.
641
+ :param dst_device: The device where the data is stored.
642
+
643
+ :return: A NixlBackend instance.
644
+ """
645
+ # Create the Nixl config
646
+ nixl_config = NixlStorageConfig.from_cache_engine_config(config, metadata)
647
+ # Create the Nixl backend
648
+ if nixl_config.dynamic_storage:
649
+ return NixlDynamicStorageBackend(nixl_config, config, metadata, loop)
650
+ else:
651
+ return NixlStaticStorageBackend(nixl_config, config, metadata, loop)
652
+
653
+
654
+ class NixlStaticStorageBackend(NixlStorageBackend):
655
+ def __init__(
656
+ self,
657
+ nixl_config: NixlStorageConfig,
658
+ config: LMCacheEngineConfig,
659
+ metadata: LMCacheMetadata,
660
+ loop: asyncio.AbstractEventLoop,
661
+ ):
662
+ super().__init__(nixl_config, config, metadata, loop)
663
+
664
+ self.cache_policy = get_cache_policy(config.cache_policy)
665
+ self.key_dict = self.cache_policy.init_mutable_mapping()
666
+
667
+ self.pool = self.createPool(
668
+ nixl_config.backend,
669
+ nixl_config.pool_size,
670
+ nixl_config.path,
671
+ nixl_config.use_direct_io,
672
+ )
673
+ assert self.pool is not None
674
+
675
+ self.agent = NixlStaticStorageAgent(
676
+ self.memory_allocator,
677
+ self.pool,
678
+ nixl_config.buffer_device,
679
+ nixl_config.backend,
680
+ nixl_config.backend_params,
681
+ )
682
+
683
+ @staticmethod
684
+ def createPool(backend: str, size: int, path: str, use_direct_io: bool):
685
+ if backend in ("GDS", "GDS_MT", "POSIX", "HF3FS"):
686
+ return NixlFilePool(size, path, use_direct_io)
687
+ elif backend in ("OBJ"):
688
+ return NixlObjectPool(size)
689
+ else:
690
+ raise ValueError(f"Unsupported NIXL backend: {backend}")
691
+
692
+ def add_key_to_dict(
693
+ self, key: CacheEngineKey, obj: MemoryObjMetadata, index: int
694
+ ) -> None:
695
+ with self.key_lock:
696
+ assert key not in self.key_dict
697
+ self.key_dict[key] = NixlKeyMetadata(
698
+ shape=obj.shape,
699
+ dtype=obj.dtype,
700
+ fmt=obj.fmt,
701
+ index=index,
702
+ )
703
+ self.cache_policy.update_on_put(key)
704
+
705
+ async def mem_to_storage(
706
+ self, keys: Sequence[CacheEngineKey], mem_objs: List[MemoryObj]
707
+ ) -> None:
708
+ mem_indices = [mem_obj.meta.address for mem_obj in mem_objs]
709
+
710
+ storage_indices = []
711
+ for i in range(len(keys)):
712
+ index = self.pool.pop()
713
+ storage_indices.append(index)
714
+ self.add_key_to_dict(keys[i], mem_objs[i].meta, index)
715
+
716
+ handle = self.agent.get_mem_to_storage_handle(mem_indices, storage_indices)
717
+ self.agent.post_blocking(handle)
718
+ self.agent.release_handle(handle)
719
+
720
+ for key in keys:
721
+ with self.progress_lock:
722
+ self.progress_set.discard(key)
723
+
724
+ def _collect_metadata_with_lock(
725
+ self, keys: list[CacheEngineKey]
726
+ ) -> list[Optional[NixlKeyMetadata]]:
727
+ """
728
+ Fast metadata collection with lock.
729
+ Returns metadata for each key, None if key doesn't exist.
730
+ """
731
+ metadata_list: list[Optional[NixlKeyMetadata]] = []
732
+ with self.key_lock:
733
+ for key in keys:
734
+ metadata = self.key_dict.get(key)
735
+ if metadata is not None:
736
+ self.cache_policy.update_on_hit(key, self.key_dict)
737
+ metadata_list.append(metadata)
738
+ return metadata_list
739
+
740
+ async def _nixl_transfer_async(
741
+ self, metadata_list: list[Optional[NixlKeyMetadata]]
742
+ ) -> list[Optional[MemoryObj]]:
743
+ """
744
+ Memory allocation and NIXL transfer without locks.
745
+ Can run async and in parallel with other transfers.
746
+ """
747
+ obj_list: list[Optional[MemoryObj]] = []
748
+ mem_indices = []
749
+ storage_indices = []
750
+
751
+ # Memory allocation outside lock
752
+ for metadata in metadata_list:
753
+ if metadata is None:
754
+ obj_list.append(None)
755
+ continue
756
+
757
+ dtype = metadata.dtype
758
+ shape = metadata.shape
759
+ fmt = metadata.fmt
760
+ assert dtype is not None
761
+ assert shape is not None
762
+ assert fmt is not None
763
+
764
+ obj = self.memory_allocator.allocate(shape, dtype, fmt)
765
+ assert obj is not None
766
+
767
+ obj_list.append(obj)
768
+
769
+ mem_indices.append(obj.meta.address)
770
+ storage_indices.append(metadata.index)
771
+
772
+ if not mem_indices:
773
+ return obj_list
774
+
775
+ handle = self.agent.get_storage_to_mem_handle(mem_indices, storage_indices)
776
+ self.agent.post_blocking(handle)
777
+ self.agent.release_handle(handle)
778
+
779
+ return obj_list
780
+
781
+ async def storage_to_mem(
782
+ self, keys: list[CacheEngineKey]
783
+ ) -> list[Optional[MemoryObj]]:
784
+ """
785
+ Combined method: collect metadata with lock, then do NIXL transfer.
786
+ """
787
+ metadata_list = self._collect_metadata_with_lock(keys)
788
+ return await self._nixl_transfer_async(metadata_list)
789
+
790
+ def contains(self, key: CacheEngineKey, pin: bool = False) -> bool:
791
+ """
792
+ Check whether key is in the storage backend.
793
+
794
+ :param key: The key to check
795
+ :param pin: Whether to pin the object in the backend.
796
+
797
+ :return: True if the key exists, False otherwise
798
+ """
799
+
800
+ with self.key_lock:
801
+ if key in self.key_dict:
802
+ if pin:
803
+ self.key_dict[key].pin()
804
+ return True
805
+ else:
806
+ return False
807
+
808
+ def exists_in_put_tasks(self, key: CacheEngineKey) -> bool:
809
+ """
810
+ Check whether key is in the ongoing submit_put_task tasks.
811
+
812
+ :param key: The key to check
813
+ :return: True if the key exists in put tasks, False otherwise
814
+ """
815
+ with self.progress_lock:
816
+ return key in self.progress_set
817
+
818
+ def batched_submit_put_task(
819
+ self,
820
+ keys: Sequence[CacheEngineKey],
821
+ memory_objs: List[MemoryObj],
822
+ transfer_spec: Any = None,
823
+ on_complete_callback: Optional[Callable[[CacheEngineKey], None]] = None,
824
+ ) -> None:
825
+ """
826
+ :param on_complete_callback: Optional callback (not yet supported for
827
+ NixlCacheBackend async operations).
828
+ """
829
+ with self.key_lock:
830
+ available_descs = self.pool.get_num_available_descs()
831
+ num_evict = len(keys) - available_descs
832
+ if num_evict > 0:
833
+ evict_keys = self.cache_policy.get_evict_candidates(
834
+ self.key_dict, num_candidates=num_evict
835
+ )
836
+
837
+ if not evict_keys:
838
+ logger.warning(
839
+ "No eviction candidates found. Backend under pressure."
840
+ )
841
+ return None
842
+
843
+ self.batched_remove(evict_keys, force=False)
844
+
845
+ with self.progress_lock:
846
+ for key in keys:
847
+ self.progress_set.add(key)
848
+
849
+ asyncio.run_coroutine_threadsafe(
850
+ self.mem_to_storage(keys, memory_objs), self.loop
851
+ )
852
+ # TODO: Add callback support for async NIXL operations
853
+
854
+ def get_blocking(self, key: CacheEngineKey) -> Optional[MemoryObj]:
855
+ """
856
+ A blocking function to get the kv cache from the storage backend.
857
+
858
+ :param key: The key of the MemoryObj.
859
+
860
+ :return: MemoryObj. None if the key does not exist.
861
+ """
862
+
863
+ future = asyncio.run_coroutine_threadsafe(self.storage_to_mem([key]), self.loop)
864
+
865
+ obj_list = future.result()
866
+ return obj_list[0]
867
+
868
+ def batched_get_blocking(
869
+ self,
870
+ keys: List[CacheEngineKey],
871
+ ) -> List[Optional[MemoryObj]]:
872
+ """
873
+ A blocking function to get the kv cache from the storage backend.
874
+
875
+ :param List[CacheEngineKey] keys: The keys of the MemoryObjs.
876
+
877
+ :return: a list of memory objects.
878
+ """
879
+
880
+ if not keys:
881
+ return []
882
+
883
+ future = asyncio.run_coroutine_threadsafe(self.storage_to_mem(keys), self.loop)
884
+
885
+ obj_list = future.result()
886
+ return obj_list
887
+
888
+ async def batched_get_non_blocking(
889
+ self,
890
+ lookup_id: str,
891
+ keys: list[CacheEngineKey],
892
+ transfer_spec: Any = None,
893
+ ) -> list[MemoryObj]:
894
+ obj_list = await self.storage_to_mem(keys)
895
+ assert None not in obj_list
896
+ return cast(list[MemoryObj], obj_list)
897
+
898
+ def remove(self, key: CacheEngineKey, force: bool = True) -> bool:
899
+ """
900
+ Remove the key from the storage backend.
901
+
902
+ :param key: The key to remove.
903
+ """
904
+
905
+ with self.key_lock:
906
+ metadata = self.key_dict.pop(key, None)
907
+ if metadata is None:
908
+ return False
909
+ if force:
910
+ self.cache_policy.update_on_force_evict(key)
911
+
912
+ self.pool.push(metadata.index)
913
+ return True
914
+
915
+ def pin(self, key: CacheEngineKey) -> bool:
916
+ with self.key_lock:
917
+ if key in self.key_dict:
918
+ self.key_dict[key].pin()
919
+ return True
920
+ else:
921
+ return False
922
+
923
+ def unpin(self, key: CacheEngineKey) -> bool:
924
+ with self.key_lock:
925
+ if key in self.key_dict:
926
+ self.key_dict[key].unpin()
927
+ return True
928
+ else:
929
+ return False
930
+
931
+ def close(self) -> None:
932
+ """
933
+ Close the storage backend.
934
+ """
935
+ self.agent.close()
936
+ self.pool.close()
937
+ self.memory_allocator.close()
938
+
939
+ if self.free_pinned_buffer:
940
+ _free_cpu_memory(self.buffer)
941
+
942
+
943
+ class NixlDynamicStorageBackend(NixlStorageBackend):
944
+ def __init__(
945
+ self,
946
+ nixl_config: NixlStorageConfig,
947
+ config: LMCacheEngineConfig,
948
+ metadata: LMCacheMetadata,
949
+ loop: asyncio.AbstractEventLoop,
950
+ cache_policy: Optional[PresenceCache] = None,
951
+ ):
952
+ super().__init__(nixl_config, config, metadata, loop)
953
+
954
+ self.async_mode = nixl_config.enable_async_put
955
+ self.enable_presence_cache = nixl_config.enable_presence_cache
956
+
957
+ # Presence cache to reduce remote contains checks
958
+ self.hit_counter = 0
959
+ self.total_counter = 0
960
+ self.key_presence_cache: Optional[PresenceCache] = None
961
+ if self.enable_presence_cache:
962
+ self.key_presence_cache = (
963
+ cache_policy if cache_policy is not None else SetPresenceCache()
964
+ )
965
+
966
+ # Initialize metadata from config
967
+ self.meta_shape: Optional[torch.Size] = None
968
+ self.meta_dtype: Optional[torch.dtype] = None
969
+ self.meta_fmt: Optional[MemoryFormat] = None
970
+ self.init_chunk_meta(metadata)
971
+
972
+ self.agent = NixlDynamicStorageAgent(
973
+ self.memory_allocator,
974
+ nixl_config.buffer_device,
975
+ nixl_config.backend,
976
+ nixl_config.backend_params,
977
+ )
978
+
979
+ def set_presence_cache(self, cache: PresenceCache) -> None:
980
+ """Configure a custom cache policy for key presence tracking."""
981
+ if self.enable_presence_cache:
982
+ self.key_presence_cache = cache
983
+
984
+ def _cache_contains(self, chunk_hash: int) -> bool:
985
+ if not self.enable_presence_cache or self.key_presence_cache is None:
986
+ return False
987
+ found = self.key_presence_cache.contains(chunk_hash)
988
+ self.hit_counter += 1 if found else 0
989
+ self.total_counter += 1
990
+ if self.total_counter % 100 == 0:
991
+ logger.debug(f"Cache hit: {self.hit_counter} vs {self.total_counter}")
992
+ return found
993
+
994
+ def _cache_add(self, chunk_hash: int) -> None:
995
+ if not self.enable_presence_cache or self.key_presence_cache is None:
996
+ return
997
+ self.key_presence_cache.add(chunk_hash)
998
+
999
+ def _cache_discard(self, chunk_hash: int) -> None:
1000
+ if not self.enable_presence_cache or self.key_presence_cache is None:
1001
+ return
1002
+ self.key_presence_cache.discard(chunk_hash)
1003
+
1004
+ def init_chunk_meta(
1005
+ self,
1006
+ metadata: Optional[LMCacheMetadata],
1007
+ ) -> None:
1008
+ """Initialize chunk metadata similar to base_connector.init_chunk_meta()"""
1009
+ if metadata is None:
1010
+ return
1011
+
1012
+ self.meta_shape = torch.Size(
1013
+ [
1014
+ metadata.kv_shape[1],
1015
+ metadata.kv_shape[0],
1016
+ metadata.kv_shape[2],
1017
+ metadata.kv_shape[3] * metadata.kv_shape[4],
1018
+ ]
1019
+ )
1020
+ self.meta_dtype = metadata.kv_dtype
1021
+ self.meta_fmt = (
1022
+ MemoryFormat.KV_MLA_FMT if metadata.use_mla else MemoryFormat.KV_2LTD
1023
+ )
1024
+ logger.info(
1025
+ f"Initialized nixl object backend metadata: "
1026
+ f"shape: {self.meta_shape}, "
1027
+ f"dtype: {self.meta_dtype}, "
1028
+ f"fmt: {self.meta_fmt}"
1029
+ )
1030
+
1031
+ def _format_object_key(self, key: CacheEngineKey) -> str:
1032
+ """
1033
+ Generate object key name based on CacheEngineKey information.
1034
+ Similar to s3_connector._format_safe_path()
1035
+ """
1036
+ key_str = key.to_string()
1037
+ # Replace slashes with underscores to make it safe for object storage
1038
+ flat_key_str = key_str.replace("/", "_").replace("@", "_")
1039
+ # URL encode for safety
1040
+ return url_quote(flat_key_str, safe="")
1041
+
1042
+ def key_exists(self, key: CacheEngineKey) -> bool:
1043
+ if self.agent.mem_type == "OBJ":
1044
+ meta_info = self._format_object_key(key)
1045
+ else:
1046
+ # Already validated in validate_nixl_backend
1047
+ raise ValueError(f"unexpected mem_type: {self.agent.mem_type}")
1048
+
1049
+ return self.agent.nixl_desc_exists(meta_info)
1050
+
1051
+ def storage_to_mem(
1052
+ self, keys: list[CacheEngineKey], pin: bool = False
1053
+ ) -> list[Optional[MemoryObj]]:
1054
+ obj_list: list[Optional[MemoryObj]] = []
1055
+ page_size = self.memory_allocator.align_bytes
1056
+ start_time = time.time()
1057
+ storage_indices = []
1058
+ mem_indices = []
1059
+ descs = []
1060
+
1061
+ # Prepare mem and storage indices
1062
+ for idx in range(len(keys)):
1063
+ # Allocate memory for the object
1064
+ assert self.meta_shape is not None
1065
+ assert self.meta_dtype is not None
1066
+ assert self.meta_fmt is not None
1067
+ obj = self.memory_allocator.allocate(
1068
+ self.meta_shape, self.meta_dtype, self.meta_fmt
1069
+ )
1070
+ if obj is None:
1071
+ # free previous allocated objects
1072
+ logger.warning("Failed to allocate memory")
1073
+ for obj in obj_list:
1074
+ self.memory_allocator.free(obj)
1075
+ return [None] * len(keys)
1076
+
1077
+ obj_list.append(obj)
1078
+ mem_indices.append(obj.meta.address)
1079
+ storage_indices.append(idx)
1080
+
1081
+ if self.agent.mem_type == "OBJ":
1082
+ for idx in range(len(keys)):
1083
+ object_key = self._format_object_key(keys[idx])
1084
+ descs.append(NixlDesc(device_id=idx, meta_info=object_key))
1085
+ else:
1086
+ # Already validated in validate_nixl_backend
1087
+ raise ValueError(f"unexpected mem_type: {self.agent.mem_type}")
1088
+
1089
+ # Create batched storage handler
1090
+ storage_reg_descs, storage_xfer_handler = (
1091
+ self.agent.create_batched_storage_handler(descs, page_size)
1092
+ )
1093
+ # Create transfer handle
1094
+ handle = self.agent.get_storage_to_mem_handle(
1095
+ mem_indices, storage_xfer_handler, storage_indices
1096
+ )
1097
+
1098
+ # Try to read the object
1099
+ try:
1100
+ self.agent.post_blocking(handle)
1101
+ xfer_state = True
1102
+ except nixlBind.nixlBackendError as exc:
1103
+ logger.warning(f"Batch Transfer failed: {exc}")
1104
+ # Treat "not found", timeout or other transfer failures as recoverable
1105
+ # Do not raise exception to avoid terminating the program
1106
+ xfer_state = False
1107
+
1108
+ self.agent.release_handle(handle)
1109
+ self.agent.release_storage_handler(storage_reg_descs, storage_xfer_handler)
1110
+
1111
+ if xfer_state:
1112
+ for i in range(len(keys)):
1113
+ key = keys[i]
1114
+ self._cache_add(key.chunk_hash)
1115
+ end_time = time.time()
1116
+ duration = end_time - start_time
1117
+ logger.debug(
1118
+ f"storage_to_mem for {len(keys)} objects size {page_size * len(keys)} "
1119
+ f"took {duration:.6f} seconds"
1120
+ )
1121
+ return obj_list
1122
+ else:
1123
+ # Free the allocated memory and discard cache if transfer failed
1124
+ for obj in obj_list:
1125
+ self.memory_allocator.free(obj)
1126
+ for key in keys:
1127
+ self._cache_discard(key.chunk_hash)
1128
+ return [None] * len(keys)
1129
+
1130
+ async def _wait_for_transfer(
1131
+ self,
1132
+ handle: NixlXferHandle,
1133
+ initial_state: str,
1134
+ keys: Sequence[CacheEngineKey],
1135
+ storage_reg_descs: nixlBind.nixlRegDList,
1136
+ storage_xfer_handler: NixlDlistHandle,
1137
+ mem_objs: List[MemoryObj],
1138
+ ):
1139
+ """Asynchronously wait for transfer to complete without blocking."""
1140
+ state = ""
1141
+ try:
1142
+ state = initial_state
1143
+ while state != "DONE" and state != "ERR":
1144
+ state = self.agent.nixl_agent.check_xfer_state(handle)
1145
+ await asyncio.sleep(0.001) # Avoid busy-waiting, yield to event loop
1146
+ if state == "ERR":
1147
+ raise RuntimeError("NIXL transfer failed")
1148
+
1149
+ finally:
1150
+ # Release the handle after transfer completes (success or failure)
1151
+ self.agent.release_handle(handle)
1152
+ self.agent.release_storage_handler(storage_reg_descs, storage_xfer_handler)
1153
+
1154
+ if state == "DONE":
1155
+ for key in keys:
1156
+ with self.progress_lock:
1157
+ self.progress_set.discard(key)
1158
+ self._cache_add(key.chunk_hash)
1159
+
1160
+ for mem_obj in mem_objs:
1161
+ mem_obj.ref_count_down()
1162
+
1163
+ async def mem_to_storage(
1164
+ self, keys: Sequence[CacheEngineKey], mem_objs: List[MemoryObj]
1165
+ ) -> None:
1166
+ start_time = time.time()
1167
+ if len(keys) == 0:
1168
+ return
1169
+
1170
+ storage_indices = range(len(keys))
1171
+ mem_indices = [mem_obj.meta.address for mem_obj in mem_objs]
1172
+ page_size = self.memory_allocator.align_bytes
1173
+ descs = []
1174
+
1175
+ if self.agent.mem_type == "OBJ":
1176
+ for idx in range(len(keys)):
1177
+ object_key = self._format_object_key(keys[idx])
1178
+ descs.append(NixlDesc(device_id=idx, meta_info=object_key))
1179
+ else:
1180
+ # Already validated in validate_nixl_backend
1181
+ raise ValueError(f"unexpected mem_type: {self.agent.mem_type}")
1182
+
1183
+ storage_reg_descs, storage_xfer_handler = (
1184
+ self.agent.create_batched_storage_handler(descs, page_size)
1185
+ )
1186
+
1187
+ handle = self.agent.get_mem_to_storage_handle(
1188
+ mem_indices, storage_xfer_handler, storage_indices
1189
+ )
1190
+
1191
+ if self.async_mode:
1192
+ initial_state = self.agent.post_async(handle)
1193
+ # Submit the async wait to the event loop and return immediately
1194
+ asyncio.create_task(
1195
+ self._wait_for_transfer(
1196
+ handle,
1197
+ initial_state,
1198
+ keys,
1199
+ storage_reg_descs,
1200
+ storage_xfer_handler,
1201
+ mem_objs,
1202
+ )
1203
+ )
1204
+ else:
1205
+ self.agent.post_blocking(handle)
1206
+ self.agent.release_handle(handle)
1207
+ self.agent.release_storage_handler(storage_reg_descs, storage_xfer_handler)
1208
+
1209
+ end_time = time.time()
1210
+ duration = end_time - start_time
1211
+ logger.debug(
1212
+ f"mem_to_storage for {len(keys)} objects size {page_size * len(keys)} "
1213
+ f"took {duration:.3f} seconds"
1214
+ )
1215
+
1216
+ for key in keys:
1217
+ with self.progress_lock:
1218
+ self.progress_set.discard(key)
1219
+ self._cache_add(key.chunk_hash)
1220
+
1221
+ def exists_in_put_tasks(self, key: CacheEngineKey) -> bool:
1222
+ """
1223
+ Check whether key is in the ongoing submit_put_task tasks.
1224
+
1225
+ :param key: The key to check
1226
+ :return: True if the key exists in put tasks, False otherwise
1227
+ """
1228
+ with self.progress_lock:
1229
+ return key in self.progress_set
1230
+
1231
+ def contains(self, key: CacheEngineKey, pin: bool = False) -> bool:
1232
+ """
1233
+ Check whether key is in the storage backend.
1234
+
1235
+ This method uses nixl querymem to check existence.
1236
+ If successful, it caches the name for later use.
1237
+
1238
+ :param key: The key to check
1239
+ :param pin: Whether to pin the object in the backend
1240
+ (Not Implemented)
1241
+
1242
+ :return: True if the key exists, False otherwise
1243
+ """
1244
+ # Check if already in progress
1245
+ if self.exists_in_put_tasks(key):
1246
+ logger.debug(f"Key {key.chunk_hash:x} is in put tasks")
1247
+ return False
1248
+
1249
+ # Check presence cache before hitting remote storage if not prefetching
1250
+ if self._cache_contains(key.chunk_hash):
1251
+ return True
1252
+
1253
+ xfer_state = self.key_exists(key)
1254
+ if xfer_state:
1255
+ self._cache_add(key.chunk_hash)
1256
+
1257
+ return xfer_state
1258
+
1259
+ async def batched_async_contains(
1260
+ self,
1261
+ lookup_id: str,
1262
+ keys: list[CacheEngineKey],
1263
+ pin: bool = False,
1264
+ ) -> int:
1265
+ if not keys:
1266
+ return 0
1267
+ """
1268
+ Nixl API query_memory also supports batched query. However when there
1269
+ are hundreds of keys to be queried and keys in the first few places
1270
+ don't exist, the batched query has to be failed fast.
1271
+ Therefore we implement batched contains() in a managed thread pool,
1272
+ which fails fast when a key doesn't exist.
1273
+ """
1274
+ n = len(keys)
1275
+ idx = 0
1276
+ batch_size = 16
1277
+
1278
+ while idx < n:
1279
+ batch = keys[idx : idx + batch_size]
1280
+ tasks = [asyncio.to_thread(self.contains, k, pin) for k in batch]
1281
+ results = await asyncio.gather(*tasks, return_exceptions=False)
1282
+
1283
+ # Stop at the first miss
1284
+ for j, ok in enumerate(results):
1285
+ if not ok:
1286
+ return idx + j
1287
+ idx += len(batch)
1288
+
1289
+ return n
1290
+
1291
+ def batched_submit_put_task(
1292
+ self,
1293
+ keys: Sequence[CacheEngineKey],
1294
+ memory_objs: List[MemoryObj],
1295
+ transfer_spec: Any = None,
1296
+ on_complete_callback: Optional[Callable[[CacheEngineKey], None]] = None,
1297
+ ) -> None:
1298
+ """
1299
+ :param on_complete_callback: Optional callback invoked once per key
1300
+ after transfer completes. Only supported in sync mode (async_mode=False).
1301
+ """
1302
+ with self.progress_lock:
1303
+ for key in keys:
1304
+ self.progress_set.add(key)
1305
+
1306
+ if self.async_mode:
1307
+ for mem_obj in memory_objs:
1308
+ mem_obj.ref_count_up()
1309
+ asyncio.run_coroutine_threadsafe(
1310
+ self.mem_to_storage(keys, memory_objs), self.loop
1311
+ )
1312
+ # Note: callback not supported in async mode
1313
+ else:
1314
+ future = asyncio.run_coroutine_threadsafe(
1315
+ self.mem_to_storage(keys, memory_objs), self.loop
1316
+ )
1317
+ future.result()
1318
+
1319
+ # Call completion callback for sync mode
1320
+ if on_complete_callback is not None:
1321
+ for key in keys:
1322
+ try:
1323
+ on_complete_callback(key)
1324
+ except Exception as e:
1325
+ logger.warning(
1326
+ f"on_complete_callback failed for key {key}: {e}"
1327
+ )
1328
+
1329
+ def get_blocking(self, key: CacheEngineKey) -> Optional[MemoryObj]:
1330
+ """
1331
+ A blocking function to get the kv cache from the storage backend.
1332
+
1333
+ :param key: The key of the MemoryObj.
1334
+
1335
+ :return: MemoryObj. None if the key does not exist.
1336
+ """
1337
+ obj_list = self.storage_to_mem([key], False)
1338
+ return obj_list[0]
1339
+
1340
+ def batched_get_blocking(
1341
+ self,
1342
+ keys: List[CacheEngineKey],
1343
+ ) -> List[Optional[MemoryObj]]:
1344
+ """
1345
+ A blocking function to get the kv cache from the storage backend.
1346
+ :param List[CacheEngineKey] keys: The keys of the MemoryObjs.
1347
+ :return: a list of memory objects.
1348
+ """
1349
+ if not keys:
1350
+ return []
1351
+
1352
+ obj_list = self.storage_to_mem(keys, False)
1353
+ return obj_list
1354
+
1355
+ async def batched_get_non_blocking(
1356
+ self,
1357
+ lookup_id: str,
1358
+ keys: list[CacheEngineKey],
1359
+ transfer_spec: Any = None,
1360
+ ) -> list[MemoryObj]:
1361
+ """
1362
+ Non blocking interface to get the kv cache from the storage backend.
1363
+ :param List[CacheEngineKey] keys: The keys of the MemoryObjs.
1364
+ :return: a list of memory objects.
1365
+ """
1366
+ obj_list = self.storage_to_mem(keys, False)
1367
+ assert None not in obj_list
1368
+ return cast(list[MemoryObj], obj_list)
1369
+
1370
+ def remove(self, key: CacheEngineKey, force: bool = True) -> bool:
1371
+ """
1372
+ Remove the key from the storage backend.
1373
+
1374
+ :param key: The key to remove.
1375
+ :param force: Whether to force removal (not used in this implementation)
1376
+ """
1377
+ self._cache_discard(key.chunk_hash)
1378
+ return True
1379
+
1380
+ def pin(self, key: CacheEngineKey) -> bool:
1381
+ """
1382
+ Not implemented yet
1383
+ """
1384
+ return False
1385
+
1386
+ def unpin(self, key: CacheEngineKey) -> bool:
1387
+ """
1388
+ Not implemented yet
1389
+ """
1390
+ return False
1391
+
1392
+ def close(self) -> None:
1393
+ """
1394
+ Close the storage backend.
1395
+ """
1396
+ self.agent.close()
1397
+ self.memory_allocator.close()
1398
+
1399
+ if self.free_pinned_buffer:
1400
+ _free_cpu_memory(self.buffer)