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,831 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """
3
+ Dynamic-file-mode Nixl L2 adapter.
4
+
5
+ Unlike the static ``NixlStoreL2Adapter`` which pre-allocates all storage
6
+ files at init time, this adapter opens/registers files per operation.
7
+
8
+ Atomic publish:
9
+ - Stores DMA-write to a per-operation ``<final_path>.tmp.<uuid>`` and
10
+ atomically ``rename()`` to the final deterministic path on completion.
11
+ This guarantees that readers (including other processes sharing the
12
+ same directory) never observe a partially-written file.
13
+
14
+ Persist (enabled by default via ``persist_enabled``, can be opted out):
15
+ - Keeps data files on disk at shutdown (no metadata dump).
16
+
17
+ Secondary lookup (always on):
18
+ - Lookup always checks secondary storage (disk) on miss and lazily
19
+ populates the in-memory index when a file is found. File names are
20
+ derived deterministically from ObjectKey.
21
+ """
22
+
23
+ # Future
24
+ from __future__ import annotations
25
+
26
+ # Standard
27
+ from typing import Optional
28
+ import asyncio
29
+ import os
30
+ import threading
31
+ import uuid
32
+
33
+ # Third Party
34
+ from nixl._api import nixl_agent as NixlAgent
35
+ from nixl._api import nixl_agent_config as NixlAgentConfig
36
+ from nixl._api import (
37
+ nixlBind,
38
+ )
39
+
40
+ # First Party
41
+ from lmcache.logging import init_logger
42
+ from lmcache.native_storage_ops import Bitmap
43
+ from lmcache.v1.distributed.api import MemoryLayoutDesc, ObjectKey
44
+ from lmcache.v1.distributed.internal_api import L1MemoryDesc
45
+ from lmcache.v1.distributed.l2_adapters.base import L2AdapterInterface, L2TaskId
46
+ from lmcache.v1.distributed.l2_adapters.config import (
47
+ L2AdapterConfigBase,
48
+ register_l2_adapter_type,
49
+ )
50
+ from lmcache.v1.distributed.l2_adapters.factory import (
51
+ register_l2_adapter_factory,
52
+ )
53
+ from lmcache.v1.distributed.l2_adapters.nixl_store_l2_adapter import (
54
+ NixlStoreObj,
55
+ )
56
+ from lmcache.v1.memory_management import MemoryObj
57
+
58
+ logger = init_logger(__name__)
59
+
60
+
61
+ # ---------------------------------------------------------------
62
+ # ObjectKey <-> file path helpers
63
+ # ---------------------------------------------------------------
64
+
65
+
66
+ def _object_key_to_filename(key: ObjectKey) -> str:
67
+ """Derive a deterministic file name from an ObjectKey.
68
+
69
+ Replaces ``/`` in model names with ``--`` to avoid creating
70
+ subdirectories (e.g. ``meta-llama/Llama-3-8B`` becomes
71
+ ``meta-llama--Llama-3-8B``).
72
+ """
73
+ safe_model_name = key.model_name.replace("/", "--")
74
+ chunk_hex = key.chunk_hash.hex()
75
+ return f"{safe_model_name}_{key.kv_rank:08x}_{chunk_hex}.bin"
76
+
77
+
78
+ # ---------------------------------------------------------------
79
+ # Dynamic Nixl storage agent
80
+ # ---------------------------------------------------------------
81
+
82
+
83
+ class DynamicNixlStorageAgent:
84
+ """Nixl storage agent that opens/registers files per operation.
85
+
86
+ The L1 memory handler is registered once at init (same as the static
87
+ agent). Storage files are registered on-demand for each store/load
88
+ and deregistered immediately after the transfer completes.
89
+ """
90
+
91
+ def __init__(
92
+ self,
93
+ device: str,
94
+ backend: str,
95
+ backend_params: dict[str, str],
96
+ l1_memory_desc: L1MemoryDesc,
97
+ ):
98
+ self.backend = backend
99
+ self.device = device
100
+ self.backend_params = backend_params
101
+ self.l1_align_bytes = l1_memory_desc.align_bytes
102
+ self.file_path = backend_params["file_path"]
103
+ self.use_direct_io = (
104
+ str(backend_params.get("use_direct_io", "false")).lower() == "true"
105
+ )
106
+
107
+ self.agent_name = "DynNixlAgent_" + str(uuid.uuid4())
108
+ nixl_conf = NixlAgentConfig(backends=[])
109
+ self.nixl_agent = NixlAgent(self.agent_name, nixl_conf)
110
+ self.nixl_agent.create_backend(backend, backend_params)
111
+
112
+ # Register L1 memory (same as static agent)
113
+ self._init_mem_handlers(
114
+ device,
115
+ l1_memory_desc.ptr,
116
+ l1_memory_desc.size,
117
+ l1_memory_desc.align_bytes,
118
+ device_id=0,
119
+ )
120
+
121
+ # ---- L1 memory registration (one-time) ----
122
+
123
+ def _init_mem_handlers(self, device, buffer_ptr, buffer_size, page_size, device_id):
124
+ reg_list = [(buffer_ptr, buffer_size, device_id, "")]
125
+ xfer_desc = [
126
+ (base_addr, page_size, device_id)
127
+ for base_addr in range(buffer_ptr, buffer_ptr + buffer_size, page_size)
128
+ ]
129
+
130
+ mem_type = "DRAM" if device == "cpu" else "VRAM"
131
+
132
+ self.mem_reg_descs = self.nixl_agent.register_memory(
133
+ reg_list, mem_type=mem_type
134
+ )
135
+ xfer_descs = self.nixl_agent.get_xfer_descs(xfer_desc, mem_type=mem_type)
136
+ self.mem_xfer_handler = self.nixl_agent.prep_xfer_dlist(
137
+ "", xfer_descs, mem_type=mem_type
138
+ )
139
+
140
+ # ---- Per-operation file helpers ----
141
+
142
+ def _open_flags(self, create: bool) -> int:
143
+ """Return os.open flags for storage files."""
144
+ flags = os.O_RDWR
145
+ if create:
146
+ # O_TRUNC ensures any orphaned file from a previous crash
147
+ # is truncated, avoiding stale trailing bytes on disk.
148
+ flags |= os.O_CREAT | os.O_TRUNC
149
+ if self.use_direct_io and hasattr(os, "O_DIRECT"):
150
+ flags |= os.O_DIRECT
151
+ return flags
152
+
153
+ def _register_single_file(self, fd: int, file_size: int, page_size: int):
154
+ """Register a single file with nixl and return (reg_descs, xfer_handler).
155
+
156
+ Returns:
157
+ Tuple of (reg_descs, xfer_handler) for later cleanup.
158
+ """
159
+ num_pages = file_size // page_size
160
+
161
+ reg_list = [(0, file_size, fd, "")]
162
+ xfer_desc = [(offset * page_size, page_size, fd) for offset in range(num_pages)]
163
+
164
+ reg_descs = self.nixl_agent.register_memory(reg_list, mem_type="FILE")
165
+ xfer_descs = self.nixl_agent.get_xfer_descs(xfer_desc, mem_type="FILE")
166
+ xfer_handler = self.nixl_agent.prep_xfer_dlist(
167
+ self.agent_name, xfer_descs, mem_type="FILE"
168
+ )
169
+ return reg_descs, xfer_handler
170
+
171
+ def _deregister_file(self, reg_descs, xfer_handler):
172
+ """Deregister a file from nixl."""
173
+ self.nixl_agent.release_dlist_handle(xfer_handler)
174
+ self.nixl_agent.deregister_memory(reg_descs)
175
+
176
+ async def dynamic_store_file(
177
+ self,
178
+ mem_indices: list[int],
179
+ file_path: str,
180
+ page_size: int,
181
+ ) -> None:
182
+ """Write-to-temp-then-rename to publish the final file atomically.
183
+
184
+ The DMA write goes to ``<file_path>.tmp.<uuid>`` in the same
185
+ directory. Only after the transfer completes successfully is the
186
+ temp file atomically renamed to the final path, ensuring that
187
+ concurrent readers (including other processes sharing the same
188
+ directory) never observe a partially-written file.
189
+ """
190
+ file_size = len(mem_indices) * page_size
191
+ tmp_path = f"{file_path}.tmp.{uuid.uuid4().hex}"
192
+ fd = os.open(tmp_path, self._open_flags(create=True))
193
+ try:
194
+ reg_descs, xfer_handler = self._register_single_file(
195
+ fd, file_size, page_size
196
+ )
197
+ try:
198
+ storage_indices = list(range(len(mem_indices)))
199
+ handle = self.nixl_agent.make_prepped_xfer(
200
+ "WRITE",
201
+ self.mem_xfer_handler,
202
+ mem_indices,
203
+ xfer_handler,
204
+ storage_indices,
205
+ )
206
+ await self._post_non_blocking(handle)
207
+ self.nixl_agent.release_xfer_handle(handle)
208
+ finally:
209
+ self._deregister_file(reg_descs, xfer_handler)
210
+ except BaseException:
211
+ # Best-effort cleanup of the temp file on failure.
212
+ try:
213
+ os.unlink(tmp_path)
214
+ except FileNotFoundError:
215
+ pass
216
+ raise
217
+ finally:
218
+ os.close(fd)
219
+
220
+ # Atomic publish: readers only ever see a complete file at file_path.
221
+ # TODO(Jiayi): Only guaranteed to be atomic within the local posix filesystems.
222
+ os.rename(tmp_path, file_path)
223
+
224
+ async def dynamic_load_file(
225
+ self,
226
+ mem_indices: list[int],
227
+ file_path: str,
228
+ page_size: int,
229
+ ) -> None:
230
+ """Open an existing file, DMA read into L1 memory, then clean up."""
231
+ file_size = len(mem_indices) * page_size
232
+ fd = os.open(file_path, self._open_flags(create=False))
233
+ try:
234
+ reg_descs, xfer_handler = self._register_single_file(
235
+ fd, file_size, page_size
236
+ )
237
+ try:
238
+ storage_indices = list(range(len(mem_indices)))
239
+ handle = self.nixl_agent.make_prepped_xfer(
240
+ "READ",
241
+ self.mem_xfer_handler,
242
+ mem_indices,
243
+ xfer_handler,
244
+ storage_indices,
245
+ )
246
+ await self._post_non_blocking(handle)
247
+ self.nixl_agent.release_xfer_handle(handle)
248
+ finally:
249
+ self._deregister_file(reg_descs, xfer_handler)
250
+ finally:
251
+ os.close(fd)
252
+
253
+ def dynamic_delete_file(self, file_path: str) -> None:
254
+ """Delete a storage file from disk."""
255
+ try:
256
+ os.unlink(file_path)
257
+ except FileNotFoundError:
258
+ logger.warning("File already deleted: %s", file_path)
259
+
260
+ # ---- Shared helpers ----
261
+
262
+ def get_memory_indices(self, raw_addr: int, mem_size: int) -> list[int]:
263
+ """Get L1 memory page indices for the given address and size."""
264
+ if raw_addr % self.l1_align_bytes != 0:
265
+ raise ValueError(
266
+ f"Raw address {raw_addr} is not aligned to "
267
+ f"page size {self.l1_align_bytes}"
268
+ )
269
+ if mem_size % self.l1_align_bytes != 0:
270
+ raise ValueError(
271
+ f"Memory size {mem_size} is not a multiple of "
272
+ f"page size {self.l1_align_bytes}"
273
+ )
274
+ num_pages = mem_size // self.l1_align_bytes
275
+ return [(raw_addr // self.l1_align_bytes + i) for i in range(num_pages)]
276
+
277
+ def get_file_path_for_key(self, key: ObjectKey) -> str:
278
+ """Return the full file path for a given ObjectKey."""
279
+ return os.path.join(self.file_path, _object_key_to_filename(key))
280
+
281
+ async def _post_non_blocking(self, handle):
282
+ """Await a nixl transfer until done."""
283
+ state = self.nixl_agent.transfer(handle)
284
+ while state != "DONE" and state != "ERR":
285
+ try:
286
+ state = self.nixl_agent.check_xfer_state(handle)
287
+ except nixlBind.nixlBackendError:
288
+ raise
289
+ await asyncio.sleep(0.01)
290
+ if state == "ERR":
291
+ raise RuntimeError("NIXL transfer failed")
292
+
293
+ def cleanup_temp_files(self) -> None:
294
+ """Remove leftover ``*.tmp.*`` files in the storage directory.
295
+
296
+ These can be left behind if a store crashed between opening the
297
+ temp file and the atomic rename. Called at shutdown as a best-effort
298
+ GC; orphans don't affect correctness because they're never matched
299
+ by the deterministic ``ObjectKey → filename`` mapping.
300
+ """
301
+ try:
302
+ entries = os.listdir(self.file_path)
303
+ except FileNotFoundError:
304
+ return
305
+ for name in entries:
306
+ # Temp suffix format: "<final_name>.tmp.<hex>"
307
+ if ".tmp." in name:
308
+ try:
309
+ os.unlink(os.path.join(self.file_path, name))
310
+ except FileNotFoundError:
311
+ pass
312
+ except OSError as e:
313
+ logger.warning(
314
+ "Failed to remove leftover temp file %s: %s", name, e
315
+ )
316
+
317
+ def close(self):
318
+ """Release L1 memory handlers."""
319
+ self.nixl_agent.release_dlist_handle(self.mem_xfer_handler)
320
+ self.nixl_agent.deregister_memory(self.mem_reg_descs)
321
+
322
+
323
+ # ---------------------------------------------------------------
324
+ # Dynamic L2 adapter
325
+ # ---------------------------------------------------------------
326
+
327
+
328
+ class DynamicNixlStoreL2Adapter(L2AdapterInterface):
329
+ """Nixl L2 adapter using dynamic per-operation file registration.
330
+
331
+ Each store creates a new file on disk; each load re-opens the file.
332
+
333
+ When ``persist_enabled`` is True (the default), data files are kept
334
+ on disk at shutdown. Lookup always checks secondary storage (disk)
335
+ for keys not in the in-memory index and populates the index lazily.
336
+ """
337
+
338
+ def __init__(
339
+ self,
340
+ config: DynamicNixlStoreL2AdapterConfig,
341
+ l1_memory_desc: L1MemoryDesc,
342
+ ):
343
+ super().__init__()
344
+ self._config = config
345
+
346
+ self._store_efd = os.eventfd(0, os.EFD_NONBLOCK | os.EFD_CLOEXEC)
347
+ self._lookup_efd = os.eventfd(0, os.EFD_NONBLOCK | os.EFD_CLOEXEC)
348
+ self._load_efd = os.eventfd(0, os.EFD_NONBLOCK | os.EFD_CLOEXEC)
349
+
350
+ # Cache data structures
351
+ self._memory_objects: dict[ObjectKey, NixlStoreObj] = {}
352
+ self._inflight_stores: set[ObjectKey] = set()
353
+ self._total_bytes: int = 0
354
+ max_capacity_gb = float(config.backend_params.get("max_capacity_gb", 0))
355
+ if max_capacity_gb <= 0:
356
+ raise ValueError("backend_params must include a positive 'max_capacity_gb'")
357
+ self._max_capacity_bytes: int = int(max_capacity_gb * (1024**3))
358
+
359
+ # Task ID management
360
+ self._next_task_id: L2TaskId = 0
361
+ self._completed_store_tasks: dict[L2TaskId, bool] = {}
362
+ self._completed_lookup_tasks: dict[L2TaskId, Bitmap] = {}
363
+ self._completed_load_tasks: dict[L2TaskId, Bitmap] = {}
364
+ self._lock = threading.Lock()
365
+
366
+ # Asyncio event loop running in a background thread
367
+ self._loop = asyncio.new_event_loop()
368
+ self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
369
+ self._loop_thread.start()
370
+
371
+ # Initialize dynamic Nixl agent (L1 memory only, no pre-allocated files)
372
+ self.nixl_agent = DynamicNixlStorageAgent(
373
+ device="cpu",
374
+ backend=config.backend,
375
+ backend_params=config.backend_params,
376
+ l1_memory_desc=l1_memory_desc,
377
+ )
378
+
379
+ self._persist_enabled = config.persist_config.persist_enabled
380
+
381
+ # --------------------
382
+ # Event Fd Interface
383
+ # --------------------
384
+
385
+ def get_store_event_fd(self) -> int:
386
+ return self._store_efd
387
+
388
+ def get_lookup_and_lock_event_fd(self) -> int:
389
+ return self._lookup_efd
390
+
391
+ def get_load_event_fd(self) -> int:
392
+ return self._load_efd
393
+
394
+ #####################
395
+ # Store Interface
396
+ #####################
397
+
398
+ def submit_store_task(
399
+ self,
400
+ keys: list[ObjectKey],
401
+ objects: list[MemoryObj],
402
+ ) -> L2TaskId:
403
+ with self._lock:
404
+ task_id = self._get_next_task_id()
405
+
406
+ asyncio.run_coroutine_threadsafe(
407
+ self._execute_store_in_the_loop(keys, objects, task_id), self._loop
408
+ )
409
+ return task_id
410
+
411
+ def pop_completed_store_tasks(self) -> dict[L2TaskId, bool]:
412
+ with self._lock:
413
+ completed = self._completed_store_tasks
414
+ self._completed_store_tasks = {}
415
+ return completed
416
+
417
+ #####################
418
+ # Lookup and Lock Interface
419
+ #####################
420
+
421
+ def submit_lookup_and_lock_task(self, keys: list[ObjectKey]) -> L2TaskId:
422
+ with self._lock:
423
+ task_id = self._get_next_task_id()
424
+
425
+ self._loop.call_soon_threadsafe(self._execute_lookup_in_the_loop, keys, task_id)
426
+ return task_id
427
+
428
+ def query_lookup_and_lock_result(self, task_id: L2TaskId) -> Bitmap | None:
429
+ with self._lock:
430
+ return self._completed_lookup_tasks.pop(task_id, None)
431
+
432
+ def submit_unlock(self, keys: list[ObjectKey]) -> None:
433
+ def _unlock_keys(keys: list[ObjectKey]) -> None:
434
+ for key in keys:
435
+ if (obj := self._memory_objects.get(key)) is not None:
436
+ obj.decrease_pin_count()
437
+
438
+ self._loop.call_soon_threadsafe(_unlock_keys, keys)
439
+
440
+ #####################
441
+ # Load Interface
442
+ #####################
443
+
444
+ def submit_load_task(
445
+ self,
446
+ keys: list[ObjectKey],
447
+ objects: list[MemoryObj],
448
+ ) -> L2TaskId:
449
+ with self._lock:
450
+ task_id = self._get_next_task_id()
451
+
452
+ asyncio.run_coroutine_threadsafe(
453
+ self._execute_load_in_loop(keys, objects, task_id), self._loop
454
+ )
455
+ return task_id
456
+
457
+ def query_load_result(self, task_id: L2TaskId) -> Bitmap | None:
458
+ with self._lock:
459
+ return self._completed_load_tasks.pop(task_id, None)
460
+
461
+ #####################
462
+ # Eviction Interface
463
+ #####################
464
+
465
+ def delete(self, keys: list[ObjectKey]) -> None:
466
+ """Delete objects from storage, removing their files from disk."""
467
+ to_delete: list[tuple[ObjectKey, str]] = []
468
+ with self._lock:
469
+ for key in keys:
470
+ obj = self._memory_objects.get(key)
471
+ if obj is None:
472
+ continue
473
+ if obj.pin_count > 0:
474
+ logger.debug(
475
+ "Skipping eviction of pinned key %s (pin_count=%d)",
476
+ key,
477
+ obj.pin_count,
478
+ )
479
+ continue
480
+ self._total_bytes -= obj.size
481
+ del self._memory_objects[key]
482
+ to_delete.append((key, self.nixl_agent.get_file_path_for_key(key)))
483
+ # Filesystem I/O outside the lock to avoid blocking concurrent
484
+ # store/lookup/load operations.
485
+ deleted_keys: list[ObjectKey] = []
486
+ for key, file_path in to_delete:
487
+ self.nixl_agent.dynamic_delete_file(file_path)
488
+ deleted_keys.append(key)
489
+ if deleted_keys:
490
+ self._notify_keys_deleted(deleted_keys)
491
+
492
+ def get_usage(self) -> tuple[float, float]:
493
+ """Return (current_usage, usage_after_ongoing_eviction) in [0, 1]."""
494
+ with self._lock:
495
+ usage = self._total_bytes / self._max_capacity_bytes
496
+ return (usage, usage)
497
+
498
+ #####################
499
+ # Status Interface
500
+ #####################
501
+
502
+ def report_status(self) -> dict:
503
+ with self._lock:
504
+ stored_object_count = len(self._memory_objects)
505
+ pinned_object_count = sum(
506
+ 1 for obj in self._memory_objects.values() if obj.pin_count > 0
507
+ )
508
+ return {
509
+ "is_healthy": self._loop_thread.is_alive(),
510
+ "type": "DynamicNixlStoreL2Adapter",
511
+ "backend": self._config.backend,
512
+ "stored_object_count": stored_object_count,
513
+ "pinned_object_count": pinned_object_count,
514
+ "event_loop_alive": self._loop_thread.is_alive(),
515
+ }
516
+
517
+ #####################
518
+ # Cleanup Interface
519
+ #####################
520
+
521
+ def close(self):
522
+ # Stop the event loop and wait for all in-flight tasks to finish
523
+ async def _stop_tasks():
524
+ tasks = [
525
+ t
526
+ for t in asyncio.all_tasks(self._loop)
527
+ if t is not asyncio.current_task()
528
+ ]
529
+ for task in tasks:
530
+ task.cancel()
531
+ if tasks:
532
+ await asyncio.gather(*tasks, return_exceptions=True)
533
+
534
+ if self._loop.is_running():
535
+ future = asyncio.run_coroutine_threadsafe(_stop_tasks(), self._loop)
536
+ future.result(timeout=5)
537
+ self._loop.call_soon_threadsafe(self._loop.stop)
538
+
539
+ self._loop_thread.join()
540
+ self._loop.close()
541
+
542
+ # If persist is enabled, keep data files on disk; otherwise clean up.
543
+ if self._persist_enabled:
544
+ logger.info("persist_enabled=True, keeping data files on disk")
545
+ else:
546
+ logger.info("persist_enabled=False, deleting all data files")
547
+ with self._lock:
548
+ for key in list(self._memory_objects.keys()):
549
+ file_path = self.nixl_agent.get_file_path_for_key(key)
550
+ self.nixl_agent.dynamic_delete_file(file_path)
551
+
552
+ # Best-effort cleanup of orphaned temp files from crashed stores.
553
+ self.nixl_agent.cleanup_temp_files()
554
+
555
+ self.nixl_agent.close()
556
+
557
+ os.close(self._store_efd)
558
+ os.close(self._lookup_efd)
559
+ os.close(self._load_efd)
560
+
561
+ ##################
562
+ # Helper functions
563
+ ##################
564
+
565
+ def _run_event_loop(self) -> None:
566
+ asyncio.set_event_loop(self._loop)
567
+ self._loop.run_forever()
568
+
569
+ def _get_next_task_id(self) -> L2TaskId:
570
+ task_id = self._next_task_id
571
+ self._next_task_id += 1
572
+ return task_id
573
+
574
+ def _signal_store_event(self) -> None:
575
+ os.eventfd_write(self._store_efd, 1)
576
+
577
+ def _signal_lookup_event(self) -> None:
578
+ os.eventfd_write(self._lookup_efd, 1)
579
+
580
+ def _signal_load_event(self) -> None:
581
+ os.eventfd_write(self._load_efd, 1)
582
+
583
+ async def _execute_store_in_the_loop(
584
+ self,
585
+ keys: list[ObjectKey],
586
+ objects: list[MemoryObj],
587
+ task_id: L2TaskId,
588
+ ) -> None:
589
+ """Store each key-object pair to its own file via dynamic DMA write."""
590
+ success = True
591
+ stored_keys: list[ObjectKey] = []
592
+ try:
593
+ for key, obj in zip(keys, objects, strict=False):
594
+ mem_addr = obj.meta.address
595
+ mem_size = obj.meta.phy_size
596
+
597
+ # Reserve the key and capacity under the lock *before*
598
+ # the DMA write so that concurrent coroutines (other
599
+ # stores, secondary lookups) see the reservation.
600
+ with self._lock:
601
+ if key in self._memory_objects or key in self._inflight_stores:
602
+ continue
603
+ if self._total_bytes + mem_size > self._max_capacity_bytes:
604
+ logger.warning(
605
+ "Storage capacity exceeded, skipping store for key %s",
606
+ key,
607
+ )
608
+ break
609
+ self._inflight_stores.add(key)
610
+ self._total_bytes += mem_size
611
+
612
+ try:
613
+ mem_indices = self.nixl_agent.get_memory_indices(mem_addr, mem_size)
614
+ file_path = self.nixl_agent.get_file_path_for_key(key)
615
+
616
+ await self.nixl_agent.dynamic_store_file(
617
+ mem_indices, file_path, self.nixl_agent.l1_align_bytes
618
+ )
619
+
620
+ store_obj = NixlStoreObj(
621
+ page_indices=[], # not used in dynamic mode
622
+ size=mem_size,
623
+ layout=MemoryLayoutDesc(
624
+ [obj.meta.shape],
625
+ [obj.meta.dtype],
626
+ ),
627
+ pin_count=1,
628
+ )
629
+ with self._lock:
630
+ self._inflight_stores.discard(key)
631
+ self._memory_objects[key] = store_obj
632
+ store_obj.decrease_pin_count()
633
+ stored_keys.append(key)
634
+ except Exception:
635
+ # Un-reserve on failure so capacity accounting
636
+ # stays correct.
637
+ with self._lock:
638
+ self._inflight_stores.discard(key)
639
+ self._total_bytes -= mem_size
640
+ raise
641
+
642
+ except Exception:
643
+ logger.exception("Dynamic NIXL store task %d failed", task_id)
644
+ success = False
645
+
646
+ if stored_keys:
647
+ self._notify_keys_stored(stored_keys)
648
+
649
+ with self._lock:
650
+ self._completed_store_tasks[task_id] = success
651
+ self._signal_store_event()
652
+
653
+ def _execute_lookup_in_the_loop(
654
+ self, keys: list[ObjectKey], task_id: L2TaskId
655
+ ) -> None:
656
+ """Look up keys and pin found objects.
657
+
658
+ Also checks secondary storage (disk) for keys not in the
659
+ in-memory index and lazily populates ``_memory_objects`` for any
660
+ data files found on disk.
661
+ """
662
+ bitmap = Bitmap(len(keys))
663
+ with self._lock:
664
+ for i, key in enumerate(keys):
665
+ obj = self._memory_objects.get(key)
666
+ if obj is None:
667
+ obj = self._secondary_lookup_locked(key)
668
+ if obj is None:
669
+ continue
670
+ bitmap.set(i)
671
+ obj.increase_pin_count()
672
+ self._completed_lookup_tasks[task_id] = bitmap
673
+ self._signal_lookup_event()
674
+
675
+ def _secondary_lookup_locked(self, key: ObjectKey) -> NixlStoreObj | None:
676
+ """Check if a data file for ``key`` exists on disk; if so, populate
677
+ ``_memory_objects`` and return the entry. Caller must hold ``_lock``.
678
+
679
+ The file size is read via ``os.stat``. Layout is left as ``None`` and
680
+ will be supplied by the caller's MemoryObj at load time.
681
+ """
682
+ # Skip keys with an in-flight store to avoid double-counting
683
+ # in _total_bytes.
684
+ if key in self._inflight_stores:
685
+ return None
686
+ file_path = self.nixl_agent.get_file_path_for_key(key)
687
+ try:
688
+ obj_size = os.stat(file_path).st_size
689
+ except FileNotFoundError:
690
+ return None
691
+
692
+ # Enforce capacity when populating lazily too.
693
+ if self._total_bytes + obj_size > self._max_capacity_bytes:
694
+ logger.debug(
695
+ "Secondary lookup hit for %s but capacity exceeded, skipping",
696
+ key,
697
+ )
698
+ return None
699
+
700
+ obj = NixlStoreObj(
701
+ page_indices=[], # not used in dynamic mode
702
+ size=obj_size,
703
+ layout=None,
704
+ )
705
+ self._memory_objects[key] = obj
706
+ self._total_bytes += obj_size
707
+ return obj
708
+
709
+ async def _execute_load_in_loop(
710
+ self,
711
+ keys: list[ObjectKey],
712
+ objects: list[MemoryObj],
713
+ task_id: L2TaskId,
714
+ ) -> None:
715
+ """Load each found key from its file via dynamic DMA read."""
716
+ bitmap = Bitmap(len(keys))
717
+ accessed_keys: list[ObjectKey] = []
718
+ try:
719
+ for i, key in enumerate(keys):
720
+ with self._lock:
721
+ storage_obj = self._memory_objects.get(key)
722
+ if storage_obj is None:
723
+ continue
724
+
725
+ mem_addr = objects[i].meta.address
726
+ mem_size = objects[i].meta.phy_size
727
+ mem_indices = self.nixl_agent.get_memory_indices(mem_addr, mem_size)
728
+ file_path = self.nixl_agent.get_file_path_for_key(key)
729
+
730
+ await self.nixl_agent.dynamic_load_file(
731
+ mem_indices, file_path, self.nixl_agent.l1_align_bytes
732
+ )
733
+
734
+ bitmap.set(i)
735
+ accessed_keys.append(key)
736
+
737
+ except Exception:
738
+ logger.exception("Dynamic NIXL load task %d failed", task_id)
739
+
740
+ if accessed_keys:
741
+ self._notify_keys_accessed(accessed_keys)
742
+ with self._lock:
743
+ self._completed_load_tasks[task_id] = bitmap
744
+ self._signal_load_event()
745
+
746
+
747
+ # ---------------------------------------------------------------------
748
+ # Config and self-registration
749
+ # ---------------------------------------------------------------------
750
+
751
+ # TODO(Jiayi): OBJ backend is not supported in the dynamic adapter yet.
752
+ # Only file-based backends are supported.
753
+ _VALID_DYNAMIC_BACKENDS = ("GDS", "GDS_MT", "POSIX", "HF3FS")
754
+
755
+
756
+ class DynamicNixlStoreL2AdapterConfig(L2AdapterConfigBase):
757
+ """Config for the dynamic-file Nixl L2 adapter.
758
+
759
+ Fields:
760
+ - backend: Nixl storage backend (GDS, GDS_MT, POSIX, HF3FS).
761
+ - backend_params: Backend-specific parameters as a dict of string
762
+ key-value pairs. Must include ``file_path`` and ``use_direct_io``.
763
+ """
764
+
765
+ def __init__(
766
+ self,
767
+ backend: str,
768
+ backend_params: dict[str, str],
769
+ ):
770
+ if backend not in _VALID_DYNAMIC_BACKENDS:
771
+ raise ValueError(
772
+ "backend must be one of %s, got %r" % (_VALID_DYNAMIC_BACKENDS, backend)
773
+ )
774
+ if "file_path" not in backend_params:
775
+ raise ValueError(
776
+ "backend_params must include 'file_path' for backend %r" % backend
777
+ )
778
+ if "use_direct_io" not in backend_params:
779
+ raise ValueError(
780
+ "backend_params must include 'use_direct_io' for backend %r" % backend
781
+ )
782
+ self.backend = backend
783
+ self.backend_params = backend_params
784
+
785
+ @classmethod
786
+ def from_dict(cls, d: dict) -> DynamicNixlStoreL2AdapterConfig:
787
+ backend = d.get("backend")
788
+ if backend not in _VALID_DYNAMIC_BACKENDS:
789
+ raise ValueError(
790
+ "backend must be one of %s, got %r" % (_VALID_DYNAMIC_BACKENDS, backend)
791
+ )
792
+
793
+ backend_params = d.get("backend_params", {})
794
+ if not isinstance(backend_params, dict):
795
+ raise ValueError("backend_params must be a dict of string key-value pairs")
796
+
797
+ return cls(backend=backend, backend_params=backend_params)
798
+
799
+ @classmethod
800
+ def help(cls) -> str:
801
+ return (
802
+ "Dynamic Nixl store L2 adapter config fields:\n"
803
+ "- backend (str): Nixl storage backend, "
804
+ "one of %s (required)\n"
805
+ "- backend_params (dict): backend-specific "
806
+ "string key-value pairs. Must include "
807
+ "'file_path' and 'use_direct_io'.\n"
808
+ "- persist_enabled (bool): if True, keep data files on disk "
809
+ "at shutdown (optional, default True)\n"
810
+ "Lookup always checks secondary storage (disk) on miss."
811
+ % (_VALID_DYNAMIC_BACKENDS,)
812
+ )
813
+
814
+
815
+ # Self-register config type and adapter factory
816
+ register_l2_adapter_type("nixl_store_dynamic", DynamicNixlStoreL2AdapterConfig)
817
+
818
+
819
+ def _create_dynamic_nixl_store_adapter(
820
+ config: L2AdapterConfigBase,
821
+ l1_memory_desc: Optional[L1MemoryDesc] = None,
822
+ ) -> L2AdapterInterface:
823
+ """Create a DynamicNixlStoreL2Adapter from config."""
824
+ if l1_memory_desc is None:
825
+ raise ValueError(
826
+ "l1_memory_desc is required to create a DynamicNixlStoreL2Adapter."
827
+ )
828
+ return DynamicNixlStoreL2Adapter(config, l1_memory_desc) # type: ignore[arg-type]
829
+
830
+
831
+ register_l2_adapter_factory("nixl_store_dynamic", _create_dynamic_nixl_store_adapter)