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,732 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Standard
3
+ from concurrent.futures import Future, ThreadPoolExecutor
4
+ from dataclasses import dataclass
5
+ from typing import Any, Callable, Generic, Optional, TypeVar, get_type_hints
6
+ import inspect
7
+ import os
8
+ import queue
9
+ import threading
10
+ import uuid
11
+
12
+ # Third Party
13
+ import msgspec
14
+ import zmq
15
+
16
+ # First Party
17
+ from lmcache.logging import init_logger
18
+ from lmcache.v1.multiprocess.affinity_pool import AffinityThreadPool
19
+ from lmcache.v1.multiprocess.custom_types import (
20
+ CudaIPCWrapper,
21
+ get_customized_decoder,
22
+ get_customized_encoder,
23
+ )
24
+ from lmcache.v1.multiprocess.futures import (
25
+ MessagingFuture,
26
+ )
27
+ from lmcache.v1.multiprocess.protocol import (
28
+ HandlerType,
29
+ RequestType,
30
+ get_payload_classes,
31
+ get_response_class,
32
+ )
33
+
34
+ logger = init_logger(__name__)
35
+
36
+ T = TypeVar("T")
37
+
38
+ # Internal type used for the client-server communication
39
+ RequestUID = int
40
+
41
+
42
+ # Helper functions
43
+ def encode_request_uid(uid: RequestUID) -> bytes:
44
+ return msgspec.msgpack.encode(uid)
45
+
46
+
47
+ def decode_request_uid(b_uid: bytes) -> RequestUID:
48
+ return msgspec.msgpack.decode(b_uid, type=RequestUID)
49
+
50
+
51
+ def unwrap_request_payloads(
52
+ b_payloads: list[bytes], payload_clss: list[Any]
53
+ ) -> list[Any]:
54
+ if len(b_payloads) != len(payload_clss):
55
+ raise ValueError("Payload count does not match expected count")
56
+
57
+ decoded_payloads = [
58
+ msgspec_decode(payload, cls=cls)
59
+ for payload, cls in zip(b_payloads, payload_clss, strict=False)
60
+ ]
61
+ return decoded_payloads
62
+
63
+
64
+ def prepare_internal_push_pull_sockets(
65
+ ctx: zmq.Context,
66
+ ) -> tuple[zmq.Socket, zmq.Socket]:
67
+ """Create 2 inproc socket pair for the zmq-poller compatible task
68
+ queue
69
+
70
+ Returns:
71
+ tuple[zmq.Socket, zmq.Socket]: The (push_socket, pull_socket)
72
+ """
73
+ inproc_url = "inproc://mq_internal_push_pull/" + str(uuid.uuid4())
74
+ push_socket = ctx.socket(zmq.PUSH)
75
+ pull_socket = ctx.socket(zmq.PULL)
76
+ pull_socket.bind(inproc_url)
77
+ push_socket.connect(inproc_url)
78
+ return push_socket, pull_socket
79
+
80
+
81
+ _SPECIAL_ENCODER_DECODERS = {
82
+ CudaIPCWrapper: (
83
+ get_customized_encoder(CudaIPCWrapper),
84
+ get_customized_decoder(CudaIPCWrapper),
85
+ ),
86
+ list[CudaIPCWrapper]: (
87
+ get_customized_encoder(list[CudaIPCWrapper]),
88
+ get_customized_decoder(list[CudaIPCWrapper]),
89
+ ),
90
+ }
91
+
92
+
93
+ def msgspec_encode(obj: Any, cls: Any) -> bytes:
94
+ # Handle special cases
95
+ if cls in _SPECIAL_ENCODER_DECODERS:
96
+ encoder, _ = _SPECIAL_ENCODER_DECODERS[cls]
97
+ return encoder.encode(obj)
98
+ return msgspec.msgpack.encode(obj)
99
+
100
+
101
+ def msgspec_decode(b_obj: bytes, cls: Any) -> Any:
102
+ # Handle special cases
103
+ if cls in _SPECIAL_ENCODER_DECODERS:
104
+ _, decoder = _SPECIAL_ENCODER_DECODERS[cls]
105
+ return decoder.decode(b_obj)
106
+ return msgspec.msgpack.decode(b_obj, type=cls)
107
+
108
+
109
+ # Main classes
110
+ class MessageQueueClient:
111
+ @dataclass
112
+ class WrappedRequest:
113
+ request_uid: RequestUID
114
+ future: MessagingFuture[Any]
115
+ request_type: RequestType
116
+ request_payloads: list[Any]
117
+
118
+ def __init__(self, server_url: str, context: zmq.Context):
119
+ # Socket
120
+ self.ctx = context
121
+ self.socket = self.ctx.socket(zmq.DEALER)
122
+ self.socket.connect(server_url)
123
+
124
+ # Input queue
125
+ self.task_notifier, self.task_waiter = prepare_internal_push_pull_sockets(
126
+ self.ctx
127
+ )
128
+ self.input_queue: queue.Queue = queue.Queue()
129
+
130
+ # Poller
131
+ self.poller = zmq.Poller()
132
+ self.poller.register(self.socket, zmq.POLLIN)
133
+ self.poller.register(self.task_waiter, zmq.POLLIN)
134
+
135
+ # main thread
136
+ self.is_finished = threading.Event()
137
+ self.worker_thread = threading.Thread(
138
+ target=self._main_loop, daemon=True, name="mq-client-thread"
139
+ )
140
+ self.worker_thread.start()
141
+
142
+ # Pending job's futures
143
+ self.request_counter = 0
144
+ self.pending_futures: dict[int, MessagingFuture[Any]] = {}
145
+
146
+ def _process_outbound_task(self):
147
+ try:
148
+ while wrapped_request := self.input_queue.get_nowait():
149
+ # wrapped_request = self.input_queue.get_nowait()
150
+
151
+ # Update the pending futures
152
+ request_uid = wrapped_request.request_uid
153
+ self.pending_futures[request_uid] = wrapped_request.future
154
+
155
+ # Send the request
156
+ b_request_uid = msgspec_encode(request_uid, cls=RequestUID)
157
+ b_request_type = msgspec_encode(
158
+ wrapped_request.request_type, cls=RequestType
159
+ )
160
+ payload_classes = get_payload_classes(wrapped_request.request_type)
161
+ if len(payload_classes) != len(wrapped_request.request_payloads):
162
+ raise ValueError("Payload count does not match expected count")
163
+
164
+ b_payloads = [
165
+ msgspec_encode(payload, cls=cls)
166
+ for payload, cls in zip(
167
+ wrapped_request.request_payloads,
168
+ payload_classes,
169
+ strict=False,
170
+ )
171
+ ]
172
+ self.socket.send_multipart([b_request_uid, b_request_type] + b_payloads)
173
+ except queue.Empty:
174
+ pass
175
+
176
+ def _main_loop(self):
177
+ # NOTE: make sure we only edit the pending_futures dict in this thread
178
+ while not self.is_finished.is_set():
179
+ socks = dict(self.poller.poll(1000))
180
+ inbound_state = socks.get(self.socket, None)
181
+ outbound_state = socks.get(self.task_waiter, None)
182
+
183
+ if outbound_state and outbound_state & zmq.POLLIN:
184
+ # Drain the notifier
185
+ while True:
186
+ try:
187
+ self.task_waiter.recv(zmq.DONTWAIT)
188
+ except zmq.Again:
189
+ break
190
+
191
+ # Process the output tasks
192
+ self._process_outbound_task()
193
+
194
+ if inbound_state and inbound_state & zmq.POLLIN:
195
+ msg = self.socket.recv_multipart()
196
+ assert len(msg) >= 2, (
197
+ "Expected at least 2 message part "
198
+ "[request_uid, request_type, *response]"
199
+ )
200
+ b_request_uid, b_request_type, *b_response = msg
201
+ request_uid = msgspec_decode(b_request_uid, cls=RequestUID)
202
+ request_type = msgspec_decode(b_request_type, cls=RequestType)
203
+ response_cls = get_response_class(request_type)
204
+
205
+ if request_uid in self.pending_futures:
206
+ future = self.pending_futures.pop(request_uid)
207
+ if b_response:
208
+ response = msgspec_decode(b_response[0], cls=response_cls)
209
+ future.set_result(response)
210
+ else:
211
+ future.set_result(None)
212
+
213
+ def submit_request(
214
+ self,
215
+ request_type: RequestType,
216
+ request_payloads: list[Any],
217
+ response_cls: Optional[T] = None,
218
+ ) -> MessagingFuture[T]:
219
+ """Submit a request to the server.
220
+
221
+ Args:
222
+ request_type (RequestType): The type of the request.
223
+ request_payloads (list[Any]): The payloads of the request.
224
+ response_cls (Optional[T]): The expected response class.
225
+ This should be get from `get_response_class(request_type)`.
226
+
227
+ Returns:
228
+ MessagingFuture[T]: A future that will hold the response.
229
+ """
230
+ future: MessagingFuture[T] = MessagingFuture()
231
+ request_uid = self.request_counter
232
+ self.request_counter += 1
233
+ self.input_queue.put(
234
+ MessageQueueClient.WrappedRequest(
235
+ request_uid=request_uid,
236
+ future=future,
237
+ request_type=request_type,
238
+ request_payloads=request_payloads,
239
+ )
240
+ )
241
+ self.task_notifier.send(b"1")
242
+ return future
243
+
244
+ def close(self) -> None:
245
+ self.is_finished.set()
246
+ self.worker_thread.join()
247
+ self.socket.close()
248
+
249
+
250
+ ResponseType = TypeVar("ResponseType", covariant=True)
251
+ StateType = TypeVar("StateType", covariant=True)
252
+
253
+
254
+ class RequestHandlerBase(Generic[ResponseType]):
255
+ def __call__(self, payloads: list[bytes]):
256
+ raise NotImplementedError
257
+
258
+ def get_response_class(self) -> ResponseType:
259
+ raise NotImplementedError
260
+
261
+ def get_handler_type(self) -> HandlerType:
262
+ raise NotImplementedError
263
+
264
+
265
+ class SyncRequestHandler(RequestHandlerBase[ResponseType]):
266
+ """
267
+ The handler for those "fast" functions that can be executed in the main loop
268
+ """
269
+
270
+ def __init__(
271
+ self,
272
+ payload_clss: list[Any],
273
+ response_cls: ResponseType,
274
+ handler: Callable[..., ResponseType],
275
+ ):
276
+ self.payload_clss = payload_clss
277
+ self.response_cls = response_cls
278
+ self.handler = handler
279
+
280
+ def __call__(self, payloads: list[bytes]) -> ResponseType:
281
+ return self.handler(*unwrap_request_payloads(payloads, self.payload_clss))
282
+
283
+ def get_response_class(self) -> ResponseType:
284
+ return self.response_cls
285
+
286
+ def get_handler_type(self) -> HandlerType:
287
+ return HandlerType.SYNC
288
+
289
+
290
+ class BlockingRequestHandler(RequestHandlerBase[ResponseType]):
291
+ """
292
+ Returns the future of the response.
293
+
294
+ The ``executor`` field is initially ``None`` and must be assigned via
295
+ :meth:`MessageQueueServer.add_normal_thread_pool` or
296
+ :meth:`MessageQueueServer.add_affinity_thread_pool` before the server
297
+ is started.
298
+ """
299
+
300
+ def __init__(
301
+ self,
302
+ payload_clss: list[Any],
303
+ response_cls: ResponseType,
304
+ handler: Callable[..., ResponseType],
305
+ ):
306
+ self.executor: ThreadPoolExecutor | AffinityThreadPool | None = None
307
+ self.payload_clss = payload_clss
308
+ self.handler = handler
309
+ self.response_cls = response_cls
310
+
311
+ def __call__(
312
+ self, payloads: list[bytes], affinity_key: int = 0
313
+ ) -> Future[ResponseType]:
314
+ assert self.executor is not None, (
315
+ "BlockingRequestHandler has no executor assigned. "
316
+ "Call add_normal_thread_pool or add_affinity_thread_pool first."
317
+ )
318
+ decoded_payloads = unwrap_request_payloads(payloads, self.payload_clss)
319
+ if isinstance(self.executor, AffinityThreadPool):
320
+ return self.executor.submit(
321
+ self.handler, *decoded_payloads, affinity_key=affinity_key
322
+ )
323
+ return self.executor.submit(self.handler, *decoded_payloads)
324
+
325
+ def get_response_class(self) -> ResponseType:
326
+ return self.response_cls
327
+
328
+ def get_handler_type(self) -> HandlerType:
329
+ return HandlerType.BLOCKING
330
+
331
+
332
+ class NonBlockingRequestHandler(Generic[ResponseType, StateType]):
333
+ """
334
+ The handler for the "fire and probe" functions that launch async tasks
335
+ and have special mechanism to probe the task status.
336
+
337
+ It requires 2 callables as the input:
338
+ - the first one is to launch the async task. This function should return
339
+ a 'state handle' that can be used to probe the task status later.
340
+ - the second one is to probe the task status and get the return value
341
+ with the 'state handle' returned by the first function.
342
+ """
343
+
344
+ # TODO: implement this in the future versions if needed
345
+ pass
346
+
347
+
348
+ class MessageQueueServer:
349
+ def __init__(self, bind_url: str, context: zmq.Context):
350
+ # Socket
351
+ self.ctx = context
352
+ self.socket = self.ctx.socket(zmq.ROUTER)
353
+ self.socket.bind(bind_url)
354
+ # Use eventfd instead of zmq PUSH/PULL sockets because blocking
355
+ # handler callbacks run on ThreadPoolExecutor threads, and zmq
356
+ # sockets are not thread-safe. eventfd_write() is atomic.
357
+ self._output_efd = os.eventfd(0, os.EFD_NONBLOCK | os.EFD_CLOEXEC)
358
+ self.output_queue: queue.Queue = queue.Queue()
359
+
360
+ # Poller
361
+ self.poller = zmq.Poller()
362
+ self.poller.register(self.socket, zmq.POLLIN)
363
+ self.poller.register(self._output_efd, zmq.POLLIN)
364
+
365
+ # Main loop thread
366
+ self.is_finished = threading.Event()
367
+ self.worker_thread = threading.Thread(
368
+ target=self._main_loop, daemon=True, name="mq-server-thread"
369
+ )
370
+
371
+ # Registered handlers: request_type -> (payload_cls, handler)
372
+ self.handlers: dict[RequestType, RequestHandlerBase[Any]] = {}
373
+
374
+ # Thread pools assigned via add_normal_thread_pool / add_affinity_thread_pool
375
+ self.extra_pools: list[ThreadPoolExecutor | AffinityThreadPool] = []
376
+
377
+ def _call_sync_handler(
378
+ self,
379
+ handler_entry: SyncRequestHandler[Any],
380
+ payloads: list[bytes],
381
+ prefix_frames: list[bytes],
382
+ ) -> Any:
383
+ """
384
+ Call the sync handler and send the response back to the client.
385
+
386
+ Args:
387
+ handler_entry (SyncRequestHandler[Any]): The handler entry.
388
+ payloads (list[bytes]): The payloads of the request.
389
+ prefix_frames (list[bytes]): The prefix frames to send back.
390
+ """
391
+ response = handler_entry(payloads)
392
+ response_cls = handler_entry.get_response_class()
393
+ b_response = msgspec_encode(response, cls=response_cls)
394
+ if response is not None:
395
+ self.socket.send_multipart(prefix_frames + [b_response])
396
+ else:
397
+ self.socket.send_multipart(prefix_frames)
398
+
399
+ def _call_blocking_handler(
400
+ self,
401
+ handler_entry: BlockingRequestHandler[Any],
402
+ payloads: list[bytes],
403
+ prefix_frames: list[bytes],
404
+ ) -> Any:
405
+ """
406
+ Call the blocking handler in a separate thread and send the response
407
+ back to the client.
408
+
409
+ Args:
410
+ handler_entry (BlockingRequestHandler[Any]): The handler entry.
411
+ payloads (list[bytes]): The payloads of the request.
412
+ prefix_frames (list[bytes]): The prefix frames to send back.
413
+ prefix_frames[0] is the zmq identity used as affinity key.
414
+ """
415
+ affinity_key = hash(prefix_frames[0])
416
+ future = handler_entry(payloads, affinity_key=affinity_key)
417
+
418
+ def _notify_response(fut: Future):
419
+ try:
420
+ response = fut.result()
421
+ response_cls = handler_entry.get_response_class()
422
+ b_response = msgspec_encode(response, cls=response_cls)
423
+ frames_to_send = (
424
+ prefix_frames + [b_response]
425
+ if response is not None
426
+ else prefix_frames
427
+ )
428
+
429
+ self.output_queue.put(frames_to_send)
430
+ os.eventfd_write(self._output_efd, 1)
431
+
432
+ except Exception:
433
+ logger.exception("Error in blocking handler")
434
+
435
+ future.add_done_callback(_notify_response)
436
+
437
+ def _call_handler(
438
+ self,
439
+ handler_entry: RequestHandlerBase[Any],
440
+ payloads: list[bytes],
441
+ prefix_frames: list[bytes],
442
+ ) -> Any:
443
+ match handler_entry.get_handler_type():
444
+ case HandlerType.SYNC:
445
+ assert isinstance(handler_entry, SyncRequestHandler)
446
+ self._call_sync_handler(handler_entry, payloads, prefix_frames)
447
+ case HandlerType.BLOCKING:
448
+ assert isinstance(handler_entry, BlockingRequestHandler)
449
+ self._call_blocking_handler(handler_entry, payloads, prefix_frames)
450
+ case HandlerType.NON_BLOCKING:
451
+ raise NotImplementedError("Non-blocking handler is not supported yet")
452
+ case _:
453
+ raise ValueError("Unknown handler type")
454
+
455
+ def _main_loop(self):
456
+ while not self.is_finished.is_set():
457
+ socks = dict(self.poller.poll(1000))
458
+ inbound_state = socks.get(self.socket, None)
459
+ outbound_state = socks.get(self._output_efd, None)
460
+
461
+ # Process the incoming requests
462
+ if inbound_state and inbound_state & zmq.POLLIN:
463
+ msg = self.socket.recv_multipart()
464
+ assert len(msg) >= 3, (
465
+ "Expected at least 3 message parts "
466
+ "[identity, request_uid, request_type, *payloads]"
467
+ )
468
+
469
+ identity, b_request_uid, b_request_type, *payloads = msg
470
+ request_type = msgspec_decode(b_request_type, cls=RequestType)
471
+
472
+ if handler_entry := self.handlers.get(request_type):
473
+ try:
474
+ self._call_handler(
475
+ handler_entry=handler_entry,
476
+ payloads=payloads,
477
+ prefix_frames=[identity, b_request_uid, b_request_type],
478
+ )
479
+ except Exception:
480
+ logger.exception("Error handling request %s", request_type)
481
+ else:
482
+ logger.error(
483
+ "No handler registered for request type %s", request_type
484
+ )
485
+ logger.error("Available handlers: %s", list(self.handlers.keys()))
486
+
487
+ # Send the responses
488
+ if outbound_state and outbound_state & zmq.POLLIN:
489
+ # Consume the eventfd counter (resets atomically)
490
+ os.eventfd_read(self._output_efd)
491
+
492
+ # Process the output tasks
493
+ try:
494
+ while frames_to_send := self.output_queue.get_nowait():
495
+ self.socket.send_multipart(frames_to_send)
496
+ except queue.Empty:
497
+ pass
498
+
499
+ def _inspect_handler_signature(self, request_type: RequestType, handler) -> bool:
500
+ """Inspect the handler signature to ensure it matches the expected
501
+ payload classes.
502
+
503
+ Args:
504
+ handler (callable): The handler function.
505
+
506
+ Returns:
507
+ bool: True if the signature matches, False otherwise.
508
+ """
509
+
510
+ def same_type(a, b) -> bool:
511
+ if a is None:
512
+ a = type(None)
513
+ if b is None:
514
+ b = type(None)
515
+ return a == b
516
+
517
+ sig = inspect.signature(handler)
518
+ hints = get_type_hints(handler)
519
+ params = [
520
+ p
521
+ for p in sig.parameters.values()
522
+ if p.kind
523
+ in (
524
+ inspect.Parameter.POSITIONAL_ONLY,
525
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
526
+ )
527
+ ]
528
+
529
+ payload_clss = get_payload_classes(request_type)
530
+ if len(params) != len(payload_clss):
531
+ logger.error(
532
+ "Handler for %s expects %d arguments, but got %d",
533
+ request_type,
534
+ len(payload_clss),
535
+ len(params),
536
+ )
537
+ return False
538
+
539
+ for i, (param, expected_cls) in enumerate(
540
+ zip(params, payload_clss, strict=False)
541
+ ):
542
+ ann = hints.get(param.name, param.annotation)
543
+ if not same_type(ann, expected_cls):
544
+ logger.error(
545
+ "Handler for %s argument %d expects type %s, but got %s",
546
+ request_type,
547
+ i,
548
+ expected_cls,
549
+ ann,
550
+ )
551
+ return False
552
+
553
+ return_ann = hints.get("return", sig.return_annotation)
554
+ expected_return_cls = get_response_class(request_type)
555
+ if not same_type(return_ann, expected_return_cls):
556
+ logger.error(
557
+ "Handler for %s expects return type %s, but got %s",
558
+ request_type,
559
+ expected_return_cls,
560
+ return_ann,
561
+ )
562
+ return False
563
+ return True
564
+
565
+ def add_handler(
566
+ self,
567
+ request_type: RequestType,
568
+ payload_clss: list[Any],
569
+ handler_type: HandlerType,
570
+ handler,
571
+ ) -> None:
572
+ """Register a handler for a specific request type.
573
+
574
+ Args:
575
+ request_type (RequestType): The type of the request to handle.
576
+ payload_clss (list[Any]): The expected payload classes for the request.
577
+ This should be get from `get_payload_classes(request_type)`.
578
+ handler (callable): The handler function that takes the payloads
579
+ as arguments.
580
+ """
581
+ if not self._inspect_handler_signature(request_type, handler):
582
+ raise ValueError(
583
+ f"Handler signature does not match for request type: {request_type}"
584
+ )
585
+
586
+ match handler_type:
587
+ case HandlerType.SYNC:
588
+ self.add_sync_handler(request_type, payload_clss, handler)
589
+ case HandlerType.BLOCKING:
590
+ self.add_blocking_handler(request_type, payload_clss, handler)
591
+ case HandlerType.NON_BLOCKING:
592
+ raise NotImplementedError("Non-blocking handler is not supported yet")
593
+ case _:
594
+ raise ValueError(f"Unknown handler type: {handler_type}")
595
+
596
+ def add_sync_handler(
597
+ self, request_type: RequestType, payload_clss: list[Any], handler
598
+ ) -> None:
599
+ response_cls = get_response_class(request_type)
600
+ self.handlers[request_type] = SyncRequestHandler(
601
+ payload_clss, response_cls, handler
602
+ )
603
+
604
+ def add_blocking_handler(
605
+ self, request_type: RequestType, payload_clss: list[Any], handler
606
+ ) -> None:
607
+ response_cls = get_response_class(request_type)
608
+ self.handlers[request_type] = BlockingRequestHandler(
609
+ payload_clss, response_cls, handler
610
+ )
611
+
612
+ def add_nonblocking_handler(
613
+ self, request_type: RequestType, payload_clss: list[Any], handler
614
+ ) -> None:
615
+ raise NotImplementedError
616
+
617
+ def _validate_blocking_handlers(
618
+ self,
619
+ request_types: list[RequestType],
620
+ method_name: str,
621
+ ) -> None:
622
+ """Validate that all request types are registered BlockingRequestHandlers."""
623
+ for request_type in request_types:
624
+ handler = self.handlers.get(request_type)
625
+ if handler is None:
626
+ raise ValueError(
627
+ f"No handler registered for request type: {request_type}. "
628
+ f"Register handlers before calling {method_name}."
629
+ )
630
+ if not isinstance(handler, BlockingRequestHandler):
631
+ raise TypeError(
632
+ f"Handler for {request_type} is "
633
+ f"{type(handler).__name__}, not BlockingRequestHandler. "
634
+ f"Only blocking handlers can use thread pools."
635
+ )
636
+
637
+ def add_normal_thread_pool(
638
+ self,
639
+ request_types: list[RequestType],
640
+ max_workers: int,
641
+ ) -> None:
642
+ """Assign a ThreadPoolExecutor to specific request types.
643
+
644
+ Use this for non-GPU blocking handlers (e.g. LOOKUP, END_SESSION).
645
+
646
+ Must be called after the handlers are registered (via add_handler /
647
+ add_blocking_handler) and before start(). Each request_type must
648
+ already be registered as a BlockingRequestHandler; otherwise a
649
+ ValueError or TypeError is raised.
650
+
651
+ Args:
652
+ request_types: The request types that should use this pool.
653
+ max_workers: Number of worker threads in the pool.
654
+ """
655
+ self._validate_blocking_handlers(request_types, "add_normal_thread_pool")
656
+ if not request_types:
657
+ return
658
+
659
+ pool = ThreadPoolExecutor(
660
+ max_workers=max_workers,
661
+ thread_name_prefix=f"normal-pool-{len(self.extra_pools)}",
662
+ )
663
+ self.extra_pools.append(pool)
664
+ for request_type in request_types:
665
+ handler = self.handlers[request_type]
666
+ assert isinstance(handler, BlockingRequestHandler)
667
+ handler.executor = pool
668
+
669
+ logger.debug(
670
+ "Created normal thread pool (max_workers=%d) for request types: %s",
671
+ max_workers,
672
+ [rt.name for rt in request_types],
673
+ )
674
+
675
+ def add_affinity_thread_pool(
676
+ self,
677
+ request_types: list[RequestType],
678
+ max_workers: int,
679
+ ) -> None:
680
+ """Assign an AffinityThreadPool to specific request types.
681
+
682
+ Use this for GPU-bound blocking handlers (e.g. STORE, RETRIEVE).
683
+ Requests from the same zmq client identity are always dispatched
684
+ to the same worker thread, eliminating the need for per-instance
685
+ GPU transfer locks.
686
+
687
+ Must be called after the handlers are registered (via add_handler /
688
+ add_blocking_handler) and before start().
689
+
690
+ Args:
691
+ request_types: The request types that should use this pool.
692
+ max_workers: Number of worker threads in the pool.
693
+ """
694
+ self._validate_blocking_handlers(request_types, "add_affinity_thread_pool")
695
+ if not request_types:
696
+ return
697
+
698
+ pool = AffinityThreadPool(
699
+ max_workers=max_workers,
700
+ thread_name_prefix=f"affinity-pool-{len(self.extra_pools)}",
701
+ )
702
+ self.extra_pools.append(pool)
703
+ for request_type in request_types:
704
+ handler = self.handlers[request_type]
705
+ assert isinstance(handler, BlockingRequestHandler)
706
+ handler.executor = pool
707
+
708
+ logger.debug(
709
+ "Created affinity thread pool (max_workers=%d) for request types: %s",
710
+ max_workers,
711
+ [rt.name for rt in request_types],
712
+ )
713
+
714
+ def start(self):
715
+ # Validate all blocking handlers have an executor assigned
716
+ for rt, handler in self.handlers.items():
717
+ if isinstance(handler, BlockingRequestHandler) and handler.executor is None:
718
+ raise RuntimeError(
719
+ f"BlockingRequestHandler for {rt} has no thread pool "
720
+ f"assigned. Call add_normal_thread_pool or "
721
+ f"add_affinity_thread_pool before start()."
722
+ )
723
+ self.worker_thread.start()
724
+
725
+ def close(self) -> None:
726
+ self.is_finished.set()
727
+ if self.worker_thread.is_alive():
728
+ self.worker_thread.join()
729
+ self.socket.close()
730
+ for pool in self.extra_pools:
731
+ pool.shutdown(wait=False)
732
+ os.close(self._output_efd)