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,795 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """
3
+ Cache hit-rate simulator driven by LMCache lookup-hash JSONL logs.
4
+
5
+ The simulator replays ``MP_LOOKUP`` events recorded by
6
+ :class:`~lmcache.v1.mp_observability.subscribers.logging.lookup_hash.LookupHashLoggingSubscriber`.
7
+ Each event contains the ordered list of *full-chunk* hashes that were looked up
8
+ for a single request, together with the sequence length and chunk size.
9
+
10
+ **Token cache hit rate** (the primary metric) is defined as::
11
+
12
+ token_hit_rate = total_hit_tokens / total_tokens
13
+
14
+ where:
15
+
16
+ * ``total_tokens`` = sum of ``seq_len`` across all requests (includes tail tokens
17
+ that do not fill a complete chunk — these are *always* a miss because LMCache
18
+ only caches complete chunks).
19
+ * ``total_hit_tokens`` = number of tokens covered by a *continuous prefix* of
20
+ cache-hit chunks at the start of each request, i.e.
21
+ ``hit_prefix_chunks × chunk_size``.
22
+
23
+ Running the simulator prints a text report **and** saves a multi-panel PNG with
24
+ seven statistical charts.
25
+
26
+ Usage (module mode)::
27
+
28
+ python3 -m lmcache.tools.cache_simulator.simulator \\
29
+ -i /path/to/lookup_hashes/ \\
30
+ --cache-capacity-gib 64 \\
31
+ -o stats.png
32
+ """
33
+
34
+ # Standard
35
+ from collections import defaultdict
36
+ from pathlib import Path
37
+ from typing import Any
38
+ import argparse
39
+ import json
40
+ import math
41
+ import sys
42
+ import warnings
43
+
44
+ # First Party
45
+ from lmcache.tools.cache_simulator.lru_cache import LRUCache, LRUCacheFast
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Dtype → bytes mapping
49
+ # ---------------------------------------------------------------------------
50
+
51
+ _DTYPE_BYTES: dict[str, int] = {
52
+ "float32": 4,
53
+ "float16": 2,
54
+ "bfloat16": 2,
55
+ "float8_e4m3fn": 1,
56
+ "float8_e5m2": 1,
57
+ "int8": 1,
58
+ "int32": 4,
59
+ "int64": 8,
60
+ }
61
+
62
+ _GIB = 2**30
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Public helpers
67
+ # ---------------------------------------------------------------------------
68
+
69
+
70
+ def compute_kv_bytes_per_chunk(event: dict[str, Any]) -> int:
71
+ """
72
+ Compute the number of KV-cache bytes that one chunk occupies.
73
+
74
+ The value is derived from the ``shapes`` and ``dtypes`` fields of a single
75
+ lookup event. Each ``(shape, dtype)`` pair represents one tensor stored
76
+ per chunk (e.g. key and value tensors for all layers); their byte sizes are
77
+ summed.
78
+
79
+ Returns 0 if ``shapes`` or ``dtypes`` is empty (caller must handle this).
80
+ """
81
+ shapes = event.get("shapes", [])
82
+ dtypes = event.get("dtypes", [])
83
+ if not shapes or not dtypes:
84
+ return 0
85
+ total = 0
86
+ for shape, dt in zip(shapes, dtypes, strict=False):
87
+ elem_bytes = _DTYPE_BYTES.get(dt, 0)
88
+ if elem_bytes == 0:
89
+ warnings.warn(
90
+ f"Unknown dtype '{dt}' — treating as 0 bytes per element.",
91
+ UserWarning,
92
+ stacklevel=2,
93
+ )
94
+ total += math.prod(shape) * elem_bytes
95
+ return total
96
+
97
+
98
+ def load_lookup_events(
99
+ paths: list[Path],
100
+ model: str | None = None,
101
+ max_samples: int | None = None,
102
+ ) -> list[dict[str, Any]]:
103
+ """
104
+ Load and return lookup events from one or more JSONL files or directories.
105
+
106
+ Parameters
107
+ ----------
108
+ paths:
109
+ Each element may be a ``.jsonl`` file or a directory. Directories are
110
+ globbed for ``lookup_hashes_*.jsonl`` files.
111
+ model:
112
+ If given, only events whose ``model_name`` exactly matches this string
113
+ are returned.
114
+ max_samples:
115
+ If given, truncate the final sorted list to this many events.
116
+
117
+ Returns
118
+ -------
119
+ list[dict]
120
+ Events sorted by ``timestamp`` ascending.
121
+ """
122
+ all_events: list[dict[str, Any]] = []
123
+
124
+ for p in paths:
125
+ files: list[Path]
126
+ if p.is_dir():
127
+ files = sorted(p.glob("lookup_hashes_*.jsonl"))
128
+ if not files:
129
+ warnings.warn(
130
+ f"Directory '{p}' contains no lookup_hashes_*.jsonl files.",
131
+ UserWarning,
132
+ stacklevel=2,
133
+ )
134
+ else:
135
+ files = [p]
136
+
137
+ for f in files:
138
+ try:
139
+ with open(f, encoding="utf-8") as fh:
140
+ for lineno, line in enumerate(fh, start=1):
141
+ line = line.strip()
142
+ if not line:
143
+ continue
144
+ try:
145
+ event = json.loads(line)
146
+ except json.JSONDecodeError as exc:
147
+ warnings.warn(
148
+ f"{f}:{lineno}: skipping malformed JSON — {exc}",
149
+ UserWarning,
150
+ stacklevel=2,
151
+ )
152
+ continue
153
+ if model is not None and event.get("model_name") != model:
154
+ continue
155
+ all_events.append(event)
156
+ except OSError as exc:
157
+ warnings.warn(
158
+ f"Could not open '{f}': {exc}",
159
+ UserWarning,
160
+ stacklevel=2,
161
+ )
162
+
163
+ all_events.sort(key=lambda e: e.get("timestamp", 0.0))
164
+
165
+ if max_samples is not None and max_samples > 0:
166
+ all_events = all_events[:max_samples]
167
+
168
+ return all_events
169
+
170
+
171
+ # ---------------------------------------------------------------------------
172
+ # Simulation
173
+ # ---------------------------------------------------------------------------
174
+
175
+
176
+ def simulate(
177
+ events: list[dict[str, Any]],
178
+ cache_capacity_bytes: int,
179
+ kv_bytes_per_chunk: int,
180
+ fast: bool = False,
181
+ ) -> dict[str, Any]:
182
+ """
183
+ Replay lookup events through an LRU cache and compute token hit-rate
184
+ statistics.
185
+
186
+ Parameters
187
+ ----------
188
+ events:
189
+ Lookup events as returned by :func:`load_lookup_events`.
190
+ cache_capacity_bytes:
191
+ Total cache capacity in bytes.
192
+ kv_bytes_per_chunk:
193
+ Bytes consumed by one cached chunk.
194
+ fast:
195
+ If ``True``, use :class:`~lmcache.tools.cache_simulator.lru_cache.LRUCacheFast`
196
+ and skip per-chunk statistics (faster for capacity sweeps).
197
+
198
+ Returns
199
+ -------
200
+ dict
201
+ Simulation results (see source for field list).
202
+ """
203
+ if kv_bytes_per_chunk <= 0:
204
+ raise ValueError(
205
+ "kv_bytes_per_chunk must be > 0. "
206
+ "Either pass --kv-bytes-per-chunk or ensure the JSONL records "
207
+ "contain non-empty 'shapes' and 'dtypes' fields."
208
+ )
209
+
210
+ cache_capacity_chunks = max(1, cache_capacity_bytes // kv_bytes_per_chunk)
211
+
212
+ cache: LRUCacheFast | LRUCache
213
+ if fast:
214
+ cache = LRUCacheFast(cache_capacity_chunks)
215
+ else:
216
+ cache = LRUCache(cache_capacity_chunks)
217
+
218
+ # ── Aggregates ──────────────────────────────────────────────────────────
219
+ total_requests = 0
220
+ total_tokens = 0
221
+ total_hit_tokens = 0
222
+
223
+ # ── Per-request (skipped in fast mode) ──────────────────────────────────
224
+ per_request_token_hit_rates: list[float] = []
225
+ hit_prefix_lengths: list[int] = []
226
+ rolling_token_hit_rate: list[float] = []
227
+ input_lengths: list[int] = []
228
+
229
+ # ── Chunk-level (skipped in fast mode) ──────────────────────────────────
230
+ chunk_reuse_counts: dict[str, int] = defaultdict(int)
231
+ chunk_last_seen: dict[str, int] = {}
232
+ global_span_distribution: list[int] = []
233
+ cache_position_distribution: list[int] = []
234
+ global_chunk_index = 0
235
+
236
+ for event in events:
237
+ hashes: list[str] = event.get("chunk_hashes", [])
238
+ seq_len: int = event.get("seq_len", 0)
239
+ chunk_sz: int = event.get("chunk_size", 1)
240
+
241
+ if not hashes and seq_len == 0:
242
+ continue
243
+
244
+ # ── Prefix hit count ────────────────────────────────────────────────
245
+ hit_prefix = 0
246
+ for h in hashes:
247
+ if cache.contains(h):
248
+ hit_prefix += 1
249
+ else:
250
+ break
251
+
252
+ # ── Token accounting ────────────────────────────────────────────────
253
+ # Tail tokens (seq_len - len(hashes)*chunk_sz) are always a miss.
254
+ hit_tokens = hit_prefix * chunk_sz
255
+ request_tokens = seq_len # includes tail tokens
256
+
257
+ total_requests += 1
258
+ total_tokens += request_tokens
259
+ total_hit_tokens += hit_tokens
260
+
261
+ if not fast:
262
+ input_lengths.append(seq_len)
263
+ per_request_token_hit_rates.append(
264
+ hit_tokens / request_tokens if request_tokens > 0 else 0.0
265
+ )
266
+ hit_prefix_lengths.append(hit_prefix)
267
+ rolling_token_hit_rate.append(
268
+ total_hit_tokens / total_tokens if total_tokens > 0 else 0.0
269
+ )
270
+
271
+ # Per-hit-chunk statistics
272
+ for i, h in enumerate(hashes[:hit_prefix]):
273
+ chunk_reuse_counts[h] += 1
274
+ if h in chunk_last_seen:
275
+ global_span_distribution.append(
276
+ global_chunk_index + i - chunk_last_seen[h]
277
+ )
278
+ if isinstance(cache, LRUCache):
279
+ cache_position_distribution.append(cache.position(h))
280
+
281
+ # ── Update cache ────────────────────────────────────────────────────
282
+ for i, h in enumerate(hashes):
283
+ if not fast:
284
+ chunk_last_seen[h] = global_chunk_index + i
285
+ if i < hit_prefix:
286
+ cache.access(h)
287
+ else:
288
+ cache.insert(h)
289
+
290
+ if not fast:
291
+ global_chunk_index += len(hashes)
292
+
293
+ token_hit_rate = total_hit_tokens / total_tokens if total_tokens > 0 else 0.0
294
+
295
+ return {
296
+ # ── Aggregates ──────────────────────────────────────────────────────
297
+ "total_requests": total_requests,
298
+ "total_tokens": total_tokens,
299
+ "total_hit_tokens": total_hit_tokens,
300
+ "total_miss_tokens": total_tokens - total_hit_tokens,
301
+ "token_hit_rate": token_hit_rate,
302
+ "eviction_count": cache.eviction_count,
303
+ "cache_size_at_end_chunks": len(cache),
304
+ "cache_capacity_chunks": cache_capacity_chunks,
305
+ "cache_capacity_bytes": cache_capacity_bytes,
306
+ "kv_bytes_per_chunk": kv_bytes_per_chunk,
307
+ # ── Per-request ─────────────────────────────────────────────────────
308
+ "per_request_token_hit_rates": per_request_token_hit_rates,
309
+ "hit_prefix_lengths": hit_prefix_lengths,
310
+ "input_lengths": input_lengths,
311
+ "rolling_token_hit_rate": rolling_token_hit_rate,
312
+ # ── Chunk-level ─────────────────────────────────────────────────────
313
+ "chunk_reuse_counts": dict(chunk_reuse_counts),
314
+ "global_span_distribution": global_span_distribution,
315
+ "cache_position_distribution": cache_position_distribution,
316
+ }
317
+
318
+
319
+ # ---------------------------------------------------------------------------
320
+ # Reporting — text
321
+ # ---------------------------------------------------------------------------
322
+
323
+
324
+ def _percentiles(values: list[float], pcts: list[int]) -> dict[str, float]:
325
+ if not values:
326
+ return {}
327
+ s = sorted(values)
328
+ n = len(s)
329
+ result = {}
330
+ for p in pcts:
331
+ idx = min(int(p / 100 * n), n - 1)
332
+ result[f"p{p}"] = s[idx]
333
+ return result
334
+
335
+
336
+ def print_statistics(results: dict[str, Any]) -> None:
337
+ sep = "=" * 60
338
+
339
+ gib = results["cache_capacity_bytes"] / _GIB
340
+ print(sep)
341
+ print("Aggregate")
342
+ print(sep)
343
+ print(f" Requests processed : {results['total_requests']:,}")
344
+ print(f" Total tokens : {results['total_tokens']:,}")
345
+ print(f" Hit tokens : {results['total_hit_tokens']:,}")
346
+ print(f" Miss tokens : {results['total_miss_tokens']:,}")
347
+ print(f" Token hit rate : {results['token_hit_rate']:.2%}")
348
+ print(
349
+ f" Cache capacity : {gib:.2f} GiB "
350
+ f"({results['cache_capacity_chunks']:,} chunks × "
351
+ f"{results['kv_bytes_per_chunk']:,} bytes/chunk)"
352
+ )
353
+ print(
354
+ f" Cache occupancy : {results['cache_size_at_end_chunks']:,} / "
355
+ f"{results['cache_capacity_chunks']:,} chunks"
356
+ )
357
+
358
+ rates = results["per_request_token_hit_rates"]
359
+ if rates:
360
+ zero_hit = sum(1 for r in rates if r == 0.0)
361
+ full_hit = sum(1 for r in rates if r == 1.0)
362
+ pcts = _percentiles(rates, [25, 50, 75, 90, 99])
363
+ print()
364
+ print(sep)
365
+ print("Stat 1 — Per-request token hit rate distribution")
366
+ print(sep)
367
+ print(
368
+ f" Requests with 0% hit rate : "
369
+ f"{zero_hit:,} ({zero_hit / len(rates):.1%})"
370
+ )
371
+ print(
372
+ f" Requests with 100% hit rate : "
373
+ f"{full_hit:,} ({full_hit / len(rates):.1%})"
374
+ )
375
+ print(f" Mean : {sum(rates) / len(rates):.2%}")
376
+ for k, v in pcts.items():
377
+ print(f" {k:4s} : {v:.2%}")
378
+
379
+ lengths = results["hit_prefix_lengths"]
380
+ if lengths:
381
+ pcts_len = _percentiles([float(x) for x in lengths], [25, 50, 75, 90, 99])
382
+ print()
383
+ print(sep)
384
+ print("Stat 2 — Hit prefix length per request (chunks)")
385
+ print(sep)
386
+ print(f" Mean : {sum(lengths) / len(lengths):.1f}")
387
+ for k, v in pcts_len.items():
388
+ print(f" {k:4s} : {v:.0f}")
389
+
390
+ reuse = sorted(results["chunk_reuse_counts"].values())
391
+ if reuse:
392
+ pcts_reuse = _percentiles([float(x) for x in reuse], [25, 50, 75, 90, 99])
393
+ print()
394
+ print(sep)
395
+ print("Stat 3 — Chunk reuse count distribution")
396
+ print(sep)
397
+ print(f" Unique chunks hit at least once : {len(reuse):,}")
398
+ print(f" Mean reuse count : {sum(reuse) / len(reuse):.1f}")
399
+ print(f" Max reuse count : {reuse[-1]:,}")
400
+ for k, v in pcts_reuse.items():
401
+ print(f" {k:4s} : {v:.0f}")
402
+
403
+ rolling = results["rolling_token_hit_rate"]
404
+ if rolling:
405
+ print()
406
+ print(sep)
407
+ print("Stat 4 — Rolling (cumulative) token hit rate over time")
408
+ print(sep)
409
+ n = len(rolling)
410
+ for frac in (0.1, 0.25, 0.5, 0.75, 1.0):
411
+ idx = max(0, min(int(n * frac) - 1, n - 1))
412
+ print(f" After request {idx + 1:>6,} : {rolling[idx]:.2%}")
413
+
414
+ print()
415
+ print(sep)
416
+ print("Stat 5 — Evictions")
417
+ print(sep)
418
+ print(f" Total evictions : {results['eviction_count']:,}")
419
+
420
+ spans = results["global_span_distribution"]
421
+ if spans:
422
+ pcts_span = _percentiles([float(x) for x in spans], [25, 50, 75, 90, 99])
423
+ print()
424
+ print(sep)
425
+ print("Stat 6 — Global span distribution (chunks between last store and hit)")
426
+ print(sep)
427
+ print(f" Total hit chunks : {len(spans):,}")
428
+ print(f" Mean span : {sum(spans) / len(spans):.1f}")
429
+ print(f" Max span : {max(spans):,}")
430
+ for k, v in pcts_span.items():
431
+ print(f" {k:4s} : {v:.0f}")
432
+
433
+ positions = results["cache_position_distribution"]
434
+ if positions:
435
+ pcts_pos = _percentiles([float(x) for x in positions], [25, 50, 75, 90, 99])
436
+ print()
437
+ print(sep)
438
+ print("Stat 7 — Cache position at hit (0 = MRU, max = LRU)")
439
+ print(sep)
440
+ print(f" Mean position : {sum(positions) / len(positions):.1f}")
441
+ print(f" Max position : {max(positions):,}")
442
+ for k, v in pcts_pos.items():
443
+ print(f" {k:4s} : {v:.0f}")
444
+
445
+ print(sep)
446
+
447
+
448
+ # ---------------------------------------------------------------------------
449
+ # Reporting — charts
450
+ # ---------------------------------------------------------------------------
451
+
452
+
453
+ def plot_statistics(
454
+ results: dict[str, Any], events: list[dict[str, Any]], output: str
455
+ ) -> None:
456
+ """
457
+ Render and save a 2×4 multi-panel figure with seven statistical charts.
458
+
459
+ Parameters
460
+ ----------
461
+ results:
462
+ Output of :func:`simulate` with ``fast=False``.
463
+ events:
464
+ The event list used to produce *results* (used for chunk_size label).
465
+ output:
466
+ Output file path (PNG).
467
+ """
468
+ cap_gib = results["cache_capacity_bytes"] / _GIB
469
+ chunk_size = events[0].get("chunk_size", "?") if events else "?"
470
+ n_req = results["total_requests"]
471
+
472
+ per_request_hit_rates = [r * 100 for r in results["per_request_token_hit_rates"]]
473
+ hit_prefix_lengths = results["hit_prefix_lengths"]
474
+ reuse_counts = sorted(results["chunk_reuse_counts"].values())
475
+ rolling = [r * 100 for r in results["rolling_token_hit_rate"]]
476
+ input_lengths = results["input_lengths"]
477
+ global_spans = results["global_span_distribution"]
478
+ cache_positions = results["cache_position_distribution"]
479
+
480
+ # Third Party
481
+ import matplotlib.pyplot as plt # noqa: PLC0415 — lazy import to avoid hard dependency
482
+
483
+ fig, axes = plt.subplots(2, 4, figsize=(22, 10))
484
+ fig.suptitle(
485
+ f"Cache simulation statistics "
486
+ f"(chunk_size={chunk_size} tokens, capacity={cap_gib:.1f} GiB, "
487
+ f"{n_req:,} requests, token hit rate={results['token_hit_rate']:.2%})",
488
+ fontsize=12,
489
+ )
490
+
491
+ # ------------------------------------------------------------------
492
+ # Plot 1 — Per-request token hit rate (non-zero requests only)
493
+ # Two small pies: left = requests hit/miss, right = tokens hit/miss
494
+ # ------------------------------------------------------------------
495
+ ax = axes[0, 0]
496
+ nonzero = [r for r in per_request_hit_rates if r > 0]
497
+ n_zero = len(per_request_hit_rates) - len(nonzero)
498
+ ax.hist(nonzero, bins=50, edgecolor="black", linewidth=0.4)
499
+ ax.set_xlabel("Token hit rate (%) — zero-hit requests excluded")
500
+ ax.set_ylabel("Number of requests")
501
+ ax.set_title("1. Per-request token hit rate")
502
+
503
+ # Left pie — requests
504
+ ax_pie = ax.inset_axes([0.01, 0.52, 0.24, 0.42])
505
+ ax_pie.patch.set_alpha(0)
506
+ wedges, _, _ = ax_pie.pie(
507
+ [len(nonzero), n_zero],
508
+ labels=["hit", "miss"],
509
+ autopct="%1.0f%%",
510
+ startangle=90,
511
+ textprops={"fontsize": 5},
512
+ colors=["#4C72B0", "#DD8452"],
513
+ )
514
+ for w in wedges:
515
+ w.set_alpha(0.6)
516
+ ax_pie.set_title("requests", fontsize=5, pad=2)
517
+ ax_pie.text(
518
+ 0.5,
519
+ -0.08,
520
+ "Fraction of requests\nwith ≥1 chunk hit",
521
+ transform=ax_pie.transAxes,
522
+ fontsize=5,
523
+ ha="center",
524
+ va="top",
525
+ color="dimgray",
526
+ )
527
+
528
+ # Right pie — tokens
529
+ ax_pie2 = ax.inset_axes([0.27, 0.52, 0.24, 0.42])
530
+ ax_pie2.patch.set_alpha(0)
531
+ wedges2, _, _ = ax_pie2.pie(
532
+ [results["total_hit_tokens"], results["total_miss_tokens"]],
533
+ labels=["hit", "miss"],
534
+ autopct="%1.0f%%",
535
+ startangle=90,
536
+ textprops={"fontsize": 5},
537
+ colors=["#4C72B0", "#DD8452"],
538
+ )
539
+ for w in wedges2:
540
+ w.set_alpha(0.6)
541
+ ax_pie2.set_title("tokens", fontsize=5, pad=2)
542
+ ax_pie2.text(
543
+ 0.5,
544
+ -0.08,
545
+ "Fraction of tokens\nserved from cache",
546
+ transform=ax_pie2.transAxes,
547
+ fontsize=5,
548
+ ha="center",
549
+ va="top",
550
+ color="dimgray",
551
+ )
552
+
553
+ # ------------------------------------------------------------------
554
+ # Plot 1b — Zoom into 97–100% hit rate
555
+ # ------------------------------------------------------------------
556
+ ax = axes[0, 1]
557
+ n_full = sum(1 for r in per_request_hit_rates if r == 100)
558
+ high = [r for r in nonzero if r >= 97]
559
+ ax.hist(high, bins=20, edgecolor="black", linewidth=0.4)
560
+ ax.set_xlim(97, 100)
561
+ ax.set_xlabel("Token hit rate (%) — 97–100% zoom")
562
+ ax.set_ylabel("Number of requests")
563
+ ax.set_title("1b. Per-request token hit rate (97–100%)")
564
+ ax.text(
565
+ 0.03,
566
+ 0.95,
567
+ f"100% hit: {n_full:,} requests",
568
+ transform=ax.transAxes,
569
+ fontsize=8,
570
+ ha="left",
571
+ va="top",
572
+ bbox=dict(boxstyle="round,pad=0.3", facecolor="wheat", alpha=0.7),
573
+ )
574
+
575
+ # ------------------------------------------------------------------
576
+ # Plot 2 — Hit prefix length per request (clean histogram, no pie)
577
+ # ------------------------------------------------------------------
578
+ ax = axes[0, 2]
579
+ nonzero_prefix = [n for n in hit_prefix_lengths if n > 0]
580
+ ax.hist(nonzero_prefix, bins=50, edgecolor="black", linewidth=0.4)
581
+ ax.set_xlabel("Hit prefix length (chunks) — zero-hit requests excluded")
582
+ ax.set_ylabel("Number of requests")
583
+ ax.set_title("2. Hit prefix length per request")
584
+
585
+ # Plot 3 — Chunk reuse count
586
+ # ------------------------------------------------------------------
587
+ ax = axes[0, 3]
588
+ if reuse_counts:
589
+ cap = min(max(reuse_counts), 100)
590
+ ax.hist(
591
+ [r for r in reuse_counts if r <= cap],
592
+ bins=range(1, cap + 2),
593
+ edgecolor="black",
594
+ linewidth=0.4,
595
+ )
596
+ if max(reuse_counts) > cap:
597
+ n_above = sum(1 for r in reuse_counts if r > cap)
598
+ pct_above = n_above / len(reuse_counts) * 100
599
+ ax.text(
600
+ 0.97,
601
+ 0.95,
602
+ f"max={max(reuse_counts):,}\n"
603
+ f"{n_above:,} chunks ({pct_above:.1f}%) above cap",
604
+ transform=ax.transAxes,
605
+ fontsize=8,
606
+ ha="right",
607
+ va="top",
608
+ bbox=dict(boxstyle="round,pad=0.3", facecolor="wheat", alpha=0.7),
609
+ )
610
+ ax.set_xlabel("Times a chunk was hit (capped at 100)")
611
+ ax.set_ylabel("Number of unique chunks")
612
+ ax.set_title("3. Chunk reuse count")
613
+
614
+ # ------------------------------------------------------------------
615
+ # Plot 4 — Rolling token hit rate over time
616
+ # ------------------------------------------------------------------
617
+ ax = axes[1, 0]
618
+ ax.plot(range(1, len(rolling) + 1), rolling, linewidth=1.5)
619
+ ax.set_xlabel("Request index")
620
+ ax.set_ylabel("Cumulative token hit rate (%)")
621
+ ax.set_title("4. Rolling token hit rate over time")
622
+ ax.set_ylim(0, 100)
623
+ ax.grid(True, linestyle="--", alpha=0.5)
624
+
625
+ # ------------------------------------------------------------------
626
+ # Plot 5 — Input length distribution
627
+ # ------------------------------------------------------------------
628
+ ax = axes[1, 1]
629
+ ax.hist(input_lengths, bins=50, edgecolor="black", linewidth=0.4)
630
+ ax.set_xlabel("Input length (tokens / seq_len)")
631
+ ax.set_ylabel("Number of requests")
632
+ ax.set_title("5. Input length per request")
633
+
634
+ # ------------------------------------------------------------------
635
+ # Plot 6 — Global span distribution
636
+ # ------------------------------------------------------------------
637
+ ax = axes[1, 2]
638
+ if global_spans:
639
+ ax.hist(global_spans, bins=50, edgecolor="black", linewidth=0.4)
640
+ ax.set_xlabel("Global span (chunks between last store and hit)")
641
+ ax.set_ylabel("Number of hit chunks")
642
+ ax.set_title("6. Global span distribution")
643
+
644
+ # ------------------------------------------------------------------
645
+ # Plot 7 — Cache position at hit time
646
+ # ------------------------------------------------------------------
647
+ ax = axes[1, 3]
648
+ if cache_positions:
649
+ ax.hist(cache_positions, bins=50, edgecolor="black", linewidth=0.4)
650
+ ax.set_xlabel("Cache position (0 = MRU, max = LRU)")
651
+ ax.set_ylabel("Number of hit chunks")
652
+ ax.set_title("7. Cache position at hit")
653
+
654
+ fig.tight_layout()
655
+ fig.savefig(output, dpi=150)
656
+ print(f"\nStats plot saved to '{output}'")
657
+
658
+
659
+ # ---------------------------------------------------------------------------
660
+ # CLI helpers — shared between the module entry point and lmcache tool
661
+ # ---------------------------------------------------------------------------
662
+
663
+
664
+ def add_simulate_arguments(parser: argparse.ArgumentParser) -> None:
665
+ """Register all ``simulate`` CLI flags onto *parser*.
666
+
667
+ Called by both the module ``main()`` and by
668
+ :class:`~lmcache.cli.commands.tool.ToolCommand` so that flag definitions
669
+ live in exactly one place.
670
+
671
+ Args:
672
+ parser: The ``ArgumentParser`` (or sub-parser) to add flags to.
673
+ """
674
+ parser.add_argument(
675
+ "-i",
676
+ "--input",
677
+ nargs="+",
678
+ required=True,
679
+ metavar="PATH",
680
+ help="One or more lookup-hash JSONL files or directories",
681
+ )
682
+ parser.add_argument(
683
+ "-n",
684
+ "--max-samples",
685
+ type=int,
686
+ default=None,
687
+ metavar="N",
688
+ help="Maximum number of events to process (default: all)",
689
+ )
690
+ parser.add_argument(
691
+ "--cache-capacity-gib",
692
+ type=float,
693
+ required=True,
694
+ metavar="GiB",
695
+ help="Cache capacity in gibibytes",
696
+ )
697
+ parser.add_argument(
698
+ "--kv-bytes-per-chunk",
699
+ type=int,
700
+ default=None,
701
+ metavar="BYTES",
702
+ help=(
703
+ "Bytes consumed by one cached chunk. "
704
+ "Auto-computed from the first event's shapes/dtypes if omitted."
705
+ ),
706
+ )
707
+ parser.add_argument(
708
+ "--model",
709
+ default=None,
710
+ metavar="NAME",
711
+ help="Filter events by model_name (exact match)",
712
+ )
713
+ parser.add_argument(
714
+ "-o",
715
+ "--output",
716
+ default="cache_stats.png",
717
+ metavar="FILE",
718
+ help="Output image path (default: cache_stats.png)",
719
+ )
720
+
721
+
722
+ def run_simulate(args: argparse.Namespace) -> None:
723
+ """Execute the simulate workflow from a parsed argument namespace.
724
+
725
+ Loads events, resolves ``kv_bytes_per_chunk``, runs the simulator, prints
726
+ a text report, and saves a statistics PNG. Called by both the module
727
+ ``main()`` and by :class:`~lmcache.cli.commands.tool.ToolCommand`.
728
+
729
+ Args:
730
+ args: Parsed CLI arguments. Must have the attributes registered by
731
+ :func:`add_simulate_arguments`.
732
+ """
733
+ paths = [Path(p) for p in args.input]
734
+ print(f"Loading lookup events from {[str(p) for p in paths]} …")
735
+ events = load_lookup_events(paths, model=args.model, max_samples=args.max_samples)
736
+ print(f"Loaded {len(events):,} event(s)")
737
+
738
+ if not events:
739
+ print("No events to process.")
740
+ sys.exit(0)
741
+
742
+ kv_bpc = args.kv_bytes_per_chunk
743
+ if kv_bpc is None:
744
+ kv_bpc = compute_kv_bytes_per_chunk(events[0])
745
+ if kv_bpc == 0:
746
+ print(
747
+ "Error: could not determine kv_bytes_per_chunk from the first event "
748
+ "(shapes/dtypes are empty). Pass --kv-bytes-per-chunk explicitly.",
749
+ file=sys.stderr,
750
+ )
751
+ sys.exit(1)
752
+ print(f"Auto-detected kv_bytes_per_chunk = {kv_bpc:,} bytes")
753
+
754
+ capacity_bytes = int(args.cache_capacity_gib * _GIB)
755
+
756
+ print("\nSimulation parameters:")
757
+ print(
758
+ f" Cache capacity : {args.cache_capacity_gib:.2f} GiB "
759
+ f"({capacity_bytes:,} bytes)"
760
+ )
761
+ print(f" KV bytes/chunk : {kv_bpc:,}")
762
+ chunk_sz = events[0].get("chunk_size", "?")
763
+ print(f" Chunk size : {chunk_sz} tokens")
764
+ if args.model:
765
+ print(f" Model filter : {args.model}")
766
+ print()
767
+
768
+ results = simulate(events, capacity_bytes, kv_bpc)
769
+ print_statistics(results)
770
+ plot_statistics(results, events, args.output)
771
+
772
+
773
+ # ---------------------------------------------------------------------------
774
+ # CLI entry point
775
+ # ---------------------------------------------------------------------------
776
+
777
+
778
+ def main() -> None:
779
+ """CLI entry point for ``python -m lmcache.tools.cache_simulator.simulator``.
780
+
781
+ Parses command-line arguments and delegates to :func:`run_simulate`.
782
+ """
783
+ parser = argparse.ArgumentParser(
784
+ description=(
785
+ "Simulate LRU token cache hit rate from lookup-hash JSONL logs. "
786
+ "Prints a text report and saves a multi-panel statistics chart."
787
+ )
788
+ )
789
+ add_simulate_arguments(parser)
790
+ args = parser.parse_args()
791
+ run_simulate(args)
792
+
793
+
794
+ if __name__ == "__main__":
795
+ main()