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,1443 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ # Future
4
+ from __future__ import annotations
5
+
6
+ # Standard
7
+ from collections import OrderedDict
8
+ from concurrent.futures import Future, ThreadPoolExecutor
9
+ from dataclasses import dataclass
10
+ from typing import Any, Callable, List, Optional, Sequence, cast
11
+ import asyncio
12
+ import ctypes
13
+ import mmap
14
+ import os
15
+ import threading
16
+
17
+ # Third Party
18
+ import torch
19
+
20
+ # First Party
21
+ from lmcache.logging import init_logger
22
+ from lmcache.utils import CacheEngineKey, DiskCacheMetadata
23
+ from lmcache.v1.config import LMCacheEngineConfig
24
+ from lmcache.v1.memory_management import MemoryFormat, MemoryObj
25
+ from lmcache.v1.metadata import LMCacheMetadata
26
+ from lmcache.v1.storage_backend.abstract_backend import StoragePluginInterface
27
+ from lmcache.v1.storage_backend.local_cpu_backend import LocalCPUBackend
28
+
29
+ if torch.cuda.is_available():
30
+ # First Party
31
+ import lmcache.c_ops as lmc_ops
32
+ else:
33
+ # First Party
34
+ import lmcache.non_cuda_equivalents as lmc_ops
35
+
36
+ logger = init_logger(__name__)
37
+
38
+
39
+ def _to_bool(value: Any) -> bool:
40
+ if isinstance(value, bool):
41
+ return value
42
+ if value is None:
43
+ return False
44
+ return str(value).strip().lower() in {"true", "1", "yes", "on"}
45
+
46
+
47
+ @dataclass
48
+ class _Entry:
49
+ """In-memory index entry for a stored chunk."""
50
+
51
+ offset: int
52
+ meta: DiskCacheMetadata
53
+ slot_id: int
54
+ generation: int
55
+
56
+
57
+ @dataclass
58
+ class _Inflight:
59
+ """In-progress put operation tracking."""
60
+
61
+ offset: int
62
+ meta: DiskCacheMetadata
63
+ slot_id: int
64
+ generation: int
65
+ canceled: bool = False
66
+
67
+
68
+ @dataclass
69
+ class _SlotState:
70
+ """Slot state for a stored DAX chunk."""
71
+
72
+ generation: int
73
+ committed: bool = False
74
+ borrow_count: int = 0
75
+ pending_free: bool = False
76
+
77
+
78
+ @dataclass
79
+ class _RestoreItem:
80
+ """Reserved DAX read metadata for one item in a batched restore."""
81
+
82
+ result_index: int
83
+ key: CacheEngineKey
84
+ offset: int
85
+ size: int
86
+ shape: torch.Size
87
+ dtype: torch.dtype
88
+ fmt: MemoryFormat
89
+ cached_positions: Optional[torch.Tensor]
90
+ slot_id: int
91
+ generation: int
92
+ memory_obj: Optional[MemoryObj] = None
93
+ slab_offset: int = 0
94
+
95
+
96
+ @dataclass
97
+ class _RestoreSpan:
98
+ """One contiguous source span copied from DAX into the staging slab."""
99
+
100
+ src_offset: int
101
+ slab_offset: int
102
+ size: int
103
+
104
+
105
+ @dataclass
106
+ class _RestoreRegion:
107
+ """One restore region executed by a persistent worker."""
108
+
109
+ region_index: int
110
+ slab_offset: int
111
+ total_bytes: int
112
+ items: list[_RestoreItem]
113
+ spans: list[_RestoreSpan]
114
+
115
+
116
+ @dataclass
117
+ class _RestoreWave:
118
+ """One wave of region work against the fixed-size retrieve slab."""
119
+
120
+ regions: list[_RestoreRegion]
121
+
122
+
123
+ class DaxBackend(StoragePluginInterface):
124
+ """Storage plugin backend for /dev/dax mmap-backed KV cache."""
125
+
126
+ def __init__(
127
+ self,
128
+ config: Optional[LMCacheEngineConfig] = None,
129
+ metadata: Optional[LMCacheMetadata] = None,
130
+ local_cpu_backend: Optional[LocalCPUBackend] = None,
131
+ loop: Optional[asyncio.AbstractEventLoop] = None,
132
+ dst_device: str = "cpu",
133
+ ) -> None:
134
+ """Initialize a DAX-backed storage backend."""
135
+ super().__init__(
136
+ dst_device=dst_device,
137
+ config=config,
138
+ metadata=metadata,
139
+ local_cpu_backend=local_cpu_backend,
140
+ loop=loop,
141
+ )
142
+ if self.config is None:
143
+ raise ValueError("DaxBackend requires config")
144
+ if self.metadata is None:
145
+ raise ValueError("DaxBackend requires metadata")
146
+
147
+ if self.metadata.world_size != 1:
148
+ raise ValueError(
149
+ "DaxBackend currently only supports TP=1 "
150
+ f"(world_size={self.metadata.world_size})"
151
+ )
152
+ if self.metadata.get_num_groups() != 1:
153
+ raise ValueError(
154
+ "DaxBackend currently supports only single-group KV layout"
155
+ )
156
+
157
+ extra = self.config.extra_config or {}
158
+ self.device_path = str(extra.get("dax.device_path", "")).strip()
159
+ if not self.device_path:
160
+ raise ValueError("extra_config['dax.device_path'] is required")
161
+
162
+ self.async_put = _to_bool(extra.get("dax.async_put", False))
163
+ if self.async_put and self.loop is None:
164
+ raise ValueError("DaxBackend async_put=true requires an asyncio event loop")
165
+
166
+ self.max_dax_size = float(extra.get("dax.max_dax_size", 0))
167
+ if self.max_dax_size <= 0:
168
+ raise ValueError("extra_config['dax.max_dax_size'] must be > 0")
169
+
170
+ if self.local_cpu_backend is None:
171
+ raise ValueError("DaxBackend requires local_cpu_backend")
172
+
173
+ # Total size in bytes of the mapped DAX arena.
174
+ self._arena_bytes = int(self.max_dax_size * 1024**3)
175
+ if self._arena_bytes <= 0:
176
+ raise ValueError("dax.max_dax_size results in zero-sized arena")
177
+
178
+ self._fd: Optional[int] = None
179
+ self._mmap_obj: Optional[mmap.mmap] = None
180
+ self._base_ptr: int = 0
181
+ # Python memoryview exposing the mapped arena for byte-level access.
182
+ self._arena_view: Optional[memoryview] = None
183
+ self._restore_executor: Optional[ThreadPoolExecutor] = None
184
+ self._restore_dispatch_executor: Optional[ThreadPoolExecutor] = None
185
+ self._retrieve_staging_slab_ptr: int = 0
186
+ self._retrieve_staging_slab_bytes: int = 0
187
+ self._restore_region_bytes: int = 0
188
+ self._restore_workers: int = 0
189
+ self._restore_max_regions: int = 0
190
+ self._open_arena()
191
+ try:
192
+ assert self.local_cpu_backend is not None
193
+ full_chunk_size = int(self.local_cpu_backend.get_full_chunk_size_bytes())
194
+ self.slot_bytes = max(1, int(full_chunk_size))
195
+ self._max_slots = self._arena_bytes // self.slot_bytes
196
+ if self._max_slots <= 0:
197
+ raise RuntimeError(
198
+ "dax.max_dax_size is too small for the configured chunk size"
199
+ )
200
+
201
+ default_restore_workers = min(8, max(1, os.cpu_count() or 1))
202
+ self._restore_workers = self._get_positive_int_extra(
203
+ extra,
204
+ "dax.restore_workers",
205
+ default_restore_workers,
206
+ )
207
+ self._restore_max_regions = self._get_positive_int_extra(
208
+ extra,
209
+ "dax.restore_max_regions",
210
+ self._restore_workers,
211
+ )
212
+ default_staging_slab_bytes = max(
213
+ 256 * 1024 * 1024,
214
+ self._restore_max_regions * self.slot_bytes,
215
+ )
216
+ self._retrieve_staging_slab_bytes = self._get_positive_int_extra(
217
+ extra,
218
+ "dax.retrieve_staging_slab_bytes",
219
+ default_staging_slab_bytes,
220
+ )
221
+ min_required_slab = self._restore_max_regions * self.slot_bytes
222
+ if self._retrieve_staging_slab_bytes < min_required_slab:
223
+ raise ValueError(
224
+ "extra_config['dax.retrieve_staging_slab_bytes'] must be at "
225
+ f"least {min_required_slab} bytes"
226
+ )
227
+ self._restore_region_bytes = (
228
+ self._retrieve_staging_slab_bytes // self._restore_max_regions
229
+ )
230
+ if self._restore_region_bytes < self.slot_bytes:
231
+ raise ValueError(
232
+ "dax.retrieve_staging_slab_bytes does not leave enough space "
233
+ "per restore region for one full chunk"
234
+ )
235
+ self._retrieve_staging_slab_ptr = int(
236
+ lmc_ops.alloc_pinned_ptr(self._retrieve_staging_slab_bytes, 0)
237
+ )
238
+ self._restore_executor = ThreadPoolExecutor(
239
+ max_workers=self._restore_workers,
240
+ thread_name_prefix="dax-restore",
241
+ )
242
+ self._restore_dispatch_executor = ThreadPoolExecutor(
243
+ max_workers=1,
244
+ thread_name_prefix="dax-restore-dispatch",
245
+ )
246
+
247
+ self._state_lock = threading.RLock()
248
+ self._state_condition = threading.Condition(self._state_lock)
249
+
250
+ self._index: dict[CacheEngineKey, _Entry] = {}
251
+ self._pin_counts: dict[CacheEngineKey, int] = {}
252
+ self._inflight: dict[CacheEngineKey, _Inflight] = {}
253
+ self._lru: "OrderedDict[CacheEngineKey, None]" = OrderedDict()
254
+ self._slot_states: dict[int, _SlotState] = {}
255
+
256
+ self._next_slot = 0
257
+ self._free_slots: set[int] = set()
258
+ self._active_ops = 0
259
+ self._active_puts = 0
260
+ self._closing = False
261
+ self._closed = False
262
+
263
+ logger.info(
264
+ "DaxBackend init: device=%s dax_size=%d slot=%d max_slots=%d "
265
+ "restore_workers=%d restore_regions=%d restore_slab=%d",
266
+ self.device_path,
267
+ self._arena_bytes,
268
+ self.slot_bytes,
269
+ self._max_slots,
270
+ self._restore_workers,
271
+ self._restore_max_regions,
272
+ self._retrieve_staging_slab_bytes,
273
+ )
274
+ except Exception:
275
+ fd, mmap_obj, arena_view = self._fd, self._mmap_obj, self._arena_view
276
+ self._fd = None
277
+ self._mmap_obj = None
278
+ self._base_ptr = 0
279
+ self._arena_view = None
280
+ self._release_restore_resources()
281
+ self._release_arena_resources(fd, mmap_obj, arena_view)
282
+ raise
283
+
284
+ def __str__(self) -> str:
285
+ return "DaxBackend"
286
+
287
+ def contains(self, key: CacheEngineKey, pin: bool = False) -> bool:
288
+ """Check whether ``key`` exists in the backend.
289
+
290
+ Args:
291
+ key: The cache key to look up.
292
+ pin: If ``True`` and the key is present, atomically
293
+ increment its pin count.
294
+
295
+ Returns:
296
+ ``True`` if the key is present, ``False`` otherwise.
297
+ """
298
+ with self._state_lock:
299
+ ok = key in self._index
300
+ if ok and pin:
301
+ self._pin_counts[key] = self._pin_counts.get(key, 0) + 1
302
+ return ok
303
+
304
+ def exists_in_put_tasks(self, key: CacheEngineKey) -> bool:
305
+ """Check whether ``key`` is tracked as an in-flight put task.
306
+
307
+ Args:
308
+ key: The cache key to check.
309
+
310
+ Returns:
311
+ ``True`` if ``key`` is in the in-flight put task set.
312
+ """
313
+ with self._state_lock:
314
+ return key in self._inflight
315
+
316
+ def pin(self, key: CacheEngineKey) -> bool:
317
+ """Increment the pin count for ``key`` if it exists.
318
+
319
+ Args:
320
+ key: The cache key to pin.
321
+
322
+ Returns:
323
+ ``True`` if the key was found and pinned, ``False`` otherwise.
324
+ """
325
+ with self._state_lock:
326
+ if key in self._index:
327
+ self._pin_counts[key] = self._pin_counts.get(key, 0) + 1
328
+ return True
329
+ return False
330
+
331
+ def unpin(self, key: CacheEngineKey) -> bool:
332
+ """Decrement the pin count for ``key``.
333
+
334
+ Args:
335
+ key: The cache key to unpin.
336
+
337
+ Returns:
338
+ ``True`` if ``key`` is present in the backend after the
339
+ operation, ``False`` otherwise.
340
+ """
341
+ with self._state_lock:
342
+ count = self._pin_counts.get(key, 0)
343
+ if count > 0:
344
+ if count == 1:
345
+ del self._pin_counts[key]
346
+ else:
347
+ self._pin_counts[key] = count - 1
348
+ return True
349
+ return key in self._index
350
+
351
+ def remove(self, key: CacheEngineKey, force: bool = True) -> bool:
352
+ """Remove ``key`` from the backend if present.
353
+
354
+ If the key is in-flight, it is marked canceled. If it is
355
+ committed, its slot is scheduled for reclamation according to
356
+ the current slot state.
357
+
358
+ Args:
359
+ key: The cache key to remove.
360
+ force: Unused; accepted for interface compatibility.
361
+
362
+ Returns:
363
+ ``True`` if the key was present (committed or in-flight).
364
+ """
365
+ del force
366
+ with self._state_lock:
367
+ existed = key in self._index or key in self._inflight
368
+ entry = self._index.pop(key, None)
369
+ inflight = self._inflight.get(key)
370
+ self._pin_counts.pop(key, None)
371
+ self._lru.pop(key, None)
372
+ if entry is not None:
373
+ self._schedule_slot_reclaim_locked(entry.slot_id, entry.generation)
374
+ if inflight is not None:
375
+ inflight.canceled = True
376
+ return existed
377
+
378
+ def batched_submit_put_task(
379
+ self,
380
+ keys: Sequence[CacheEngineKey],
381
+ objs: List[MemoryObj],
382
+ transfer_spec: Any = None,
383
+ on_complete_callback: Optional[Callable[[CacheEngineKey], None]] = None,
384
+ ) -> Optional[List[Future]]:
385
+ """Store a batch of memory objects in the DAX arena."""
386
+ del transfer_spec
387
+ if len(keys) != len(objs):
388
+ raise ValueError(
389
+ "keys and objs must have the same length, "
390
+ f"got {len(keys)} and {len(objs)}"
391
+ )
392
+ futures: List[Future] = []
393
+
394
+ for key, obj in zip(keys, objs, strict=True):
395
+ should_finish_put = False
396
+ try:
397
+ # Multi-tensor objects are not yet supported.
398
+ num_shapes = len(obj.get_shapes())
399
+ if num_shapes > 1:
400
+ logger.error(
401
+ "DaxBackend does not support multi-tensor allocations: "
402
+ "key=%s has %d tensors. "
403
+ "Use single-tensor format or extend metadata.",
404
+ key,
405
+ num_shapes,
406
+ )
407
+ continue
408
+ size = int(obj.get_size())
409
+ obj_metadata = obj.metadata
410
+ shape = obj_metadata.shape
411
+ dtype = obj_metadata.dtype
412
+ cached_positions = obj_metadata.cached_positions
413
+ fmt = obj_metadata.fmt
414
+
415
+ with self._state_lock:
416
+ if self._closing:
417
+ raise RuntimeError("DaxBackend is closing")
418
+ if key in self._index or key in self._inflight:
419
+ continue
420
+
421
+ if size > self.slot_bytes:
422
+ raise ValueError(
423
+ f"DaxBackend: object size {size} for key {key} "
424
+ f"exceeds slot size {self.slot_bytes}"
425
+ )
426
+ while True:
427
+ try:
428
+ slot_id = self._allocate_slot_locked()
429
+ break
430
+ except RuntimeError:
431
+ if not self._evict_one_locked():
432
+ raise
433
+ offset = slot_id * self.slot_bytes
434
+ generation = self._reserve_slot_state_locked(slot_id)
435
+
436
+ meta = DiskCacheMetadata(
437
+ path=f"{self.device_path}@{offset}",
438
+ size=size,
439
+ shape=shape,
440
+ dtype=dtype,
441
+ cached_positions=cached_positions,
442
+ fmt=fmt,
443
+ pin_count=0,
444
+ )
445
+
446
+ self._inflight[key] = _Inflight(
447
+ offset=offset,
448
+ meta=meta,
449
+ slot_id=slot_id,
450
+ generation=generation,
451
+ canceled=False,
452
+ )
453
+ self._active_puts += 1
454
+ should_finish_put = True
455
+
456
+ if self.async_put and self.loop is not None and self.loop.is_running():
457
+ obj.ref_count_up()
458
+ try:
459
+ fut = asyncio.run_coroutine_threadsafe(
460
+ self._submit_write(
461
+ key=key,
462
+ offset=offset,
463
+ size=size,
464
+ memory_obj=obj,
465
+ on_complete_callback=on_complete_callback,
466
+ ),
467
+ self.loop,
468
+ )
469
+ except Exception:
470
+ with self._state_lock:
471
+ self._finalize_inflight_locked(key, write_failed=True)
472
+ obj.ref_count_down()
473
+ raise
474
+ futures.append(fut)
475
+ should_finish_put = False
476
+ continue
477
+
478
+ try:
479
+ self._do_write(offset, obj, size)
480
+ except Exception as e:
481
+ with self._state_lock:
482
+ self._finalize_inflight_locked(key, write_failed=True)
483
+ raise RuntimeError(
484
+ f"DaxBackend write failed for key {key}: {e}"
485
+ ) from e
486
+
487
+ with self._state_lock:
488
+ should_invoke_callback = self._finalize_inflight_locked(
489
+ key,
490
+ write_failed=False,
491
+ )
492
+
493
+ if should_invoke_callback:
494
+ self._invoke_on_complete_callback(key, on_complete_callback)
495
+ finally:
496
+ if should_finish_put:
497
+ with self._state_lock:
498
+ if self._active_puts > 0:
499
+ self._active_puts -= 1
500
+ else:
501
+ logger.warning(
502
+ "DaxBackend active put count underflow for key %s", key
503
+ )
504
+ self._state_condition.notify_all()
505
+
506
+ return futures or None
507
+
508
+ def get_blocking(self, key: CacheEngineKey) -> Optional[MemoryObj]:
509
+ """Return the memory object for a key, or ``None`` if unavailable."""
510
+ with self._state_lock:
511
+ if self._closing:
512
+ return None
513
+ entry = self._index.get(key)
514
+ if entry is None:
515
+ return None
516
+ meta = entry.meta
517
+ shape = meta.shape
518
+ dtype = meta.dtype
519
+ fmt = meta.fmt
520
+ if shape is None or dtype is None or fmt is None:
521
+ return None
522
+ state = self._slot_states.get(entry.slot_id)
523
+ if (
524
+ state is None
525
+ or state.generation != entry.generation
526
+ or not state.committed
527
+ ):
528
+ return None
529
+ state.borrow_count += 1
530
+ self._active_ops += 1
531
+ offset, size = entry.offset, int(meta.size)
532
+ cached_positions = meta.cached_positions
533
+ slot_id, generation = entry.slot_id, entry.generation
534
+
535
+ assert self.local_cpu_backend is not None
536
+ memory_obj: Optional[MemoryObj] = None
537
+ read_ok = False
538
+ try:
539
+ memory_obj = self.local_cpu_backend.allocate(shape, dtype, fmt)
540
+ if memory_obj is None:
541
+ return None
542
+ self._do_read(offset, memory_obj, size)
543
+ memory_obj.metadata.cached_positions = cached_positions
544
+ read_ok = True
545
+ return memory_obj
546
+ except Exception:
547
+ if memory_obj is not None:
548
+ memory_obj.ref_count_down()
549
+ raise
550
+ finally:
551
+ with self._state_lock:
552
+ if self._active_ops > 0:
553
+ self._active_ops -= 1
554
+ state = self._slot_states.get(slot_id)
555
+ if state is not None and state.generation == generation:
556
+ if state.borrow_count > 0:
557
+ state.borrow_count -= 1
558
+ if read_ok:
559
+ current = self._index.get(key)
560
+ if (
561
+ current is not None
562
+ and current.slot_id == slot_id
563
+ and current.generation == generation
564
+ ):
565
+ self._touch_locked(key)
566
+ if state.pending_free and state.borrow_count == 0:
567
+ state.pending_free = False
568
+ self._free_slot_locked(slot_id)
569
+ self._state_condition.notify_all()
570
+
571
+ async def batched_async_contains(
572
+ self,
573
+ lookup_id: str,
574
+ keys: list[CacheEngineKey],
575
+ pin: bool = False,
576
+ ) -> int:
577
+ """Return the number of consecutive keys present in the index.
578
+
579
+ Iterates ``keys`` in order and stops at the first miss.
580
+
581
+ Args:
582
+ lookup_id: Caller-supplied identifier (not used by this backend).
583
+ keys: Ordered list of cache keys to check.
584
+ pin: If ``True``, pin each found key.
585
+
586
+ Returns:
587
+ The count of consecutive hits from the start of ``keys``.
588
+ """
589
+ del lookup_id
590
+ hit = 0
591
+ with self._state_lock:
592
+ for key in keys:
593
+ if key not in self._index:
594
+ break
595
+ if pin:
596
+ self._pin_counts[key] = self._pin_counts.get(key, 0) + 1
597
+ hit += 1
598
+ return hit
599
+
600
+ async def batched_get_non_blocking(
601
+ self,
602
+ lookup_id: str,
603
+ keys: list[CacheEngineKey],
604
+ transfer_spec: Any = None,
605
+ ) -> list[MemoryObj]:
606
+ """Retrieve memory objects for consecutive keys asynchronously.
607
+
608
+ Schedules one batched restore job on the persistent dispatch
609
+ executor and returns only the consecutive hit prefix. Stops at the
610
+ first key that is not found or is no longer readable.
611
+
612
+ Args:
613
+ lookup_id: Caller-supplied identifier (not used by this backend).
614
+ keys: Ordered list of cache keys to retrieve.
615
+ transfer_spec: Transfer hint (not used by this backend).
616
+
617
+ Returns:
618
+ A list of ``MemoryObj`` instances for the consecutive hits.
619
+ """
620
+ del lookup_id, transfer_spec
621
+ if not keys:
622
+ return []
623
+
624
+ dispatch_executor = self._restore_dispatch_executor
625
+ if dispatch_executor is None:
626
+ raise RuntimeError("DaxBackend restore dispatch executor is not available")
627
+
628
+ loop = asyncio.get_running_loop()
629
+ result = await loop.run_in_executor(
630
+ dispatch_executor,
631
+ self._restore_batch,
632
+ list(keys),
633
+ True,
634
+ )
635
+ return cast(list[MemoryObj], result)
636
+
637
+ def batched_get_blocking(
638
+ self,
639
+ keys: List[CacheEngineKey],
640
+ ) -> List[Optional[MemoryObj]]:
641
+ """Restore a batch of DAX-backed cache entries synchronously.
642
+
643
+ The returned list preserves the input order. Entries that are missing
644
+ or no longer readable remain ``None`` so callers keep positional
645
+ alignment with ``keys``.
646
+
647
+ Args:
648
+ keys: Ordered cache keys to restore from the DAX arena.
649
+
650
+ Returns:
651
+ A list aligned with ``keys`` containing restored ``MemoryObj``
652
+ instances or ``None`` for entries that could not be read.
653
+ """
654
+ if not keys:
655
+ return []
656
+
657
+ dispatch_executor = self._restore_dispatch_executor
658
+ if dispatch_executor is None:
659
+ raise RuntimeError("DaxBackend restore dispatch executor is not available")
660
+
661
+ batch_keys = list(keys)
662
+ if threading.current_thread().name.startswith("dax-restore-dispatch"):
663
+ return cast(
664
+ List[Optional[MemoryObj]],
665
+ self._restore_batch(batch_keys, False),
666
+ )
667
+
668
+ future = dispatch_executor.submit(
669
+ self._restore_batch,
670
+ batch_keys,
671
+ False,
672
+ )
673
+ return cast(List[Optional[MemoryObj]], future.result())
674
+
675
+ def batched_contains(
676
+ self,
677
+ keys: List[CacheEngineKey],
678
+ pin: bool = False,
679
+ ) -> int:
680
+ """Return the number of consecutive keys present in the index.
681
+
682
+ Synchronous variant of :meth:`batched_async_contains`.
683
+
684
+ Args:
685
+ keys: Ordered list of cache keys to check.
686
+ pin: If ``True``, pin each found key.
687
+
688
+ Returns:
689
+ The count of consecutive hits from the start of ``keys``.
690
+ """
691
+ hit = 0
692
+ with self._state_lock:
693
+ for key in keys:
694
+ if key not in self._index:
695
+ break
696
+ if pin:
697
+ self._pin_counts[key] = self._pin_counts.get(key, 0) + 1
698
+ hit += 1
699
+ return hit
700
+
701
+ def batched_remove(
702
+ self,
703
+ keys: list[CacheEngineKey],
704
+ force: bool = True,
705
+ ) -> int:
706
+ """Remove multiple keys from the backend.
707
+
708
+ Args:
709
+ keys: The cache keys to remove.
710
+ force: Passed through to :meth:`remove`.
711
+
712
+ Returns:
713
+ The number of keys that were actually present and removed.
714
+ """
715
+ removed = 0
716
+ for key in keys:
717
+ removed += int(self.remove(key, force=force))
718
+ return removed
719
+
720
+ def get_allocator_backend(self) -> LocalCPUBackend:
721
+ """Return the CPU allocator backend used for read buffers.
722
+
723
+ Raises:
724
+ RuntimeError: If no ``local_cpu_backend`` is available.
725
+ """
726
+ if self.local_cpu_backend is None:
727
+ raise RuntimeError("DaxBackend has no allocator backend available")
728
+ return self.local_cpu_backend
729
+
730
+ def close(self) -> None:
731
+ """Quiesce outstanding operations and release the mapped DAX arena."""
732
+ restore_executor = None
733
+ restore_dispatch_executor = None
734
+ staging_slab_ptr = 0
735
+ with self._state_lock:
736
+ if self._closed:
737
+ return
738
+ self._closing = True
739
+ while self._active_puts > 0 or self._active_ops > 0:
740
+ if not self._state_condition.wait(timeout=30.0):
741
+ logger.warning(
742
+ "DaxBackend close: still waiting for %d puts, %d ops",
743
+ self._active_puts,
744
+ self._active_ops,
745
+ )
746
+ if self._closed:
747
+ return
748
+ self._closed = True
749
+ restore_executor = self._restore_executor
750
+ restore_dispatch_executor = self._restore_dispatch_executor
751
+ staging_slab_ptr = self._retrieve_staging_slab_ptr
752
+ self._restore_executor = None
753
+ self._restore_dispatch_executor = None
754
+ self._retrieve_staging_slab_ptr = 0
755
+ self._retrieve_staging_slab_bytes = 0
756
+ self._restore_region_bytes = 0
757
+ self._index.clear()
758
+ self._inflight.clear()
759
+ self._lru.clear()
760
+ self._pin_counts.clear()
761
+ self._slot_states.clear()
762
+ self._free_slots.clear()
763
+ fd = self._fd
764
+ mmap_obj = self._mmap_obj
765
+ arena_view = self._arena_view
766
+ self._fd = None
767
+ self._mmap_obj = None
768
+ self._base_ptr = 0
769
+ self._arena_view = None
770
+
771
+ if restore_dispatch_executor is not None:
772
+ restore_dispatch_executor.shutdown(wait=True)
773
+ if restore_executor is not None:
774
+ restore_executor.shutdown(wait=True)
775
+ self._release_restore_resources(
776
+ restore_slab_ptr=staging_slab_ptr,
777
+ )
778
+ self._release_arena_resources(fd, mmap_obj, arena_view)
779
+
780
+ # ------------------------------------------------------------------
781
+ # Private / helper methods
782
+ # ------------------------------------------------------------------
783
+
784
+ @staticmethod
785
+ def _get_positive_int_extra(
786
+ extra_config: dict[str, Any],
787
+ key: str,
788
+ default: int,
789
+ ) -> int:
790
+ value = extra_config.get(key, default)
791
+ try:
792
+ parsed = int(value)
793
+ except (TypeError, ValueError) as e:
794
+ raise ValueError(f"extra_config['{key}'] must be a positive integer") from e
795
+ if parsed <= 0:
796
+ raise ValueError(f"extra_config['{key}'] must be a positive integer")
797
+ return parsed
798
+
799
+ def _release_restore_resources(
800
+ self,
801
+ restore_slab_ptr: Optional[int] = None,
802
+ ) -> None:
803
+ """Shut down restore workers and free the pinned retrieve slab.
804
+
805
+ Args:
806
+ restore_slab_ptr: Optional explicit slab pointer to free. When not
807
+ provided, the backend releases its current staging slab and
808
+ clears the associated bookkeeping fields.
809
+ """
810
+ dispatch_executor = self._restore_dispatch_executor
811
+ if dispatch_executor is not None:
812
+ dispatch_executor.shutdown(wait=True)
813
+ self._restore_dispatch_executor = None
814
+
815
+ restore_executor = self._restore_executor
816
+ if restore_executor is not None:
817
+ restore_executor.shutdown(wait=True)
818
+ self._restore_executor = None
819
+
820
+ ptr = (
821
+ self._retrieve_staging_slab_ptr
822
+ if restore_slab_ptr is None
823
+ else restore_slab_ptr
824
+ )
825
+ if ptr:
826
+ try:
827
+ lmc_ops.free_pinned_ptr(ptr)
828
+ except Exception as e:
829
+ logger.warning("Failed to free DAX retrieve slab: %s", e)
830
+
831
+ if restore_slab_ptr is None:
832
+ self._retrieve_staging_slab_ptr = 0
833
+ self._retrieve_staging_slab_bytes = 0
834
+ self._restore_region_bytes = 0
835
+
836
+ @staticmethod
837
+ def _release_arena_resources(
838
+ fd: Optional[int],
839
+ mmap_obj: Optional[mmap.mmap],
840
+ arena_view: Optional[memoryview],
841
+ ) -> None:
842
+ """Release the mapped DAX arena resources in close order.
843
+
844
+ Args:
845
+ fd: File descriptor for the DAX device.
846
+ mmap_obj: Mmap object backing the arena mapping.
847
+ arena_view: Memoryview exported from the mmap.
848
+ """
849
+ if arena_view is not None:
850
+ try:
851
+ arena_view.release()
852
+ except Exception as e:
853
+ logger.warning("Failed to release DAX memoryview: %s", e)
854
+
855
+ if mmap_obj is not None:
856
+ try:
857
+ mmap_obj.close()
858
+ except Exception as e:
859
+ logger.warning("Failed to close DAX mmap: %s", e)
860
+
861
+ if fd is not None:
862
+ try:
863
+ os.close(fd)
864
+ except Exception as e:
865
+ logger.warning("Failed to close DAX fd: %s", e)
866
+
867
+ def _open_arena(self) -> None:
868
+ fd: Optional[int] = None
869
+ mmap_obj: Optional[mmap.mmap] = None
870
+ arena_view: Optional[memoryview] = None
871
+ try:
872
+ fd = os.open(self.device_path, os.O_RDWR)
873
+ except OSError as e:
874
+ raise RuntimeError(
875
+ f"Failed to open dax device {self.device_path}: {e}"
876
+ ) from e
877
+ try:
878
+ try:
879
+ capacity_bytes = os.fstat(fd).st_size
880
+ if capacity_bytes > 0 and self._arena_bytes > capacity_bytes:
881
+ raise RuntimeError(
882
+ f"dax.max_dax_size ({self._arena_bytes} bytes) exceeds "
883
+ f"device capacity ({capacity_bytes} bytes)"
884
+ )
885
+ except OSError:
886
+ # Some dax devices may not report size via fstat.
887
+ logger.warning(
888
+ "Could not determine DAX device capacity via fstat; "
889
+ "skipping dax.max_dax_size validation"
890
+ )
891
+
892
+ mmap_obj = mmap.mmap(
893
+ fd,
894
+ self._arena_bytes,
895
+ flags=mmap.MAP_SHARED,
896
+ prot=mmap.PROT_READ | mmap.PROT_WRITE,
897
+ )
898
+ base_ptr = ctypes.addressof(ctypes.c_char.from_buffer(mmap_obj))
899
+ arena_view = memoryview(mmap_obj)
900
+ self._fd = fd
901
+ self._mmap_obj = mmap_obj
902
+ self._base_ptr = base_ptr
903
+ self._arena_view = arena_view
904
+ except Exception as e:
905
+ DaxBackend._release_arena_resources(fd, mmap_obj, arena_view)
906
+ if isinstance(e, RuntimeError):
907
+ raise
908
+ raise RuntimeError(
909
+ f"Failed to mmap dax arena ({self._arena_bytes} bytes) from "
910
+ f"{self.device_path}: {e}"
911
+ ) from e
912
+
913
+ def _reserve_slot_state_locked(self, slot_id: int) -> int:
914
+ existing = self._slot_states.get(slot_id)
915
+ new_gen = (existing.generation if existing is not None else 0) + 1
916
+ self._slot_states[slot_id] = _SlotState(generation=new_gen)
917
+ return new_gen
918
+
919
+ def _mark_slot_committed_locked(self, slot_id: int, generation: int) -> None:
920
+ state = self._slot_states.get(slot_id)
921
+ if state is None or state.generation != generation:
922
+ return
923
+ state.committed = True
924
+ state.pending_free = False
925
+
926
+ def _schedule_slot_reclaim_locked(self, slot_id: int, generation: int) -> None:
927
+ """Mark a slot uncommitted and free it immediately or defer if borrowed."""
928
+ state = self._slot_states.get(slot_id)
929
+ if state is None or state.generation != generation:
930
+ return
931
+ state.committed = False
932
+ if state.borrow_count == 0:
933
+ state.pending_free = False
934
+ self._free_slot_locked(slot_id)
935
+ else:
936
+ state.pending_free = True
937
+
938
+ def _finalize_inflight_locked(
939
+ self,
940
+ key: CacheEngineKey,
941
+ write_failed: bool,
942
+ ) -> bool:
943
+ """Resolve an in-flight put: commit on success, reclaim on failure."""
944
+ inflight = self._inflight.pop(key, None)
945
+ if inflight is None:
946
+ return False
947
+ if inflight.canceled or write_failed:
948
+ self._schedule_slot_reclaim_locked(inflight.slot_id, inflight.generation)
949
+ return False
950
+ self._mark_slot_committed_locked(inflight.slot_id, inflight.generation)
951
+ self._index[key] = _Entry(
952
+ offset=inflight.offset,
953
+ meta=inflight.meta,
954
+ slot_id=inflight.slot_id,
955
+ generation=inflight.generation,
956
+ )
957
+ self._touch_locked(key)
958
+ return True
959
+
960
+ def _invoke_on_complete_callback(
961
+ self,
962
+ key: CacheEngineKey,
963
+ on_complete_callback: Optional[Callable[[CacheEngineKey], None]],
964
+ ) -> None:
965
+ if on_complete_callback is None:
966
+ return
967
+ try:
968
+ on_complete_callback(key)
969
+ except Exception as e:
970
+ logger.warning("on_complete_callback failed for key %s: %s", key, e)
971
+
972
+ def _allocate_slot_locked(self) -> int:
973
+ if self._free_slots:
974
+ return self._free_slots.pop()
975
+ if self._next_slot < self._max_slots:
976
+ slot = self._next_slot
977
+ self._next_slot += 1
978
+ return slot
979
+ raise RuntimeError("No free slots available; eviction required")
980
+
981
+ def _free_slot_locked(self, slot_id: int) -> None:
982
+ if slot_id < 0:
983
+ return
984
+ self._free_slots.add(slot_id)
985
+
986
+ def _touch_locked(self, key: CacheEngineKey) -> None:
987
+ self._lru.pop(key, None)
988
+ self._lru[key] = None
989
+
990
+ def _evict_one_locked(self) -> bool:
991
+ for victim in list(self._lru.keys()):
992
+ if self._pin_counts.get(victim, 0) > 0 or victim in self._inflight:
993
+ continue
994
+ entry = self._index.get(victim)
995
+ if entry is None:
996
+ continue
997
+ state = self._slot_states.get(entry.slot_id)
998
+ if (
999
+ state is None
1000
+ or state.generation != entry.generation
1001
+ or state.borrow_count > 0
1002
+ ):
1003
+ continue
1004
+ self._index.pop(victim, None)
1005
+ self._lru.pop(victim, None)
1006
+ self._pin_counts.pop(victim, None)
1007
+ self._schedule_slot_reclaim_locked(entry.slot_id, entry.generation)
1008
+ return True
1009
+ return False
1010
+
1011
+ def _reserve_restore_items(
1012
+ self,
1013
+ keys: Sequence[CacheEngineKey],
1014
+ *,
1015
+ prefix_only: bool,
1016
+ ) -> tuple[list[_RestoreItem], list[Optional[MemoryObj]]]:
1017
+ """Reserve readable entries and build the aligned result list.
1018
+
1019
+ Args:
1020
+ keys: Ordered keys requested by the caller.
1021
+ prefix_only: When ``True``, stop at the first miss or unreadable
1022
+ entry so async-prefetch keeps prefix-hit semantics.
1023
+
1024
+ Returns:
1025
+ A tuple containing the reserved restore items and a result list
1026
+ aligned with ``keys`` that is prefilled with ``None`` placeholders.
1027
+ """
1028
+ results: list[Optional[MemoryObj]] = [None] * len(keys)
1029
+ reserved: list[_RestoreItem] = []
1030
+
1031
+ with self._state_lock:
1032
+ if self._closing:
1033
+ return reserved, results
1034
+
1035
+ for result_index, key in enumerate(keys):
1036
+ entry = self._index.get(key)
1037
+ if entry is None:
1038
+ if prefix_only:
1039
+ break
1040
+ continue
1041
+
1042
+ meta = entry.meta
1043
+ shape = meta.shape
1044
+ dtype = meta.dtype
1045
+ fmt = meta.fmt
1046
+ if shape is None or dtype is None or fmt is None:
1047
+ if prefix_only:
1048
+ break
1049
+ continue
1050
+
1051
+ state = self._slot_states.get(entry.slot_id)
1052
+ if (
1053
+ state is None
1054
+ or state.generation != entry.generation
1055
+ or not state.committed
1056
+ ):
1057
+ if prefix_only:
1058
+ break
1059
+ continue
1060
+
1061
+ state.borrow_count += 1
1062
+ reserved.append(
1063
+ _RestoreItem(
1064
+ result_index=result_index,
1065
+ key=key,
1066
+ offset=entry.offset,
1067
+ size=int(meta.size),
1068
+ shape=shape,
1069
+ dtype=dtype,
1070
+ fmt=fmt,
1071
+ cached_positions=meta.cached_positions,
1072
+ slot_id=entry.slot_id,
1073
+ generation=entry.generation,
1074
+ )
1075
+ )
1076
+
1077
+ if reserved:
1078
+ self._active_ops += 1
1079
+
1080
+ return reserved, results
1081
+
1082
+ def _allocate_restore_outputs(self, reserved: Sequence[_RestoreItem]) -> None:
1083
+ """Allocate CPU restore buffers for the reserved DAX items.
1084
+
1085
+ Args:
1086
+ reserved: Restore items whose output ``MemoryObj`` fields will be
1087
+ populated in-place.
1088
+
1089
+ Raises:
1090
+ RuntimeError: If the local CPU allocator cannot provide enough
1091
+ output buffers for the batch.
1092
+ """
1093
+ assert self.local_cpu_backend is not None
1094
+
1095
+ grouped_items: OrderedDict[
1096
+ tuple[tuple[int, ...], torch.dtype, MemoryFormat], list[_RestoreItem]
1097
+ ] = OrderedDict()
1098
+ for item in reserved:
1099
+ grouped_items.setdefault(
1100
+ (tuple(item.shape), item.dtype, item.fmt),
1101
+ [],
1102
+ ).append(item)
1103
+
1104
+ for group_items in grouped_items.values():
1105
+ first = group_items[0]
1106
+ outputs: Optional[list[MemoryObj]] = None
1107
+ if len(group_items) > 1:
1108
+ outputs = self.local_cpu_backend.batched_allocate(
1109
+ first.shape,
1110
+ first.dtype,
1111
+ len(group_items),
1112
+ first.fmt,
1113
+ )
1114
+
1115
+ if outputs is None:
1116
+ outputs = []
1117
+ for _ in group_items:
1118
+ memory_obj = self.local_cpu_backend.allocate(
1119
+ first.shape,
1120
+ first.dtype,
1121
+ first.fmt,
1122
+ )
1123
+ if memory_obj is None:
1124
+ for allocated in outputs:
1125
+ allocated.ref_count_down()
1126
+ raise RuntimeError(
1127
+ "DaxBackend batched restore allocation failed"
1128
+ )
1129
+ outputs.append(memory_obj)
1130
+
1131
+ for item, memory_obj in zip(group_items, outputs, strict=True):
1132
+ item.memory_obj = memory_obj
1133
+
1134
+ def _build_restore_waves(
1135
+ self,
1136
+ reserved: Sequence[_RestoreItem],
1137
+ ) -> list[_RestoreWave]:
1138
+ """Plan slab-backed restore work as waves of parallel regions.
1139
+
1140
+ Args:
1141
+ reserved: Restore items that already have output buffers assigned.
1142
+
1143
+ Returns:
1144
+ A list of restore waves, where each wave contains region copies that
1145
+ can run in parallel without overlapping slab space.
1146
+ """
1147
+ if not reserved:
1148
+ return []
1149
+
1150
+ sorted_items = sorted(reserved, key=lambda item: item.offset)
1151
+ waves: list[_RestoreWave] = []
1152
+ next_item_idx = 0
1153
+
1154
+ while next_item_idx < len(sorted_items):
1155
+ regions: list[_RestoreRegion] = []
1156
+ for region_index in range(self._restore_max_regions):
1157
+ if next_item_idx >= len(sorted_items):
1158
+ break
1159
+
1160
+ region_items: list[_RestoreItem] = []
1161
+ region_spans: list[_RestoreSpan] = []
1162
+ used_bytes = 0
1163
+
1164
+ while next_item_idx < len(sorted_items):
1165
+ item = sorted_items[next_item_idx]
1166
+ if item.size > self._restore_region_bytes:
1167
+ raise RuntimeError(
1168
+ f"DaxBackend restore item size {item.size} exceeds "
1169
+ "region capacity "
1170
+ f"{self._restore_region_bytes}"
1171
+ )
1172
+ if (
1173
+ used_bytes > 0
1174
+ and used_bytes + item.size > self._restore_region_bytes
1175
+ ):
1176
+ break
1177
+
1178
+ item.slab_offset = used_bytes
1179
+ region_items.append(item)
1180
+ if (
1181
+ region_spans
1182
+ and region_spans[-1].src_offset + region_spans[-1].size
1183
+ == item.offset
1184
+ and region_spans[-1].slab_offset + region_spans[-1].size
1185
+ == item.slab_offset
1186
+ ):
1187
+ region_spans[-1].size += item.size
1188
+ else:
1189
+ region_spans.append(
1190
+ _RestoreSpan(
1191
+ src_offset=item.offset,
1192
+ slab_offset=item.slab_offset,
1193
+ size=item.size,
1194
+ )
1195
+ )
1196
+
1197
+ used_bytes += item.size
1198
+ next_item_idx += 1
1199
+
1200
+ regions.append(
1201
+ _RestoreRegion(
1202
+ region_index=region_index,
1203
+ slab_offset=region_index * self._restore_region_bytes,
1204
+ total_bytes=used_bytes,
1205
+ items=region_items,
1206
+ spans=region_spans,
1207
+ )
1208
+ )
1209
+
1210
+ waves.append(_RestoreWave(regions=regions))
1211
+
1212
+ return waves
1213
+
1214
+ def _batched_memcpy(
1215
+ self,
1216
+ src_ptrs: Sequence[int],
1217
+ dst_ptrs: Sequence[int],
1218
+ sizes: Sequence[int],
1219
+ ) -> None:
1220
+ """Copy a batch of byte ranges, preferring the native helper.
1221
+
1222
+ Args:
1223
+ src_ptrs: Source addresses for each copy.
1224
+ dst_ptrs: Destination addresses for each copy.
1225
+ sizes: Byte counts for each copy.
1226
+ """
1227
+ if not src_ptrs:
1228
+ return
1229
+ if hasattr(lmc_ops, "batched_memcpy"):
1230
+ lmc_ops.batched_memcpy(list(src_ptrs), list(dst_ptrs), list(sizes))
1231
+ return
1232
+
1233
+ for src_ptr, dst_ptr, size in zip(src_ptrs, dst_ptrs, sizes, strict=True):
1234
+ ctypes.memmove(
1235
+ ctypes.c_void_p(dst_ptr),
1236
+ ctypes.c_void_p(src_ptr),
1237
+ size,
1238
+ )
1239
+
1240
+ def _restore_region(self, region: _RestoreRegion) -> None:
1241
+ """Restore one region from DAX into the assigned output buffers.
1242
+
1243
+ Args:
1244
+ region: Copy plan describing the DAX spans to stage and the output
1245
+ buffers to populate from the shared slab.
1246
+
1247
+ Raises:
1248
+ RuntimeError: If the shared retrieve slab is unavailable.
1249
+ """
1250
+ if region.total_bytes <= 0 or not region.items:
1251
+ return
1252
+ if self._retrieve_staging_slab_ptr == 0:
1253
+ raise RuntimeError("DaxBackend retrieve slab is not allocated")
1254
+
1255
+ slab_base_ptr = self._retrieve_staging_slab_ptr + region.slab_offset
1256
+ dax_src_ptrs = [self._base_ptr + span.src_offset for span in region.spans]
1257
+ slab_dst_ptrs = [slab_base_ptr + span.slab_offset for span in region.spans]
1258
+ dax_copy_sizes = [span.size for span in region.spans]
1259
+ self._batched_memcpy(dax_src_ptrs, slab_dst_ptrs, dax_copy_sizes)
1260
+
1261
+ slab_src_ptrs = [slab_base_ptr + item.slab_offset for item in region.items]
1262
+ dst_ptrs = [cast(MemoryObj, item.memory_obj).data_ptr for item in region.items]
1263
+ out_sizes = [item.size for item in region.items]
1264
+ self._batched_memcpy(slab_src_ptrs, dst_ptrs, out_sizes)
1265
+
1266
+ def _run_restore_waves(self, waves: Sequence[_RestoreWave]) -> None:
1267
+ """Execute restore waves and wait for all region copies to finish.
1268
+
1269
+ Args:
1270
+ waves: Ordered restore waves produced by
1271
+ :meth:`_build_restore_waves`.
1272
+
1273
+ Raises:
1274
+ RuntimeError: If the restore worker pool is unavailable.
1275
+ """
1276
+ restore_executor = self._restore_executor
1277
+ if restore_executor is None:
1278
+ raise RuntimeError("DaxBackend restore executor is not available")
1279
+
1280
+ for wave in waves:
1281
+ futures = [
1282
+ restore_executor.submit(self._restore_region, region)
1283
+ for region in wave.regions
1284
+ if region.items
1285
+ ]
1286
+ for future in futures:
1287
+ future.result()
1288
+
1289
+ def _cleanup_restore_outputs(self, reserved: Sequence[_RestoreItem]) -> None:
1290
+ """Release any output buffers allocated for a failed restore batch.
1291
+
1292
+ Args:
1293
+ reserved: Restore items whose temporary output buffers should be
1294
+ decremented and cleared.
1295
+ """
1296
+ for item in reserved:
1297
+ if item.memory_obj is not None:
1298
+ item.memory_obj.ref_count_down()
1299
+ item.memory_obj = None
1300
+
1301
+ def _finalize_reserved_items(
1302
+ self,
1303
+ reserved: Sequence[_RestoreItem],
1304
+ *,
1305
+ touched_keys: Optional[set[CacheEngineKey]] = None,
1306
+ ) -> None:
1307
+ """Release restore borrows and update post-restore slot state.
1308
+
1309
+ Args:
1310
+ reserved: Restore items previously reserved by
1311
+ :meth:`_reserve_restore_items`.
1312
+ touched_keys: Keys that completed successfully and should refresh
1313
+ their LRU state before borrow counts are dropped.
1314
+ """
1315
+ if not reserved:
1316
+ return
1317
+ touched_keys = touched_keys or set()
1318
+ with self._state_lock:
1319
+ if self._active_ops > 0:
1320
+ self._active_ops -= 1
1321
+ else:
1322
+ logger.warning("DaxBackend active op count underflow during restore")
1323
+
1324
+ for item in reserved:
1325
+ state = self._slot_states.get(item.slot_id)
1326
+ if state is None or state.generation != item.generation:
1327
+ continue
1328
+ if state.borrow_count > 0:
1329
+ state.borrow_count -= 1
1330
+
1331
+ if item.key in touched_keys:
1332
+ current = self._index.get(item.key)
1333
+ if (
1334
+ current is not None
1335
+ and current.slot_id == item.slot_id
1336
+ and current.generation == item.generation
1337
+ ):
1338
+ self._touch_locked(item.key)
1339
+
1340
+ if state.pending_free and state.borrow_count == 0:
1341
+ state.pending_free = False
1342
+ self._free_slot_locked(item.slot_id)
1343
+
1344
+ self._state_condition.notify_all()
1345
+
1346
+ def _restore_batch(
1347
+ self,
1348
+ keys: list[CacheEngineKey],
1349
+ prefix_only: bool,
1350
+ ) -> list[Optional[MemoryObj]]:
1351
+ """Restore one batch of keys through the staged DAX retrieve pipeline.
1352
+
1353
+ Args:
1354
+ keys: Ordered keys to restore.
1355
+ prefix_only: When ``True``, return only the consecutive readable
1356
+ prefix used by async-prefetch retrieval. When ``False``, return
1357
+ an input-aligned list and preserve ``None`` holes for misses.
1358
+
1359
+ Returns:
1360
+ Restored outputs for the batch. The returned list is input-aligned
1361
+ for blocking retrieval and prefix-compacted for async-prefetch.
1362
+ """
1363
+ reserved, results = self._reserve_restore_items(keys, prefix_only=prefix_only)
1364
+ if not reserved:
1365
+ return [] if prefix_only else results
1366
+
1367
+ touched_keys: set[CacheEngineKey] = set()
1368
+ try:
1369
+ self._allocate_restore_outputs(reserved)
1370
+ waves = self._build_restore_waves(reserved)
1371
+ self._run_restore_waves(waves)
1372
+ for item in reserved:
1373
+ memory_obj = cast(MemoryObj, item.memory_obj)
1374
+ memory_obj.metadata.cached_positions = item.cached_positions
1375
+ results[item.result_index] = memory_obj
1376
+ touched_keys.add(item.key)
1377
+ except Exception:
1378
+ self._cleanup_restore_outputs(reserved)
1379
+ self._finalize_reserved_items(reserved)
1380
+ raise
1381
+
1382
+ self._finalize_reserved_items(reserved, touched_keys=touched_keys)
1383
+ if prefix_only:
1384
+ return cast(
1385
+ list[Optional[MemoryObj]],
1386
+ [cast(MemoryObj, results[item.result_index]) for item in reserved],
1387
+ )
1388
+ return results
1389
+
1390
+ def _do_write(self, offset: int, memory_obj: MemoryObj, size: int) -> None:
1391
+ ctypes.memmove(
1392
+ ctypes.c_void_p(self._base_ptr + offset),
1393
+ ctypes.c_void_p(memory_obj.data_ptr),
1394
+ size,
1395
+ )
1396
+
1397
+ def _do_read(self, offset: int, memory_obj: MemoryObj, size: int) -> None:
1398
+ ctypes.memmove(
1399
+ ctypes.c_void_p(memory_obj.data_ptr),
1400
+ ctypes.c_void_p(self._base_ptr + offset),
1401
+ size,
1402
+ )
1403
+
1404
+ async def _submit_write(
1405
+ self,
1406
+ key: CacheEngineKey,
1407
+ offset: int,
1408
+ size: int,
1409
+ memory_obj: MemoryObj,
1410
+ on_complete_callback: Optional[Callable[[CacheEngineKey], None]] = None,
1411
+ ) -> None:
1412
+ write_error: Optional[Exception] = None
1413
+ should_invoke_callback = False
1414
+ try:
1415
+ try:
1416
+ await asyncio.to_thread(self._do_write, offset, memory_obj, size)
1417
+ except Exception as e:
1418
+ write_error = e
1419
+ logger.warning("Async DAX write failed for key %s: %s", key, e)
1420
+ finally:
1421
+ with self._state_lock:
1422
+ should_invoke_callback = self._finalize_inflight_locked(
1423
+ key,
1424
+ write_failed=write_error is not None,
1425
+ )
1426
+
1427
+ if write_error is not None:
1428
+ raise RuntimeError(
1429
+ f"DaxBackend write failed for key {key}: {write_error}"
1430
+ ) from write_error
1431
+
1432
+ if should_invoke_callback:
1433
+ self._invoke_on_complete_callback(key, on_complete_callback)
1434
+ finally:
1435
+ memory_obj.ref_count_down()
1436
+ with self._state_lock:
1437
+ if self._active_puts > 0:
1438
+ self._active_puts -= 1
1439
+ else:
1440
+ logger.warning(
1441
+ "DaxBackend active put count underflow for key %s", key
1442
+ )
1443
+ self._state_condition.notify_all()