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,1199 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Standard
3
+ from collections import OrderedDict
4
+ from concurrent.futures import Future, ThreadPoolExecutor
5
+ from typing import Any, Callable, List, Optional, Sequence, Tuple, Union
6
+ import asyncio
7
+ import ctypes
8
+ import json
9
+ import mmap
10
+ import os
11
+ import struct
12
+ import threading
13
+ import time
14
+ import urllib.parse
15
+ import uuid
16
+
17
+ # Third Party
18
+ import aiofile
19
+ import numpy as np
20
+ import torch
21
+
22
+ # First Party
23
+ from lmcache.logging import init_logger
24
+ from lmcache.utils import CacheEngineKey, DiskCacheMetadata, _lmcache_nvtx_annotate
25
+ from lmcache.v1.config import LMCacheEngineConfig
26
+ from lmcache.v1.memory_management import (
27
+ CuFileMemoryAllocator,
28
+ HipFileMemoryAllocator,
29
+ MemoryFormat,
30
+ MemoryObj,
31
+ )
32
+ from lmcache.v1.metadata import LMCacheMetadata
33
+ from lmcache.v1.storage_backend.abstract_backend import AllocatorBackendInterface
34
+ from lmcache.v1.storage_backend.path_sharder import PathSharder
35
+
36
+ logger = init_logger(__name__)
37
+
38
+ _METADATA_FILE_SUFFIX = ".metadata"
39
+ _DATA_FILE_SUFFIX = ".kvcache.safetensors"
40
+ _WEKA_DATA_FILE_SUFFIX = ".weka1"
41
+ _METADATA_VERSION = 1
42
+ _METADATA_MAX_SIZE = 4096 # reserve 4K for metadata.
43
+ # TODO: It is possible to read this 4KB block without triggering read-ahead by
44
+ # various means.
45
+ _DEFAULT_THREAD_COUNT = 4
46
+
47
+
48
+ class UnsupportedMetadataVersion(Exception):
49
+ pass
50
+
51
+
52
+ torch_dtypes = {
53
+ torch.half: "F16",
54
+ torch.bfloat16: "BF16",
55
+ torch.float32: "F32",
56
+ torch.float64: "F64",
57
+ torch.uint8: "U8",
58
+ torch.uint16: "U16",
59
+ torch.uint32: "U32",
60
+ torch.uint64: "U64",
61
+ torch.int8: "I8",
62
+ torch.int16: "I16",
63
+ torch.int32: "I32",
64
+ torch.int64: "I64",
65
+ torch.float8_e4m3fn: "F8E4M3FN",
66
+ torch.float8_e5m2: "F8E5M2",
67
+ }
68
+
69
+ torch_dtypes_inverse = dict([(v, k) for k, v in torch_dtypes.items()])
70
+
71
+
72
+ def get_fstype(path):
73
+ with open("/proc/mounts", "r") as f:
74
+ lines = f.readlines()
75
+
76
+ # Find the best matching mount point
77
+ best_match = ""
78
+ best_fstype = ""
79
+ for line in lines:
80
+ parts = line.split()
81
+ if len(parts) >= 3:
82
+ _, mount_point, fstype = parts[0], parts[1], parts[2]
83
+ if path.startswith(mount_point) and len(mount_point) > len(best_match):
84
+ best_match = mount_point
85
+ best_fstype = fstype
86
+
87
+ if not best_fstype:
88
+ raise RuntimeError(f"Unable to detect fstype for {path}")
89
+
90
+ return best_fstype
91
+
92
+
93
+ def pack_metadata(tensor, fmt: MemoryFormat, **extra_metadata) -> bytes:
94
+ if tensor.dtype not in torch_dtypes:
95
+ raise RuntimeError(f"unhandled dtype {tensor.dtype}")
96
+
97
+ # Metadata
98
+ data_size = tensor.numel() * tensor.element_size()
99
+ tensor_meta = {
100
+ "dtype": torch_dtypes[tensor.dtype],
101
+ "shape": list(tensor.size()),
102
+ "data_offsets": [0, data_size],
103
+ "fmt": fmt.value,
104
+ "__metadata__": extra_metadata,
105
+ }
106
+ meta = {"kvcache": tensor_meta}
107
+ str_meta = json.dumps(meta).encode("utf-8")
108
+ meta_len = len(str_meta)
109
+ assert meta_len <= _METADATA_MAX_SIZE - 8
110
+
111
+ # Align to _METADATA_MAX_SIZE - 8
112
+ str_meta += b" " * (_METADATA_MAX_SIZE - 8 - meta_len)
113
+
114
+ # Pack it all up so it is sized _METADATA_MAX_SIZE exactly.
115
+ return struct.pack("<Q", len(str_meta)) + str_meta
116
+
117
+
118
+ def unpack_metadata(buffer: bytes):
119
+ meta_len = struct.unpack("<Q", buffer[:8])[0]
120
+
121
+ str_meta = buffer[8 : 8 + meta_len]
122
+ json_meta = str_meta.rstrip(b" ")
123
+
124
+ meta = json.loads(json_meta.decode("utf-8"))
125
+ tensor_meta = meta["kvcache"]
126
+
127
+ shape = tensor_meta["shape"]
128
+ dtype_str = tensor_meta["dtype"]
129
+ data_offsets = tensor_meta["data_offsets"]
130
+ fmt = MemoryFormat(tensor_meta["fmt"])
131
+
132
+ nbytes = data_offsets[1] - data_offsets[0]
133
+ dtype = torch_dtypes_inverse[dtype_str]
134
+
135
+ return torch.Size(shape), dtype, nbytes, fmt, tensor_meta["__metadata__"]
136
+
137
+
138
+ def rand_suffix(n: int):
139
+ # Generates a random UUID hex string (e.g. "a8098c1a")
140
+ return uuid.uuid4().hex[:n]
141
+
142
+
143
+ async def save_metadata(path: str, tmp: str, metadata: bytes):
144
+ tmp_path = path + tmp
145
+ async with aiofile.async_open(tmp_path, "wb") as f:
146
+ await f.write(metadata)
147
+ os.rename(tmp_path, path)
148
+
149
+
150
+ def get_extra_config_bool(key, config: LMCacheEngineConfig) -> bool | None:
151
+ """Extract a boolean value from the config's extra_config dict.
152
+
153
+ Args:
154
+ key: The key to look up in extra_config.
155
+ config: The LMCacheEngineConfig instance.
156
+
157
+ Returns:
158
+ The boolean value if present, or None if not set.
159
+
160
+ Raises:
161
+ RuntimeError: If the value is not a valid boolean representation.
162
+ """
163
+ value = config.extra_config.get(key, None)
164
+ if value is None:
165
+ return None
166
+
167
+ if isinstance(value, str):
168
+ bool_value = value.lower() == "true"
169
+ elif value in [False, True]:
170
+ bool_value = value
171
+ else:
172
+ raise RuntimeError(f"Invalid value `{value}` for `{key}` in extra_config")
173
+
174
+ logger.info(f"Getting {key} = {bool_value} from extra_config")
175
+ return bool_value
176
+
177
+
178
+ class GdsBackend(AllocatorBackendInterface):
179
+ """
180
+ Originally based on the open sourced WekaGdsBackend, this is a backend that
181
+ leverages GPU Direct Storage APIs to issue GDS requests directly to the
182
+ GDS-supported remote filesystem. In order to use it, users need to specify
183
+ `gds_path` and `gds_buffer_size` in their LMCache config.
184
+
185
+ The GDS library to use is controlled by the `gds_backend` config field
186
+ (default: ``"cufile"``). Setting ``use_gds=False`` disables the GDS API
187
+ and falls back to POSIX I/O via cudart.
188
+
189
+ Cache Directory Structure created by this Backend:
190
+ /{gds_path}/{first_level}/{second_level}/{data & metadata} This structure
191
+ is semi-arbitrary. We create two levels in the directory hierarchy to
192
+ parallelize loading the data during initialization in the Python code.
193
+
194
+ NOTE: If GPUDirect is not supported on that other filesystem, then the GDS
195
+ library will fall back to POSIX I/O.
196
+ """
197
+
198
+ def __init__(
199
+ self,
200
+ config: LMCacheEngineConfig,
201
+ metadata: LMCacheMetadata,
202
+ loop: asyncio.AbstractEventLoop,
203
+ dst_device: str = "cuda",
204
+ ):
205
+ assert dst_device.startswith("cuda")
206
+ super().__init__(dst_device=dst_device)
207
+
208
+ self.config = config
209
+ self.loop = loop
210
+ self.dst_device = dst_device
211
+
212
+ assert config.gds_path is not None, "Need to specify gds_path for GdsBackend"
213
+
214
+ sharder = PathSharder(
215
+ raw_csv=config.gds_path,
216
+ strategy=config.gds_path_sharding,
217
+ dst_device=dst_device,
218
+ )
219
+ self.gds_paths = sharder.all_paths
220
+ self.gds_path = sharder.selected
221
+ self.fstype = get_fstype(self.gds_path)
222
+
223
+ # Log the fstype - this is useful in reports and varying optimizations
224
+ # based on the kind of fstype used.
225
+ logger.info(
226
+ f"GDS backend using fstype '{self.fstype}' on path '{self.gds_path}'"
227
+ f" ({len(self.gds_paths)} path(s) configured)"
228
+ )
229
+
230
+ self.use_gds = config.use_gds
231
+ self.gds_backend = config.gds_backend
232
+ # _user_set_keys is populated by LMCacheEngineConfig when a field is
233
+ # explicitly provided via a config file, environment variable, or
234
+ # keyword argument — as opposed to falling back to its default value.
235
+ # We check it here so that the tmpfs/overlayfs auto-disable logic
236
+ # below can distinguish "the user never mentioned use_gds (default
237
+ # True)" from "the user explicitly wrote use_gds: true". In the
238
+ # first case we auto-disable GDS on unsupported filesystems; in the
239
+ # second we respect the user's explicit intent and leave it enabled.
240
+ user_set_keys: set[str] = getattr(config, "_user_set_keys", set())
241
+ use_gds_explicitly_set = "use_gds" in user_set_keys
242
+
243
+ # Now initialize the memory allocator
244
+ self.memory_allocator = self.initialize_allocator(config, metadata)
245
+
246
+ self.data_suffix = _DATA_FILE_SUFFIX
247
+ self._thread_pool = None
248
+
249
+ if self.fstype in ["tmpfs", "overlayfs"]:
250
+ # TODO: we can replace the auto-detection of unsupported GDS
251
+ # file systems by doing a small GDS API test on them. If a
252
+ # read/write test fails, we can fallback to not using GDS APIs.
253
+ if use_gds_explicitly_set:
254
+ logger.warning("No automatic disabling of GDS usage due to fstype")
255
+ else:
256
+ logger.info("Automatic disabling of GDS usage due to fstype")
257
+ self.use_gds = False
258
+ elif self.fstype == "wekafs":
259
+ logger.info("Weka filesystem detected, GDS usage is enforced")
260
+ assert self.use_gds
261
+ self.data_suffix = _WEKA_DATA_FILE_SUFFIX
262
+
263
+ # Always enable the thread pool for parallel I/O
264
+ self.use_thread_pool = self.use_gds
265
+
266
+ if self.use_thread_pool:
267
+ thread_count = _DEFAULT_THREAD_COUNT
268
+ if config.extra_config is not None:
269
+ thread_count = config.extra_config.get(
270
+ "gds_io_threads", _DEFAULT_THREAD_COUNT
271
+ )
272
+ self._thread_pool = ThreadPoolExecutor(
273
+ max_workers=thread_count, thread_name_prefix="gds-io"
274
+ )
275
+
276
+ if self.use_gds:
277
+ logger.info("Using GDS backend '%s'", self.gds_backend)
278
+ if self.gds_backend == "cufile":
279
+ # HACK(Jiayi): cufile import is buggy on some hardware
280
+ # (e.g., without GPUDirect), so it's temporarily put here.
281
+ # Third Party
282
+ import cufile
283
+
284
+ self.cudart = None
285
+ self.gds_module = cufile
286
+ self._gds_driver = self.gds_module.CuFileDriver()
287
+ elif self.gds_backend == "hipfile":
288
+ # HACK: hipfile import may be buggy on some hardware
289
+ # (e.g., without GPUDirect), so it's temporarily put here.
290
+ # Third Party
291
+ import hipfile
292
+
293
+ self.cudart = None
294
+ self.gds_module = hipfile
295
+ self._gds_driver = self.gds_module.CuFileDriver()
296
+ else:
297
+ raise ValueError(f"Unsupported gds_backend '{self.gds_backend}'")
298
+ else:
299
+ logger.info("GDS disabled, using POSIX fallback")
300
+ self.gds_module = None
301
+ self.cudart = ctypes.CDLL("libcudart.so")
302
+
303
+ self.use_direct_io = False
304
+
305
+ # Values for retrying allocations and loads in case of failures potentially
306
+ # due to memory pressure
307
+ self.max_alloc_attempts = (config.extra_config or {}).get(
308
+ "max_alloc_attempts", 10
309
+ )
310
+ self.alloc_attempt_delay_secs = (config.extra_config or {}).get(
311
+ "allocation_attempt_delay_secs", 0.1
312
+ )
313
+
314
+ if config.extra_config is not None:
315
+ use_direct_io = get_extra_config_bool("use_direct_io", config)
316
+ if use_direct_io is not None:
317
+ self.use_direct_io = use_direct_io
318
+
319
+ for p in self.gds_paths:
320
+ os.makedirs(p, exist_ok=True)
321
+
322
+ self.stats = None # TODO: plug into LMCache Statistics
323
+
324
+ self.hot_lock = threading.Lock()
325
+ self.hot_cache: OrderedDict[CacheEngineKey, DiskCacheMetadata] = OrderedDict()
326
+ self.metadata_dirs: set[str] = set()
327
+
328
+ self.put_lock = threading.Lock()
329
+ self.put_tasks: set[CacheEngineKey] = set()
330
+
331
+ if hasattr(self.memory_allocator, "base_pointer"):
332
+ logger.debug(f"Using base pointer {self.memory_allocator.base_pointer}")
333
+ self.gds_base_pointer = self.memory_allocator.base_pointer
334
+ else:
335
+ logger.info("No base pointer found, GDS will use bounce buffers")
336
+ self.gds_base_pointer = None
337
+ self._scan_metadata_future = asyncio.run_coroutine_threadsafe(
338
+ self._scan_metadata(), self.loop
339
+ )
340
+ self.save_metadata_tasks: set[asyncio.Task] = set()
341
+
342
+ # flag for extra assertions to catch bugs but harm performance
343
+ self._debug_asserts = False
344
+ # flag to use O_NOATIME during metadata file read for performance improvement
345
+ self._use_noatime = True
346
+
347
+ async def _scan_metadata(self):
348
+ # TODO: even though we only run it once on startup, this is still
349
+ # not super scalable - test whether Rust code will be faster here, or
350
+ # whether we can serialize meta-data in groups for faster loading.
351
+ tasks = []
352
+ start = time.perf_counter()
353
+ for p in self.gds_paths:
354
+ with os.scandir(p) as it:
355
+ for entry in it:
356
+ if not entry.is_dir():
357
+ continue
358
+ l1_dir = os.path.basename(entry.name)
359
+ if len(l1_dir) != 2:
360
+ continue
361
+ tasks.append(
362
+ asyncio.to_thread(
363
+ self._scan_metadata_subdir,
364
+ os.path.join(p, l1_dir),
365
+ l1_dir,
366
+ )
367
+ )
368
+ # TODO: If Python 3.11+, can we use TaskGroup instead?
369
+ await asyncio.gather(*tasks)
370
+ end = time.perf_counter()
371
+ logger.info(
372
+ f"Read {len(self.hot_cache)} cache entries from persistent "
373
+ f"storage in {end - start:.2f} seconds"
374
+ )
375
+
376
+ def _scan_metadata_subdir(self, path, l1_dir):
377
+ target_suffix = self.data_suffix + _METADATA_FILE_SUFFIX
378
+ with os.scandir(path) as it:
379
+ for entry in it:
380
+ if not entry.is_dir():
381
+ continue
382
+ l2_dir = os.path.basename(entry.name)
383
+ if len(l2_dir) != 2:
384
+ continue
385
+ with os.scandir(os.path.join(path, l2_dir)) as it2:
386
+ for fentry in it2:
387
+ if not fentry.is_file():
388
+ continue
389
+ if not fentry.name.endswith(target_suffix):
390
+ continue
391
+ filename = os.path.basename(fentry.name)
392
+ key_str = urllib.parse.unquote(filename[: -len(target_suffix)])
393
+ try:
394
+ key = CacheEngineKey.from_string(key_str)
395
+ except ValueError as e:
396
+ logger.error(
397
+ f"Filename {filename} can't be converted "
398
+ f"back into cache key: {e}"
399
+ )
400
+ continue
401
+ try:
402
+ self._read_metadata(key, fentry.path, l1_dir + l2_dir)
403
+ except UnsupportedMetadataVersion:
404
+ logger.error(
405
+ "Unsupported metadata version for "
406
+ f"{fentry.path}, ignoring"
407
+ )
408
+
409
+ def _read_metadata_info(self, filename: str):
410
+ # Use O_NOATIME to prevent updating access time and improve performance
411
+ # Instead of using Python's open() and read(), we use the OS's open() and
412
+ # read() because it is faster - the metadata file is small and we don't
413
+ # need any buffering.
414
+ # Additionally, we use O_NOATIME to improve performance
415
+ if self._use_noatime:
416
+ try:
417
+ fd = os.open(filename, os.O_RDONLY | os.O_NOATIME)
418
+ except (
419
+ # PermissionError: User doesn't own the file
420
+ # AttributeError: O_NOATIME not available on this platform
421
+ # OSError: Filesystem doesn't support O_NOATIME (EINVAL)
422
+ PermissionError,
423
+ AttributeError,
424
+ OSError,
425
+ ): # fallback to normal open if O_NOATIME is not supported
426
+ self._use_noatime = False
427
+ logger.info(
428
+ "O_NOATIME flag not supported during metadata file read, "
429
+ "falling back to normal open"
430
+ )
431
+ fd = os.open(filename, os.O_RDONLY)
432
+ else:
433
+ fd = os.open(filename, os.O_RDONLY)
434
+ try:
435
+ buf = os.read(fd, _METADATA_MAX_SIZE)
436
+ finally:
437
+ os.close(fd)
438
+ return unpack_metadata(buf)
439
+
440
+ def _read_metadata(
441
+ self,
442
+ key: CacheEngineKey,
443
+ filename: str,
444
+ subdir_key: str,
445
+ ):
446
+ shape, dtype, size, fmt, extra_metadata = self._read_metadata_info(filename)
447
+ if extra_metadata["lmcache_version"] != str(_METADATA_VERSION):
448
+ raise RuntimeError("unhandled lmcache metadata")
449
+ logger.debug(
450
+ f"Read metadata for {key} from {filename}: "
451
+ f"shape={shape}, dtype={dtype}, size={size}, fmt={fmt}, "
452
+ f"extra_metadata={extra_metadata}"
453
+ )
454
+ # TODO(extra_metadata)
455
+ # TODO(Jiayi): need to support `cached_positions`.
456
+ # Currently we just fill it as None.
457
+ metadata = DiskCacheMetadata(
458
+ filename.removesuffix(_METADATA_FILE_SUFFIX),
459
+ size,
460
+ shape,
461
+ dtype,
462
+ None,
463
+ fmt,
464
+ )
465
+ with self.hot_lock:
466
+ self.metadata_dirs.add(subdir_key)
467
+ self.hot_cache[key] = metadata
468
+ return metadata
469
+
470
+ def __str__(self):
471
+ return self.__class__.__name__
472
+
473
+ def contains(self, key: CacheEngineKey, pin: bool = False) -> bool:
474
+ # TODO: implement pin() semantics
475
+ with self.hot_lock:
476
+ res = key in self.hot_cache
477
+ if res:
478
+ return True
479
+ if self._try_to_read_metadata(key):
480
+ return True
481
+ return False
482
+
483
+ def _try_to_read_metadata(self, key: CacheEngineKey) -> Optional[DiskCacheMetadata]:
484
+ for p in self.gds_paths:
485
+ path, subdir_key, _, _ = self._key_to_path(key, base_path=p)
486
+ path += _METADATA_FILE_SUFFIX
487
+ if os.path.exists(path):
488
+ try:
489
+ return self._read_metadata(key, path, subdir_key)
490
+ except FileNotFoundError:
491
+ logger.warning(
492
+ f"[GDS] File not found for key {key.to_string()} "
493
+ f"at expected path {path}, returning None"
494
+ )
495
+ except PermissionError:
496
+ logger.warning(
497
+ f"[GDS]: Permission Denied for PID {os.getpid()} on {path},"
498
+ f" returning None"
499
+ )
500
+ except UnsupportedMetadataVersion:
501
+ logger.error(f"Unsupported metadata version for {path}, ignoring")
502
+ except (OSError, IOError) as e:
503
+ logger.error(
504
+ f"Failed to read metadata file {path}: {type(e).__name__}: "
505
+ f"{e}. File may be corrupted or inaccessible. "
506
+ f"Ignoring cache entry for key {key.to_string()}."
507
+ )
508
+ except Exception as e:
509
+ logger.error(
510
+ f"Unexpected error reading metadata file {path}: "
511
+ f"{type(e).__name__}: {e}. Ignoring cache entry for key "
512
+ f"{key.to_string()}."
513
+ )
514
+
515
+ return None
516
+
517
+ def _key_to_path(
518
+ self,
519
+ key: CacheEngineKey,
520
+ base_path: Optional[str] = None,
521
+ ) -> Tuple[str, str, str, str]:
522
+ hash = str(key.chunk_hash)
523
+ l1_dir = hash[:2]
524
+ l2_dir = hash[2:4]
525
+ key_str = key.to_string()
526
+ if base_path is None:
527
+ base_path = self.gds_path
528
+ return (
529
+ os.path.join(
530
+ base_path,
531
+ l1_dir,
532
+ l2_dir,
533
+ urllib.parse.quote(key_str, safe="") + self.data_suffix,
534
+ ),
535
+ l1_dir + l2_dir,
536
+ l1_dir,
537
+ l2_dir,
538
+ )
539
+
540
+ def exists_in_put_tasks(self, key: CacheEngineKey) -> bool:
541
+ with self.put_lock:
542
+ return key in self.put_tasks
543
+
544
+ def submit_put_task(
545
+ self,
546
+ key: CacheEngineKey,
547
+ memory_obj: MemoryObj,
548
+ on_complete_callback: Optional[Callable[[CacheEngineKey], None]] = None,
549
+ ) -> Future:
550
+ """
551
+ Submit a put task to store KV cache to GDS asynchronously.
552
+
553
+ :param on_complete_callback: Optional callback invoked after the GDS
554
+ write completes. Callback exceptions are caught and logged.
555
+ """
556
+ assert memory_obj.tensor is not None
557
+ memory_obj.ref_count_up()
558
+
559
+ with self.put_lock:
560
+ self.put_tasks.add(key)
561
+
562
+ future = asyncio.run_coroutine_threadsafe(
563
+ self._async_save_bytes_to_disk(key, memory_obj, on_complete_callback),
564
+ self.loop,
565
+ )
566
+ return future
567
+
568
+ def batched_submit_put_task(
569
+ self,
570
+ keys: Sequence[CacheEngineKey],
571
+ memory_objs: List[MemoryObj],
572
+ transfer_spec: Any = None,
573
+ on_complete_callback: Optional[Callable[[CacheEngineKey], None]] = None,
574
+ ) -> Union[List[Future], None]:
575
+ """
576
+ Submit batched put tasks to store KV caches to GDS asynchronously.
577
+
578
+ :param on_complete_callback: Optional callback invoked once per key
579
+ after that key's write completes (not once per batch).
580
+ """
581
+ futures = []
582
+ for key, memory_obj in zip(keys, memory_objs, strict=False):
583
+ future = self.submit_put_task(
584
+ key, memory_obj, on_complete_callback=on_complete_callback
585
+ )
586
+ futures.append(future)
587
+ return futures
588
+
589
+ async def _async_save_bytes_to_disk(
590
+ self,
591
+ key: CacheEngineKey,
592
+ memory_obj: MemoryObj,
593
+ on_complete_callback: Optional[Callable[[CacheEngineKey], None]] = None,
594
+ ) -> None:
595
+ """
596
+ Convert KV to bytes and async store bytes to disk.
597
+
598
+ :param on_complete_callback: Optional callback invoked after the GDS
599
+ write completes for this key. Callback exceptions are caught.
600
+ """
601
+ try:
602
+ kv_chunk = memory_obj.tensor
603
+ assert kv_chunk is not None
604
+ path, subdir_key, l1_dir, l2_dir = self._key_to_path(key)
605
+ # TODO: maybe remove `metadata_dirs` and insert mkdir calls
606
+ # only for the case where creating the CuFile fails on ENOENT. It
607
+ # also makes the code more resilient to out-of-band deletions
608
+ if subdir_key not in self.metadata_dirs:
609
+ os.makedirs(os.path.join(self.gds_path, l1_dir, l2_dir), exist_ok=True)
610
+ self.metadata_dirs.add(subdir_key)
611
+ tmp = ".tmp" + rand_suffix(8)
612
+ fmt = memory_obj.metadata.fmt
613
+ try:
614
+ metadata = await asyncio.to_thread(
615
+ self._save_gds,
616
+ path,
617
+ tmp,
618
+ kv_chunk,
619
+ fmt,
620
+ self.gds_base_pointer,
621
+ memory_obj.metadata.address,
622
+ )
623
+ except Exception as e:
624
+ logger.error(
625
+ f"GDS write operation failed for key {key.to_string()} at "
626
+ f"path {path}: tensor_shape={kv_chunk.shape}, "
627
+ f"tensor_dtype={kv_chunk.dtype}, "
628
+ f"tensor_size_bytes={kv_chunk.nbytes}, error={e}",
629
+ exc_info=True,
630
+ )
631
+ return
632
+
633
+ # Register key in cache
634
+ logger.debug(
635
+ f"Saved {kv_chunk.numel()} elements of {kv_chunk.dtype} "
636
+ f"to {path} with metadata {metadata}"
637
+ )
638
+ self.insert_key(key, memory_obj)
639
+ try:
640
+ task = asyncio.create_task(
641
+ save_metadata(path + _METADATA_FILE_SUFFIX, tmp, metadata)
642
+ )
643
+ self.save_metadata_tasks.add(task)
644
+ task.add_done_callback(self.save_metadata_tasks.discard)
645
+ # Add callback to check for exceptions during task execution
646
+ task.add_done_callback(
647
+ lambda t: self._handle_metadata_write_completion(t, key, path)
648
+ )
649
+ except Exception as e:
650
+ logger.error(
651
+ f"POSIX metadata write operation failed for key {key.to_string()} "
652
+ f"at path {path + _METADATA_FILE_SUFFIX}: "
653
+ f"metadata_size_bytes={len(metadata)}, "
654
+ f"tmp_suffix={tmp}, error={e}",
655
+ exc_info=True,
656
+ )
657
+ with self.hot_lock:
658
+ self.hot_cache.pop(key, None)
659
+ return
660
+ finally:
661
+ memory_obj.ref_count_down()
662
+ with self.put_lock:
663
+ self.put_tasks.discard(key)
664
+
665
+ # Call the completion callback if provided
666
+ if on_complete_callback is not None:
667
+ try:
668
+ on_complete_callback(key)
669
+ except Exception as e:
670
+ logger.error(
671
+ f"on_complete_callback failed for key {key.to_string()}: {e}",
672
+ exc_info=True,
673
+ )
674
+
675
+ def _handle_metadata_write_completion(
676
+ self, task: asyncio.Task, key: CacheEngineKey, path: str
677
+ ) -> None:
678
+ """Handle completion of metadata write task, checking for exceptions."""
679
+ try:
680
+ # Retrieve exception if task failed
681
+ exception = task.exception()
682
+ if exception is not None:
683
+ logger.error(
684
+ f"Metadata write task failed for key {key.to_string()} "
685
+ f"at path {path + _METADATA_FILE_SUFFIX}: {exception}",
686
+ exc_info=exception,
687
+ )
688
+ with self.hot_lock:
689
+ self.hot_cache.pop(key, None)
690
+ except Exception as e:
691
+ # Exception calling task.exception() (e.g., task was cancelled)
692
+ logger.error(
693
+ f"Error checking metadata write task status for key "
694
+ f"{key.to_string()}: {e}",
695
+ exc_info=True,
696
+ )
697
+
698
+ def insert_key(self, key: CacheEngineKey, memory_obj: MemoryObj) -> None:
699
+ path, _, _, _ = self._key_to_path(key)
700
+ size = memory_obj.get_physical_size()
701
+ shape = memory_obj.metadata.shape
702
+ dtype = memory_obj.metadata.dtype
703
+ fmt = memory_obj.metadata.fmt
704
+ with self.hot_lock:
705
+ # TODO(Jiayi): need to support `cached_positions`.
706
+ self.hot_cache[key] = DiskCacheMetadata(path, size, shape, dtype, None, fmt)
707
+
708
+ def submit_prefetch_task(
709
+ self,
710
+ key: CacheEngineKey,
711
+ ) -> bool:
712
+ # with self.hot_lock:
713
+ # entry = self.hot_cache.get(key)
714
+ # if entry is None:
715
+ # return None
716
+
717
+ # path = entry.path
718
+ # dtype = entry.dtype
719
+ # shape = entry.shape
720
+ # fmt = entry.fmt
721
+ # assert dtype is not None
722
+ # assert shape is not None
723
+ # assert fmt is not None
724
+ # return asyncio.run_coroutine_threadsafe(
725
+ # self._async_load_bytes_from_disk(key, path, dtype, shape,fmt), self.loop
726
+ # )
727
+
728
+ # TODO(Jiayi): Need to modify this when prefetch interface is determined.
729
+
730
+ # TODO(Jiayi): add `test_gds_backend_sanity` back after implementing this
731
+ return False
732
+
733
+ async def _async_load_bytes_from_disk(
734
+ self,
735
+ key: CacheEngineKey,
736
+ path: str,
737
+ dtype: torch.dtype,
738
+ shape: torch.Size,
739
+ fmt: MemoryFormat,
740
+ ) -> Optional[MemoryObj]:
741
+ return self._load_bytes_from_disk_with_allocation(
742
+ key, path, dtype, shape, fmt=fmt
743
+ )
744
+
745
+ def get_blocking(
746
+ self,
747
+ key: CacheEngineKey,
748
+ ) -> Optional[MemoryObj]:
749
+ with self.hot_lock:
750
+ entry = self.hot_cache.get(key)
751
+ if entry is None:
752
+ return None
753
+
754
+ path = entry.path
755
+ dtype = entry.dtype
756
+ shape = entry.shape
757
+ fmt = entry.fmt
758
+ logger.warning(entry)
759
+ assert dtype is not None
760
+ assert shape is not None
761
+ assert fmt is not None
762
+ return self._load_bytes_from_disk_with_allocation(
763
+ key, path, dtype=dtype, shape=shape, fmt=fmt
764
+ )
765
+
766
+ def _load_bytes_from_disk_with_allocation(
767
+ self,
768
+ key: CacheEngineKey,
769
+ path: str,
770
+ dtype: torch.dtype,
771
+ shape: torch.Size,
772
+ fmt: MemoryFormat,
773
+ ) -> Optional[MemoryObj]:
774
+ """
775
+ Load byte array from disk by first allocating memory, then loading.
776
+
777
+ Args:
778
+ key: Cache key for error handling
779
+ path: File path to load from
780
+ dtype: Data type for memory allocation
781
+ shape: Shape for memory allocation
782
+
783
+ Returns:
784
+ A new memory object with loaded data, or None if allocation or
785
+ loading failed
786
+ """
787
+ memory_obj = self.memory_allocator.allocate(shape, dtype, fmt=fmt)
788
+ if memory_obj is None:
789
+ logger.error("Memory allocation failed during sync disk load.")
790
+ return None
791
+ if self._debug_asserts:
792
+ assert memory_obj.tensor is not None
793
+ assert memory_obj.tensor.is_cuda
794
+ assert torch.device(self.dst_device) == torch.device(
795
+ memory_obj.tensor.device
796
+ )
797
+
798
+ return self._load_bytes_from_disk_with_memory(key, path, memory_obj)
799
+
800
+ def _load_bytes_from_disk_with_memory(
801
+ self,
802
+ key: CacheEngineKey,
803
+ path: str,
804
+ memory_obj: Optional[MemoryObj],
805
+ ) -> Optional[MemoryObj]:
806
+ """
807
+ Load byte array from disk into a pre-allocated memory object.
808
+
809
+ Args:
810
+ key: Cache key for error handling
811
+ path: File path to load from
812
+ memory_obj: Pre-allocated memory object to load data into
813
+
814
+ Returns:
815
+ The memory object with loaded data, or None if loading failed
816
+ """
817
+ if memory_obj is None or not memory_obj.is_valid():
818
+ return None
819
+
820
+ offset = _METADATA_MAX_SIZE
821
+ if self.gds_base_pointer is None:
822
+ tensor = memory_obj.tensor
823
+ assert tensor is not None
824
+ if self._debug_asserts:
825
+ assert tensor.is_cuda
826
+ assert torch.device(self.dst_device) == torch.device(tensor.device)
827
+ addr = ctypes.c_void_p(tensor.data_ptr())
828
+ dev_offset = 0
829
+ else:
830
+ addr = ctypes.c_void_p(self.gds_base_pointer)
831
+ dev_offset = memory_obj.metadata.address
832
+ ret = self._load_gds(path, offset, addr, memory_obj.get_size(), dev_offset)
833
+ if ret != memory_obj.get_size():
834
+ if ret < 0:
835
+ logger.error(
836
+ f"Error loading {path}: ret: {ret} removing entry from cache"
837
+ )
838
+ with self.hot_lock:
839
+ self.hot_cache.pop(key)
840
+ else:
841
+ # TODO: we should probably count errors and
842
+ # remove the entry if it's a persistent problem.
843
+ logger.error(
844
+ f"Error loading {path}: got only {ret} bytes "
845
+ f"out of {memory_obj.get_size()}, ignoring"
846
+ )
847
+ memory_obj.ref_count_down()
848
+ return None
849
+ return memory_obj
850
+
851
+ def get_non_blocking(
852
+ self,
853
+ key: CacheEngineKey,
854
+ location: Optional[str] = None,
855
+ ) -> Optional[Future]:
856
+ # TODO: Using a dummy wrapper around prefetch for now.
857
+ if not self.submit_prefetch_task(key):
858
+ return None
859
+ return Future()
860
+
861
+ def batched_get_blocking(
862
+ self,
863
+ keys: List[CacheEngineKey],
864
+ ) -> List[Optional[MemoryObj]]:
865
+ if self.use_thread_pool:
866
+ logger.debug("Using batched_get_blocking with thread pool implementation")
867
+ return self._batched_get_blocking_by_thread_pool_impl(keys)
868
+ else:
869
+ return super().batched_get_blocking(keys)
870
+
871
+ def _batched_get_blocking_by_thread_pool_impl(
872
+ self,
873
+ keys: List[CacheEngineKey],
874
+ ) -> list[MemoryObj | None]:
875
+ paths: list[str | None] = []
876
+ dtypes: list[torch.dtype | None] = []
877
+ shapes: list[torch.Size | None] = []
878
+ fmts: list[MemoryFormat | None] = []
879
+ with self.hot_lock:
880
+ for key in keys:
881
+ entry = self.hot_cache.get(key)
882
+ if entry is None:
883
+ logger.error(f"Lookup failed during get_blocking for {key}")
884
+ paths.append(None)
885
+ dtypes.append(None)
886
+ shapes.append(None)
887
+ fmts.append(None)
888
+ continue
889
+ paths.append(entry.path)
890
+ dtypes.append(entry.dtype)
891
+ shapes.append(entry.shape)
892
+ fmts.append(entry.fmt)
893
+
894
+ memory_objs: list[MemoryObj | None] = []
895
+ gds_reads, gds_read_bytes = 0, 0
896
+ for dtype, shape, path, fmt in zip(dtypes, shapes, paths, fmts, strict=True):
897
+ if path is None:
898
+ memory_objs.append(None)
899
+ continue
900
+ memory_obj = self.memory_allocator.allocate(shape, dtype, fmt=fmt)
901
+ if memory_obj is None:
902
+ logger.error(f"Memory allocation failed during get_blocking for {path}")
903
+ else:
904
+ gds_reads += 1
905
+ gds_read_bytes += memory_obj.get_size()
906
+ memory_objs.append(memory_obj)
907
+
908
+ start_time = time.perf_counter()
909
+ assert self._thread_pool is not None
910
+ results = list(
911
+ self._thread_pool.map(
912
+ self._load_bytes_from_disk_with_memory, keys, paths, memory_objs
913
+ )
914
+ )
915
+ total_time = time.perf_counter() - start_time
916
+ logger.info(
917
+ f"Time taken for batched_get_blocking: {total_time:.3f}s |"
918
+ f" {gds_read_bytes / 1024 / 1024}MiB | {gds_reads} ops."
919
+ )
920
+ return results
921
+
922
+ @_lmcache_nvtx_annotate
923
+ @torch.inference_mode()
924
+ def _save_gds(
925
+ self,
926
+ path: str,
927
+ tmp: str,
928
+ kv_chunk: torch.Tensor,
929
+ fmt: MemoryFormat,
930
+ base_pointer: int,
931
+ device_offset: int,
932
+ ):
933
+ if base_pointer is None:
934
+ addr = ctypes.c_void_p(kv_chunk.data_ptr())
935
+ dev_offset = 0
936
+ else:
937
+ addr = ctypes.c_void_p(base_pointer)
938
+ dev_offset = device_offset
939
+ tmp_path = path + tmp
940
+ offset = _METADATA_MAX_SIZE
941
+ # TODO: We can add the chunk's metadata here, e.g. Tensor parallelism shard
942
+ # and pipeline parallelism index.
943
+ metadata = pack_metadata(
944
+ kv_chunk, fmt=fmt, lmcache_version=str(_METADATA_VERSION)
945
+ )
946
+ try:
947
+ with open(tmp_path, "wb") as f:
948
+ f.write(metadata)
949
+ if self.gds_module:
950
+ with self.gds_module.CuFile(
951
+ tmp_path, "r+", use_direct_io=self.use_direct_io
952
+ ) as f:
953
+ f.write(
954
+ addr, kv_chunk.nbytes, file_offset=offset, dev_offset=dev_offset
955
+ )
956
+ elif self.cudart:
957
+ # mmap the file
958
+ fd = os.open(tmp_path, os.O_RDWR)
959
+ nbytes = kv_chunk.nbytes
960
+ os.ftruncate(fd, nbytes + offset)
961
+ mm = mmap.mmap(
962
+ fd, nbytes + offset, prot=mmap.PROT_WRITE, flags=mmap.MAP_SHARED
963
+ )
964
+ os.close(fd)
965
+
966
+ # get mapped file address
967
+ arr = np.frombuffer(mm, dtype=np.uint8)
968
+ buf_addr = arr.__array_interface__["data"][0]
969
+
970
+ assert addr.value is not None
971
+ res = self.cudart.cudaMemcpy(
972
+ ctypes.c_void_p(buf_addr + offset),
973
+ ctypes.c_void_p(int(addr.value) + device_offset),
974
+ ctypes.c_size_t(nbytes),
975
+ ctypes.c_int(2),
976
+ )
977
+ if res:
978
+ raise RuntimeError(f"cudaMemcpy failed {res}")
979
+ del arr
980
+ mm.close()
981
+
982
+ except Exception as e:
983
+ logger.error(f"Error saving {tmp_path}: {e}", exc_info=True)
984
+ raise e
985
+ os.rename(tmp_path, path)
986
+ return metadata
987
+
988
+ def _load_gds(
989
+ self,
990
+ gds_path: str,
991
+ file_offset: int,
992
+ gpu_pointer: ctypes.c_void_p,
993
+ size_in_bytes: int,
994
+ dev_offset: int,
995
+ ) -> int:
996
+ """Read data from disk into a GPU buffer"""
997
+ try:
998
+ if self.gds_module:
999
+ with self.gds_module.CuFile(
1000
+ gds_path, "r", use_direct_io=self.use_direct_io
1001
+ ) as f:
1002
+ return f.read(
1003
+ gpu_pointer,
1004
+ size_in_bytes,
1005
+ file_offset=file_offset,
1006
+ dev_offset=dev_offset,
1007
+ )
1008
+ elif self.cudart:
1009
+ fd = os.open(gds_path, os.O_RDONLY)
1010
+ file_size = os.fstat(fd).st_size
1011
+
1012
+ # Check if file is large enough for the requested read
1013
+ if file_size < file_offset + size_in_bytes:
1014
+ os.close(fd)
1015
+ logger.error(
1016
+ f"File {gds_path} is too small: size={file_size}, "
1017
+ f"but need at least {file_offset + size_in_bytes} bytes "
1018
+ f"(offset={file_offset}, requested={size_in_bytes})"
1019
+ )
1020
+ return -1
1021
+
1022
+ mm = mmap.mmap(
1023
+ fd,
1024
+ file_size,
1025
+ prot=mmap.PROT_READ,
1026
+ flags=mmap.MAP_PRIVATE | mmap.MAP_POPULATE, # type: ignore [attr-defined]
1027
+ )
1028
+ os.close(fd)
1029
+
1030
+ arr = np.frombuffer(mm, dtype=np.uint8)
1031
+ addr = arr.__array_interface__["data"][0]
1032
+
1033
+ assert gpu_pointer.value is not None
1034
+ res = self.cudart.cudaMemcpy(
1035
+ ctypes.c_void_p(int(gpu_pointer.value) + dev_offset),
1036
+ ctypes.c_void_p(addr + file_offset),
1037
+ ctypes.c_size_t(size_in_bytes),
1038
+ ctypes.c_int(1),
1039
+ )
1040
+
1041
+ if res != 0:
1042
+ raise RuntimeError(f"cudaMemcpy failed with code {res}")
1043
+ del arr
1044
+ mm.close()
1045
+ return size_in_bytes
1046
+ else:
1047
+ raise RuntimeError(
1048
+ "Both gds_module and cudart are None, this should not happen"
1049
+ )
1050
+ except Exception as e:
1051
+ # return -1 on any exception, and log the error.
1052
+ # The caller will handle the error by removing the cache entry and
1053
+ # returning None.
1054
+ logger.error(f"GDS read failed for {gds_path}: {e}", exc_info=True)
1055
+ return -1
1056
+
1057
+ def pin(self, key: CacheEngineKey) -> bool:
1058
+ # NOTE (ApostaC): Since gds doesn't have eviction now, we don't need
1059
+ # to implement pin and unpin
1060
+ return False
1061
+
1062
+ def unpin(self, key: CacheEngineKey) -> bool:
1063
+ # NOTE (ApostaC): Since gds doesn't have eviction now, we don't need
1064
+ # to implement pin and unpin
1065
+ return False
1066
+
1067
+ def remove(self, key: CacheEngineKey, force: bool = True):
1068
+ raise NotImplementedError("Remote backend does not support remove now.")
1069
+
1070
+ def initialize_allocator(
1071
+ self, config: LMCacheEngineConfig, metadata: LMCacheMetadata
1072
+ ) -> Union[CuFileMemoryAllocator, HipFileMemoryAllocator]:
1073
+ assert config.gds_buffer_size is not None
1074
+ allocator_cls = (
1075
+ HipFileMemoryAllocator
1076
+ if self.gds_backend == "hipfile"
1077
+ else CuFileMemoryAllocator
1078
+ )
1079
+ return allocator_cls(config.gds_buffer_size * 1024**2)
1080
+
1081
+ def allocate(
1082
+ self,
1083
+ shapes: Union[torch.Size, list[torch.Size]],
1084
+ dtypes: Union[torch.dtype, list[torch.dtype]],
1085
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
1086
+ eviction: bool = True,
1087
+ busy_loop: bool = True,
1088
+ ) -> Optional[MemoryObj]:
1089
+ """
1090
+ Allocate a memory object of shape and dtype
1091
+ """
1092
+ if eviction:
1093
+ logger.warning("GDS Backend does not support eviction")
1094
+
1095
+ logger.debug(f"Allocating memory with busy loop: {busy_loop}")
1096
+
1097
+ max_attempts = self.max_alloc_attempts if busy_loop else 1
1098
+ num_attempts = 0
1099
+
1100
+ # try up to max_attempts
1101
+ while True:
1102
+ memory_obj = self.memory_allocator.allocate(shapes, dtypes, fmt)
1103
+ if memory_obj is not None: # success
1104
+ return memory_obj
1105
+
1106
+ num_attempts += 1
1107
+ if num_attempts < max_attempts: # keep trying until max attempts is reached
1108
+ logger.debug(
1109
+ f"Unable to allocate memory object after {num_attempts} "
1110
+ f"attempt(s) of GDS backend allocate(). "
1111
+ f"Waiting {self.alloc_attempt_delay_secs} seconds before retrying."
1112
+ )
1113
+ if self.alloc_attempt_delay_secs > 0:
1114
+ time.sleep(self.alloc_attempt_delay_secs)
1115
+ else: # break to failure case after max attempts is reached
1116
+ break
1117
+
1118
+ logger.warning(
1119
+ f"GDS allocation failed after {num_attempts} attempt(s). Returning None."
1120
+ )
1121
+ if not self.memory_allocator.memcheck():
1122
+ logger.error(
1123
+ "GDS allocation failed and memory allocator "
1124
+ "is inconsistent. This is a bug in the memory allocator."
1125
+ )
1126
+ return None
1127
+
1128
+ def batched_allocate(
1129
+ self,
1130
+ shapes: Union[torch.Size, list[torch.Size]],
1131
+ dtypes: Union[torch.dtype, list[torch.dtype]],
1132
+ batch_size: int,
1133
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
1134
+ eviction: bool = True,
1135
+ busy_loop: bool = True,
1136
+ ) -> Optional[list[MemoryObj]]:
1137
+ """
1138
+ Batched allocate `batch_size` memory objects of shape and dtype
1139
+ """
1140
+ if eviction:
1141
+ logger.warning("GDS Backend does not support eviction")
1142
+
1143
+ logger.debug(
1144
+ f"Batched allocating memory in GDS backend with busy loop: {busy_loop}"
1145
+ )
1146
+
1147
+ max_attempts = self.max_alloc_attempts if busy_loop else 1
1148
+ num_attempts = 0
1149
+
1150
+ # try up to max_attempts
1151
+ while True:
1152
+ memory_objs = self.memory_allocator.batched_allocate(
1153
+ shapes, dtypes, batch_size, fmt
1154
+ )
1155
+ if memory_objs is not None: # success
1156
+ return memory_objs
1157
+
1158
+ num_attempts += 1
1159
+ if num_attempts < max_attempts: # keep trying until max attempts is reached
1160
+ logger.debug(
1161
+ f"Unable to allocate memory object after {num_attempts} "
1162
+ f"attempt(s) of GDS backend batched_allocate(). "
1163
+ f"Waiting {self.alloc_attempt_delay_secs} seconds before retrying."
1164
+ )
1165
+ if self.alloc_attempt_delay_secs > 0:
1166
+ time.sleep(self.alloc_attempt_delay_secs)
1167
+ else: # break to failure case after max attempts is reached
1168
+ break
1169
+
1170
+ logger.warning(
1171
+ f"GDS batched allocation failed after {num_attempts} "
1172
+ f"attempt(s). Returning None."
1173
+ )
1174
+ if not self.memory_allocator.memcheck():
1175
+ logger.error(
1176
+ "GDS batched allocation failed and memory allocator "
1177
+ "is inconsistent. This is a bug in the memory allocator."
1178
+ )
1179
+ return None
1180
+
1181
+ def get_allocator_backend(self):
1182
+ return self
1183
+
1184
+ def get_memory_allocator(self):
1185
+ return self.memory_allocator
1186
+
1187
+ def close(self) -> None:
1188
+ # Wait for initial metadata scan to complete
1189
+ try:
1190
+ self._scan_metadata_future.result(timeout=30)
1191
+ except Exception as e:
1192
+ logger.warning(
1193
+ f"Exception while waiting for metadata scan: {e}",
1194
+ exc_info=True,
1195
+ )
1196
+ self.memory_allocator.close()
1197
+ if self._thread_pool is not None:
1198
+ self._thread_pool.shutdown(wait=True)
1199
+ logger.info("GDS backend closed.")