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,983 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ # Future
4
+ from __future__ import annotations
5
+
6
+ # Standard
7
+ from dataclasses import dataclass, field
8
+ from typing import Optional
9
+ import asyncio
10
+ import os
11
+ import threading
12
+ import uuid
13
+
14
+ # Third Party
15
+ from nixl._api import nixl_agent as NixlAgent
16
+ from nixl._api import nixl_agent_config as NixlAgentConfig
17
+ from nixl._api import nixl_prepped_dlist_handle as NixlDlistHandle
18
+ from nixl._api import nixl_xfer_handle as NixlXferHandle
19
+ from nixl._api import (
20
+ nixlBind,
21
+ )
22
+
23
+ # First Party
24
+ from lmcache.logging import init_logger
25
+ from lmcache.native_storage_ops import Bitmap
26
+ from lmcache.v1.distributed.api import MemoryLayoutDesc, ObjectKey
27
+ from lmcache.v1.distributed.internal_api import L1MemoryDesc
28
+ from lmcache.v1.distributed.l2_adapters.base import L2AdapterInterface, L2TaskId
29
+ from lmcache.v1.distributed.l2_adapters.config import (
30
+ L2AdapterConfigBase,
31
+ register_l2_adapter_type,
32
+ )
33
+ from lmcache.v1.distributed.l2_adapters.factory import (
34
+ register_l2_adapter_factory,
35
+ )
36
+ from lmcache.v1.memory_management import MemoryObj
37
+
38
+ logger = init_logger(__name__)
39
+
40
+ # Main class
41
+
42
+
43
+ @dataclass
44
+ class NixlStoreObj:
45
+ """
46
+ The object stored in Nixl L2 cache.
47
+ Can be used for both file and object.
48
+ """
49
+
50
+ page_indices: list[int]
51
+
52
+ size: int # in bytes
53
+
54
+ layout: Optional[MemoryLayoutDesc] = None
55
+
56
+ pin_count: int = 0
57
+ _lock: threading.Lock = field(
58
+ default_factory=threading.Lock, repr=False, compare=False
59
+ )
60
+
61
+ def increase_pin_count(self):
62
+ with self._lock:
63
+ self.pin_count += 1
64
+
65
+ def decrease_pin_count(self):
66
+ with self._lock:
67
+ if self.pin_count > 0:
68
+ self.pin_count -= 1
69
+ else:
70
+ logger.warning(
71
+ "Trying to decrease pin count of object at page indices %s below 0",
72
+ self.page_indices,
73
+ )
74
+
75
+
76
+ class NixlObjPool:
77
+ """Thread-safe pool of integer indices representing pre-allocated storage slots."""
78
+
79
+ def __init__(self, num_total_objs: int):
80
+ """
81
+ Args:
82
+ num_total_objs: Total number of storage slots to manage.
83
+ """
84
+ self.indices = list(range(num_total_objs))
85
+ self._total = num_total_objs
86
+ self._lock = threading.Lock()
87
+
88
+ def batched_allocate(self, num_objs: int) -> list[int]:
89
+ """
90
+ Allocate a batch of storage slot indices.
91
+
92
+ Args:
93
+ num_objs: Number of indices to allocate.
94
+
95
+ Returns:
96
+ list[int]: The allocated indices.
97
+
98
+ Raises:
99
+ RuntimeError: If fewer than ``num_objs`` slots remain in the pool.
100
+ """
101
+ with self._lock:
102
+ if num_objs > len(self.indices):
103
+ logger.debug("NixlObjPool allocation failure.")
104
+ return []
105
+ allocated = self.indices[:num_objs]
106
+ self.indices = self.indices[num_objs:]
107
+ return allocated
108
+
109
+ def batched_free(self, obj_indices: list[int]) -> None:
110
+ """
111
+ Return a batch of storage slot indices back to the pool.
112
+
113
+ Args:
114
+ obj_indices: Indices previously obtained from ``batched_allocate``.
115
+ """
116
+ with self._lock:
117
+ self.indices.extend(obj_indices)
118
+
119
+ def get_usage(self) -> tuple[float, float]:
120
+ """
121
+ Return (current_usage, usage_after_ongoing_eviction) in [0, 1].
122
+
123
+ Both values are identical because slot frees are synchronous.
124
+ """
125
+ with self._lock:
126
+ if self._total == 0:
127
+ return (0.0, 0.0)
128
+ usage = (self._total - len(self.indices)) / self._total
129
+ return (usage, usage)
130
+
131
+
132
+ class NixlStorageAgent:
133
+ agent_name: str
134
+ nixl_agent: NixlAgent
135
+ mem_reg_descs: nixlBind.nixlRegDList
136
+ mem_xfer_handler: NixlDlistHandle
137
+
138
+ def __init__(
139
+ self,
140
+ device: str,
141
+ backend: str,
142
+ backend_params: dict[str, str],
143
+ pool_size: int,
144
+ l1_memory_desc: L1MemoryDesc,
145
+ ):
146
+ """
147
+ Initialize the NixlStorageAgent.
148
+
149
+ Args:
150
+ device: Device type of the L1 memory buffer (e.g. "cpu", "cuda").
151
+ backend: Nixl storage backend to use. One of: GDS, GDS_MT, POSIX,
152
+ HF3FS (file-based) or OBJ (object-based).
153
+ backend_params: Backend-specific parameters. File-based backends
154
+ require "file_path" and "use_direct_io" keys.
155
+ pool_size: Number of storage descriptor slots to pre-allocate.
156
+ l1_memory_desc: Descriptor of the L1 memory buffer to register with Nixl
157
+ for data transfers.
158
+ """
159
+ self.backend = backend
160
+ self.pool_size = pool_size
161
+ self.device = device
162
+ self.backend_params = backend_params
163
+
164
+ self.l1_align_bytes = l1_memory_desc.align_bytes
165
+
166
+ self.agent_name = "NixlAgent_" + str(uuid.uuid4())
167
+ nixl_conf = NixlAgentConfig(backends=[])
168
+ self.nixl_agent = NixlAgent(self.agent_name, nixl_conf)
169
+ self.nixl_agent.create_backend(backend, backend_params)
170
+
171
+ self.init_mem_handlers(
172
+ self.device,
173
+ l1_memory_desc.ptr,
174
+ l1_memory_desc.size,
175
+ l1_memory_desc.align_bytes,
176
+ device_id=0, # 0 indicates cpu
177
+ )
178
+
179
+ if self.backend in ["GDS", "GDS_MT", "POSIX", "HF3FS"]:
180
+ file_size = int(
181
+ self.backend_params.get("file_size", l1_memory_desc.align_bytes)
182
+ )
183
+ pages_per_file = file_size // l1_memory_desc.align_bytes
184
+ self.pool = NixlObjPool(num_total_objs=self.pool_size * pages_per_file)
185
+ self.init_storage_handlers_file(
186
+ num_files=self.pool_size,
187
+ page_size=l1_memory_desc.align_bytes,
188
+ file_size=file_size,
189
+ file_path=self.backend_params["file_path"],
190
+ # TODO(Jiayi): Need to make argument parsing more elegant
191
+ use_direct_io=str(self.backend_params["use_direct_io"]).lower()
192
+ == "true",
193
+ )
194
+ elif self.backend in ["OBJ"]:
195
+ self.pool = NixlObjPool(num_total_objs=self.pool_size)
196
+ self.init_storage_handlers_object(
197
+ page_size=l1_memory_desc.align_bytes,
198
+ num_pages=self.pool_size,
199
+ )
200
+ else:
201
+ raise TypeError(f"Unsupported backend type: {self.backend}")
202
+
203
+ def init_mem_handlers(self, device, buffer_ptr, buffer_size, page_size, device_id):
204
+ """
205
+ Initialize memory handlers for the given device and buffer.
206
+ """
207
+ reg_list = [(buffer_ptr, buffer_size, device_id, "")]
208
+ xfer_desc = [
209
+ (base_addr, page_size, device_id)
210
+ for base_addr in range(buffer_ptr, buffer_ptr + buffer_size, page_size)
211
+ ]
212
+
213
+ if device == "cpu":
214
+ mem_type = "DRAM"
215
+ else:
216
+ mem_type = "VRAM"
217
+
218
+ reg_descs = self.nixl_agent.register_memory(reg_list, mem_type=mem_type)
219
+ xfer_descs = self.nixl_agent.get_xfer_descs(xfer_desc, mem_type=mem_type)
220
+ xfer_handler = self.nixl_agent.prep_xfer_dlist(
221
+ "", xfer_descs, mem_type=mem_type
222
+ )
223
+
224
+ self.mem_reg_descs = reg_descs
225
+ self.mem_xfer_handler = xfer_handler
226
+
227
+ def init_storage_handlers_file(
228
+ self,
229
+ num_files: int,
230
+ page_size: int,
231
+ file_size: int,
232
+ file_path: str,
233
+ use_direct_io: bool,
234
+ ):
235
+ """Initialize storage handlers for file-based backends.
236
+
237
+ Each file holds ``file_size // page_size`` pages at successive offsets.
238
+ ``file_size`` must be a multiple of ``page_size``.
239
+
240
+ Args:
241
+ num_files: Number of storage files to create.
242
+ page_size: Granularity of L1 memory pages (transfer unit size).
243
+ file_size: Size in bytes of each storage file. Must be a multiple
244
+ of ``page_size``.
245
+ file_path: Directory where storage files are created.
246
+ use_direct_io: Whether to open files with O_DIRECT.
247
+ """
248
+ if file_size % page_size != 0:
249
+ raise ValueError(
250
+ f"file_size ({file_size}) must be a multiple of page_size ({page_size})"
251
+ )
252
+
253
+ pages_per_file = file_size // page_size
254
+ num_pages = num_files * pages_per_file
255
+
256
+ # Create file descriptors for Nixl to register
257
+ fds: list[int] = []
258
+ flags = os.O_CREAT | os.O_RDWR
259
+ if use_direct_io:
260
+ if hasattr(os, "O_DIRECT"):
261
+ flags |= os.O_DIRECT
262
+ else:
263
+ logger.warning(
264
+ "use_direct_io is True, but O_DIRECT is not available on "
265
+ "this system. Falling back to buffered I/O."
266
+ )
267
+ for i in range(num_files):
268
+ filename = f"obj_{i}_{uuid.uuid4().hex[0:4]}.bin"
269
+ tmp_path = os.path.join(file_path, filename)
270
+ fd = os.open(tmp_path, flags)
271
+ fds.append(fd)
272
+
273
+ # Register each file covering the full file_size.
274
+ # Build one xfer_desc entry per page slot (page index i maps to
275
+ # offset (i % pages_per_file) * page_size inside fd[i // pages_per_file]).
276
+ reg_list = []
277
+ xfer_desc = []
278
+ for fd in fds:
279
+ reg_list.append((0, file_size, fd, ""))
280
+ for page_idx in range(num_pages):
281
+ fd = fds[page_idx // pages_per_file]
282
+ offset = (page_idx % pages_per_file) * page_size
283
+ xfer_desc.append((offset, page_size, fd))
284
+ reg_descs = self.nixl_agent.register_memory(reg_list, mem_type="FILE")
285
+ xfer_descs = self.nixl_agent.get_xfer_descs(xfer_desc, mem_type="FILE")
286
+ xfer_handler = self.nixl_agent.prep_xfer_dlist(
287
+ self.agent_name, xfer_descs, mem_type="FILE"
288
+ )
289
+
290
+ self.storage_fds = fds
291
+ self.storage_reg_descs = reg_descs
292
+ self.storage_xfer_descs = xfer_descs
293
+ self.storage_xfer_handler = xfer_handler
294
+
295
+ def init_storage_handlers_object(
296
+ self,
297
+ page_size: int,
298
+ num_pages: int,
299
+ ):
300
+ """Initialize storage handlers for object-based backends."""
301
+
302
+ # Create object keys for Nixl to register
303
+ keys = []
304
+
305
+ for i in range(num_pages):
306
+ key = f"obj_{i}_{uuid.uuid4().hex[0:4]}"
307
+ keys.append(key)
308
+
309
+ # Register and prepare xfer handler
310
+ reg_list = []
311
+ xfer_desc = []
312
+ for i, key in enumerate(keys):
313
+ reg_list.append((0, page_size, i, key))
314
+ xfer_desc.append((0, page_size, i))
315
+ reg_descs = self.nixl_agent.register_memory(reg_list, mem_type="OBJ")
316
+ xfer_descs = self.nixl_agent.get_xfer_descs(xfer_desc, mem_type="OBJ")
317
+ xfer_handler = self.nixl_agent.prep_xfer_dlist(
318
+ self.agent_name, xfer_descs, mem_type="OBJ"
319
+ )
320
+
321
+ self.storage_reg_descs = reg_descs
322
+ self.storage_xfer_descs = xfer_descs
323
+ self.storage_xfer_handler = xfer_handler
324
+
325
+ def get_mem_to_storage_handle(self, mem_indices, storage_indices) -> NixlXferHandle:
326
+ """Get a Nixl transfer handle for transferring data from memory to storage."""
327
+
328
+ return self.nixl_agent.make_prepped_xfer(
329
+ "WRITE",
330
+ self.mem_xfer_handler,
331
+ mem_indices,
332
+ self.storage_xfer_handler,
333
+ storage_indices,
334
+ )
335
+
336
+ def get_storage_to_mem_handle(self, mem_indices, storage_indices) -> NixlXferHandle:
337
+ """Get a Nixl transfer handle for transferring data from storage to memory."""
338
+ return self.nixl_agent.make_prepped_xfer(
339
+ "READ",
340
+ self.mem_xfer_handler,
341
+ mem_indices,
342
+ self.storage_xfer_handler,
343
+ storage_indices,
344
+ )
345
+
346
+ def post_blocking(self, handle: NixlXferHandle):
347
+ """Post a Nixl transfer handle and block until the transfer is done."""
348
+
349
+ state = self.nixl_agent.transfer(handle)
350
+
351
+ while state != "DONE" and state != "ERR":
352
+ try:
353
+ state = self.nixl_agent.check_xfer_state(handle)
354
+ except nixlBind.nixlBackendError:
355
+ raise
356
+
357
+ if state == "ERR":
358
+ raise RuntimeError("NIXL transfer failed")
359
+
360
+ async def post_non_blocking(self, handle: NixlXferHandle):
361
+ """Post a Nixl transfer handle and await until the transfer is done."""
362
+
363
+ state = self.nixl_agent.transfer(handle)
364
+
365
+ while state != "DONE" and state != "ERR":
366
+ try:
367
+ state = self.nixl_agent.check_xfer_state(handle)
368
+ except nixlBind.nixlBackendError:
369
+ raise
370
+
371
+ # TODO(Jiayi): Tune this for better perf
372
+ await asyncio.sleep(0.01)
373
+
374
+ if state == "ERR":
375
+ raise RuntimeError("NIXL transfer failed")
376
+
377
+ def get_storage_indices(self, num_objs: int) -> list[int]:
378
+ # TODO(Jiayi): Support eviction
379
+ return self.pool.batched_allocate(num_objs)
380
+
381
+ def get_memory_indices(self, raw_addr: int, mem_size: int) -> list[int]:
382
+ """Get memory indices for the given raw address and size."""
383
+ # TODO(Jiayi): Now we assume the memory is contiguous and page-aligned. We may
384
+ # want to support more flexible memory layout in the future.
385
+ if raw_addr % self.l1_align_bytes != 0:
386
+ raise ValueError(
387
+ f"Raw address {raw_addr} is not aligned to "
388
+ f"page size {self.l1_align_bytes}"
389
+ )
390
+ if mem_size % self.l1_align_bytes != 0:
391
+ raise ValueError(
392
+ f"Memory size {mem_size} is not a multiple of "
393
+ f"page size {self.l1_align_bytes}"
394
+ )
395
+ num_pages = mem_size // self.l1_align_bytes
396
+ return [(raw_addr // self.l1_align_bytes + i) for i in range(num_pages)]
397
+
398
+ def release_handle(self, handle):
399
+ self.nixl_agent.release_xfer_handle(handle)
400
+
401
+ def close(self):
402
+ self.nixl_agent.release_dlist_handle(self.storage_xfer_handler)
403
+ self.nixl_agent.release_dlist_handle(self.mem_xfer_handler)
404
+ self.nixl_agent.deregister_memory(self.storage_reg_descs)
405
+ self.nixl_agent.deregister_memory(self.mem_reg_descs)
406
+ for fd in getattr(self, "storage_fds", []):
407
+ os.close(fd)
408
+
409
+
410
+ class NixlStoreL2Adapter(L2AdapterInterface):
411
+ """
412
+ A Nixl-based L2 adapter
413
+ """
414
+
415
+ def __init__(self, config: NixlStoreL2AdapterConfig, l1_memory_desc: L1MemoryDesc):
416
+ """
417
+ Initialize the NixlStoreL2Adapter.
418
+
419
+ Args:
420
+ config: Nixl-specific adapter configuration including backend type,
421
+ backend parameters, and storage pool size.
422
+ l1_memory_desc: Descriptor of the L1 memory buffer to register with the
423
+ Nixl backend for DMA transfers.
424
+ """
425
+ super().__init__()
426
+ self._config = config
427
+
428
+ self._store_efd = os.eventfd(0, os.EFD_NONBLOCK | os.EFD_CLOEXEC)
429
+ self._lookup_efd = os.eventfd(0, os.EFD_NONBLOCK | os.EFD_CLOEXEC)
430
+ self._load_efd = os.eventfd(0, os.EFD_NONBLOCK | os.EFD_CLOEXEC)
431
+
432
+ # Cache data structures
433
+ self._memory_objects: dict[ObjectKey, NixlStoreObj] = {}
434
+
435
+ # Task ID management
436
+ self._next_task_id: L2TaskId = 0
437
+ self._completed_store_tasks: dict[L2TaskId, bool] = {}
438
+ self._completed_lookup_tasks: dict[L2TaskId, Bitmap] = {}
439
+ self._completed_load_tasks: dict[L2TaskId, Bitmap] = {}
440
+ self._lock = threading.Lock() # lock for all shared state
441
+
442
+ # Asyncio event loop running in a background thread
443
+ self._loop = asyncio.new_event_loop()
444
+ self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
445
+ self._loop_thread.start()
446
+
447
+ # Initialize Nixl agent
448
+ self.nixl_agent = NixlStorageAgent(
449
+ device="cpu",
450
+ backend=config.backend,
451
+ backend_params=config.backend_params,
452
+ pool_size=config.pool_size,
453
+ l1_memory_desc=l1_memory_desc,
454
+ )
455
+
456
+ # --------------------
457
+ # Event Fd Interface
458
+ # --------------------
459
+
460
+ def get_store_event_fd(self) -> int:
461
+ return self._store_efd
462
+
463
+ def get_lookup_and_lock_event_fd(self) -> int:
464
+ return self._lookup_efd
465
+
466
+ def get_load_event_fd(self) -> int:
467
+ return self._load_efd
468
+
469
+ #####################
470
+ # Store Interface
471
+ #####################
472
+
473
+ def submit_store_task(
474
+ self,
475
+ keys: list[ObjectKey],
476
+ objects: list[MemoryObj],
477
+ ) -> L2TaskId:
478
+ """
479
+ Submit a store task to store a batch of memory objects associated with
480
+ a batch of keys.
481
+
482
+ Args:
483
+ keys (list[ObjectKey]): the list of keys to be stored.
484
+ objects (list[MemoryObj]): the list of memory objects to be stored.
485
+ The length of the objects list should be the same as the length of
486
+ the keys list.
487
+
488
+ Returns:
489
+ L2TaskId: the task id of the submitted store task.
490
+ """
491
+ with self._lock:
492
+ task_id = self._get_next_task_id()
493
+
494
+ asyncio.run_coroutine_threadsafe(
495
+ self._execute_store_in_the_loop(keys, objects, task_id), self._loop
496
+ )
497
+
498
+ return task_id
499
+
500
+ def pop_completed_store_tasks(self) -> dict[L2TaskId, bool]:
501
+ """
502
+ Pop all the completed store tasks with a flag indicating
503
+ whether the task is successful or not.
504
+
505
+ Returns:
506
+ dict[L2TaskId, bool]: a dictionary mapping the task id to a boolean flag
507
+ indicating whether the task is successful or not. True means
508
+ successful, and False means failed.
509
+ """
510
+ with self._lock:
511
+ completed = self._completed_store_tasks
512
+ self._completed_store_tasks = {}
513
+ return completed
514
+
515
+ #####################
516
+ # Lookup and Lock Interface
517
+ #####################
518
+
519
+ def submit_lookup_and_lock_task(self, keys: list[ObjectKey]) -> L2TaskId:
520
+ with self._lock:
521
+ task_id = self._get_next_task_id()
522
+
523
+ # Schedule the lookup operation in the event loop thread
524
+ self._loop.call_soon_threadsafe(self._execute_lookup_in_the_loop, keys, task_id)
525
+ return task_id
526
+
527
+ def query_lookup_and_lock_result(self, task_id: L2TaskId) -> Bitmap | None:
528
+ with self._lock:
529
+ return self._completed_lookup_tasks.pop(task_id, None)
530
+
531
+ def submit_unlock(self, keys: list[ObjectKey]) -> None:
532
+ def _unlock_keys(keys: list[ObjectKey]) -> None:
533
+ """
534
+ Unlock keys in the event loop thread.
535
+ """
536
+ for key in keys:
537
+ if (obj := self._memory_objects.get(key)) is not None:
538
+ obj.decrease_pin_count()
539
+
540
+ # Schedule the unlock operation in the event loop thread
541
+ self._loop.call_soon_threadsafe(_unlock_keys, keys)
542
+
543
+ #####################
544
+ # Load Interface
545
+ ######################
546
+
547
+ def submit_load_task(
548
+ self,
549
+ keys: list[ObjectKey],
550
+ objects: list[MemoryObj],
551
+ ) -> L2TaskId:
552
+ with self._lock:
553
+ task_id = self._get_next_task_id()
554
+
555
+ # Schedule the load operation in the event loop thread
556
+ asyncio.run_coroutine_threadsafe(
557
+ self._execute_load_in_loop(keys, objects, task_id), self._loop
558
+ )
559
+
560
+ return task_id
561
+
562
+ def query_load_result(self, task_id: L2TaskId) -> Bitmap | None:
563
+ with self._lock:
564
+ return self._completed_load_tasks.pop(task_id, None)
565
+
566
+ def close(self):
567
+ # Stop the event loop and wait for the thread to finish
568
+ async def _stop_tasks():
569
+ tasks = [
570
+ t
571
+ for t in asyncio.all_tasks(self._loop)
572
+ if t is not asyncio.current_task()
573
+ ]
574
+ for task in tasks:
575
+ task.cancel()
576
+ if tasks:
577
+ await asyncio.gather(*tasks, return_exceptions=True)
578
+
579
+ if self._loop.is_running():
580
+ future = asyncio.run_coroutine_threadsafe(_stop_tasks(), self._loop)
581
+
582
+ future.result(timeout=5) # Wait for tasks to be cancelled, with a timeout
583
+
584
+ self._loop.call_soon_threadsafe(self._loop.stop)
585
+
586
+ self._loop_thread.join()
587
+ self._loop.close()
588
+
589
+ os.close(self._store_efd)
590
+ os.close(self._lookup_efd)
591
+ os.close(self._load_efd)
592
+
593
+ #####################
594
+ # Eviction Interface
595
+ #####################
596
+
597
+ def delete(self, keys: list[ObjectKey]) -> None:
598
+ """
599
+ Delete a batch of objects from Nixl storage, freeing their page slots.
600
+
601
+ Pinned objects (pin_count > 0) are skipped to avoid racing with an
602
+ in-flight load; the eviction controller will retry them on the next
603
+ cycle once they are unpinned.
604
+ """
605
+ # TODO(Jiayi): Optimize lock usage here
606
+ deleted_keys: list[ObjectKey] = []
607
+ with self._lock:
608
+ for key in keys:
609
+ obj = self._memory_objects.get(key)
610
+ if obj is None:
611
+ continue
612
+ if obj.pin_count > 0:
613
+ logger.debug(
614
+ "Skipping eviction of pinned key %s (pin_count=%d)",
615
+ key,
616
+ obj.pin_count,
617
+ )
618
+ continue
619
+ del self._memory_objects[key]
620
+ self.nixl_agent.pool.batched_free(obj.page_indices)
621
+ deleted_keys.append(key)
622
+ if deleted_keys:
623
+ self._notify_keys_deleted(deleted_keys)
624
+
625
+ def get_usage(self) -> tuple[float, float]:
626
+ """
627
+ Return (current_usage, usage_after_ongoing_eviction) based on pool slots.
628
+ """
629
+ return self.nixl_agent.pool.get_usage()
630
+
631
+ #####################
632
+ # Status Interface
633
+ #####################
634
+
635
+ def report_status(self) -> dict:
636
+ """Return a status dict for the Nixl L2 adapter."""
637
+ # NOTE(Jiayi): This function looks pretty slow.
638
+ with self._lock:
639
+ stored_object_count = len(self._memory_objects)
640
+ pinned_object_count = sum(
641
+ 1 for obj in self._memory_objects.values() if obj.pin_count > 0
642
+ )
643
+ pool = self.nixl_agent.pool
644
+ with pool._lock:
645
+ pool_free_slots = len(pool.indices)
646
+ return {
647
+ "is_healthy": self._loop_thread.is_alive(),
648
+ "type": "NixlStoreL2Adapter",
649
+ "backend": self._config.backend,
650
+ "stored_object_count": stored_object_count,
651
+ "pinned_object_count": pinned_object_count,
652
+ "pool_size": self._config.pool_size,
653
+ "pool_free_slots": pool_free_slots,
654
+ "event_loop_alive": self._loop_thread.is_alive(),
655
+ }
656
+
657
+ ##################
658
+ # Helper functions
659
+ ##################
660
+
661
+ def _run_event_loop(self) -> None:
662
+ """Run the asyncio event loop in a background thread."""
663
+ asyncio.set_event_loop(self._loop)
664
+ self._loop.run_forever()
665
+
666
+ def _get_next_task_id(self) -> L2TaskId:
667
+ """Get the next task ID and increment the counter."""
668
+ task_id = self._next_task_id
669
+ self._next_task_id += 1
670
+ return task_id
671
+
672
+ def _evict_if_needed(
673
+ self,
674
+ ) -> None:
675
+ """
676
+ Evict objects from the cache using desired caching policy.
677
+ """
678
+
679
+ # TODO(Jiayi): Support eviction
680
+
681
+ pass
682
+
683
+ def _signal_store_event(self) -> None:
684
+ """Signal the store event fd to notify completion."""
685
+ os.eventfd_write(self._store_efd, 1)
686
+
687
+ async def _execute_store_in_the_loop(
688
+ self,
689
+ keys: list[ObjectKey],
690
+ objects: list[MemoryObj],
691
+ task_id: L2TaskId,
692
+ ) -> None:
693
+ """
694
+ Coroutine that performs a batched store to Nixl storage.
695
+
696
+ For each key-object pair, memory page indices are mapped to storage
697
+ slot indices and a single batched DMA write is issued. On success the
698
+ key-to-storage mapping is recorded in ``_memory_objects``. On transfer
699
+ failure, all allocated storage slots are freed and the task is marked
700
+ as failed.
701
+
702
+ Args:
703
+ keys: Keys identifying each object to store.
704
+ objects: Memory objects whose contents will be written to storage.
705
+ Must be the same length as ``keys``.
706
+ task_id: Identifier used to report completion via
707
+ ``_completed_store_tasks``.
708
+ """
709
+ success = True
710
+ try:
711
+ # Get memory page indices and storage slot indices
712
+ mem_indices_flat = []
713
+ storage_indices_flat = []
714
+ stored_keys = []
715
+ storage_objs = []
716
+ for key, obj in zip(keys, objects, strict=False):
717
+ # Skip if key already exists to avoid leaking pool slots
718
+ with self._lock:
719
+ if key in self._memory_objects:
720
+ continue
721
+
722
+ mem_addr = obj.meta.address
723
+ mem_size = obj.meta.phy_size
724
+ mem_indices = self.nixl_agent.get_memory_indices(mem_addr, mem_size)
725
+ storage_indices = self.nixl_agent.get_storage_indices(
726
+ num_objs=len(mem_indices)
727
+ )
728
+
729
+ if storage_indices == []:
730
+ break
731
+
732
+ mem_indices_flat.extend(mem_indices)
733
+ storage_indices_flat.extend(storage_indices)
734
+
735
+ stored_keys.append(key)
736
+ storage_objs.append(
737
+ NixlStoreObj(
738
+ page_indices=storage_indices,
739
+ size=obj.meta.phy_size,
740
+ layout=MemoryLayoutDesc(
741
+ [obj.meta.shape],
742
+ [obj.meta.dtype],
743
+ ),
744
+ pin_count=1,
745
+ )
746
+ )
747
+
748
+ if not mem_indices_flat:
749
+ # Nothing to store (all keys already existed or pool empty)
750
+ with self._lock:
751
+ self._completed_store_tasks[task_id] = True
752
+ self._signal_store_event()
753
+ return
754
+
755
+ handle = self.nixl_agent.get_mem_to_storage_handle(
756
+ mem_indices_flat,
757
+ storage_indices_flat,
758
+ )
759
+
760
+ await self.nixl_agent.post_non_blocking(handle)
761
+ self.nixl_agent.release_handle(handle)
762
+
763
+ with self._lock:
764
+ for key, storage_obj in zip(stored_keys, storage_objs, strict=False):
765
+ self._memory_objects[key] = storage_obj
766
+ storage_obj.decrease_pin_count()
767
+ self._notify_keys_stored(stored_keys)
768
+
769
+ # success is only set to false for transfer failures
770
+ except Exception:
771
+ logger.exception("NIXL store task %d failed", task_id)
772
+ success = False
773
+
774
+ # free storage indices if transfer fails
775
+ self.nixl_agent.pool.batched_free(storage_indices_flat)
776
+
777
+ with self._lock:
778
+ self._completed_store_tasks[task_id] = success
779
+
780
+ self._signal_store_event()
781
+
782
+ def _signal_lookup_event(self) -> None:
783
+ """Signal the lookup event fd to notify completion."""
784
+ os.eventfd_write(self._lookup_efd, 1)
785
+
786
+ def _execute_lookup_in_the_loop(
787
+ self, keys: list[ObjectKey], task_id: L2TaskId
788
+ ) -> None:
789
+ """
790
+ Performs a batched lookup and pin in the event loop.
791
+
792
+ For each key present in ``_memory_objects``, its bit is set in the
793
+ result bitmap and its pin count is incremented to prevent eviction
794
+ while the caller holds the lock. Keys not found are left unset.
795
+
796
+ Args:
797
+ keys: Keys to look up.
798
+ task_id: Identifier used to report completion via
799
+ ``_completed_lookup_tasks``.
800
+ """
801
+ bitmap = Bitmap(len(keys))
802
+ with self._lock:
803
+ for i, key in enumerate(keys):
804
+ if (obj := self._memory_objects.get(key)) is None:
805
+ continue
806
+ bitmap.set(i)
807
+ obj.increase_pin_count()
808
+ self._completed_lookup_tasks[task_id] = bitmap
809
+ self._signal_lookup_event()
810
+
811
+ def _signal_load_event(self) -> None:
812
+ """Signal the load event fd to notify completion."""
813
+ os.eventfd_write(self._load_efd, 1)
814
+
815
+ async def _execute_load_in_loop(
816
+ self,
817
+ keys: list[ObjectKey],
818
+ objects: list[MemoryObj],
819
+ task_id: L2TaskId,
820
+ ) -> None:
821
+ """
822
+ Coroutine that performs a batched load from Nixl storage into L1 memory.
823
+
824
+ For each key that exists in ``_memory_objects``, the corresponding
825
+ storage page indices are gathered and a single batched DMA read is
826
+ issued into the provided memory objects. Keys that are not found are
827
+ silently skipped and their bit in the result bitmap is left unset.
828
+
829
+ Args:
830
+ keys: Keys identifying each object to load.
831
+ objects: Pre-allocated memory objects that will receive the loaded
832
+ data. Must be the same length as ``keys``.
833
+ task_id: Identifier used to report completion via
834
+ ``_completed_load_tasks``.
835
+ """
836
+ bitmap = Bitmap(len(keys))
837
+ accessed_keys: list[ObjectKey] = []
838
+ try:
839
+ mem_indices_flat = []
840
+ storage_indices_flat = []
841
+
842
+ with self._lock:
843
+ for i, key in enumerate(keys):
844
+ if (storage_obj := self._memory_objects.get(key)) is None:
845
+ continue
846
+ mem_addr = objects[i].meta.address
847
+ mem_size = objects[i].meta.phy_size
848
+ mem_indices = self.nixl_agent.get_memory_indices(mem_addr, mem_size)
849
+
850
+ mem_indices_flat.extend(mem_indices)
851
+ storage_indices_flat.extend(storage_obj.page_indices)
852
+
853
+ bitmap.set(i)
854
+ accessed_keys.append(key)
855
+
856
+ if mem_indices_flat:
857
+ handle = self.nixl_agent.get_storage_to_mem_handle(
858
+ mem_indices_flat,
859
+ storage_indices_flat,
860
+ )
861
+ await self.nixl_agent.post_non_blocking(handle)
862
+ self.nixl_agent.release_handle(handle)
863
+ except Exception:
864
+ logger.exception("NIXL load task %d failed", task_id)
865
+
866
+ if accessed_keys:
867
+ self._notify_keys_accessed(accessed_keys)
868
+ with self._lock:
869
+ self._completed_load_tasks[task_id] = bitmap
870
+ self._signal_load_event()
871
+
872
+
873
+ # ---------------------------------------------------------------------
874
+ # Config and self-registration
875
+ # ---------------------------------------------------------------------
876
+
877
+ _VALID_NIXL_BACKENDS = (
878
+ "GDS",
879
+ "GDS_MT",
880
+ "POSIX",
881
+ "HF3FS",
882
+ "OBJ",
883
+ )
884
+ _FILE_BACKENDS = ("GDS", "GDS_MT", "POSIX", "HF3FS")
885
+
886
+
887
+ class NixlStoreL2AdapterConfig(L2AdapterConfigBase):
888
+ """
889
+ Config for a Nixl-store-based L2 adapter.
890
+
891
+ Fields:
892
+ - backend: Nixl storage backend
893
+ (GDS, GDS_MT, POSIX, HF3FS, OBJ).
894
+ - backend_params: Backend-specific parameters as a
895
+ dict of string key-value pairs. For file-based
896
+ backends (GDS, GDS_MT, POSIX, HF3FS), must include
897
+ ``file_path``. May also include ``use_direct_io``
898
+ (default ``"false"``) and other backend-specific
899
+ keys.
900
+ - pool_size: Number of storage descriptors to
901
+ pre-allocate (must be > 0).
902
+ """
903
+
904
+ def __init__(
905
+ self,
906
+ backend: str,
907
+ backend_params: dict[str, str],
908
+ pool_size: int,
909
+ ):
910
+ if backend in _FILE_BACKENDS:
911
+ if "file_path" not in backend_params:
912
+ raise ValueError(
913
+ "backend_params must include "
914
+ "'file_path' for file-based "
915
+ "backend %r" % backend
916
+ )
917
+ if "use_direct_io" not in backend_params:
918
+ raise ValueError(
919
+ "backend_params must include "
920
+ "'use_direct_io' for file-based "
921
+ "backend %r" % backend
922
+ )
923
+ self.backend = backend
924
+ self.backend_params = backend_params
925
+ self.pool_size = pool_size
926
+
927
+ @classmethod
928
+ def from_dict(cls, d: dict) -> "NixlStoreL2AdapterConfig":
929
+ backend = d.get("backend")
930
+ if backend not in _VALID_NIXL_BACKENDS:
931
+ raise ValueError(
932
+ "backend must be one of %s, got %r" % (_VALID_NIXL_BACKENDS, backend)
933
+ )
934
+
935
+ backend_params = d.get("backend_params", {})
936
+ if not isinstance(backend_params, dict):
937
+ raise ValueError("backend_params must be a dict of string key-value pairs")
938
+
939
+ pool_size = d.get("pool_size")
940
+ if not isinstance(pool_size, int) or pool_size <= 0:
941
+ raise ValueError("pool_size must be a positive integer")
942
+
943
+ return cls(
944
+ backend=backend,
945
+ backend_params=backend_params,
946
+ pool_size=pool_size,
947
+ )
948
+
949
+ @classmethod
950
+ def help(cls) -> str:
951
+ return (
952
+ "Nixl store L2 adapter config fields:\n"
953
+ "- backend (str): Nixl storage backend, "
954
+ "one of %s (required)\n"
955
+ "- backend_params (dict): backend-specific "
956
+ "string key-value pairs (optional, "
957
+ "default empty). File-based backends "
958
+ "require file_path. Optional keys include "
959
+ "'use_direct_io' (default 'false') and "
960
+ "'file_size' (int, size in bytes of each "
961
+ "storage file slot; defaults to the L1 "
962
+ "page size if not set).\n"
963
+ "- pool_size (int): number of storage "
964
+ "descriptors to pre-allocate (required, "
965
+ ">0)" % (_VALID_NIXL_BACKENDS,)
966
+ )
967
+
968
+
969
+ # Self-register config type and adapter factory
970
+ register_l2_adapter_type("nixl_store", NixlStoreL2AdapterConfig)
971
+
972
+
973
+ def _create_nixl_store_adapter(
974
+ config: L2AdapterConfigBase,
975
+ l1_memory_desc: Optional[L1MemoryDesc] = None,
976
+ ) -> L2AdapterInterface:
977
+ """Create a NixlStoreL2Adapter from config."""
978
+ if l1_memory_desc is None:
979
+ raise ValueError("l1_memory_desc is required to create a NixlStoreL2Adapter.")
980
+ return NixlStoreL2Adapter(config, l1_memory_desc) # type: ignore[arg-type]
981
+
982
+
983
+ register_l2_adapter_factory("nixl_store", _create_nixl_store_adapter)