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,987 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Standard
3
+ from dataclasses import dataclass
4
+ from enum import IntEnum, auto
5
+ from multiprocessing import shared_memory
6
+ from typing import AsyncIterator, List, Optional, Tuple
7
+ import asyncio
8
+ import json
9
+ import time
10
+ import urllib.parse
11
+
12
+ # Third Party
13
+ import aiohttp
14
+ import torch
15
+
16
+ # First Party
17
+ from lmcache.logging import init_logger
18
+ from lmcache.observability import LMCStatsMonitor
19
+ from lmcache.utils import CacheEngineKey
20
+ from lmcache.v1.memory_management import MemoryObj
21
+ from lmcache.v1.protocol import RemoteMetadata
22
+ from lmcache.v1.storage_backend.connector.base_connector import RemoteConnector
23
+ from lmcache.v1.storage_backend.job_executor.pq_executor import AsyncPQExecutor
24
+ from lmcache.v1.storage_backend.local_cpu_backend import LocalCPUBackend
25
+
26
+ logger = init_logger(__name__)
27
+
28
+
29
+ # Constants
30
+ METADATA_SIZE_BYTES = 28 # RemoteMetadata is 7 int32 fields
31
+ METADATA_SHAPE_DIMS = 4 # Number of shape dimensions in metadata
32
+ DEFAULT_CHUNK_SIZE_BYTES = 65536 # 64KB default for streaming
33
+ HTTP_OK = 200
34
+ HTTP_NO_CONTENT = 204
35
+ HTTP_NOT_FOUND = 404
36
+ HTTP_CONFLICT = 409
37
+
38
+
39
+ class Priorities(IntEnum):
40
+ """Priority levels for job execution in the priority queue."""
41
+
42
+ LEASE = 0 # Highest priority - lease acquisition/release
43
+ PREFETCH = auto() # Medium priority - prefetching data
44
+ PUT = auto() # Lower priority - storing data
45
+
46
+
47
+ @dataclass
48
+ class LeaseInfo:
49
+ """Information about a lease obtained from ai-toolkit daemon.
50
+
51
+ A lease represents temporary exclusive access to cached data in shared memory.
52
+ The daemon manages leases to prevent data from being evicted while in use.
53
+ """
54
+
55
+ lease_id: str
56
+ offsets: List[Tuple[int, int]] # (offset, length) pairs in shared memory
57
+
58
+
59
+ class SageMakerHyperPodConnector(RemoteConnector):
60
+ """
61
+ SageMaker HyperPod remote connector for communicating with KV cache daemon
62
+ in SageMaker HyperPod.
63
+
64
+ This connector provides high-performance access to KV cache data stored in
65
+ a remote SageMaker HyperPod service using:
66
+ - Shared memory (data plane) - zero-copy access via shared memory segment
67
+ - HTTP (control plane) - lease acquisition, release, and PUT operations
68
+
69
+ The connector uses a lease-based protocol with immediate release after all reads
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ sagemaker_hyperpod_url: str,
75
+ loop: asyncio.AbstractEventLoop,
76
+ local_cpu_backend: LocalCPUBackend,
77
+ bucket_name: str,
78
+ shared_memory_name: Optional[str],
79
+ max_concurrent_requests: int,
80
+ max_connections: int,
81
+ max_connections_per_host: int,
82
+ timeout_ms: int,
83
+ lease_ttl_s: float = 10.0,
84
+ put_stream_chunk_bytes: int = DEFAULT_CHUNK_SIZE_BYTES,
85
+ max_lease_size_mb: Optional[float] = None,
86
+ **kwargs, # Accept and ignore unused legacy parameters
87
+ ):
88
+ """
89
+ Initialize SageMaker HyperPod connector.
90
+
91
+ Args:
92
+ sagemaker_hyperpod_url: Base URL of the ai-toolkit daemon
93
+ loop: Event loop for async operations
94
+ local_cpu_backend: Backend for local memory allocation
95
+ bucket_name: Bucket name for KV storage namespace
96
+ shared_memory_name: Name of shared memory segment
97
+ (if None, shared memory disabled)
98
+ max_concurrent_requests: Maximum concurrent control plane requests
99
+ max_connections: Maximum total HTTP connections in pool
100
+ max_connections_per_host: Maximum HTTP connections per host
101
+ timeout_ms: Timeout for lease acquisition requests
102
+ lease_ttl_s: Server-side lease timeout (default: 10s)
103
+ put_stream_chunk_bytes: Chunk size for
104
+ streaming PUT requests (default: 64KB)
105
+ **kwargs: Unused legacy parameters (ignored for backward compatibility)
106
+ """
107
+ # initialize base class, which includes some common attributes
108
+ super().__init__(local_cpu_backend.config, local_cpu_backend.metadata)
109
+
110
+ # Core configuration
111
+ self.base_url = sagemaker_hyperpod_url.rstrip("/")
112
+ self.loop = loop
113
+ self.local_cpu_backend = local_cpu_backend
114
+ self.bucket_name = bucket_name
115
+ self.shared_memory_name = shared_memory_name
116
+ self.lease_ttl_s = lease_ttl_s
117
+ self.put_stream_chunk_bytes = max(1024, put_stream_chunk_bytes) # Minimum 1KB
118
+ self.max_lease_size_bytes = (
119
+ int(max_lease_size_mb * 1024 * 1024) if max_lease_size_mb else None
120
+ )
121
+
122
+ # HTTP configuration
123
+ self.max_concurrent_requests = max(1, max_concurrent_requests)
124
+ self.max_connections = max(1, max_connections)
125
+ self.max_connections_per_host = max(1, max_connections_per_host)
126
+ self.timeout_ms = max(100, timeout_ms) # Minimum 100ms
127
+
128
+ # HTTP session (lazy initialized)
129
+ self.http_session: Optional[aiohttp.ClientSession] = None
130
+ self.session_lock = asyncio.Lock()
131
+
132
+ # Concurrency control
133
+ self.control_inflight = asyncio.Semaphore(self.max_concurrent_requests)
134
+ self.put_inflight = asyncio.Semaphore(self.max_concurrent_requests)
135
+ self.pq_executor = AsyncPQExecutor(loop)
136
+
137
+ # Shared memory
138
+ self.shared_memory_obj: Optional[shared_memory.SharedMemory] = None
139
+ self.shared_memory_map: Optional[memoryview] = None
140
+ if self.shared_memory_name:
141
+ self._init_shared_memory()
142
+
143
+ # Observability
144
+ self._stats_monitor = LMCStatsMonitor.GetOrCreate()
145
+
146
+ # Statistics for monitoring
147
+ self.stats = {
148
+ "get_success": 0,
149
+ "get_failure": 0,
150
+ "put_success": 0,
151
+ "put_failure": 0,
152
+ "lease_acquired": 0,
153
+ "lease_released": 0,
154
+ "lease_release_failed": 0,
155
+ }
156
+
157
+ logger.info(
158
+ f"SageMaker HyperPod Connector initialized: url={self.base_url}, "
159
+ f"bucket={self.bucket_name}, shared_memory={self.shared_memory_name}, "
160
+ f"connections={self.max_connections}, lease_ttl={lease_ttl_s}s"
161
+ )
162
+
163
+ def _init_shared_memory(self):
164
+ """Initialize shared memory connection to ai-toolkit daemon."""
165
+ try:
166
+ self.shared_memory_obj = shared_memory.SharedMemory(
167
+ name=self.shared_memory_name, create=False
168
+ )
169
+ self.shared_memory_map = memoryview(self.shared_memory_obj.buf)
170
+ size_mb = len(self.shared_memory_map) / (1024**2)
171
+ logger.info(
172
+ f"Shared memory opened: {self.shared_memory_name} ({size_mb:.2f} MB)"
173
+ )
174
+ except FileNotFoundError:
175
+ logger.error(
176
+ f"Shared memory segment '{self.shared_memory_name}' not found. "
177
+ "Ensure ai-toolkit daemon is running."
178
+ )
179
+ self.shared_memory_map = None
180
+ raise
181
+ except Exception as e:
182
+ logger.error(f"Failed to initialize shared memory: {e}")
183
+ self.shared_memory_map = None
184
+ raise
185
+
186
+ async def _ensure_http_session(self) -> aiohttp.ClientSession:
187
+ """Ensure HTTP session with connection pooling is initialized."""
188
+ if self.http_session is None:
189
+ async with self.session_lock:
190
+ if self.http_session is None: # Double-check locking
191
+ connector = aiohttp.TCPConnector(
192
+ limit=self.max_connections,
193
+ limit_per_host=self.max_connections_per_host,
194
+ ttl_dns_cache=300,
195
+ use_dns_cache=True,
196
+ keepalive_timeout=30,
197
+ enable_cleanup_closed=True,
198
+ )
199
+
200
+ timeout = aiohttp.ClientTimeout(
201
+ total=30,
202
+ connect=5,
203
+ sock_read=10,
204
+ )
205
+
206
+ self.http_session = aiohttp.ClientSession(
207
+ connector=connector,
208
+ timeout=timeout,
209
+ headers={"User-Agent": "LMCache-SageMaker-HyperPod/1.0"},
210
+ )
211
+ logger.info(
212
+ f"HTTP session created with {self.max_connections} "
213
+ f"max connections"
214
+ )
215
+
216
+ return self.http_session
217
+
218
+ async def _http_request(
219
+ self,
220
+ method: str,
221
+ url: str,
222
+ data=None,
223
+ params=None,
224
+ timeout: float = 5.0,
225
+ headers=None,
226
+ gate: Optional[asyncio.Semaphore] = None,
227
+ ):
228
+ """Execute HTTP request with optional semaphore gate."""
229
+ if gate is None:
230
+ return await self._http_request_impl(
231
+ method, url, data=data, params=params, timeout=timeout, headers=headers
232
+ )
233
+ async with gate:
234
+ return await self._http_request_impl(
235
+ method, url, data=data, params=params, timeout=timeout, headers=headers
236
+ )
237
+
238
+ async def _http_request_impl(
239
+ self,
240
+ method: str,
241
+ url: str,
242
+ data=None,
243
+ params=None,
244
+ timeout: float = 5.0,
245
+ headers=None,
246
+ ):
247
+ """Execute HTTP request with connection pooling and error handling."""
248
+ try:
249
+ session = await self._ensure_http_session()
250
+ request_timeout = aiohttp.ClientTimeout(total=timeout)
251
+
252
+ async with session.request(
253
+ method,
254
+ url,
255
+ data=data,
256
+ params=params,
257
+ timeout=request_timeout,
258
+ headers=headers,
259
+ ) as response:
260
+ # Parse JSON response if available
261
+ body_json = None
262
+ content_type = response.headers.get("Content-Type", "")
263
+ if content_type.startswith("application/json"):
264
+ try:
265
+ body_json = await response.json()
266
+ except aiohttp.ContentTypeError as e:
267
+ logger.warning(
268
+ f"JSON parsing failed for {method} {url}:"
269
+ f"invalid content-type - {e}"
270
+ )
271
+ except json.JSONDecodeError as e:
272
+ logger.warning(
273
+ f"JSON parsing failed for {method} {url}:"
274
+ f"malformed JSON - {e}"
275
+ )
276
+ except Exception as e:
277
+ logger.warning(f"JSON parsing failed for {method} {url}: {e}")
278
+
279
+ return {
280
+ "status": response.status,
281
+ "json": body_json,
282
+ }
283
+
284
+ except asyncio.TimeoutError:
285
+ logger.warning(f"HTTP {method} timeout: {url}")
286
+ return None
287
+ except aiohttp.ClientError as e:
288
+ logger.error(f"HTTP {method} client error: {url} - {e}")
289
+ return None
290
+ except Exception as e:
291
+ logger.error(f"HTTP {method} failed: {url} - {e}")
292
+ return None
293
+
294
+ def _key_to_string(self, key: CacheEngineKey) -> str:
295
+ """Convert CacheEngineKey to URL-safe string format."""
296
+ key_str = key.to_string()
297
+ return urllib.parse.quote(key_str, safe="")
298
+
299
+ async def _release_lease(self, key: CacheEngineKey, lease_id: str) -> bool:
300
+ """
301
+ Release a lease to free server resources immediately.
302
+
303
+ Args:
304
+ key: The cache key
305
+ lease_id: The lease ID to release
306
+
307
+ Returns:
308
+ True if release successful, False on error
309
+ """
310
+ key_str = self._key_to_string(key)
311
+ url = f"{self.base_url}/v1/leases/{lease_id}/release"
312
+
313
+ try:
314
+ result = await self._http_request(
315
+ "POST",
316
+ url,
317
+ timeout=5.0,
318
+ gate=self.control_inflight,
319
+ )
320
+
321
+ if result and result["status"] == HTTP_OK:
322
+ self.stats["lease_released"] += 1
323
+ logger.debug(f"Lease released: key={key_str}, lease_id={lease_id}")
324
+ return True
325
+ else:
326
+ status = result["status"] if result else "TIMEOUT"
327
+ self.stats["lease_release_failed"] += 1
328
+ logger.warning(
329
+ f"Lease release failed: key={key_str}, lease_id={lease_id}, "
330
+ f"status={status}"
331
+ )
332
+ return False
333
+
334
+ except Exception as e:
335
+ self.stats["lease_release_failed"] += 1
336
+ logger.warning(
337
+ f"Lease release error: key={key_str}, lease_id={lease_id} - {e}"
338
+ )
339
+ return False
340
+
341
+ async def _acquire_lease(self, key: CacheEngineKey) -> Optional[LeaseInfo]:
342
+ """
343
+ Acquire a lease for the given key.
344
+
345
+ A lease prevents the daemon from evicting data while we're reading it.
346
+ The response includes offset information for shared memory access.
347
+
348
+ Args:
349
+ key: The cache key to acquire lease for
350
+
351
+ Returns:
352
+ LeaseInfo if successful, None otherwise
353
+ """
354
+ key_str = self._key_to_string(key)
355
+ url = f"{self.base_url}/v1/kv/{self.bucket_name}/{key_str}/leases"
356
+ params = {
357
+ "timeout_ms": self.timeout_ms,
358
+ "ttl_s": self.lease_ttl_s,
359
+ }
360
+
361
+ result = await self._http_request(
362
+ "POST",
363
+ url,
364
+ params=params,
365
+ timeout=self.timeout_ms / 1000.0,
366
+ gate=self.control_inflight,
367
+ )
368
+
369
+ if not result or result["status"] != HTTP_OK or not result["json"]:
370
+ logger.debug(f"Lease acquisition failed: key={key_str}")
371
+ return None
372
+
373
+ lease_data = result["json"]
374
+ offsets = [(o["offset"], o["len"]) for o in lease_data.get("offsets", [])]
375
+
376
+ if not offsets:
377
+ logger.debug(f"Lease has no offsets: key={key_str}")
378
+ return None
379
+
380
+ lease_info = LeaseInfo(
381
+ lease_id=lease_data["id"],
382
+ offsets=offsets,
383
+ )
384
+
385
+ total_size = sum(length for _, length in offsets)
386
+
387
+ if (
388
+ self.max_lease_size_bytes is not None
389
+ and total_size > self.max_lease_size_bytes
390
+ ):
391
+ logger.warning(
392
+ f"Lease size {total_size / 1024:.2f} KB exceeds limit "
393
+ f"{self.max_lease_size_bytes / 1024:.2f} KB, releasing"
394
+ )
395
+ await self._release_lease(key, lease_info.lease_id)
396
+ return None
397
+
398
+ self.stats["lease_acquired"] += 1
399
+
400
+ logger.debug(
401
+ f"Lease acquired: key={key_str}, lease_id={lease_info.lease_id}, "
402
+ f"size={total_size / 1024:.2f} KB, blocks={len(offsets)}"
403
+ )
404
+
405
+ return lease_info
406
+
407
+ async def _executor_submit_lease_acquisition(
408
+ self, key: CacheEngineKey
409
+ ) -> Optional[LeaseInfo]:
410
+ """Submit lease acquisition to priority executor."""
411
+ return await self.pq_executor.submit_job(
412
+ self._acquire_lease,
413
+ key=key,
414
+ priority=Priorities.LEASE,
415
+ )
416
+
417
+ def _read_from_shared_memory(
418
+ self, key: CacheEngineKey, lease_info: LeaseInfo
419
+ ) -> Optional[MemoryObj]:
420
+ """
421
+ Read data from shared memory using lease offsets.
422
+
423
+ Data format: [RemoteMetadata header (28 bytes)] + [KV cache payload]
424
+ Data may be fragmented across multiple blocks in shared memory.
425
+
426
+ Args:
427
+ key: The cache key being read
428
+ lease_info: Lease information with memory offsets
429
+
430
+ Returns:
431
+ MemoryObj containing the data, or None on error
432
+ """
433
+ if self.shared_memory_map is None:
434
+ logger.error("Shared memory not available")
435
+ return None
436
+
437
+ if not lease_info.offsets:
438
+ logger.error("No offsets in lease")
439
+ return None
440
+
441
+ shm_size = len(self.shared_memory_map)
442
+ for offset, length in lease_info.offsets:
443
+ if offset < 0 or length < 0:
444
+ logger.error(
445
+ f"Invalid offset or length: offset={offset}, length={length}"
446
+ )
447
+ return None
448
+ if offset + length > shm_size:
449
+ logger.error(
450
+ f"Offset out of bounds: offset={offset}, length={length}, "
451
+ f"shm_size={shm_size}"
452
+ )
453
+ return None
454
+
455
+ memory_obj = None
456
+ try:
457
+ # Validate total size
458
+ total_size = sum(length for _, length in lease_info.offsets)
459
+ if total_size < METADATA_SIZE_BYTES:
460
+ logger.error(f"Insufficient data for metadata: {total_size} bytes")
461
+ return None
462
+
463
+ # Read metadata header (may span multiple blocks)
464
+ header = self._read_bytes_from_offsets(
465
+ lease_info.offsets, 0, METADATA_SIZE_BYTES
466
+ )
467
+ if len(header) < METADATA_SIZE_BYTES:
468
+ logger.error("Failed to read complete metadata header")
469
+ return None
470
+
471
+ # Parse metadata
472
+ metadata = RemoteMetadata.deserialize(header)
473
+ if metadata.length <= 0:
474
+ logger.error(f"Invalid payload length: {metadata.length}")
475
+ return None
476
+
477
+ # Restore original shape (remove padding zeros)
478
+ actual_shape = self._parse_shape(metadata.shapes[0])
479
+
480
+ # Allocate local CPU memory
481
+ memory_obj = self.local_cpu_backend.allocate(
482
+ actual_shape,
483
+ metadata.dtypes[0],
484
+ metadata.fmt,
485
+ )
486
+ if memory_obj is None:
487
+ logger.error(f"Failed to allocate memory for key {key.to_string()}")
488
+ return None
489
+
490
+ # Get writable view
491
+ view = self._get_writable_view(memory_obj.byte_array)
492
+
493
+ # Copy payload data from shared memory (skip header)
494
+ copied = self._copy_bytes_from_offsets(
495
+ lease_info.offsets, METADATA_SIZE_BYTES, metadata.length, view
496
+ )
497
+
498
+ if copied != metadata.length:
499
+ logger.error(
500
+ f"Data size mismatch: expected {metadata.length}, got {copied}"
501
+ )
502
+ memory_obj.ref_count_down()
503
+ return None
504
+
505
+ logger.debug(
506
+ "Read from shared memory: key=%s, shape=%s, dtype=%s, size=%s bytes",
507
+ key.to_string(),
508
+ actual_shape,
509
+ metadata.dtypes[0],
510
+ metadata.length,
511
+ )
512
+
513
+ return memory_obj
514
+
515
+ except Exception as e:
516
+ logger.error(
517
+ f"Error reading from shared memory: key={key.to_string()} - {e}"
518
+ )
519
+ if memory_obj is not None:
520
+ memory_obj.ref_count_down()
521
+ return None
522
+
523
+ def _read_bytes_from_offsets(
524
+ self, offsets: List[Tuple[int, int]], skip_bytes: int, read_bytes: int
525
+ ) -> bytearray:
526
+ """Read bytes from shared memory offsets, skipping initial bytes."""
527
+ if self.shared_memory_map is None:
528
+ logger.error("Shared memory not available")
529
+ return bytearray()
530
+
531
+ result = bytearray(read_bytes)
532
+ filled = 0
533
+ bytes_to_skip = skip_bytes
534
+ shm_size = len(self.shared_memory_map)
535
+
536
+ for offset, length in offsets:
537
+ if filled >= read_bytes:
538
+ break
539
+
540
+ # Skip header bytes in first chunk(s)
541
+ if bytes_to_skip > 0:
542
+ if length <= bytes_to_skip:
543
+ bytes_to_skip -= length
544
+ continue
545
+ offset += bytes_to_skip
546
+ length -= bytes_to_skip
547
+ bytes_to_skip = 0
548
+
549
+ if length <= 0:
550
+ continue
551
+
552
+ take = min(read_bytes - filled, length)
553
+
554
+ if offset < 0 or take <= 0:
555
+ logger.error(f"Invalid read parameters: offset={offset}, take={take}")
556
+ break
557
+ if offset + take > shm_size:
558
+ logger.error(
559
+ f"Read would exceed shared memory bounds: "
560
+ f"offset={offset}, take={take}, shm_size={shm_size}"
561
+ )
562
+ break
563
+
564
+ result[filled : filled + take] = self.shared_memory_map[
565
+ offset : offset + take
566
+ ]
567
+ filled += take
568
+
569
+ return result
570
+
571
+ def _copy_bytes_from_offsets(
572
+ self,
573
+ offsets: List[Tuple[int, int]],
574
+ skip_bytes: int,
575
+ copy_bytes: int,
576
+ dest_view: memoryview,
577
+ ) -> int:
578
+ """Copy bytes from shared memory offsets to destination view."""
579
+ if self.shared_memory_map is None:
580
+ logger.error("Shared memory not available")
581
+ return 0
582
+
583
+ copied = 0
584
+ bytes_to_skip = skip_bytes
585
+ shm_size = len(self.shared_memory_map)
586
+
587
+ for offset, length in offsets:
588
+ if copied >= copy_bytes:
589
+ break
590
+
591
+ # Skip header bytes
592
+ if bytes_to_skip > 0:
593
+ if length <= bytes_to_skip:
594
+ bytes_to_skip -= length
595
+ continue
596
+ offset += bytes_to_skip
597
+ length -= bytes_to_skip
598
+ bytes_to_skip = 0
599
+
600
+ if length <= 0:
601
+ continue
602
+
603
+ take = min(copy_bytes - copied, length)
604
+
605
+ if offset < 0 or take <= 0:
606
+ logger.error(f"Invalid copy parameters: offset={offset}, take={take}")
607
+ break
608
+ if offset + take > shm_size:
609
+ logger.error(
610
+ f"Copy would exceed shared memory bounds: "
611
+ f"offset={offset}, take={take}, shm_size={shm_size}"
612
+ )
613
+ break
614
+
615
+ dest_view[copied : copied + take] = self.shared_memory_map[
616
+ offset : offset + take
617
+ ]
618
+ copied += take
619
+
620
+ return copied
621
+
622
+ @staticmethod
623
+ def _parse_shape(shape: torch.Size) -> torch.Size:
624
+ """Parse shape from metadata, removing padding zeros."""
625
+ actual_shape_list: List[int] = []
626
+ for dim in shape:
627
+ if dim == 0 and len(actual_shape_list) > 0:
628
+ break
629
+ actual_shape_list.append(dim)
630
+ return torch.Size(actual_shape_list) if actual_shape_list else torch.Size([1])
631
+
632
+ @staticmethod
633
+ def _get_writable_view(byte_array) -> memoryview:
634
+ """Get a writable memoryview from byte array."""
635
+ if isinstance(byte_array, memoryview):
636
+ view = byte_array
637
+ if getattr(view, "format", None) == "<B":
638
+ view = view.cast("B")
639
+ else:
640
+ view = memoryview(byte_array)
641
+ return view
642
+
643
+ async def exists(self, key: CacheEngineKey) -> bool:
644
+ """
645
+ Check if a key exists in remote storage.
646
+
647
+ Acquires a lease, checks existence, then releases immediately.
648
+ """
649
+ lease = await self._executor_submit_lease_acquisition(key)
650
+ if lease is None:
651
+ return False
652
+
653
+ try:
654
+ return True
655
+ finally:
656
+ await self._release_lease(key, lease.lease_id)
657
+
658
+ def exists_sync(self, key: CacheEngineKey) -> bool:
659
+ """Check if a key exists in remote storage (sync wrapper)."""
660
+ future = asyncio.run_coroutine_threadsafe(self.exists(key), self.loop)
661
+ return bool(future.result())
662
+
663
+ async def get(self, key: CacheEngineKey) -> Optional[MemoryObj]:
664
+ """
665
+ Retrieve KV cache data for the given key with metrics reporting.
666
+
667
+ Flow:
668
+ 1. Acquire a new lease
669
+ 2. Read from shared memory using lease offsets
670
+ 3. Release lease immediately (in finally block)
671
+ 4. Report metrics
672
+
673
+ Args:
674
+ key: The cache key to retrieve
675
+
676
+ Returns:
677
+ MemoryObj containing the KV cache data, or None if not found
678
+ """
679
+ begin = time.perf_counter()
680
+
681
+ lease_info = await self._executor_submit_lease_acquisition(key)
682
+
683
+ if lease_info is None:
684
+ self.stats["get_failure"] += 1
685
+ logger.debug(f"GET failed (no lease): key={key.to_string()}")
686
+ return None
687
+
688
+ try:
689
+ memory_obj = self._read_from_shared_memory(key, lease_info)
690
+
691
+ if memory_obj is not None:
692
+ end = time.perf_counter()
693
+ obj_size = memory_obj.get_size()
694
+
695
+ # Report metrics for successful get
696
+ self._stats_monitor.update_interval_remote_time_to_get(
697
+ (end - begin) * 1000
698
+ )
699
+ self._stats_monitor.update_interval_remote_read_metrics(obj_size)
700
+
701
+ self.stats["get_success"] += 1
702
+ logger.debug(
703
+ f"GET success: key={key.to_string()}, "
704
+ f"shape={memory_obj.get_shape()}, "
705
+ f"size={obj_size / 1e6:.3f} MBytes in {(end - begin) * 1000:.3f}ms"
706
+ )
707
+ else:
708
+ self.stats["get_failure"] += 1
709
+ logger.error(
710
+ f"Failed to read from shared memory: key={key.to_string()}"
711
+ )
712
+
713
+ return memory_obj
714
+
715
+ except Exception as e:
716
+ self.stats["get_failure"] += 1
717
+ logger.error(f"GET error: key={key.to_string()} - {e}")
718
+ return None
719
+
720
+ finally:
721
+ # Always release lease immediately after read
722
+ await self._release_lease(key, lease_info.lease_id)
723
+
724
+ async def batched_get(
725
+ self, keys: List[CacheEngineKey]
726
+ ) -> List[Optional[MemoryObj]]:
727
+ """Get multiple keys in parallel. Metrics reported per individual get."""
728
+ tasks = [self.get(key) for key in keys]
729
+ return await asyncio.gather(*tasks)
730
+
731
+ def support_batched_put(self) -> bool:
732
+ """Indicate support for batched PUT operations."""
733
+ return True
734
+
735
+ async def batched_put(
736
+ self, keys: List[CacheEngineKey], memory_objs: List[MemoryObj]
737
+ ):
738
+ """Store multiple objects in parallel. Metrics reported per individual put."""
739
+ await asyncio.gather(
740
+ *(self.put(key, mem) for key, mem in zip(keys, memory_objs, strict=True))
741
+ )
742
+
743
+ async def put(self, key: CacheEngineKey, memory_obj: MemoryObj):
744
+ """Store data to ai-toolkit (queued with priority)."""
745
+ return await self.pq_executor.submit_job(
746
+ self._put,
747
+ key=key,
748
+ memory_obj=memory_obj,
749
+ priority=Priorities.PUT,
750
+ )
751
+
752
+ async def _put(self, key: CacheEngineKey, memory_obj: MemoryObj):
753
+ """Internal PUT operation - sends data via HTTP streaming."""
754
+ key_str = self._key_to_string(key)
755
+ url = f"{self.base_url}/v1/kv/{self.bucket_name}/{key_str}"
756
+
757
+ begin = time.perf_counter()
758
+
759
+ try:
760
+ # Build streaming payload (header + data)
761
+ payload_len, payload_iter = self._build_put_stream(memory_obj)
762
+
763
+ logger.debug(
764
+ f"PUT: key={key_str}, size={payload_len / 1024:.2f} KB, "
765
+ f"shape={memory_obj.get_shape()}"
766
+ )
767
+
768
+ # Send HTTP PUT request with streaming
769
+ result = await self._http_request(
770
+ "PUT",
771
+ url,
772
+ data=payload_iter,
773
+ timeout=self.timeout_ms / 1000.0,
774
+ headers={"Content-Length": str(payload_len)},
775
+ gate=self.put_inflight,
776
+ )
777
+
778
+ end = time.perf_counter()
779
+
780
+ if result and result["status"] == HTTP_OK:
781
+ self.stats["put_success"] += 1
782
+
783
+ # Report metrics for successful put
784
+ self._stats_monitor.update_interval_remote_time_to_put(
785
+ (end - begin) * 1000
786
+ )
787
+ self._stats_monitor.update_interval_remote_write_metrics(payload_len)
788
+
789
+ logger.info(
790
+ f"PUT success: key={key_str}, size={payload_len / 1024:.2f} KB "
791
+ f"in {(end - begin) * 1000:.3f}ms"
792
+ )
793
+ elif result and result["status"] == HTTP_CONFLICT:
794
+ # 409 Conflict = key already exists (not an error)
795
+ self.stats["put_success"] += 1
796
+
797
+ # Still report metrics for conflict case
798
+ self._stats_monitor.update_interval_remote_time_to_put(
799
+ (end - begin) * 1000
800
+ )
801
+ self._stats_monitor.update_interval_remote_write_metrics(payload_len)
802
+
803
+ logger.debug(f"PUT skipped (already exists): key={key_str}")
804
+ else:
805
+ status = result["status"] if result else "TIMEOUT"
806
+ self.stats["put_failure"] += 1
807
+ logger.error(f"PUT failed: key={key_str}, status={status}")
808
+
809
+ except Exception as e:
810
+ self.stats["put_failure"] += 1
811
+ logger.error(f"PUT exception: key={key_str} - {e}")
812
+
813
+ def _build_put_stream(self, memory_obj: MemoryObj) -> Tuple[int, AsyncIterator]:
814
+ """
815
+ Build streaming payload: [RemoteMetadata (28 bytes)] + [KV cache data]
816
+
817
+ Args:
818
+ memory_obj: The memory object to stream
819
+
820
+ Returns:
821
+ Tuple of (total_length, async_generator)
822
+ """
823
+ # Prepare data view
824
+ kv_view = self._get_writable_view(memory_obj.byte_array)
825
+ kv_len = len(kv_view)
826
+
827
+ # Prepare metadata
828
+ shape = list(memory_obj.get_shape())
829
+ padded_shape = (shape + [0] * METADATA_SHAPE_DIMS)[:METADATA_SHAPE_DIMS]
830
+
831
+ metadata = RemoteMetadata(
832
+ kv_len,
833
+ [torch.Size(padded_shape)],
834
+ [memory_obj.get_dtype()],
835
+ memory_obj.get_memory_format(),
836
+ )
837
+
838
+ # Serialize metadata header
839
+ header = bytearray(METADATA_SIZE_BYTES)
840
+ metadata.serialize_into(header)
841
+ header_bytes = bytes(header)
842
+
843
+ total_len = len(header_bytes) + kv_len
844
+ chunk_size = self.put_stream_chunk_bytes
845
+
846
+ async def generator() -> AsyncIterator:
847
+ # First yield header
848
+ yield header_bytes
849
+ # Then yield data in chunks
850
+ offset = 0
851
+ while offset < kv_len:
852
+ next_offset = min(kv_len, offset + chunk_size)
853
+ yield kv_view[offset:next_offset]
854
+ offset = next_offset
855
+
856
+ return total_len, generator()
857
+
858
+ def support_batched_async_contains(self) -> bool:
859
+ """Indicate support for batched async contains operation."""
860
+ return True
861
+
862
+ async def _batched_async_contains(
863
+ self, lookup_id: str, keys: List[CacheEngineKey], pin: bool = False
864
+ ) -> int:
865
+ """
866
+ Check existence of keys sequentially until first miss.
867
+
868
+ Args:
869
+ lookup_id: Lookup identifier (for logging/tracking)
870
+ keys: List of keys to check
871
+ pin: Whether to pin data (unused, for API compatibility)
872
+
873
+ Returns:
874
+ Number of consecutive hits from the start
875
+ """
876
+ num_hits = 0
877
+ for key in keys:
878
+ lease = None
879
+ try:
880
+ lease = await self._executor_submit_lease_acquisition(key)
881
+ if lease is None:
882
+ break
883
+
884
+ num_hits += 1
885
+
886
+ except Exception as exc:
887
+ logger.debug(f"Lease acquisition failed for {key}: {exc}")
888
+ break
889
+ finally:
890
+ if lease is not None:
891
+ await self._release_lease(key, lease.lease_id)
892
+
893
+ return num_hits
894
+
895
+ async def batched_async_contains(
896
+ self, lookup_id: str, keys: List[CacheEngineKey], pin: bool = False
897
+ ) -> int:
898
+ """Check existence of multiple keys (queued with priority)."""
899
+ return await self.pq_executor.submit_job(
900
+ self._batched_async_contains,
901
+ lookup_id=lookup_id,
902
+ keys=keys,
903
+ pin=pin,
904
+ priority=Priorities.LEASE,
905
+ )
906
+
907
+ def support_batched_get_non_blocking(self) -> bool:
908
+ """Indicate support for non-blocking batched GET."""
909
+ return True
910
+
911
+ async def _batched_get_non_blocking(
912
+ self, lookup_id: str, keys: List[CacheEngineKey]
913
+ ) -> List[MemoryObj]:
914
+ """Prefetch multiple keys and filter out None results."""
915
+ results = await self.batched_get(keys)
916
+ return [r for r in results if r is not None]
917
+
918
+ async def batched_get_non_blocking(
919
+ self, lookup_id: str, keys: List[CacheEngineKey]
920
+ ) -> List[MemoryObj]:
921
+ """Prefetch multiple keys (queued with priority)."""
922
+ return await self.pq_executor.submit_job(
923
+ self._batched_get_non_blocking,
924
+ lookup_id=lookup_id,
925
+ keys=keys,
926
+ priority=Priorities.PREFETCH,
927
+ )
928
+
929
+ def support_batched_get(self) -> bool:
930
+ """Indicate support for batched GET operations."""
931
+ return True
932
+
933
+ async def list(self) -> List[str]:
934
+ """List operation not supported by ai-toolkit."""
935
+ return []
936
+
937
+ def remove_sync(self, key: CacheEngineKey) -> bool:
938
+ """Remove operation not supported by ai-toolkit."""
939
+ return True
940
+
941
+ def support_ping(self) -> bool:
942
+ """Indicate ping operation is not supported."""
943
+ return False
944
+
945
+ async def ping(self) -> int:
946
+ """Ping operation not implemented."""
947
+ raise NotImplementedError(
948
+ "Ping operation not supported by SageMaker HyperPod connector"
949
+ )
950
+
951
+ async def close(self):
952
+ """Clean up all resources and log statistics."""
953
+ # Log final statistics
954
+ logger.info(
955
+ f"SageMaker HyperPod Connector Statistics: "
956
+ f"GET(ok/fail)={self.stats['get_success']}/{self.stats['get_failure']}, "
957
+ f"PUT(ok/fail)={self.stats['put_success']}/{self.stats['put_failure']}, "
958
+ f"leases(acq/rel/fail)={self.stats['lease_acquired']}/"
959
+ f"{self.stats['lease_released']}/{self.stats['lease_release_failed']}"
960
+ )
961
+
962
+ # Shutdown priority queue executor
963
+ try:
964
+ await self.pq_executor.shutdown(wait=True)
965
+ except Exception as e:
966
+ logger.warning(f"Error shutting down executor: {e}")
967
+
968
+ # Close HTTP session
969
+ if self.http_session is not None:
970
+ try:
971
+ await self.http_session.close()
972
+ except Exception as e:
973
+ logger.warning(f"Error closing HTTP session: {e}")
974
+ self.http_session = None
975
+
976
+ # Release shared memory
977
+ if self.shared_memory_map is not None:
978
+ self.shared_memory_map = None
979
+
980
+ if self.shared_memory_obj is not None:
981
+ try:
982
+ self.shared_memory_obj.close()
983
+ except Exception as e:
984
+ logger.warning(f"Error closing shared memory object: {e}")
985
+ self.shared_memory_obj = None
986
+
987
+ logger.info("SageMaker HyperPod connector closed")