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,289 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """Per-qualname latency statistics for trace replay.
4
+
5
+ The replay driver times every dispatched call. Timings feed into this
6
+ collector, which computes count + mean + percentiles (p50, p90, p99)
7
+ per qualname.
8
+
9
+ The shape is deliberately simpler than
10
+ :class:`lmcache.cli.commands.bench.engine_bench.stats.StatsCollector`
11
+ — that one is tailored to OpenAI-style streaming inference (TTFT,
12
+ decode speed, etc.), which is not applicable to in-process storage
13
+ replay. Sharing the computation code between the two would push a
14
+ small helper deep into the bench module; keeping this collector local
15
+ keeps the storage-replay code self-contained and easy to evolve.
16
+ """
17
+
18
+ # Future
19
+ from __future__ import annotations
20
+
21
+ # Standard
22
+ from dataclasses import dataclass, field
23
+ import csv
24
+ import json
25
+ import statistics
26
+ import threading
27
+
28
+ # First Party
29
+ from lmcache.logging import init_logger
30
+
31
+ logger = init_logger(__name__)
32
+
33
+
34
+ @dataclass
35
+ class OpStats:
36
+ """Aggregate timing stats for one qualname.
37
+
38
+ Attributes:
39
+ qualname: The qualname these stats cover.
40
+ count: Number of successful replays for this qualname.
41
+ error_count: Number of replays that raised.
42
+ total_s: Total wall time spent replaying this qualname.
43
+ mean_ms: Mean per-call latency in milliseconds.
44
+ p50_ms: 50th-percentile latency in milliseconds.
45
+ p90_ms: 90th-percentile latency in milliseconds.
46
+ p99_ms: 99th-percentile latency in milliseconds.
47
+ min_ms: Minimum observed latency in milliseconds.
48
+ max_ms: Maximum observed latency in milliseconds.
49
+ """
50
+
51
+ qualname: str
52
+ count: int
53
+ error_count: int
54
+ total_s: float
55
+ mean_ms: float
56
+ p50_ms: float
57
+ p90_ms: float
58
+ p99_ms: float
59
+ min_ms: float
60
+ max_ms: float
61
+
62
+
63
+ @dataclass
64
+ class _Bucket:
65
+ """Internal per-qualname sample bucket."""
66
+
67
+ latencies_ms: list[float] = field(default_factory=list)
68
+ errors: int = 0
69
+
70
+
71
+ def _percentile(sorted_values: list[float], pct: float) -> float:
72
+ """Return the *pct*-th percentile from an already-sorted list.
73
+
74
+ Uses nearest-rank with no interpolation: for N samples, the
75
+ p-th percentile is the sample at index ``ceil(p/100 * N) - 1``.
76
+ Returns 0.0 for empty input.
77
+
78
+ Args:
79
+ sorted_values: Ascending-sorted list of values.
80
+ pct: Percentile in ``[0, 100]``.
81
+
82
+ Returns:
83
+ The percentile value, or 0.0 for empty input.
84
+ """
85
+ if not sorted_values:
86
+ return 0.0
87
+ if pct <= 0:
88
+ return sorted_values[0]
89
+ if pct >= 100:
90
+ return sorted_values[-1]
91
+ # bisect_left gives the insertion index; the nearest-rank formula
92
+ # maps p to ceil(p/100 * N) which equals floor((p/100 * N - eps) + 1).
93
+ n = len(sorted_values)
94
+ idx = max(0, min(n - 1, int((pct / 100.0) * n)))
95
+ return sorted_values[idx]
96
+
97
+
98
+ class ReplayStatsCollector:
99
+ """Thread-safe per-qualname latency collector.
100
+
101
+ The replay driver is single-threaded at the dispatcher boundary,
102
+ but the underlying StorageManager performs async work on helper
103
+ threads whose timings may eventually feed back here. A lock keeps
104
+ concurrent ``record()`` calls safe.
105
+ """
106
+
107
+ def __init__(self) -> None:
108
+ self._lock = threading.Lock()
109
+ self._buckets: dict[str, _Bucket] = {}
110
+ self._wall_start_s: float | None = None
111
+ self._wall_end_s: float | None = None
112
+
113
+ def mark_start(self, wall_time_s: float) -> None:
114
+ """Record the wall-clock time when replay began.
115
+
116
+ Called once before the first ``record()``; replay-duration
117
+ metrics are derived from start/end marks.
118
+
119
+ Args:
120
+ wall_time_s: ``time.time()`` at replay start.
121
+ """
122
+ with self._lock:
123
+ self._wall_start_s = wall_time_s
124
+
125
+ def mark_end(self, wall_time_s: float) -> None:
126
+ """Record the wall-clock time when replay finished.
127
+
128
+ Args:
129
+ wall_time_s: ``time.time()`` at replay end.
130
+ """
131
+ with self._lock:
132
+ self._wall_end_s = wall_time_s
133
+
134
+ def record(self, qualname: str, latency_s: float, failed: bool = False) -> None:
135
+ """Record one replayed call.
136
+
137
+ Args:
138
+ qualname: Qualified name of the replayed function.
139
+ latency_s: Elapsed seconds for the call.
140
+ failed: ``True`` if the call raised. Failed calls still
141
+ contribute to the count but do not add a latency
142
+ sample — the raising path's timing is not comparable
143
+ to successful calls.
144
+ """
145
+ with self._lock:
146
+ bucket = self._buckets.get(qualname)
147
+ if bucket is None:
148
+ bucket = _Bucket()
149
+ self._buckets[qualname] = bucket
150
+ if failed:
151
+ bucket.errors += 1
152
+ return
153
+ # Append is O(1); the one-shot sort in :meth:`summary` is
154
+ # O(N log N), which beats the O(N) shift from keeping the
155
+ # list sorted on insert. For large traces (>1M records
156
+ # per qualname) the driver should sample or switch to an
157
+ # approximation; for now, exact percentiles suffice.
158
+ bucket.latencies_ms.append(latency_s * 1000.0)
159
+
160
+ def total_duration_s(self) -> float:
161
+ """Return replay wall-clock duration in seconds.
162
+
163
+ Returns:
164
+ ``mark_end - mark_start`` if both were set, else 0.0.
165
+ """
166
+ with self._lock:
167
+ if self._wall_start_s is None or self._wall_end_s is None:
168
+ return 0.0
169
+ return max(0.0, self._wall_end_s - self._wall_start_s)
170
+
171
+ def summary(self) -> dict[str, OpStats]:
172
+ """Return a per-qualname :class:`OpStats` snapshot.
173
+
174
+ Returns:
175
+ A dict keyed by qualname. A qualname with only errors
176
+ still appears, with zero latency stats.
177
+ """
178
+ with self._lock:
179
+ result: dict[str, OpStats] = {}
180
+ for qualname, bucket in self._buckets.items():
181
+ # Sort once per summary call — ``record`` keeps the
182
+ # list unsorted (O(1) append) so the total work is
183
+ # O(N log N) per summary rather than O(N) per insert.
184
+ lats = sorted(bucket.latencies_ms)
185
+ if lats:
186
+ mean = statistics.fmean(lats)
187
+ total_s = sum(lats) / 1000.0
188
+ p50 = _percentile(lats, 50)
189
+ p90 = _percentile(lats, 90)
190
+ p99 = _percentile(lats, 99)
191
+ lo, hi = lats[0], lats[-1]
192
+ else:
193
+ mean = total_s = p50 = p90 = p99 = lo = hi = 0.0
194
+ result[qualname] = OpStats(
195
+ qualname=qualname,
196
+ count=len(lats),
197
+ error_count=bucket.errors,
198
+ total_s=total_s,
199
+ mean_ms=mean,
200
+ p50_ms=p50,
201
+ p90_ms=p90,
202
+ p99_ms=p99,
203
+ min_ms=lo,
204
+ max_ms=hi,
205
+ )
206
+ return result
207
+
208
+ def export_csv(self, path: str) -> None:
209
+ """Write per-qualname stats to a CSV file.
210
+
211
+ Columns: ``qualname, count, errors, mean_ms, p50_ms, p90_ms,
212
+ p99_ms, min_ms, max_ms``. One row per qualname.
213
+
214
+ Args:
215
+ path: File path to write. Overwritten if it exists.
216
+ """
217
+ summary = self.summary()
218
+ with open(path, "w", newline="") as f:
219
+ w = csv.writer(f)
220
+ w.writerow(
221
+ [
222
+ "qualname",
223
+ "count",
224
+ "errors",
225
+ "mean_ms",
226
+ "p50_ms",
227
+ "p90_ms",
228
+ "p99_ms",
229
+ "min_ms",
230
+ "max_ms",
231
+ ]
232
+ )
233
+ for qn in sorted(summary):
234
+ s = summary[qn]
235
+ w.writerow(
236
+ [
237
+ s.qualname,
238
+ s.count,
239
+ s.error_count,
240
+ f"{s.mean_ms:.6f}",
241
+ f"{s.p50_ms:.6f}",
242
+ f"{s.p90_ms:.6f}",
243
+ f"{s.p99_ms:.6f}",
244
+ f"{s.min_ms:.6f}",
245
+ f"{s.max_ms:.6f}",
246
+ ]
247
+ )
248
+
249
+ def export_json(self, path: str) -> None:
250
+ """Write per-qualname stats + replay duration to a JSON file.
251
+
252
+ Schema::
253
+
254
+ {
255
+ "duration_s": <float>,
256
+ "ops": {
257
+ "<qualname>": {
258
+ "count": int, "errors": int,
259
+ "mean_ms": float, "p50_ms": float,
260
+ "p90_ms": float, "p99_ms": float,
261
+ "min_ms": float, "max_ms": float
262
+ },
263
+ ...
264
+ }
265
+ }
266
+
267
+ Args:
268
+ path: File path to write. Overwritten if it exists.
269
+ """
270
+ summary = self.summary()
271
+ payload = {
272
+ "duration_s": self.total_duration_s(),
273
+ "ops": {
274
+ qn: {
275
+ "count": s.count,
276
+ "errors": s.error_count,
277
+ "mean_ms": s.mean_ms,
278
+ "p50_ms": s.p50_ms,
279
+ "p90_ms": s.p90_ms,
280
+ "p99_ms": s.p99_ms,
281
+ "min_ms": s.min_ms,
282
+ "max_ms": s.max_ms,
283
+ }
284
+ for qn, s in summary.items()
285
+ },
286
+ }
287
+ with open(path, "w") as f:
288
+ json.dump(payload, f, indent=2, sort_keys=True)
289
+ f.write("\n")
@@ -0,0 +1,11 @@
1
+ LMCache is a high-performance key–value (KV) cache management system designed to accelerate large language model (LLM) inference by efficiently storing, transferring, and reusing intermediate attention states. As modern LLM serving increasingly becomes bottlenecked by memory bandwidth, redundant computation, and cross-device communication, LMCache provides a system-level solution that decouples KV cache storage from the model execution pipeline and enables scalable, low-latency reuse across requests, processes, and even distributed nodes.
2
+
3
+ At its core, LMCache targets one of the most expensive components of autoregressive inference: the KV cache generated during the prefill phase. In conventional serving systems, this cache is tightly coupled to a single process or GPU, making it difficult to reuse across requests or share between instances. As a result, repeated prompts or multi-turn conversations often trigger redundant computation, increasing both latency and resource consumption. LMCache addresses this limitation by introducing a unified KV cache abstraction that can be externally managed, retrieved asynchronously, and seamlessly reintegrated into the decoding pipeline.
4
+
5
+ A key design principle of LMCache is minimizing the impact of cache operations on the critical path of inference. By supporting asynchronous KV retrieval and background prefetching, LMCache allows decoding to proceed without blocking on cache transfers. This is particularly important in disaggregated or multi-process (MP) deployments, where different inference engines may collaborate through a shared cache backend. In such settings, LMCache enables one instance to reuse KV states computed by another, significantly reducing time-to-first-token (TTFT) and improving tail latency. Empirical results show substantial gains in multi-turn and stateful workloads, where reuse opportunities are abundant.
6
+
7
+ LMCache also provides flexible support for heterogeneous memory and storage backends, including GPU memory, host memory, persistent storage (e.g., SSD or DAX devices), and high-speed interconnects such as RDMA. Through pluggable connectors and configurable transfer policies, it can adapt to diverse deployment environments, from single-node setups to large-scale distributed clusters. This flexibility enables system designers to balance trade-offs between latency, capacity, and cost, while maintaining high throughput.
8
+
9
+ Beyond performance optimization, LMCache emphasizes observability and integration. It exposes detailed metrics for cache lookup, retrieval, and storage operations, allowing users to understand system behavior and diagnose bottlenecks. It is designed to integrate seamlessly with popular serving frameworks such as vLLM, requiring minimal changes to existing workflows while unlocking advanced caching capabilities.
10
+
11
+ In summary, LMCache rethinks KV cache management as a first-class system component for LLM serving. By enabling efficient reuse, asynchronous data movement, and cross-instance sharing, it addresses fundamental inefficiencies in current inference pipelines and paves the way for more scalable, responsive, and resource-efficient AI systems.
lmcache/cli/main.py ADDED
@@ -0,0 +1,42 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """LMCache CLI entry point.
3
+
4
+ Subcommands are explicitly registered in
5
+ ``lmcache.cli.commands.ALL_COMMANDS``.
6
+ """
7
+
8
+ # Standard
9
+ import argparse
10
+ import sys
11
+
12
+ # First Party
13
+ from lmcache.cli.commands import ALL_COMMANDS
14
+ from lmcache.logging import init_logger
15
+
16
+ logger = init_logger(__name__)
17
+
18
+
19
+ def main() -> None:
20
+ """CLI entry point registered as ``lmcache`` in *pyproject.toml*."""
21
+ parser = argparse.ArgumentParser(
22
+ prog="lmcache",
23
+ description="LMCache — KV cache management for LLM serving",
24
+ )
25
+ subparsers = parser.add_subparsers(dest="command")
26
+
27
+ for cmd in ALL_COMMANDS:
28
+ cmd.register(subparsers)
29
+
30
+ args = parser.parse_args()
31
+
32
+ if not hasattr(args, "func"):
33
+ parser.print_help()
34
+ sys.exit(1)
35
+
36
+ try:
37
+ args.func(args)
38
+ except KeyboardInterrupt:
39
+ sys.exit(130)
40
+ except Exception: # noqa: BLE001
41
+ logger.exception("Command failed")
42
+ sys.exit(1)
@@ -0,0 +1,29 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Metrics package — collector, handlers, and formatters."""
3
+
4
+ # First Party
5
+ from lmcache.cli.metrics.formatter import (
6
+ JsonFormatter,
7
+ MetricsFormatter,
8
+ TerminalFormatter,
9
+ get_formatter,
10
+ )
11
+ from lmcache.cli.metrics.handler import (
12
+ FileHandler,
13
+ MetricsHandler,
14
+ StreamHandler,
15
+ )
16
+ from lmcache.cli.metrics.metrics import Metrics
17
+ from lmcache.cli.metrics.section import Section
18
+
19
+ __all__ = [
20
+ "FileHandler",
21
+ "JsonFormatter",
22
+ "get_formatter",
23
+ "Metrics",
24
+ "MetricsFormatter",
25
+ "MetricsHandler",
26
+ "Section",
27
+ "StreamHandler",
28
+ "TerminalFormatter",
29
+ ]
@@ -0,0 +1,171 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Metrics formatters — control *how* metrics are rendered.
3
+
4
+ A formatter converts a title + sections into a string (or dict).
5
+ Formatters are attached to handlers, separating rendering from destination.
6
+ """
7
+
8
+ # Standard
9
+ from typing import Any
10
+ import abc
11
+ import inspect
12
+ import json
13
+
14
+ # First Party
15
+ from lmcache.cli.metrics.section import Section, sections_to_dict
16
+
17
+
18
+ class MetricsFormatter(abc.ABC):
19
+ """Base class for metrics formatters."""
20
+
21
+ @abc.abstractmethod
22
+ def format(self, title: str, sections: list[Section]) -> str:
23
+ """Render metrics into a string.
24
+
25
+ Args:
26
+ title: The report title.
27
+ sections: Ordered list of ``Section`` objects.
28
+
29
+ Returns:
30
+ The formatted string.
31
+ """
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Formatter registry
36
+ # ---------------------------------------------------------------------------
37
+
38
+ _FORMATTER_REGISTRY: dict[str, type[MetricsFormatter]] = {}
39
+
40
+
41
+ def register_formatter(name: str):
42
+ """Decorator that registers a ``MetricsFormatter`` subclass under *name*.
43
+
44
+ Args:
45
+ name: The format name used for CLI lookup (e.g. ``"json"``).
46
+
47
+ Returns:
48
+ A class decorator that registers the class and returns it unchanged.
49
+ """
50
+
51
+ def decorator(cls: type[MetricsFormatter]) -> type[MetricsFormatter]:
52
+ _FORMATTER_REGISTRY[name] = cls
53
+ return cls
54
+
55
+ return decorator
56
+
57
+
58
+ def get_formatter(name: str, **kwargs: Any) -> MetricsFormatter:
59
+ """Instantiate a formatter by its registered name.
60
+
61
+ Args:
62
+ name: Registered format name (e.g. ``"terminal"``, ``"json"``).
63
+ **kwargs: Forwarded to the formatter constructor (e.g. ``width``).
64
+
65
+ Returns:
66
+ A new formatter instance.
67
+
68
+ Raises:
69
+ ValueError: If *name* is not registered.
70
+ """
71
+ cls = _FORMATTER_REGISTRY.get(name)
72
+ if cls is None:
73
+ available = ", ".join(sorted(_FORMATTER_REGISTRY))
74
+ raise ValueError(f"Unknown format {name!r}. Available: {available}")
75
+ # Only forward kwargs that the constructor accepts.
76
+ sig = inspect.signature(cls.__init__)
77
+ valid = {k: v for k, v in kwargs.items() if k in sig.parameters}
78
+ return cls(**valid)
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Built-in formatters
83
+ # ---------------------------------------------------------------------------
84
+
85
+
86
+ def _format_value(value: Any) -> str:
87
+ """Format a metric value for terminal display."""
88
+ if value is None:
89
+ return "N/A"
90
+ if isinstance(value, float):
91
+ return f"{value:.2f}"
92
+ return str(value)
93
+
94
+
95
+ @register_formatter("terminal")
96
+ class TerminalFormatter(MetricsFormatter):
97
+ """Plain ASCII table formatter for terminal output.
98
+
99
+ Title is centered in ``=`` borders, section headers are centered in
100
+ ``-`` borders, key-value lines have left-aligned labels and
101
+ right-aligned values.
102
+
103
+ Args:
104
+ width: Target total character width for the output.
105
+ """
106
+
107
+ def __init__(self, width: int = 48) -> None:
108
+ self._width = width
109
+
110
+ def format(self, title: str, sections: list[Section]) -> str:
111
+ """Render metrics as an ASCII table.
112
+
113
+ Args:
114
+ title: The report title.
115
+ sections: Ordered list of ``Section`` objects.
116
+
117
+ Returns:
118
+ Rendered multi-line string.
119
+ """
120
+ width = self._width
121
+ lines: list[str] = []
122
+
123
+ # Title bar
124
+ title_text = f" {title} "
125
+ lines.append(title_text.center(width, "="))
126
+
127
+ for section in sections:
128
+ # Section header (skip for unnamed section)
129
+ if section.label is not None:
130
+ header_text = f" {section.label} "
131
+ lines.append(header_text.center(width, "-"))
132
+
133
+ for _key, label, value in section.entries:
134
+ formatted = _format_value(value)
135
+ label_part = f"{label}:"
136
+ padding = width - len(label_part) - len(formatted)
137
+ if padding < 1:
138
+ padding = 1
139
+ lines.append(f"{label_part}{' ' * padding}{formatted}")
140
+
141
+ # Footer
142
+ lines.append("=" * width)
143
+
144
+ return "\n".join(lines)
145
+
146
+
147
+ @register_formatter("json")
148
+ class JsonFormatter(MetricsFormatter):
149
+ """Renders metrics as a JSON string.
150
+
151
+ Args:
152
+ indent: JSON indentation level.
153
+ """
154
+
155
+ def __init__(self, indent: int = 2) -> None:
156
+ self._indent = indent
157
+
158
+ def format(self, title: str, sections: list[Section]) -> str:
159
+ """Render metrics as indented JSON.
160
+
161
+ Args:
162
+ title: The report title.
163
+ sections: Ordered list of ``Section`` objects.
164
+
165
+ Returns:
166
+ JSON string.
167
+ """
168
+ return json.dumps(
169
+ sections_to_dict(title, sections),
170
+ indent=self._indent,
171
+ )
@@ -0,0 +1,94 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Metrics handlers — control *where* metrics are written.
3
+
4
+ Each handler pairs a destination (stream, file, …) with a
5
+ :class:`~lmcache.cli.metrics.formatter.MetricsFormatter` that controls
6
+ the rendering.
7
+ """
8
+
9
+ # Standard
10
+ from typing import IO, Optional, Union
11
+ import abc
12
+ import os
13
+ import sys
14
+
15
+ # First Party
16
+ from lmcache.cli.metrics.formatter import (
17
+ JsonFormatter,
18
+ MetricsFormatter,
19
+ TerminalFormatter,
20
+ )
21
+ from lmcache.cli.metrics.section import Section
22
+
23
+
24
+ class MetricsHandler(abc.ABC):
25
+ """Base class for metrics handlers."""
26
+
27
+ def __init__(self, formatter: MetricsFormatter) -> None:
28
+ self.formatter = formatter
29
+
30
+ @abc.abstractmethod
31
+ def emit(self, title: str, sections: list[Section]) -> None:
32
+ """Format and write metrics to the destination.
33
+
34
+ Args:
35
+ title: The report title.
36
+ sections: Ordered list of ``Section`` objects.
37
+ """
38
+
39
+
40
+ class StreamHandler(MetricsHandler):
41
+ """Writes formatted metrics to a text stream.
42
+
43
+ Args:
44
+ formatter: The formatter to use for rendering.
45
+ stream: Writable text stream. Defaults to ``sys.stdout``.
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ formatter: Optional[MetricsFormatter] = None,
51
+ stream: Optional[IO[str]] = None,
52
+ ) -> None:
53
+ super().__init__(formatter or TerminalFormatter())
54
+ self._stream = stream
55
+
56
+ def emit(self, title: str, sections: list[Section]) -> None:
57
+ """Format and write metrics to the stream.
58
+
59
+ Args:
60
+ title: The report title.
61
+ sections: Ordered list of ``Section`` objects.
62
+ """
63
+ stream = self._stream or sys.stdout
64
+ stream.write(self.formatter.format(title, sections))
65
+ stream.write("\n")
66
+
67
+
68
+ class FileHandler(MetricsHandler):
69
+ """Writes formatted metrics to a file.
70
+
71
+ Args:
72
+ path: Destination file path.
73
+ formatter: The formatter to use for rendering. Defaults to
74
+ :class:`~lmcache.cli.metrics.formatter.JsonFormatter`.
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ path: Union[str, os.PathLike],
80
+ formatter: Optional[MetricsFormatter] = None,
81
+ ) -> None:
82
+ super().__init__(formatter or JsonFormatter())
83
+ self.path = path
84
+
85
+ def emit(self, title: str, sections: list[Section]) -> None:
86
+ """Format and write metrics to the file.
87
+
88
+ Args:
89
+ title: The report title.
90
+ sections: Ordered list of ``Section`` objects.
91
+ """
92
+ with open(self.path, "w") as f:
93
+ f.write(self.formatter.format(title, sections))
94
+ f.write("\n")