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,1361 @@
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 collections.abc import Mapping
9
+ from dataclasses import dataclass
10
+ from typing import Any, Callable, List, Optional, Sequence
11
+ import asyncio
12
+ import ctypes
13
+ import json
14
+ import struct
15
+ import threading
16
+ import time
17
+ import zlib
18
+
19
+ # Third Party
20
+ import torch
21
+
22
+ # First Party
23
+ from lmcache.logging import init_logger
24
+ from lmcache.utils import CacheEngineKey, DiskCacheMetadata
25
+ from lmcache.v1.memory_management import MemoryFormat, MemoryObj
26
+ from lmcache.v1.storage_backend.abstract_backend import (
27
+ AllocatorBackendInterface,
28
+ StoragePluginInterface,
29
+ )
30
+
31
+ logger = init_logger(__name__)
32
+
33
+
34
+ _DEFAULT_META_MAGIC = b"LMCIDX01"
35
+ _DEFAULT_META_VERSION = 1
36
+ _META_HEADER_STRUCT = struct.Struct("<8sIQQI")
37
+ TPRankKey = int | str
38
+ PerTPDevicePaths = Mapping[TPRankKey, str]
39
+
40
+
41
+ def _round_up(x: int, align: int) -> int:
42
+ """Round up to nearest multiple of alignment (required for O_DIRECT)."""
43
+ return ((x + align - 1) // align) * align
44
+
45
+
46
+ def _validate_per_tp_device_paths(per_tp_devices: PerTPDevicePaths) -> None:
47
+ """Validate per-TP device mapping and enforce unique paths."""
48
+ values = list(per_tp_devices.values())
49
+ if len(values) != len(set(values)):
50
+ raise ValueError(
51
+ "Duplicate device path configured in rust_raw_block.per_tp_device_paths"
52
+ )
53
+
54
+
55
+ def _get_per_tp_device_path(
56
+ per_tp_devices: PerTPDevicePaths, tp_rank: int
57
+ ) -> Optional[str]:
58
+ """Return the configured device path for a TP rank.
59
+
60
+ Looks up both string and integer forms of ``tp_rank`` so YAML mappings
61
+ with either quoted or unquoted numeric keys are accepted.
62
+ """
63
+ return per_tp_devices.get(str(tp_rank), per_tp_devices.get(tp_rank))
64
+
65
+
66
+ @dataclass
67
+ class _Entry:
68
+ """In-memory index entry for a stored chunk."""
69
+
70
+ offset: int
71
+ size: int
72
+ meta: DiskCacheMetadata
73
+
74
+
75
+ @dataclass
76
+ class _Inflight:
77
+ offset: int
78
+ meta: DiskCacheMetadata
79
+ canceled: bool = False
80
+
81
+
82
+ class RustRawBlockBackend(StoragePluginInterface):
83
+ """
84
+ A storage plugin backend that stores KV chunks into a block device (raw)
85
+ using a Rust extension for pread/pwrite.
86
+
87
+ Features:
88
+ - High-throughput I/O via direct block device access
89
+ - O_DIRECT support to bypass page cache (requires aligned buffers)
90
+ - On-device metadata checkpoint for restart recovery
91
+ - Efficient buffer operations via Rust extension
92
+
93
+ - TP > 1 support via per-TP partitions
94
+
95
+ TP > 1 Support:
96
+ ----------------
97
+ When using Tensor Parallelism (TP > 1), each TP worker must use a
98
+ separate partition to avoid metadata conflicts and data corruption.
99
+
100
+ Configuration:
101
+ For TP > 1, you must explicitly configure device paths for each TP worker:
102
+ extra_config:
103
+ rust_raw_block.per_tp_device_paths:
104
+ "0": "/dev/nvme0n1p1"
105
+ "1": "/dev/nvme0n1p2"
106
+ "2": "/dev/nvme0n1p3"
107
+ "3": "/dev/nvme0n1p4"
108
+
109
+ Note: Partitions must be pre-created on the device before use.
110
+ """
111
+
112
+ def __init__(
113
+ self,
114
+ config=None,
115
+ metadata=None,
116
+ local_cpu_backend=None,
117
+ loop: Optional[asyncio.AbstractEventLoop] = None,
118
+ dst_device: str = "cpu",
119
+ ):
120
+ super().__init__(
121
+ dst_device=dst_device,
122
+ config=config,
123
+ metadata=metadata,
124
+ local_cpu_backend=local_cpu_backend,
125
+ loop=loop,
126
+ )
127
+ if self.loop is None:
128
+ raise ValueError("RustRawBlockBackend requires an asyncio event loop")
129
+ if self.local_cpu_backend is None:
130
+ raise ValueError("RustRawBlockBackend requires local_cpu_backend")
131
+ if self.config is None:
132
+ raise ValueError("RustRawBlockBackend requires config")
133
+
134
+ extra = self.config.extra_config or {}
135
+
136
+ # Support TP > 1 via per-TP device paths.
137
+ # Each TP worker uses its own partition to avoid conflicts.
138
+ self.device_path: str
139
+ if self.metadata is not None and self.metadata.world_size > 1:
140
+ tp_rank = self.metadata.worker_id
141
+ per_tp_devices = extra.get("rust_raw_block.per_tp_device_paths", {})
142
+ if not isinstance(per_tp_devices, Mapping):
143
+ raise ValueError(
144
+ "rust_raw_block.per_tp_device_paths must be a mapping from "
145
+ "TP rank to device path"
146
+ )
147
+
148
+ if not per_tp_devices:
149
+ raise ValueError(
150
+ "For TP > 1, rust_raw_block.per_tp_device_paths is required. "
151
+ "Each TP worker must have an explicit device path configured."
152
+ )
153
+ _validate_per_tp_device_paths(per_tp_devices)
154
+
155
+ tp_rank_str = str(tp_rank)
156
+ device_path = _get_per_tp_device_path(per_tp_devices, tp_rank)
157
+ if not device_path:
158
+ raise ValueError(
159
+ f"No device path configured for TP rank {tp_rank_str}. "
160
+ f"Available ranks: {list(per_tp_devices.keys())}"
161
+ )
162
+ self.device_path = device_path
163
+ logger.info(
164
+ f"RustRawBlockBackend: TP={self.metadata.world_size} mode, "
165
+ f"using explicit device path for rank {tp_rank}: {self.device_path}"
166
+ )
167
+ else:
168
+ self.device_path = extra.get("rust_raw_block.device_path", "")
169
+ if not self.device_path:
170
+ raise ValueError(
171
+ "extra_config['rust_raw_block.device_path'] is required"
172
+ )
173
+
174
+ self.capacity_bytes: int = int(extra.get("rust_raw_block.capacity_bytes", 0))
175
+ self.block_align: int = int(extra.get("rust_raw_block.block_align", 4096))
176
+ self.header_bytes: int = int(extra.get("rust_raw_block.header_bytes", 4096))
177
+ self.use_odirect: bool = bool(extra.get("rust_raw_block.use_odirect", False))
178
+ # Try direct aligned O_DIRECT I/O when allocator buffers permit.
179
+ self.enable_zero_copy: bool = bool(
180
+ extra.get("rust_raw_block.enable_zero_copy", True)
181
+ )
182
+
183
+ # On-device metadata region config.
184
+ self.meta_total_bytes: int = int(
185
+ extra.get("rust_raw_block.meta_total_bytes", 128 * 1024 * 1024)
186
+ )
187
+ meta_magic_raw = extra.get("rust_raw_block.meta_magic", "LMCIDX01")
188
+ if isinstance(meta_magic_raw, str):
189
+ self.meta_magic: bytes = meta_magic_raw.encode("ascii")
190
+ elif isinstance(meta_magic_raw, bytes):
191
+ self.meta_magic = meta_magic_raw
192
+ else:
193
+ raise ValueError("rust_raw_block.meta_magic must be str or bytes")
194
+ if len(self.meta_magic) != 8:
195
+ raise ValueError("rust_raw_block.meta_magic must be exactly 8 bytes")
196
+ try:
197
+ self.meta_magic_text: str = self.meta_magic.decode("ascii")
198
+ except UnicodeDecodeError as e:
199
+ raise ValueError("rust_raw_block.meta_magic must be ASCII bytes") from e
200
+ self.meta_version: int = int(
201
+ extra.get("rust_raw_block.meta_version", _DEFAULT_META_VERSION)
202
+ )
203
+ if self.meta_version <= 0:
204
+ raise ValueError("rust_raw_block.meta_version must be > 0")
205
+ self.meta_checkpoint_interval_sec: int = int(
206
+ extra.get("rust_raw_block.meta_checkpoint_interval_sec", 60)
207
+ )
208
+ self.meta_idle_quiet_ms: int = int(
209
+ extra.get("rust_raw_block.meta_idle_quiet_ms", 100)
210
+ )
211
+ self.meta_enable_periodic: bool = bool(
212
+ extra.get("rust_raw_block.meta_enable_periodic", True)
213
+ )
214
+ self.meta_verify_on_load: bool = bool(
215
+ extra.get("rust_raw_block.meta_verify_on_load", True)
216
+ )
217
+ self._meta_copy_count: int = 2
218
+
219
+ get_full_chunk_size_bytes = getattr(
220
+ self.local_cpu_backend, "get_full_chunk_size_bytes", None
221
+ )
222
+ if callable(get_full_chunk_size_bytes):
223
+ full_chunk_bytes = int(get_full_chunk_size_bytes())
224
+ else:
225
+ get_full_chunk_size = getattr(
226
+ self.local_cpu_backend, "get_full_chunk_size", None
227
+ )
228
+ if not callable(get_full_chunk_size):
229
+ raise ValueError(
230
+ "local_cpu_backend must expose get_full_chunk_size_bytes() "
231
+ "or get_full_chunk_size()"
232
+ )
233
+ full_chunk_bytes = int(get_full_chunk_size())
234
+ default_slot_bytes = _round_up(
235
+ self.header_bytes + full_chunk_bytes, self.block_align
236
+ )
237
+ self.slot_bytes: int = int(
238
+ extra.get("rust_raw_block.slot_bytes", default_slot_bytes)
239
+ )
240
+
241
+ if self.slot_bytes < self.header_bytes + 1:
242
+ raise ValueError("rust_raw_block.slot_bytes too small")
243
+ if self.slot_bytes % self.block_align != 0:
244
+ raise ValueError(
245
+ "rust_raw_block.slot_bytes must be multiple of block_align"
246
+ )
247
+ if self.header_bytes % self.block_align != 0:
248
+ raise ValueError(
249
+ "rust_raw_block.header_bytes must be multiple of block_align"
250
+ )
251
+ if self.meta_total_bytes <= 0:
252
+ raise ValueError("rust_raw_block.meta_total_bytes must be > 0")
253
+ if self.meta_total_bytes % self.block_align != 0:
254
+ raise ValueError(
255
+ "rust_raw_block.meta_total_bytes must align to block_align"
256
+ )
257
+ if self.meta_total_bytes <= self.block_align:
258
+ raise ValueError(
259
+ "rust_raw_block.meta_total_bytes must be > block_align "
260
+ "(room for metadata header + payload)"
261
+ )
262
+ self._meta_container_bytes: int = (
263
+ (self.meta_total_bytes // self._meta_copy_count) // self.block_align
264
+ ) * self.block_align
265
+ if self._meta_container_bytes <= self.block_align:
266
+ raise ValueError(
267
+ "rust_raw_block.meta_total_bytes must provide room for at least "
268
+ "two metadata copies (header + payload)"
269
+ )
270
+
271
+ self._lock = threading.Lock()
272
+ self._index: dict[CacheEngineKey, _Entry] = {}
273
+ self._pinned: set[CacheEngineKey] = set()
274
+ self._inflight: dict[CacheEngineKey, _Inflight] = {}
275
+ self._lru: "OrderedDict[CacheEngineKey, None]" = OrderedDict()
276
+
277
+ self._next_slot: int = 0
278
+ self._free_slots: list[int] = []
279
+ self._max_slots: int = 0
280
+ self._effective_capacity_bytes: int = 0
281
+ self._data_base_offset: int = 0
282
+
283
+ self._dbg_first_n: int = int(extra.get("rust_raw_block.debug_first_n", 4) or 0)
284
+ self._dbg_every_n: int = int(
285
+ extra.get("rust_raw_block.debug_every_n", 256) or 0
286
+ )
287
+ self._dbg_put_batches: int = 0
288
+ self._dbg_put_keys: int = 0
289
+ self._dbg_put_bytes: int = 0
290
+ self._dbg_get_calls: int = 0
291
+ self._dbg_get_bytes: int = 0
292
+
293
+ self._raw = None
294
+
295
+ self._put_lock = threading.Lock()
296
+ self._put_tasks: set[CacheEngineKey] = set()
297
+
298
+ # Metadata checkpoint state.
299
+ self._meta_seq: int = 0
300
+ self._meta_dirty_total: int = 0
301
+ self._meta_persisted: int = 0
302
+ self._inflight_io_count: int = 0
303
+ self._last_io_ts: float = time.monotonic()
304
+ self._meta_stop_evt = threading.Event()
305
+ self._meta_thread: Optional[threading.Thread] = None
306
+
307
+ self._ensure_capacity_and_layout()
308
+
309
+ logger.info(
310
+ "RustRawBlockBackend init: device=%s cap=%s slot=%d align=%d header=%d "
311
+ "meta_total=%d data_base=%d zero_copy=%s",
312
+ self.device_path,
313
+ self.capacity_bytes,
314
+ self.slot_bytes,
315
+ self.block_align,
316
+ self.header_bytes,
317
+ self.meta_total_bytes,
318
+ self._data_base_offset,
319
+ self.enable_zero_copy,
320
+ )
321
+
322
+ # Load latest checkpoint from device (no JSON fallback).
323
+ self._load_checkpoint_from_device()
324
+
325
+ if self.meta_enable_periodic:
326
+ self._meta_thread = threading.Thread(
327
+ target=self._checkpoint_loop,
328
+ name="rust-raw-block-meta-checkpoint",
329
+ daemon=True,
330
+ )
331
+ self._meta_thread.start()
332
+
333
+ def _dbg_should_log(self, n: int) -> bool:
334
+ if not logger.isEnabledFor(10):
335
+ return False
336
+ if self._dbg_first_n and n <= self._dbg_first_n:
337
+ return True
338
+ if self._dbg_every_n and n % self._dbg_every_n == 0:
339
+ return True
340
+ return False
341
+
342
+ def _dbg_key_short(self, key: CacheEngineKey) -> str:
343
+ try:
344
+ return f"chunk_hash={int(key.chunk_hash)}"
345
+ except Exception:
346
+ return "chunk_hash=?"
347
+
348
+ def __str__(self) -> str:
349
+ return "RustRawBlockBackend"
350
+
351
+ def _rawdev(self):
352
+ if self._raw is None:
353
+ try:
354
+ # Third Party
355
+ from lmcache_rust_raw_block_io import RawBlockDevice # type: ignore
356
+ except Exception as e:
357
+ raise RuntimeError(
358
+ "Rust raw-block extension is not installed. "
359
+ "Install / build `rust_raw_block_io` and retry."
360
+ ) from e
361
+ self._raw = RawBlockDevice(
362
+ self.device_path,
363
+ writable=True,
364
+ use_odirect=self.use_odirect,
365
+ alignment=self.block_align,
366
+ )
367
+ return self._raw
368
+
369
+ def _build_direct_odirect_view(
370
+ self,
371
+ memory_obj: MemoryObj,
372
+ payload_len: int,
373
+ total_len: int,
374
+ buffer_len: int,
375
+ *,
376
+ zero_tail: bool,
377
+ ) -> Optional[memoryview]:
378
+ """Build direct physical-memory view for O_DIRECT without staging copy."""
379
+ if not self.use_odirect or not self.enable_zero_copy:
380
+ return None
381
+
382
+ ptr_val = getattr(memory_obj, "data_ptr", None)
383
+ if callable(ptr_val):
384
+ try:
385
+ ptr_val = ptr_val()
386
+ except Exception:
387
+ ptr_val = None
388
+ if ptr_val is None:
389
+ return None
390
+
391
+ if buffer_len <= 0:
392
+ return None
393
+
394
+ try:
395
+ ptr = int(ptr_val)
396
+ except Exception:
397
+ return None
398
+
399
+ if ptr <= 0 or ptr % self.block_align != 0:
400
+ return None
401
+ if buffer_len < payload_len:
402
+ return None
403
+
404
+ view_len = min(buffer_len, total_len)
405
+ if view_len < payload_len:
406
+ return None
407
+
408
+ try:
409
+ raw = (ctypes.c_ubyte * view_len).from_address(ptr)
410
+ view = memoryview(raw)
411
+ if zero_tail and total_len > payload_len and view_len >= total_len:
412
+ ctypes.memset(ptr + payload_len, 0, total_len - payload_len)
413
+ return view
414
+ except Exception:
415
+ return None
416
+
417
+ def _ensure_capacity_and_layout(self) -> None:
418
+ if self._effective_capacity_bytes > 0 and self._max_slots > 0:
419
+ return
420
+
421
+ device_size = int(self._rawdev().size_bytes())
422
+ requested = self.capacity_bytes if self.capacity_bytes > 0 else device_size
423
+ self._effective_capacity_bytes = min(requested, device_size)
424
+ self.capacity_bytes = self._effective_capacity_bytes
425
+
426
+ if self.meta_total_bytes >= self._effective_capacity_bytes:
427
+ raise RuntimeError("metadata region exceeds usable device capacity")
428
+
429
+ self._data_base_offset = self.meta_total_bytes
430
+ data_bytes = self._effective_capacity_bytes - self._data_base_offset
431
+ self._max_slots = data_bytes // self.slot_bytes
432
+ if self._max_slots <= 0:
433
+ raise RuntimeError(
434
+ "raw block capacity too small for slot size after metadata"
435
+ )
436
+
437
+ def _slot_to_offset(self, slot: int) -> int:
438
+ return self._data_base_offset + slot * self.slot_bytes
439
+
440
+ def _offset_to_slot(self, offset: int) -> int:
441
+ return (offset - self._data_base_offset) // self.slot_bytes
442
+
443
+ def _allocate_slot(self) -> int:
444
+ self._ensure_capacity_and_layout()
445
+
446
+ if self._free_slots:
447
+ return self._slot_to_offset(self._free_slots.pop())
448
+
449
+ if self._next_slot < self._max_slots:
450
+ slot = self._next_slot
451
+ self._next_slot += 1
452
+ return self._slot_to_offset(slot)
453
+
454
+ raise RuntimeError("No free slots available; eviction required")
455
+
456
+ def _touch(self, key: CacheEngineKey) -> None:
457
+ self._lru.pop(key, None)
458
+ self._lru[key] = None
459
+
460
+ def _append_free_slot_locked(self, slot: int) -> None:
461
+ if slot < 0 or slot >= self._max_slots:
462
+ return
463
+ if slot in self._free_slots:
464
+ return
465
+ self._free_slots.append(slot)
466
+
467
+ def _evict_one(self) -> bool:
468
+ for victim in list(self._lru.keys()):
469
+ if victim in self._pinned or victim in self._inflight:
470
+ continue
471
+ entry = self._index.pop(victim, None)
472
+ if entry is None:
473
+ self._lru.pop(victim, None)
474
+ continue
475
+ self._lru.pop(victim, None)
476
+ self._pinned.discard(victim)
477
+ self._append_free_slot_locked(self._offset_to_slot(int(entry.offset)))
478
+ self._meta_dirty_total += 1
479
+ return True
480
+ return False
481
+
482
+ def contains(self, key: CacheEngineKey, pin: bool = False) -> bool:
483
+ with self._lock:
484
+ ok = key in self._index
485
+ if ok and pin:
486
+ self._pinned.add(key)
487
+ return ok
488
+
489
+ def exists_in_put_tasks(self, key: CacheEngineKey) -> bool:
490
+ with self._put_lock:
491
+ return key in self._put_tasks
492
+
493
+ def pin(self, key: CacheEngineKey) -> bool:
494
+ with self._lock:
495
+ if key in self._index:
496
+ self._pinned.add(key)
497
+ return True
498
+ return False
499
+
500
+ def unpin(self, key: CacheEngineKey) -> bool:
501
+ with self._lock:
502
+ if key in self._pinned:
503
+ self._pinned.remove(key)
504
+ return True
505
+ return key in self._index
506
+
507
+ def remove(self, key: CacheEngineKey, force: bool = True) -> bool: # noqa: ARG002
508
+ with self._lock:
509
+ existed = key in self._index or key in self._inflight
510
+ entry = self._index.pop(key, None)
511
+ inflight = self._inflight.get(key)
512
+ self._pinned.discard(key)
513
+ self._lru.pop(key, None)
514
+ if entry is not None:
515
+ self._append_free_slot_locked(self._offset_to_slot(int(entry.offset)))
516
+ self._meta_dirty_total += 1
517
+ if inflight is not None:
518
+ inflight.canceled = True
519
+ return existed
520
+
521
+ def batched_submit_put_task(
522
+ self,
523
+ keys: Sequence[CacheEngineKey],
524
+ objs: List[MemoryObj],
525
+ transfer_spec: Any = None, # noqa: ARG002
526
+ on_complete_callback: Optional[Callable[[CacheEngineKey], None]] = None,
527
+ ):
528
+ if logger.isEnabledFor(10):
529
+ self._dbg_put_batches += 1
530
+ self._dbg_put_keys += int(len(keys))
531
+ try:
532
+ self._dbg_put_bytes += int(sum(len(o.byte_array) for o in objs))
533
+ except Exception:
534
+ pass
535
+ if self._dbg_should_log(self._dbg_put_batches):
536
+ logger.debug(
537
+ "RustRawBlockBackend PUT: keys=%d inflight=%d indexed=%d",
538
+ len(keys),
539
+ len(self._inflight),
540
+ len(self._index),
541
+ )
542
+
543
+ futures = []
544
+ for key, obj in zip(keys, objs, strict=False):
545
+ with self._put_lock:
546
+ if key in self._put_tasks:
547
+ continue
548
+ self._put_tasks.add(key)
549
+
550
+ with self._lock:
551
+ if key in self._index or key in self._inflight:
552
+ with self._put_lock:
553
+ self._put_tasks.discard(key)
554
+ continue
555
+ while True:
556
+ try:
557
+ offset = self._allocate_slot()
558
+ break
559
+ except RuntimeError:
560
+ if not self._evict_one():
561
+ with self._put_lock:
562
+ self._put_tasks.discard(key)
563
+ raise
564
+
565
+ meta = DiskCacheMetadata(
566
+ path=f"{self.device_path}@{offset}",
567
+ size=len(obj.byte_array),
568
+ shape=obj.metadata.shape,
569
+ dtype=obj.metadata.dtype,
570
+ cached_positions=obj.metadata.cached_positions,
571
+ fmt=obj.metadata.fmt,
572
+ pin_count=0,
573
+ )
574
+ self._inflight[key] = _Inflight(offset=offset, meta=meta)
575
+
576
+ header = self._encode_header(key, meta.size)
577
+ obj.ref_count_up()
578
+ assert self.loop is not None
579
+ fut = asyncio.run_coroutine_threadsafe(
580
+ self._submit_write(
581
+ key=key,
582
+ offset=offset,
583
+ header=header,
584
+ memory_obj=obj,
585
+ on_complete_callback=on_complete_callback,
586
+ ),
587
+ self.loop,
588
+ )
589
+ futures.append(fut)
590
+ return futures or None
591
+
592
+ def _prepare_write_payload(self, memory_obj: MemoryObj) -> tuple[Any, int, int]:
593
+ """Prepare payload view and aligned lengths for write path."""
594
+ buf = memory_obj.byte_array
595
+ if hasattr(buf, "cast"):
596
+ buf = buf.cast("B")
597
+ payload_len = len(memory_obj.byte_array)
598
+ total_len = payload_len
599
+ if self.use_odirect:
600
+ total_len = _round_up(payload_len, self.block_align)
601
+ if total_len > (self.slot_bytes - self.header_bytes):
602
+ raise RuntimeError(f"O_DIRECT payload {total_len} > slot capacity")
603
+ direct_view = self._build_direct_odirect_view(
604
+ memory_obj=memory_obj,
605
+ payload_len=payload_len,
606
+ total_len=total_len,
607
+ buffer_len=len(buf),
608
+ zero_tail=True,
609
+ )
610
+ if direct_view is not None:
611
+ buf = direct_view
612
+ return buf, payload_len, total_len
613
+
614
+ async def _submit_write(
615
+ self,
616
+ key: CacheEngineKey,
617
+ offset: int,
618
+ header: bytes,
619
+ memory_obj: MemoryObj,
620
+ on_complete_callback: Optional[Callable[[CacheEngineKey], None]] = None,
621
+ ) -> None:
622
+ try:
623
+ buf, payload_len, total_len = self._prepare_write_payload(memory_obj)
624
+
625
+ def _do_write():
626
+ with self._lock:
627
+ self._inflight_io_count += 1
628
+ try:
629
+ raw_dev = self._rawdev()
630
+ hdr_total = (
631
+ _round_up(len(header), self.block_align)
632
+ if self.use_odirect
633
+ else len(header)
634
+ )
635
+ raw_dev.pwrite_from_buffer(offset, header, len(header), hdr_total)
636
+ raw_dev.pwrite_from_buffer(
637
+ offset + self.header_bytes, buf, payload_len, total_len
638
+ )
639
+ except Exception as e:
640
+ logger.error(
641
+ f"Write failed for key {self._dbg_key_short(key)}: {e}"
642
+ )
643
+ raise
644
+ finally:
645
+ with self._lock:
646
+ self._inflight_io_count -= 1
647
+ self._last_io_ts = time.monotonic()
648
+
649
+ write_error: Optional[Exception] = None
650
+ try:
651
+ await asyncio.to_thread(_do_write)
652
+ except Exception as e:
653
+ write_error = e
654
+
655
+ with self._lock:
656
+ inflight = self._inflight.pop(key, None)
657
+ if inflight is not None:
658
+ if inflight.canceled or write_error is not None:
659
+ self._append_free_slot_locked(
660
+ self._offset_to_slot(int(inflight.offset))
661
+ )
662
+ self._meta_dirty_total += 1
663
+ else:
664
+ self._index[key] = _Entry(
665
+ offset=inflight.offset,
666
+ size=inflight.meta.size,
667
+ meta=inflight.meta,
668
+ )
669
+ self._touch(key)
670
+ self._meta_dirty_total += 1
671
+
672
+ if write_error is None:
673
+ if on_complete_callback is not None:
674
+ try:
675
+ on_complete_callback(key)
676
+ except Exception as e:
677
+ logger.warning(
678
+ f"on_complete_callback failed for key {key}: {e}"
679
+ )
680
+ else:
681
+ raise write_error
682
+ finally:
683
+ memory_obj.ref_count_down()
684
+ with self._put_lock:
685
+ self._put_tasks.discard(key)
686
+
687
+ def _encode_header(self, key: CacheEngineKey, payload_len: int) -> bytes:
688
+ magic = b"LMCBLK01"
689
+ chunk_hash = int(key.chunk_hash) & ((1 << 64) - 1)
690
+ hdr = bytearray(self.header_bytes)
691
+ hdr[0:8] = magic
692
+ hdr[8:16] = chunk_hash.to_bytes(8, "little", signed=False)
693
+ hdr[16:24] = int(payload_len).to_bytes(8, "little", signed=False)
694
+ return bytes(hdr)
695
+
696
+ def _decode_slot_header(self, hdr: bytes) -> Optional[tuple[int, int]]:
697
+ if len(hdr) < 24:
698
+ return None
699
+ if hdr[0:8] != b"LMCBLK01":
700
+ return None
701
+ chunk_hash = int.from_bytes(hdr[8:16], "little", signed=False)
702
+ payload_len = int.from_bytes(hdr[16:24], "little", signed=False)
703
+ return chunk_hash, payload_len
704
+
705
+ def _read_slot_header(self, offset: int) -> Optional[tuple[int, int]]:
706
+ raw = self._rawdev()
707
+ buf = bytearray(self.header_bytes)
708
+ try:
709
+ with self._lock:
710
+ self._inflight_io_count += 1
711
+ raw.pread_into(offset, buf, self.header_bytes, self.header_bytes)
712
+ return self._decode_slot_header(buf)
713
+ except Exception:
714
+ return None
715
+ finally:
716
+ with self._lock:
717
+ self._inflight_io_count -= 1
718
+ self._last_io_ts = time.monotonic()
719
+
720
+ def _batched_get_prefix(self, keys: Sequence[CacheEngineKey]) -> list[MemoryObj]:
721
+ if not keys:
722
+ return []
723
+
724
+ items: list[tuple[CacheEngineKey, _Entry]] = []
725
+ with self._lock:
726
+ for key in keys:
727
+ entry = self._index.get(key)
728
+ if entry is None:
729
+ break
730
+ items.append((key, entry))
731
+ if not items:
732
+ return []
733
+ self._inflight_io_count += 1
734
+
735
+ loaded: list[MemoryObj] = []
736
+ touched: list[CacheEngineKey] = []
737
+ try:
738
+ raw_dev = self._rawdev()
739
+ for key, entry in items:
740
+ meta = entry.meta
741
+ assert meta.shape is not None and meta.dtype is not None
742
+ assert self.local_cpu_backend is not None
743
+ memory_obj = self.local_cpu_backend.allocate(
744
+ meta.shape, meta.dtype, meta.fmt
745
+ )
746
+ if memory_obj is None:
747
+ logger.error(
748
+ "Failed to allocate memory for key %s",
749
+ self._dbg_key_short(key),
750
+ )
751
+ break
752
+
753
+ try:
754
+ payload_len = int(meta.size)
755
+ total_len = (
756
+ _round_up(payload_len, self.block_align)
757
+ if self.use_odirect
758
+ else payload_len
759
+ )
760
+ if logger.isEnabledFor(10):
761
+ self._dbg_get_calls += 1
762
+ self._dbg_get_bytes += payload_len
763
+ if self._dbg_should_log(self._dbg_get_calls):
764
+ logger.debug(
765
+ "RustRawBlockBackend GET: %s offset=%d size=%d",
766
+ self._dbg_key_short(key),
767
+ int(entry.offset),
768
+ payload_len,
769
+ )
770
+
771
+ buf = memory_obj.byte_array
772
+ try:
773
+ buf = buf.cast("B")
774
+ except Exception:
775
+ pass
776
+ direct_view = self._build_direct_odirect_view(
777
+ memory_obj=memory_obj,
778
+ payload_len=payload_len,
779
+ total_len=total_len,
780
+ buffer_len=len(buf),
781
+ zero_tail=False,
782
+ )
783
+ if direct_view is not None:
784
+ raw_dev.pread_into(
785
+ entry.offset + self.header_bytes,
786
+ direct_view,
787
+ total_len if len(direct_view) >= total_len else payload_len,
788
+ total_len,
789
+ )
790
+ else:
791
+ raw_dev.pread_into(
792
+ entry.offset + self.header_bytes,
793
+ buf,
794
+ payload_len,
795
+ total_len,
796
+ )
797
+
798
+ memory_obj.metadata.cached_positions = meta.cached_positions
799
+ except Exception as e:
800
+ memory_obj.ref_count_down()
801
+ logger.error(
802
+ "Read failed for key %s: %s", self._dbg_key_short(key), e
803
+ )
804
+ raise
805
+
806
+ loaded.append(memory_obj)
807
+ touched.append(key)
808
+ except Exception:
809
+ for memory_obj in loaded:
810
+ memory_obj.ref_count_down()
811
+ loaded.clear()
812
+ touched.clear()
813
+ raise
814
+ finally:
815
+ with self._lock:
816
+ for key in touched:
817
+ self._touch(key)
818
+ self._inflight_io_count -= 1
819
+ self._last_io_ts = time.monotonic()
820
+ return loaded
821
+
822
+ def get_blocking(self, key: CacheEngineKey) -> Optional[MemoryObj]:
823
+ loaded = self._batched_get_prefix([key])
824
+ return loaded[0] if loaded else None
825
+
826
+ def batched_get_blocking(
827
+ self,
828
+ keys: List[CacheEngineKey],
829
+ ) -> List[Optional[MemoryObj]]:
830
+ """
831
+ Get a batch of cache entries until the first miss.
832
+
833
+ :param List[CacheEngineKey] keys: Ordered keys to retrieve.
834
+
835
+ :return: A list aligned to ``keys`` where the successful prefix contains
836
+ loaded memory objects and the remaining suffix is ``None``.
837
+
838
+ :raises Exception: Propagates raw-device initialization or read failures.
839
+ """
840
+ if not keys:
841
+ return []
842
+
843
+ loaded = self._batched_get_prefix(keys)
844
+ return [*loaded, *([None] * (len(keys) - len(loaded)))]
845
+
846
+ async def batched_async_contains(
847
+ self,
848
+ lookup_id: str,
849
+ keys: list[CacheEngineKey],
850
+ pin: bool = False,
851
+ ) -> int:
852
+ del lookup_id
853
+ hit = 0
854
+ with self._lock:
855
+ for k in keys:
856
+ if k not in self._index:
857
+ break
858
+ if pin:
859
+ self._pinned.add(k)
860
+ hit += 1
861
+ return hit
862
+
863
+ async def batched_get_non_blocking(
864
+ self,
865
+ lookup_id: str,
866
+ keys: list[CacheEngineKey],
867
+ transfer_spec: Any = None,
868
+ ) -> list[MemoryObj]:
869
+ """
870
+ Asynchronously get a batch of cache entries until the first miss.
871
+
872
+ :param str lookup_id: Lookup identifier used by the storage manager.
873
+ :param list[CacheEngineKey] keys: Ordered keys to retrieve.
874
+ :param Any transfer_spec: Unused transfer hint for API compatibility.
875
+
876
+ :return: The successfully loaded prefix of ``keys`` in input order.
877
+
878
+ :raises Exception: Propagates raw-device initialization or read failures.
879
+ """
880
+ del lookup_id, transfer_spec
881
+ return await asyncio.to_thread(self._batched_get_prefix, keys)
882
+
883
+ def get_allocator_backend(self) -> "AllocatorBackendInterface":
884
+ assert self.local_cpu_backend is not None
885
+ return self.local_cpu_backend
886
+
887
+ def close(self) -> None:
888
+ if logger.isEnabledFor(10):
889
+ logger.debug(
890
+ "RustRawBlockBackend stats: put=%d/%d/%d get=%d/%d",
891
+ self._dbg_put_batches,
892
+ self._dbg_put_keys,
893
+ self._dbg_put_bytes,
894
+ self._dbg_get_calls,
895
+ self._dbg_get_bytes,
896
+ )
897
+
898
+ # Stop checkpoint background thread.
899
+ self._meta_stop_evt.set()
900
+ if self._meta_thread is not None:
901
+ self._meta_thread.join(timeout=5)
902
+ self._meta_thread = None
903
+
904
+ # Wait briefly for inflight put tasks to drain.
905
+ deadline = time.monotonic() + 10.0
906
+ while True:
907
+ with self._put_lock:
908
+ pending = len(self._put_tasks)
909
+ if pending == 0 or time.monotonic() >= deadline:
910
+ break
911
+ time.sleep(0.01)
912
+
913
+ # Force final metadata checkpoint.
914
+ try:
915
+ self._checkpoint_once(force=True)
916
+ except Exception as e:
917
+ logger.warning(f"Failed to write final on-device metadata checkpoint: {e}")
918
+
919
+ if self._raw is not None:
920
+ try:
921
+ self._raw.close()
922
+ except Exception as e:
923
+ logger.warning(f"Failed to close raw block device: {e}")
924
+ finally:
925
+ self._raw = None
926
+
927
+ # ------------------- On-device metadata checkpoint -------------------
928
+
929
+ def _checkpoint_loop(self) -> None:
930
+ interval = max(1, self.meta_checkpoint_interval_sec)
931
+ while not self._meta_stop_evt.wait(interval):
932
+ try:
933
+ self._checkpoint_once(force=False)
934
+ except Exception as e:
935
+ logger.warning(f"Periodic metadata checkpoint failed: {e}")
936
+
937
+ def _meta_payload_capacity(self) -> int:
938
+ return self._meta_container_bytes - self.block_align
939
+
940
+ def _meta_container_offsets(self) -> list[int]:
941
+ return [
942
+ idx * self._meta_container_bytes for idx in range(self._meta_copy_count)
943
+ ]
944
+
945
+ def _read_meta_header(self, container_offset: int) -> Optional[dict[str, int]]:
946
+ raw = self._rawdev()
947
+ buf = bytearray(self.block_align)
948
+ try:
949
+ raw.pread_into(container_offset, buf, self.block_align, self.block_align)
950
+ except Exception:
951
+ return None
952
+
953
+ hdr = bytes(buf[: _META_HEADER_STRUCT.size])
954
+ magic, version, seq, payload_len, crc = _META_HEADER_STRUCT.unpack(hdr)
955
+ if magic != self.meta_magic or version != self.meta_version:
956
+ return None
957
+
958
+ payload_cap = self._meta_payload_capacity()
959
+ if payload_len <= 0 or payload_len > payload_cap:
960
+ return None
961
+
962
+ return {
963
+ "seq": int(seq),
964
+ "payload_len": int(payload_len),
965
+ "crc": int(crc),
966
+ "container_offset": int(container_offset),
967
+ }
968
+
969
+ def _load_meta_payload(self, header: dict[str, int]) -> Optional[bytes]:
970
+ raw = self._rawdev()
971
+ payload_len = int(header["payload_len"])
972
+ payload_off = int(header["container_offset"]) + self.block_align
973
+ total_len = _round_up(payload_len, self.block_align)
974
+ buf = bytearray(total_len)
975
+ try:
976
+ raw.pread_into(payload_off, buf, payload_len, total_len)
977
+ except Exception:
978
+ return None
979
+
980
+ payload = bytes(buf[:payload_len])
981
+ crc = zlib.crc32(payload) & 0xFFFFFFFF
982
+ if crc != int(header["crc"]):
983
+ return None
984
+ return payload
985
+
986
+ def _select_latest_checkpoint(
987
+ self,
988
+ ) -> tuple[Optional[dict[str, int]], Optional[bytes]]:
989
+ best_header: Optional[dict[str, int]] = None
990
+ best_payload: Optional[bytes] = None
991
+ for offset in self._meta_container_offsets():
992
+ header = self._read_meta_header(offset)
993
+ if header is None:
994
+ continue
995
+ payload = self._load_meta_payload(header)
996
+ if payload is None:
997
+ continue
998
+ if best_header is None or int(header["seq"]) > int(best_header["seq"]):
999
+ best_header = header
1000
+ best_payload = payload
1001
+ return best_header, best_payload
1002
+
1003
+ def _snapshot_state(self) -> tuple[dict[str, Any], int]:
1004
+ with self._lock:
1005
+ dirty_total = self._meta_dirty_total
1006
+ snapshot = {
1007
+ "version": 1,
1008
+ "device_path": self.device_path,
1009
+ "capacity_bytes": self.capacity_bytes,
1010
+ "block_align": self.block_align,
1011
+ "header_bytes": self.header_bytes,
1012
+ "slot_bytes": self.slot_bytes,
1013
+ "meta_total_bytes": self.meta_total_bytes,
1014
+ "meta_magic": self.meta_magic_text,
1015
+ "meta_version": self.meta_version,
1016
+ "data_base_offset": self._data_base_offset,
1017
+ "next_slot": self._next_slot,
1018
+ "free_slots": list(self._free_slots),
1019
+ "lru_keys": [k.to_string() for k in self._lru.keys()],
1020
+ "entries": {
1021
+ k.to_string(): {
1022
+ "offset": e.offset,
1023
+ "size": e.meta.size,
1024
+ "shape": list(e.meta.shape)
1025
+ if e.meta.shape is not None
1026
+ else None,
1027
+ "dtype": k._dtype_str,
1028
+ "fmt": (
1029
+ e.meta.fmt.name
1030
+ if e.meta.fmt is not None and hasattr(e.meta.fmt, "name")
1031
+ else str(e.meta.fmt)
1032
+ if e.meta.fmt is not None
1033
+ else None
1034
+ ),
1035
+ "cached_positions": (
1036
+ e.meta.cached_positions.tolist()
1037
+ if e.meta.cached_positions is not None
1038
+ and hasattr(e.meta.cached_positions, "tolist")
1039
+ else None
1040
+ ),
1041
+ }
1042
+ for k, e in self._index.items()
1043
+ },
1044
+ }
1045
+ return snapshot, dirty_total
1046
+
1047
+ def _write_checkpoint(self, payload: bytes, dirty_total_snapshot: int) -> bool:
1048
+ payload_cap = self._meta_payload_capacity()
1049
+ if len(payload) > payload_cap:
1050
+ logger.warning(
1051
+ "Metadata payload too large (%d > %d), skipping checkpoint",
1052
+ len(payload),
1053
+ payload_cap,
1054
+ )
1055
+ return False
1056
+
1057
+ next_seq = self._meta_seq + 1
1058
+ target_idx = int((next_seq - 1) % self._meta_copy_count)
1059
+ target = self._meta_container_offsets()[target_idx]
1060
+
1061
+ payload_len = len(payload)
1062
+ payload_total_len = _round_up(payload_len, self.block_align)
1063
+ payload_off = target + self.block_align
1064
+ crc = zlib.crc32(payload) & 0xFFFFFFFF
1065
+
1066
+ header_block = bytearray(self.block_align)
1067
+ header_block[: _META_HEADER_STRUCT.size] = _META_HEADER_STRUCT.pack(
1068
+ self.meta_magic,
1069
+ self.meta_version,
1070
+ int(next_seq),
1071
+ int(payload_len),
1072
+ int(crc),
1073
+ )
1074
+
1075
+ raw = self._rawdev()
1076
+
1077
+ raw.pwrite_from_buffer(payload_off, payload, payload_len, payload_total_len)
1078
+ raw.pwrite_from_buffer(target, header_block, self.block_align, self.block_align)
1079
+
1080
+ with self._lock:
1081
+ self._meta_seq = int(next_seq)
1082
+ self._meta_persisted = max(self._meta_persisted, int(dirty_total_snapshot))
1083
+
1084
+ return True
1085
+
1086
+ def _checkpoint_once(self, force: bool) -> bool:
1087
+ with self._lock:
1088
+ dirty = self._meta_dirty_total > self._meta_persisted
1089
+ idle_ok = self._inflight_io_count == 0 and (
1090
+ time.monotonic() - self._last_io_ts
1091
+ ) >= (self.meta_idle_quiet_ms / 1000.0)
1092
+
1093
+ if not dirty:
1094
+ return False
1095
+ if not force and not idle_ok:
1096
+ return False
1097
+
1098
+ snapshot, dirty_total_snapshot = self._snapshot_state()
1099
+ payload = json.dumps(snapshot, separators=(",", ":"), ensure_ascii=True).encode(
1100
+ "utf-8"
1101
+ )
1102
+
1103
+ ok = self._write_checkpoint(payload, dirty_total_snapshot)
1104
+ if ok and logger.isEnabledFor(10):
1105
+ logger.debug(
1106
+ "RustRawBlockBackend: checkpoint saved seq=%d entries=%d",
1107
+ self._meta_seq,
1108
+ len(snapshot.get("entries", {})),
1109
+ )
1110
+ return ok
1111
+
1112
+ def _is_valid_checkpoint_entry(self, offset: int, size: int) -> bool:
1113
+ if offset < self._data_base_offset:
1114
+ return False
1115
+ rel = offset - self._data_base_offset
1116
+ if rel % self.slot_bytes != 0:
1117
+ return False
1118
+ slot = rel // self.slot_bytes
1119
+ if slot < 0 or slot >= self._max_slots:
1120
+ return False
1121
+ return 0 < size <= (self.slot_bytes - self.header_bytes)
1122
+
1123
+ def _apply_loaded_state(self, data: dict[str, Any]) -> bool:
1124
+ if not isinstance(data, dict):
1125
+ return False
1126
+ if int(data.get("version", 0)) != 1:
1127
+ return False
1128
+ if data.get("device_path") and data.get("device_path") != self.device_path:
1129
+ logger.warning("Device metadata device_path mismatch; ignoring metadata")
1130
+ return False
1131
+ if int(data.get("slot_bytes", self.slot_bytes)) != self.slot_bytes:
1132
+ logger.warning("Device metadata slot_bytes mismatch; ignoring metadata")
1133
+ return False
1134
+ if (
1135
+ int(data.get("meta_total_bytes", self.meta_total_bytes))
1136
+ != self.meta_total_bytes
1137
+ ):
1138
+ logger.warning(
1139
+ "Device metadata meta_total_bytes mismatch; ignoring metadata"
1140
+ )
1141
+ return False
1142
+ if str(data.get("meta_magic", self.meta_magic_text)) != self.meta_magic_text:
1143
+ logger.warning("Device metadata meta_magic mismatch; ignoring metadata")
1144
+ return False
1145
+ if int(data.get("meta_version", self.meta_version)) != self.meta_version:
1146
+ logger.warning("Device metadata meta_version mismatch; ignoring metadata")
1147
+ return False
1148
+
1149
+ try:
1150
+ next_slot = int(data.get("next_slot", 0))
1151
+ except Exception:
1152
+ logger.warning("Device metadata next_slot is invalid; ignoring metadata")
1153
+ return False
1154
+ if next_slot < 0 or next_slot > self._max_slots:
1155
+ logger.warning(
1156
+ "Device metadata next_slot out of range (%d); ignoring metadata",
1157
+ next_slot,
1158
+ )
1159
+ return False
1160
+
1161
+ raw_free_slots = data.get("free_slots", [])
1162
+ if not isinstance(raw_free_slots, list):
1163
+ logger.warning("Device metadata free_slots is invalid; ignoring metadata")
1164
+ return False
1165
+ free_slots: list[int] = []
1166
+ seen_slots: set[int] = set()
1167
+ for raw_slot in raw_free_slots:
1168
+ try:
1169
+ slot = int(raw_slot)
1170
+ except Exception:
1171
+ logger.warning(
1172
+ "Device metadata free_slots contains non-integer; ignoring metadata"
1173
+ )
1174
+ return False
1175
+ if slot < 0 or slot >= self._max_slots:
1176
+ logger.warning(
1177
+ "Device metadata free_slots contains out-of-range slot %d; "
1178
+ "ignoring metadata",
1179
+ slot,
1180
+ )
1181
+ return False
1182
+ if slot in seen_slots:
1183
+ continue
1184
+ seen_slots.add(slot)
1185
+ free_slots.append(slot)
1186
+
1187
+ with self._lock:
1188
+ self._next_slot = next_slot
1189
+ self._free_slots = free_slots
1190
+ self._index.clear()
1191
+ self._lru.clear()
1192
+
1193
+ entries = data.get("entries", {})
1194
+ if isinstance(entries, dict):
1195
+ for k_str, entry in entries.items():
1196
+ if not isinstance(entry, dict):
1197
+ logger.warning(
1198
+ "Invalid entry in metadata for key '%s': not a dict.", k_str
1199
+ )
1200
+ continue
1201
+ try:
1202
+ key = CacheEngineKey.from_string(k_str)
1203
+ except Exception as e:
1204
+ logger.warning(
1205
+ "Failed to parse key string '%s' from metadata: %s",
1206
+ k_str,
1207
+ e,
1208
+ )
1209
+ continue
1210
+
1211
+ offset = int(entry.get("offset", 0))
1212
+ size = int(entry.get("size", 0))
1213
+ shape_list = entry.get("shape")
1214
+ fmt_name = entry.get("fmt")
1215
+ cached_positions_list = entry.get("cached_positions")
1216
+
1217
+ if not self._is_valid_checkpoint_entry(offset, size):
1218
+ logger.warning(
1219
+ "Skipping invalid checkpoint entry for key '%s': "
1220
+ "offset=%d size=%d",
1221
+ k_str,
1222
+ offset,
1223
+ size,
1224
+ )
1225
+ continue
1226
+
1227
+ shape = (
1228
+ torch.Size(list(shape_list)) if shape_list is not None else None
1229
+ )
1230
+ fmt = (
1231
+ MemoryFormat[fmt_name]
1232
+ if isinstance(fmt_name, str)
1233
+ and fmt_name in MemoryFormat.__members__
1234
+ else MemoryFormat.UNDEFINED
1235
+ )
1236
+ cached_positions = (
1237
+ torch.tensor(cached_positions_list, dtype=torch.long)
1238
+ if cached_positions_list is not None
1239
+ else None
1240
+ )
1241
+
1242
+ meta = DiskCacheMetadata(
1243
+ path=f"{self.device_path}@{offset}",
1244
+ size=size,
1245
+ shape=shape,
1246
+ dtype=key.dtype,
1247
+ cached_positions=cached_positions,
1248
+ fmt=fmt,
1249
+ pin_count=0,
1250
+ )
1251
+ self._index[key] = _Entry(offset=offset, size=size, meta=meta)
1252
+
1253
+ if self.metadata is not None and self._index:
1254
+ first_loaded_key = next(iter(self._index))
1255
+ expected_worker_id = int(self.metadata.worker_id)
1256
+ loaded_worker_id = int(first_loaded_key.worker_id)
1257
+ if loaded_worker_id != expected_worker_id:
1258
+ logger.warning(
1259
+ "RustRawBlockBackend: loaded metadata may belong to another "
1260
+ "worker (device=%s, current_worker_id=%d, "
1261
+ "first_entry_worker_id=%d, first_entry_key=%s)",
1262
+ self.device_path,
1263
+ expected_worker_id,
1264
+ loaded_worker_id,
1265
+ first_loaded_key.to_string(),
1266
+ )
1267
+
1268
+ # Remove free-slot entries that overlap with loaded index slots.
1269
+ used_slots = {
1270
+ self._offset_to_slot(int(entry.offset))
1271
+ for entry in self._index.values()
1272
+ }
1273
+ self._free_slots = [
1274
+ slot for slot in self._free_slots if slot not in used_slots
1275
+ ]
1276
+
1277
+ lru_keys = data.get("lru_keys", [])
1278
+ if isinstance(lru_keys, list) and lru_keys:
1279
+ for k_str in lru_keys:
1280
+ try:
1281
+ key = CacheEngineKey.from_string(k_str)
1282
+ except Exception:
1283
+ continue
1284
+ if key in self._index:
1285
+ self._lru[key] = None
1286
+ else:
1287
+ for key in self._index:
1288
+ self._lru[key] = None
1289
+
1290
+ # Loaded state should start as clean.
1291
+ self._meta_dirty_total = 0
1292
+ self._meta_persisted = 0
1293
+
1294
+ if self.meta_verify_on_load:
1295
+ self._validate_loaded_entries()
1296
+ return True
1297
+
1298
+ def _validate_loaded_entries(self) -> None:
1299
+ to_drop: list[CacheEngineKey] = []
1300
+ with self._lock:
1301
+ entries = list(self._index.items())
1302
+
1303
+ for key, entry in entries:
1304
+ slot_hdr = self._read_slot_header(int(entry.offset))
1305
+ if slot_hdr is None:
1306
+ to_drop.append(key)
1307
+ continue
1308
+ chunk_hash, payload_len = slot_hdr
1309
+ if int(chunk_hash) != (int(key.chunk_hash) & ((1 << 64) - 1)):
1310
+ to_drop.append(key)
1311
+ continue
1312
+ if int(payload_len) != int(entry.size):
1313
+ to_drop.append(key)
1314
+
1315
+ if not to_drop:
1316
+ return
1317
+
1318
+ with self._lock:
1319
+ for key in to_drop:
1320
+ removed_entry: Optional[_Entry] = None
1321
+ if key in self._index:
1322
+ removed_entry = self._index.pop(key)
1323
+ self._lru.pop(key, None)
1324
+ self._pinned.discard(key)
1325
+ if removed_entry is not None:
1326
+ self._append_free_slot_locked(
1327
+ self._offset_to_slot(int(removed_entry.offset))
1328
+ )
1329
+ self._meta_dirty_total += 1
1330
+
1331
+ logger.warning(
1332
+ "RustRawBlockBackend: dropped %d stale metadata entries after slot-header "
1333
+ "validation",
1334
+ len(to_drop),
1335
+ )
1336
+
1337
+ def _load_checkpoint_from_device(self) -> None:
1338
+ header, payload = self._select_latest_checkpoint()
1339
+ if header is None:
1340
+ logger.info(
1341
+ "RustRawBlockBackend: no valid on-device metadata checkpoint found"
1342
+ )
1343
+ return
1344
+ assert payload is not None
1345
+ try:
1346
+ data = json.loads(payload.decode("utf-8"))
1347
+ except Exception:
1348
+ logger.warning("RustRawBlockBackend: failed to decode metadata payload")
1349
+ return
1350
+ applied = self._apply_loaded_state(data)
1351
+ if not applied:
1352
+ logger.warning("RustRawBlockBackend: metadata payload rejected by checks")
1353
+ return
1354
+ self._meta_seq = int(header["seq"])
1355
+ logger.info(
1356
+ "RustRawBlockBackend: loaded on-device metadata checkpoint "
1357
+ "(entries=%d, next_slot=%d, seq=%d)",
1358
+ len(self._index),
1359
+ self._next_slot,
1360
+ self._meta_seq,
1361
+ )