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,747 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """
3
+ File-system based L2 adapter using aiofiles for async I/O.
4
+
5
+ Stores KV cache objects as raw tensor bytes on disk (no metadata
6
+ header). Each ObjectKey maps to a separate ``.data`` file whose
7
+ name encodes all key fields so it can be reversed on startup.
8
+ """
9
+
10
+ # Future
11
+ from __future__ import annotations
12
+
13
+ # Standard
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING, Optional, Union
16
+ import asyncio
17
+ import os
18
+ import threading
19
+
20
+ if TYPE_CHECKING:
21
+ from lmcache.v1.distributed.internal_api import (
22
+ L1MemoryDesc,
23
+ )
24
+
25
+ # Third Party
26
+ import aiofiles
27
+ import aiofiles.os
28
+
29
+ # First Party
30
+ from lmcache.logging import init_logger
31
+ from lmcache.native_storage_ops import Bitmap
32
+ from lmcache.v1.distributed.api import ObjectKey
33
+ from lmcache.v1.distributed.l2_adapters.base import (
34
+ L2AdapterInterface,
35
+ L2TaskId,
36
+ )
37
+ from lmcache.v1.distributed.l2_adapters.config import (
38
+ L2AdapterConfigBase,
39
+ register_l2_adapter_type,
40
+ )
41
+ from lmcache.v1.distributed.l2_adapters.factory import (
42
+ register_l2_adapter_factory,
43
+ )
44
+ from lmcache.v1.memory_management import MemoryObj
45
+
46
+ logger = init_logger(__name__)
47
+
48
+ _KEY_SEP = "@"
49
+ # ``@`` in both ``model_name`` and ``cache_salt`` is rejected by
50
+ # ObjectKey.__post_init__, so splitting on ``@`` is unambiguous.
51
+ # Kept in sync with native_connector_l2_adapter.py and
52
+ # csrc/storage_backends/fs/connector.cpp.
53
+ _PATH_SLASH_REPLACEMENT = "-SEP-"
54
+ _FILE_EXT = ".data"
55
+
56
+
57
+ def _readinto_full(
58
+ f, # typing: IO[bytes]
59
+ buf: Union[bytearray, memoryview, bytes],
60
+ ) -> int:
61
+ """Loop readinto() until *buf* is full or EOF.
62
+
63
+ A single ``readinto()`` may return fewer bytes than
64
+ *len(buf)* even when more data is available. This
65
+ helper keeps reading until the buffer is completely
66
+ filled or the file reaches EOF.
67
+
68
+ Returns:
69
+ Total number of bytes read.
70
+ """
71
+ mv = memoryview(buf) if not isinstance(buf, memoryview) else buf
72
+ total = 0
73
+ while total < len(mv):
74
+ n = f.readinto(mv[total:])
75
+ if n is None or n == 0:
76
+ break
77
+ total += n
78
+ return total
79
+
80
+
81
+ async def _async_readinto_full(
82
+ f, # aiofiles async file handle
83
+ buf: Union[bytearray, memoryview, bytes],
84
+ ) -> int:
85
+ """Async version of :func:`_readinto_full`."""
86
+ mv = memoryview(buf) if not isinstance(buf, memoryview) else buf
87
+ total = 0
88
+ while total < len(mv):
89
+ n = await f.readinto(mv[total:])
90
+ if n is None or n == 0:
91
+ break
92
+ total += n
93
+ return total
94
+
95
+
96
+ def _object_key_to_filename(key: ObjectKey) -> str:
97
+ """Build a reversible, filesystem-safe filename.
98
+
99
+ Unsalted::
100
+
101
+ <safe_model>@0x<kv_rank_hex>@<chunk_hash_hex>.data
102
+
103
+ Salted (trailing ``cache_salt``)::
104
+
105
+ <safe_model>@0x<kv_rank_hex>@<chunk_hash_hex>@<cache_salt>.data
106
+
107
+ The 3-field unsalted shape is bit-identical to the pre-cache_salt
108
+ format, so existing un-salted cache directories remain valid and
109
+ no migration is needed.
110
+
111
+ ``kv_rank`` is written in ``0x`` prefixed hex so each byte
112
+ of the bitmap ``(ws<<24)|(rank<<16)|(local_ws<<8)|local``
113
+ is directly readable.
114
+ """
115
+ safe_model = key.model_name.replace("/", _PATH_SLASH_REPLACEMENT)
116
+ base = f"{safe_model}{_KEY_SEP}{key.kv_rank:#010x}{_KEY_SEP}{key.chunk_hash.hex()}"
117
+ if key.cache_salt:
118
+ return f"{base}{_KEY_SEP}{key.cache_salt}{_FILE_EXT}"
119
+ return f"{base}{_FILE_EXT}"
120
+
121
+
122
+ def _filename_to_object_key(
123
+ filename: str,
124
+ ) -> Optional[ObjectKey]:
125
+ """Reverse ``_object_key_to_filename``.
126
+
127
+ Accepts both the 3-field unsalted shape and the 4-field salted
128
+ shape (trailing ``cache_salt``). Returns ``None`` for anything
129
+ else. Since ``model_name`` is guaranteed not to contain ``@``,
130
+ plain ``split`` suffices — no marker, no rsplit.
131
+ """
132
+ if not filename.endswith(_FILE_EXT):
133
+ return None
134
+ stem = filename[: -len(_FILE_EXT)]
135
+ parts = stem.split(_KEY_SEP)
136
+ if len(parts) == 3:
137
+ safe_model, kv_rank_str, chunk_hash_hex = parts
138
+ cache_salt = ""
139
+ elif len(parts) == 4:
140
+ safe_model, kv_rank_str, chunk_hash_hex, cache_salt = parts
141
+ else:
142
+ return None
143
+
144
+ model_name = safe_model.replace(_PATH_SLASH_REPLACEMENT, "/")
145
+ try:
146
+ chunk_hash = bytes.fromhex(chunk_hash_hex)
147
+ kv_rank = int(kv_rank_str, 16)
148
+ # ObjectKey.__post_init__ raises ValueError when the decoded
149
+ # model_name / cache_salt violate the forbidden-char or length
150
+ # invariants (e.g. a stray file from another tool on disk).
151
+ # The contract here is to return None for anything unparsable,
152
+ # so keep the constructor inside the try block.
153
+ return ObjectKey(
154
+ chunk_hash=chunk_hash,
155
+ model_name=model_name,
156
+ kv_rank=kv_rank,
157
+ cache_salt=cache_salt,
158
+ )
159
+ except ValueError:
160
+ return None
161
+
162
+
163
+ class FSL2AdapterConfig(L2AdapterConfigBase):
164
+ """
165
+ Config for the filesystem-backed L2 adapter.
166
+
167
+ Fields:
168
+ - base_path: directory for storing KV cache files.
169
+ - relative_tmp_dir: optional relative sub-dir for
170
+ temp files (same as fs_connector_relative_tmp_dir).
171
+ """
172
+
173
+ def __init__(
174
+ self,
175
+ base_path: str,
176
+ relative_tmp_dir: Optional[str] = None,
177
+ read_ahead_size: Optional[int] = None,
178
+ use_odirect: bool = False,
179
+ ):
180
+ """Initialize FSL2AdapterConfig.
181
+
182
+ Args:
183
+ base_path: Directory for storing KV cache files.
184
+ relative_tmp_dir: Relative sub-dir under
185
+ base_path for temp files during writes.
186
+ read_ahead_size: If set, trigger filesystem
187
+ readahead by issuing a small initial read
188
+ of this many bytes before reading the rest.
189
+ use_odirect: If True, bypass the OS page cache
190
+ using O_DIRECT for both reads and writes.
191
+ Requires buffer sizes aligned to the
192
+ filesystem block size.
193
+ """
194
+ self.base_path = base_path
195
+ self.relative_tmp_dir = relative_tmp_dir
196
+ self.read_ahead_size = read_ahead_size
197
+ self.use_odirect = use_odirect
198
+
199
+ @classmethod
200
+ def from_dict(cls, d: dict) -> "FSL2AdapterConfig":
201
+ base_path = d.get("base_path")
202
+ if not isinstance(base_path, str) or not base_path:
203
+ raise ValueError("base_path must be a non-empty string")
204
+ relative_tmp_dir = d.get("relative_tmp_dir", None)
205
+ if relative_tmp_dir is not None:
206
+ if not isinstance(relative_tmp_dir, str):
207
+ raise ValueError("relative_tmp_dir must be a string")
208
+ read_ahead_size = d.get("read_ahead_size", None)
209
+ if read_ahead_size is not None:
210
+ if not isinstance(read_ahead_size, int) or read_ahead_size <= 0:
211
+ raise ValueError("read_ahead_size must be a positive integer")
212
+ use_odirect = d.get("use_odirect", False)
213
+ if not isinstance(use_odirect, bool):
214
+ raise ValueError("use_odirect must be a boolean")
215
+ return cls(
216
+ base_path=base_path,
217
+ relative_tmp_dir=relative_tmp_dir,
218
+ read_ahead_size=read_ahead_size,
219
+ use_odirect=use_odirect,
220
+ )
221
+
222
+ @classmethod
223
+ def help(cls) -> str:
224
+ return (
225
+ "FS L2 adapter config fields:\n"
226
+ "- base_path (str): directory for KV cache "
227
+ "files (required)\n"
228
+ "- relative_tmp_dir (str): relative sub-dir "
229
+ "for temp files (optional, same as "
230
+ "fs_connector_relative_tmp_dir)\n"
231
+ "- read_ahead_size (int): trigger fs "
232
+ "readahead by reading this many bytes first "
233
+ "(optional)\n"
234
+ "- use_odirect (bool): bypass page cache "
235
+ "via O_DIRECT (optional, default false)"
236
+ )
237
+
238
+
239
+ class FSL2Adapter(L2AdapterInterface):
240
+ """
241
+ File-system backed L2 adapter with async I/O via *aiofiles*.
242
+
243
+ Each file stores **only** the raw tensor bytes (no metadata
244
+ header), which gives maximum I/O throughput. The file name
245
+ itself encodes the full ``ObjectKey`` so it is reversible.
246
+
247
+ Thread safety is ensured via a lock for shared bookkeeping
248
+ and an asyncio event loop running on a dedicated daemon
249
+ thread.
250
+ """
251
+
252
+ def __init__(self, config: FSL2AdapterConfig):
253
+ super().__init__()
254
+ self._config = config
255
+ base = config.base_path
256
+ self._base_path = Path(base)
257
+ self._base_path.mkdir(parents=True, exist_ok=True)
258
+
259
+ # Temp-file strategy aligned with FSConnector:
260
+ # if relative_tmp_dir is set, write to a sub-dir;
261
+ # otherwise fall back to a .tmp suffix.
262
+ self._relative_tmp_dir: Optional[Path] = None
263
+ if config.relative_tmp_dir is not None:
264
+ self._relative_tmp_dir = Path(config.relative_tmp_dir)
265
+ if (
266
+ self._relative_tmp_dir.is_absolute()
267
+ or ".." in self._relative_tmp_dir.parts
268
+ ):
269
+ raise ValueError("Invalid relative_tmp_dir: " + config.relative_tmp_dir)
270
+ (self._base_path / self._relative_tmp_dir).mkdir(
271
+ parents=False, exist_ok=True
272
+ )
273
+
274
+ # I/O tuning options aligned with FSConnector
275
+ self._read_ahead_size = config.read_ahead_size
276
+ self._use_odirect = config.use_odirect
277
+ self._os_disk_bs = 0
278
+ if self._use_odirect:
279
+ stat = os.statvfs(self._base_path)
280
+ self._os_disk_bs = stat.f_bsize
281
+
282
+ self._store_efd = os.eventfd(0, os.EFD_NONBLOCK | os.EFD_CLOEXEC)
283
+ self._lookup_efd = os.eventfd(0, os.EFD_NONBLOCK | os.EFD_CLOEXEC)
284
+ self._load_efd = os.eventfd(0, os.EFD_NONBLOCK | os.EFD_CLOEXEC)
285
+
286
+ # Task bookkeeping
287
+ self._next_task_id: L2TaskId = 0
288
+ self._completed_store_tasks: dict[L2TaskId, bool] = {}
289
+ self._completed_lookup_tasks: dict[L2TaskId, Bitmap] = {}
290
+ self._completed_load_tasks: dict[L2TaskId, Bitmap] = {}
291
+ self._lock = threading.Lock()
292
+
293
+ # Background asyncio event loop
294
+ self._loop = asyncio.new_event_loop()
295
+ self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
296
+ self._loop_thread.start()
297
+
298
+ logger.info(
299
+ "Initialized FSL2Adapter with base_path=%s, "
300
+ "relative_tmp_dir=%s, "
301
+ "read_ahead_size=%s, use_odirect=%s",
302
+ self._base_path,
303
+ self._relative_tmp_dir,
304
+ self._read_ahead_size,
305
+ self._use_odirect,
306
+ )
307
+
308
+ # ------------------------------------------------------------------
309
+ # Event Fd Interface
310
+ # ------------------------------------------------------------------
311
+
312
+ def get_store_event_fd(self) -> int:
313
+ return self._store_efd
314
+
315
+ def get_lookup_and_lock_event_fd(self) -> int:
316
+ return self._lookup_efd
317
+
318
+ def get_load_event_fd(self) -> int:
319
+ return self._load_efd
320
+
321
+ # ------------------------------------------------------------------
322
+ # Store Interface
323
+ # ------------------------------------------------------------------
324
+
325
+ def submit_store_task(
326
+ self,
327
+ keys: list[ObjectKey],
328
+ objects: list[MemoryObj],
329
+ ) -> L2TaskId:
330
+ with self._lock:
331
+ task_id = self._get_next_task_id()
332
+
333
+ asyncio.run_coroutine_threadsafe(
334
+ self._execute_store(keys, objects, task_id),
335
+ self._loop,
336
+ )
337
+ return task_id
338
+
339
+ def pop_completed_store_tasks(
340
+ self,
341
+ ) -> dict[L2TaskId, bool]:
342
+ with self._lock:
343
+ completed = self._completed_store_tasks
344
+ self._completed_store_tasks = {}
345
+ return completed
346
+
347
+ # ------------------------------------------------------------------
348
+ # Lookup and Lock Interface
349
+ # ------------------------------------------------------------------
350
+
351
+ def submit_lookup_and_lock_task(self, keys: list[ObjectKey]) -> L2TaskId:
352
+ with self._lock:
353
+ task_id = self._get_next_task_id()
354
+
355
+ asyncio.run_coroutine_threadsafe(
356
+ self._execute_lookup(keys, task_id),
357
+ self._loop,
358
+ )
359
+ return task_id
360
+
361
+ def query_lookup_and_lock_result(self, task_id: L2TaskId) -> Bitmap | None:
362
+ with self._lock:
363
+ return self._completed_lookup_tasks.pop(task_id, None)
364
+
365
+ def submit_unlock(self, keys: list[ObjectKey]) -> None:
366
+ # No-op: FS adapter has no eviction, so locking
367
+ # between lookup and load is unnecessary.
368
+ pass
369
+
370
+ # ------------------------------------------------------------------
371
+ # Load Interface
372
+ # ------------------------------------------------------------------
373
+
374
+ def submit_load_task(
375
+ self,
376
+ keys: list[ObjectKey],
377
+ objects: list[MemoryObj],
378
+ ) -> L2TaskId:
379
+ with self._lock:
380
+ task_id = self._get_next_task_id()
381
+
382
+ asyncio.run_coroutine_threadsafe(
383
+ self._execute_load(keys, objects, task_id),
384
+ self._loop,
385
+ )
386
+ return task_id
387
+
388
+ def query_load_result(self, task_id: L2TaskId) -> Bitmap | None:
389
+ with self._lock:
390
+ return self._completed_load_tasks.pop(task_id, None)
391
+
392
+ # ------------------------------------------------------------------
393
+ # Status Interface
394
+ # ------------------------------------------------------------------
395
+
396
+ def report_status(self) -> dict:
397
+ """Return a status dict for the FS L2 adapter."""
398
+ return {
399
+ "is_healthy": self._loop_thread.is_alive(),
400
+ "type": "FSL2Adapter",
401
+ "base_path": str(self._base_path),
402
+ "use_odirect": self._use_odirect,
403
+ "event_loop_alive": self._loop_thread.is_alive(),
404
+ }
405
+
406
+ # ------------------------------------------------------------------
407
+ # Eviction Interface
408
+ # ------------------------------------------------------------------
409
+
410
+ def delete(self, keys: list[ObjectKey]) -> None:
411
+ # Not implemented for the filesystem adapter.
412
+ pass
413
+
414
+ def get_usage(self) -> tuple[float, float]:
415
+ # Not implemented for the filesystem adapter.
416
+ return (-1.0, -1.0)
417
+
418
+ # ------------------------------------------------------------------
419
+ # Cleanup
420
+ # ------------------------------------------------------------------
421
+
422
+ def close(self) -> None:
423
+ async def _stop_tasks():
424
+ tasks = [
425
+ t
426
+ for t in asyncio.all_tasks(self._loop)
427
+ if t is not asyncio.current_task()
428
+ ]
429
+ for task in tasks:
430
+ task.cancel()
431
+ if tasks:
432
+ await asyncio.gather(*tasks, return_exceptions=True)
433
+
434
+ if self._loop.is_running():
435
+ fut = asyncio.run_coroutine_threadsafe(_stop_tasks(), self._loop)
436
+ try:
437
+ fut.result(timeout=5)
438
+ except Exception:
439
+ pass
440
+ self._loop.call_soon_threadsafe(self._loop.stop)
441
+
442
+ self._loop_thread.join()
443
+ self._loop.close()
444
+
445
+ os.close(self._store_efd)
446
+ os.close(self._lookup_efd)
447
+ os.close(self._load_efd)
448
+ logger.info("FSL2Adapter closed")
449
+
450
+ # ------------------------------------------------------------------
451
+ # Internal helpers
452
+ # ------------------------------------------------------------------
453
+
454
+ def _run_event_loop(self) -> None:
455
+ asyncio.set_event_loop(self._loop)
456
+ self._loop.run_forever()
457
+
458
+ def _get_next_task_id(self) -> L2TaskId:
459
+ tid = self._next_task_id
460
+ self._next_task_id += 1
461
+ return tid
462
+
463
+ def _key_to_path(self, key: ObjectKey) -> Path:
464
+ return self._base_path / _object_key_to_filename(key)
465
+
466
+ async def _key_exists_on_disk(
467
+ self,
468
+ key: ObjectKey,
469
+ ) -> bool:
470
+ """Check whether the file for *key* exists on disk.
471
+
472
+ Uses ``aiofiles.os.path.exists`` so the check is
473
+ non-blocking and always reflects the real FS state,
474
+ which is critical for multi-node shared-FS setups.
475
+ """
476
+ path = self._key_to_path(key)
477
+ return await aiofiles.os.path.exists(path)
478
+
479
+ def _key_to_file_and_tmp_path(self, key: ObjectKey) -> tuple[Path, Path]:
480
+ """Return ``(final_path, tmp_path)``.
481
+
482
+ When ``relative_tmp_dir`` is configured, the temp file
483
+ is placed under that sub-directory (same behaviour as
484
+ ``FSConnector._get_file_and_tmp_path``). Otherwise a
485
+ ``.tmp`` suffix is used.
486
+ """
487
+ fname = _object_key_to_filename(key)
488
+ final = self._base_path / fname
489
+ if self._relative_tmp_dir is not None:
490
+ tmp = self._base_path / self._relative_tmp_dir / fname
491
+ else:
492
+ tmp = final.with_suffix(".tmp")
493
+ return final, tmp
494
+
495
+ # ---- O_DIRECT helpers -----------------------------------------------
496
+
497
+ def _read_with_odirect(
498
+ self,
499
+ file_path: Path,
500
+ dst_buf: Union[bytearray, memoryview, bytes],
501
+ ) -> int:
502
+ """Synchronous O_DIRECT read into *dst_buf*.
503
+
504
+ Returns the number of bytes actually read.
505
+ Runs in an executor (not on the event loop).
506
+ """
507
+ fd = -1
508
+ size = len(dst_buf)
509
+ try:
510
+ aligned = self._os_disk_bs > 0 and size % self._os_disk_bs == 0
511
+ if not aligned:
512
+ logger.warning(
513
+ "Cannot use O_DIRECT for %s, size is not aligned.",
514
+ file_path,
515
+ )
516
+ with open(file_path, "rb") as f:
517
+ return _readinto_full(f, dst_buf)
518
+
519
+ fd = os.open(
520
+ str(file_path),
521
+ os.O_RDONLY | getattr(os, "O_DIRECT", 0),
522
+ )
523
+ with os.fdopen(fd, "rb", buffering=0) as fdo:
524
+ fd = -1 # now managed by fdopen
525
+ return _readinto_full(fdo, dst_buf)
526
+ except Exception:
527
+ logger.exception("Failed to O_DIRECT read %s", file_path)
528
+ return 0
529
+ finally:
530
+ if fd >= 0:
531
+ try:
532
+ os.close(fd)
533
+ except OSError:
534
+ pass
535
+
536
+ def _write_with_odirect(self, file_path: Path, buf: bytes) -> None:
537
+ """Synchronous O_DIRECT write of *buf*.
538
+
539
+ Runs in an executor (not on the event loop).
540
+ """
541
+ fd = -1
542
+ try:
543
+ fd = os.open(
544
+ str(file_path),
545
+ os.O_CREAT | os.O_WRONLY | getattr(os, "O_DIRECT", 0),
546
+ 0o644,
547
+ )
548
+ os.write(fd, buf)
549
+ except Exception:
550
+ logger.exception("Failed to O_DIRECT write %s", file_path)
551
+ raise
552
+ finally:
553
+ if fd >= 0:
554
+ try:
555
+ os.close(fd)
556
+ except OSError:
557
+ pass
558
+
559
+ # ---- store ----------------------------------------------------------
560
+
561
+ async def _execute_store(
562
+ self,
563
+ keys: list[ObjectKey],
564
+ objects: list[MemoryObj],
565
+ task_id: L2TaskId,
566
+ ) -> None:
567
+ success = True
568
+ try:
569
+ for key, obj in zip(keys, objects, strict=True):
570
+ file_path, tmp_path = self._key_to_file_and_tmp_path(key)
571
+
572
+ # Skip if already stored on disk
573
+ if await aiofiles.os.path.exists(file_path):
574
+ continue
575
+ buf = obj.byte_array
576
+ size = len(buf)
577
+
578
+ try:
579
+ # Decide whether O_DIRECT is usable
580
+ do_odirect = self._use_odirect
581
+ if do_odirect:
582
+ aligned = self._os_disk_bs > 0 and size % self._os_disk_bs == 0
583
+ if not aligned:
584
+ logger.warning(
585
+ "Cannot use O_DIRECT for "
586
+ "writing size %d, not "
587
+ "aligned to block size "
588
+ "%d.",
589
+ size,
590
+ self._os_disk_bs,
591
+ )
592
+ do_odirect = False
593
+
594
+ if do_odirect:
595
+ await self._loop.run_in_executor(
596
+ None,
597
+ self._write_with_odirect,
598
+ tmp_path,
599
+ buf,
600
+ )
601
+ else:
602
+ async with aiofiles.open(tmp_path, "wb") as f:
603
+ await f.write(buf)
604
+
605
+ await aiofiles.os.replace(tmp_path, file_path)
606
+ logger.debug(
607
+ "FSL2Adapter stored key %s (%d bytes)",
608
+ file_path.name,
609
+ size,
610
+ )
611
+ except Exception:
612
+ logger.exception(
613
+ "FSL2Adapter failed to store %s",
614
+ file_path,
615
+ )
616
+ if await aiofiles.os.path.exists(tmp_path):
617
+ await aiofiles.os.unlink(tmp_path)
618
+ success = False
619
+ except Exception:
620
+ logger.exception(
621
+ "FSL2Adapter store task %s failed",
622
+ task_id,
623
+ )
624
+ success = False
625
+
626
+ with self._lock:
627
+ self._completed_store_tasks[task_id] = success
628
+ os.eventfd_write(self._store_efd, 1)
629
+
630
+ # ---- lookup ---------------------------------------------------------
631
+
632
+ async def _execute_lookup(
633
+ self,
634
+ keys: list[ObjectKey],
635
+ task_id: L2TaskId,
636
+ ) -> None:
637
+ bitmap = Bitmap(len(keys))
638
+ for i, key in enumerate(keys):
639
+ if not await self._key_exists_on_disk(key):
640
+ continue
641
+ bitmap.set(i)
642
+
643
+ with self._lock:
644
+ self._completed_lookup_tasks[task_id] = bitmap
645
+ os.eventfd_write(self._lookup_efd, 1)
646
+
647
+ # ---- load -----------------------------------------------------------
648
+
649
+ async def _execute_load(
650
+ self,
651
+ keys: list[ObjectKey],
652
+ objects: list[MemoryObj],
653
+ task_id: L2TaskId,
654
+ ) -> None:
655
+ bitmap = Bitmap(len(keys))
656
+ for i, key in enumerate(keys):
657
+ file_path = self._key_to_path(key)
658
+ try:
659
+ dst_buf = objects[i].byte_array
660
+ expected = len(dst_buf)
661
+ num_read: Optional[int] = None
662
+
663
+ # O_DIRECT path (sync, via executor)
664
+ if self._use_odirect:
665
+ num_read = await self._loop.run_in_executor(
666
+ None,
667
+ self._read_with_odirect,
668
+ file_path,
669
+ dst_buf,
670
+ )
671
+ if num_read != expected:
672
+ logger.warning(
673
+ "Incomplete O_DIRECT read for %s: expected %d, got %d",
674
+ file_path.name,
675
+ expected,
676
+ num_read or 0,
677
+ )
678
+ else:
679
+ bitmap.set(i)
680
+ logger.debug(
681
+ "FSL2Adapter loaded key %s (%d bytes, O_DIRECT)",
682
+ file_path.name,
683
+ num_read,
684
+ )
685
+ continue
686
+
687
+ # Standard async path with optional
688
+ # read-ahead
689
+ expected = len(dst_buf)
690
+ async with aiofiles.open(file_path, "rb") as f:
691
+ if self._read_ahead_size is None:
692
+ num_read = await _async_readinto_full(f, dst_buf)
693
+ else:
694
+ if not isinstance(dst_buf, memoryview):
695
+ dst_buf = memoryview(dst_buf)
696
+ # Trigger readahead with a
697
+ # small initial read
698
+ ra = self._read_ahead_size
699
+ n_head = await _async_readinto_full(f, dst_buf[:ra])
700
+ if n_head == ra:
701
+ n_tail = await _async_readinto_full(f, dst_buf[ra:])
702
+ num_read = n_head + n_tail
703
+ else:
704
+ num_read = n_head
705
+
706
+ if num_read != expected:
707
+ logger.warning(
708
+ "Incomplete read for %s: expected %d, got %d",
709
+ file_path.name,
710
+ expected,
711
+ num_read,
712
+ )
713
+ continue
714
+
715
+ bitmap.set(i)
716
+ logger.debug(
717
+ "FSL2Adapter loaded key %s (%d bytes)",
718
+ file_path.name,
719
+ num_read,
720
+ )
721
+ except FileNotFoundError:
722
+ continue
723
+ except Exception:
724
+ logger.exception(
725
+ "FSL2Adapter failed to load %s",
726
+ file_path,
727
+ )
728
+ continue
729
+
730
+ with self._lock:
731
+ self._completed_load_tasks[task_id] = bitmap
732
+ os.eventfd_write(self._load_efd, 1)
733
+
734
+
735
+ # Self-register config type and adapter factory
736
+ register_l2_adapter_type("fs", FSL2AdapterConfig)
737
+
738
+
739
+ def _create_fs_adapter(
740
+ config: L2AdapterConfigBase,
741
+ l1_memory_desc: "Optional[L1MemoryDesc]" = None,
742
+ ) -> L2AdapterInterface:
743
+ """Create an FSL2Adapter from config."""
744
+ return FSL2Adapter(config) # type: ignore[arg-type]
745
+
746
+
747
+ register_l2_adapter_factory("fs", _create_fs_adapter)