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,1713 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Standard
3
+ from collections.abc import Iterable
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING, Any, Generator, Optional, Union
6
+ import os
7
+
8
+ # Third Party
9
+ from vllm.config import (
10
+ VllmConfig,
11
+ )
12
+ from vllm.distributed.kv_transfer.kv_connector.v1.base import (
13
+ KVConnectorBase_V1,
14
+ KVConnectorMetadata,
15
+ KVConnectorRole,
16
+ )
17
+ from vllm.distributed.parallel_state import (
18
+ get_pp_group,
19
+ )
20
+ from vllm.sampling_params import SamplingParams
21
+ from vllm.v1.core.sched.output import SchedulerOutput
22
+ from vllm.v1.request import RequestStatus
23
+ from vllm.version import __version__ as VLLM_VERSION
24
+ import torch
25
+
26
+ # First Party
27
+ # Use LMCache's own math utilities instead of vllm's
28
+ # (avoids dependency on vllm internal changes like https://github.com/vllm-project/vllm/pull/27188)
29
+ from lmcache import utils
30
+ from lmcache.integration.vllm.utils import (
31
+ ENGINE_NAME,
32
+ apply_mm_hashes_to_token_ids,
33
+ extract_mm_features,
34
+ lmcache_get_or_create_config,
35
+ )
36
+ from lmcache.integration.vllm.vllm_service_factory import VllmServiceFactory
37
+ from lmcache.logging import init_logger
38
+ from lmcache.observability import LMCStatsMonitor, PrometheusLogger
39
+ from lmcache.utils import CacheStoreEvent, _lmcache_nvtx_annotate, cdiv
40
+ from lmcache.v1.cache_engine import LMCacheEngine
41
+ from lmcache.v1.compute.blend import LMCBlenderBuilder
42
+ from lmcache.v1.config import LMCacheEngineConfig
43
+ from lmcache.v1.config_base import validate_and_set_config_value
44
+ from lmcache.v1.manager import LMCacheManager
45
+
46
+ if TYPE_CHECKING:
47
+ # Third Party
48
+ from vllm.attention.backends.abstract import AttentionMetadata
49
+ from vllm.forward_context import ForwardContext
50
+ from vllm.multimodal.inputs import PlaceholderRange
51
+ from vllm.v1.core.kv_cache_manager import KVCacheManager
52
+ from vllm.v1.core.sched.output import NewRequestData
53
+ from vllm.v1.request import Request
54
+
55
+ # First Party
56
+ from lmcache.v1.lookup_client.abstract_client import LookupClientInterface
57
+
58
+ logger = init_logger(__name__)
59
+
60
+
61
+ @dataclass
62
+ class LoadSpec:
63
+ # Number of tokens cached in vLLM
64
+ vllm_cached_tokens: int
65
+ # Number of tokens that are cached in LMCache
66
+ lmcache_cached_tokens: int
67
+ # Whether the scheduler allow us to load the tokens
68
+ can_load: bool
69
+
70
+
71
+ @dataclass
72
+ class SaveSpec:
73
+ # Skip already saved tokens
74
+ skip_leading_tokens: int
75
+ # Whether the scheduler allow us to save the tokens
76
+ can_save: bool
77
+
78
+
79
+ @dataclass
80
+ class DisaggSpec:
81
+ req_id: str
82
+ receiver_id: str
83
+ receiver_host: str
84
+ receiver_init_port: int
85
+ receiver_alloc_port: int
86
+ is_last_prefill: bool = False
87
+ num_transferred_tokens: int = 0
88
+
89
+
90
+ tmp_disagg_tracker: dict[str, DisaggSpec] = {}
91
+
92
+
93
+ def extract_request_configs(sampling_params: SamplingParams) -> Optional[dict]:
94
+ request_configs = None
95
+ if sampling_params and sampling_params.extra_args is not None:
96
+ if kv_transfer_params := sampling_params.extra_args.get("kv_transfer_params"):
97
+ for k, v in kv_transfer_params.items():
98
+ if k.startswith("lmcache."):
99
+ if request_configs is None:
100
+ request_configs = {}
101
+ request_configs[k] = v
102
+ return request_configs
103
+
104
+
105
+ @dataclass
106
+ class RequestTracker:
107
+ # Request id
108
+ req_id: str
109
+
110
+ # Total prompt token length
111
+ prompt_len: int
112
+
113
+ # The token ids that has been scheduled so far
114
+ token_ids: list[int]
115
+
116
+ # The block ids that has been allocated so far
117
+ # NOTE: allocated blocks could be more than the number of tokens
118
+ allocated_block_ids: list[int]
119
+
120
+ # The number of tokens that has been saved
121
+ num_saved_tokens: int = 0
122
+
123
+ # Disagg spec for the request
124
+ disagg_spec: Optional[DisaggSpec] = None
125
+
126
+ # Multimodal hashes and positions
127
+ mm_hashes: Optional[list[str]] = None
128
+ mm_positions: Optional[list["PlaceholderRange"]] = None
129
+
130
+ # The configs of the request, includes tags and other configs
131
+ request_configs: Optional[dict] = None
132
+
133
+ # Whether the request is in decode phase
134
+ is_decode_phase = False
135
+
136
+ # Whether the request cache should be saved
137
+ skip_save: bool = False
138
+
139
+ # The number of tokens that are cached in LMCache for this request
140
+ num_lmcache_cached_tokens: int = 0
141
+
142
+ @_lmcache_nvtx_annotate
143
+ @staticmethod
144
+ def from_new_request(
145
+ lmcache_config: LMCacheEngineConfig,
146
+ new_request: "NewRequestData",
147
+ num_tokens_to_compute: int,
148
+ lmcache_cached_tokens: int,
149
+ skip_save: bool,
150
+ ) -> "RequestTracker":
151
+ """Create the request tracker from a new request.
152
+
153
+ Args:
154
+ lmcache_config (LMCacheEngineConfig): the LMCache engine config.
155
+ new_request (NewRequestData): the new request data.
156
+ num_tokens_to_compute (int): the number of tokens that will
157
+ be 'computed', including the `num_computed_tokens` (vLLM's
158
+ local cache hit) and new tokens that will be scheduled.
159
+ lmcache_cached_tokens (int): the number of tokens that are
160
+ cached in LMCache.
161
+ request_priority (int): the priority of the request
162
+ skip_save (bool): whether the request cache should be saved
163
+ """
164
+ # vLLM 0.9.0 update: request.block_ids changed from list[int] to
165
+ # tuple[list[int]]
166
+ # Need to check the type of request.block_ids
167
+
168
+ unfolded_block_ids = []
169
+
170
+ if not isinstance(new_request.block_ids[0], list):
171
+ unfolded_block_ids = new_request.block_ids.copy()
172
+ else:
173
+ # According to the vLLM code
174
+ # (https://github.com/vllm-project/vllm/blob/main/vllm/v1/core/
175
+ # sched/scheduler.py#L943),
176
+ # only one KVCacheGroup is supported in connector for now.
177
+
178
+ # TODO: Please support multiple KVCacheGroup in connector.
179
+ # NOTE: Also, `update` method in RequestTracker should be
180
+ # updated accordingly.
181
+ unfolded_block_ids = new_request.block_ids[0].copy()
182
+
183
+ # NOTE: Initialized in `update_state_after_alloc`
184
+ disagg_spec = tmp_disagg_tracker.pop(new_request.req_id, None)
185
+
186
+ request_configs = extract_request_configs(new_request.sampling_params)
187
+
188
+ mm_hashes, mm_positions = extract_mm_features(new_request, modify=True)
189
+
190
+ return RequestTracker(
191
+ req_id=new_request.req_id,
192
+ prompt_len=len(new_request.prompt_token_ids),
193
+ token_ids=new_request.prompt_token_ids[:num_tokens_to_compute].copy(),
194
+ allocated_block_ids=unfolded_block_ids,
195
+ num_saved_tokens=lmcache_cached_tokens,
196
+ disagg_spec=disagg_spec,
197
+ mm_hashes=mm_hashes,
198
+ mm_positions=mm_positions,
199
+ skip_save=skip_save,
200
+ request_configs=request_configs,
201
+ num_lmcache_cached_tokens=lmcache_cached_tokens,
202
+ )
203
+
204
+ def update(
205
+ self,
206
+ new_token_ids: list[int],
207
+ new_block_ids: Union[Optional[tuple[list[int], ...]], list[int]],
208
+ preempted: bool = False,
209
+ lmcache_cached_tokens: int = 0,
210
+ vllm_cached_tokens: int = 0,
211
+ all_token_ids: Optional[list[int]] = None,
212
+ ) -> None:
213
+ """Update the request tracker when a running request is
214
+ scheduled again
215
+
216
+ vllm_cached_tokens: the number of tokens that are cached in vLLM
217
+ is only used for preempted requests
218
+ all_token_ids: the full token list from the vLLM request, used to
219
+ restore token_ids for preempted requests to ensure chunk keys match
220
+ """
221
+
222
+ if new_block_ids is None:
223
+ # https://github.com/vllm-project/vllm/commit/
224
+ # b029de9902aa3ac58806c8c17776c7074175b6db#
225
+ # diff-cafd89ce8a698a56acb24ada62831cbc7a980782f78a52d1742ba238031f296cL94
226
+ new_block_ids = []
227
+ elif len(new_block_ids) == 0:
228
+ new_block_ids = []
229
+ elif isinstance(new_block_ids, tuple):
230
+ new_block_ids = new_block_ids[0]
231
+ elif isinstance(new_block_ids, list):
232
+ # If input is a list, flatten it to handle potential nesting.
233
+ # This also correctly processes already-flat lists.
234
+ new_block_ids = [
235
+ i
236
+ for elem in new_block_ids
237
+ for i in (elem if isinstance(elem, list) else [elem])
238
+ ]
239
+ else:
240
+ raise ValueError(f"Unsupported new_block_ids type {type(new_block_ids)}")
241
+
242
+ if preempted:
243
+ assert all_token_ids is not None, (
244
+ f"Preempted request {self.req_id} has no all_token_ids"
245
+ )
246
+ # the block ids will change after preemption
247
+ self.allocated_block_ids = new_block_ids
248
+ # reset the number of saved tokens
249
+ self.num_saved_tokens = lmcache_cached_tokens
250
+ num_computed_tokens = max(lmcache_cached_tokens, vllm_cached_tokens)
251
+
252
+ # FIX: For preempted requests, restore token_ids from the full
253
+ # token list to ensure chunk keys match what was used during
254
+ # lookup. The lookup uses request.all_token_ids, so we need the
255
+ # same tokens for retrieve.
256
+ num_tokens_needed = max(
257
+ num_computed_tokens + len(new_token_ids),
258
+ lmcache_cached_tokens,
259
+ )
260
+ self.token_ids = all_token_ids[:num_tokens_needed]
261
+ else:
262
+ self.allocated_block_ids.extend(new_block_ids)
263
+ self.token_ids.extend(new_token_ids)
264
+
265
+ # When a request is scheduled again, and the number of new tokens
266
+ # is 1 (excluding chunked prefill), the request is in decode phase.
267
+ # TODO: Need to further exclude the case of chunked prefill with 1 token.
268
+ if len(new_token_ids) == 1:
269
+ self.is_decode_phase = True
270
+
271
+
272
+ @dataclass
273
+ class ReqMeta:
274
+ # Request id
275
+ req_id: str
276
+ # Request tokens
277
+ token_ids: list[int] # torch.Tensor
278
+ # Slot mapping
279
+ slot_mapping: torch.Tensor
280
+
281
+ # Whether is last prefill or not
282
+ is_last_prefill: bool = False
283
+
284
+ # Skip save or not
285
+ save_spec: Optional[SaveSpec] = None
286
+ # load_spec
287
+ load_spec: Optional[LoadSpec] = None
288
+ # disagg spec
289
+ disagg_spec: Optional[DisaggSpec] = None
290
+ # the configs of the request
291
+ request_configs: Optional[dict] = None
292
+
293
+ @staticmethod
294
+ def from_request_tracker(
295
+ tracker: RequestTracker,
296
+ block_size: int,
297
+ lmcache_chunk_size: int = 256,
298
+ load_spec: Optional[LoadSpec] = None,
299
+ discard_partial_chunks: bool = True,
300
+ save_decode_cache: bool = False,
301
+ ) -> Optional["ReqMeta"]:
302
+ """Create the request metadata from a request tracker.
303
+
304
+ Args:
305
+ tracker (RequestTracker): the request tracker.
306
+ block_size (int): the block size in vLLM.
307
+ lmcache_chunk_size (int): the chunk size for LMCache.
308
+ load_spec (Optional[LoadSpec]): the load spec for KV cache loading.
309
+ discard_partial_chunks (bool): whether to discard partial chunks.
310
+ save_decode_cache (bool): whether to save the cache in decode phase.
311
+
312
+ Returns:
313
+ the request metadata if we need to perform load/save
314
+ operations, None otherwise.
315
+ """
316
+ input_token_ids = tracker.token_ids
317
+ input_token_len = len(input_token_ids)
318
+
319
+ is_last_prefill = False
320
+ if input_token_len >= tracker.prompt_len:
321
+ is_last_prefill = True
322
+
323
+ # For save operation: do not save if the following condition is met
324
+ # 1. has already been saved before (num_saved_tokens > 0)
325
+ # 2. number of unsaved tokens is not reached the chunk boundary
326
+ # 3. if save_decode_cache is False and it is in decode phase
327
+
328
+ skip_leading_tokens = tracker.num_saved_tokens
329
+ chunk_boundary = (
330
+ cdiv(tracker.num_saved_tokens + 1, lmcache_chunk_size) * lmcache_chunk_size
331
+ )
332
+
333
+ # NOTE(vladnosiv): for disagg, you cannot skip saving, as saving is a transfer
334
+ # Check if request_configs has lmcache.skip_save set to True
335
+ request_skip = (tracker.request_configs or {}).get("lmcache.skip_save", False)
336
+
337
+ skip_save = tracker.disagg_spec is None and (
338
+ tracker.skip_save
339
+ or (tracker.num_saved_tokens > 0 and input_token_len < chunk_boundary)
340
+ or (tracker.is_decode_phase and not save_decode_cache)
341
+ or request_skip
342
+ )
343
+
344
+ if skip_save and load_spec is None:
345
+ return None
346
+
347
+ # Calculate number of tokens to save based on discard_partial_chunks
348
+ # setting
349
+
350
+ # NOTE(vladnosiv): for the input_token_len chunk prefill,
351
+ # we are required to discard partial chunks,
352
+ # as new tokens will be added in the next iteration.
353
+ if not is_last_prefill or discard_partial_chunks:
354
+ num_tokens_to_save = (
355
+ input_token_len // lmcache_chunk_size * lmcache_chunk_size
356
+ )
357
+ else:
358
+ num_tokens_to_save = input_token_len
359
+
360
+ # If we need to save, update the number of saved tokens
361
+ if not skip_save:
362
+ tracker.num_saved_tokens = num_tokens_to_save
363
+ save_spec = SaveSpec(skip_leading_tokens, not skip_save)
364
+
365
+ # Calculate the token ids and slot mappings for load and save
366
+ token_ids = input_token_ids[:num_tokens_to_save]
367
+
368
+ # If the request has multimodal hashes, apply them to the token ids
369
+ if tracker.mm_hashes:
370
+ # TODO: Optimize this
371
+ token_ids = torch.tensor(token_ids)
372
+ assert tracker.mm_positions is not None, (
373
+ "tracker got mm_hashes but no mm_positions"
374
+ )
375
+ apply_mm_hashes_to_token_ids(
376
+ token_ids, tracker.mm_hashes, tracker.mm_positions
377
+ )
378
+ token_ids = token_ids.tolist()
379
+
380
+ num_blocks = len(tracker.allocated_block_ids)
381
+
382
+ if len(token_ids) > num_blocks * block_size:
383
+ logger.error(
384
+ "The number of tokens is more than the number of blocks"
385
+ " for request %s. "
386
+ "Something might be wrong in scheduling logic!",
387
+ tracker.req_id,
388
+ )
389
+ logger.error(
390
+ "Num tokens: %d, num blocks: %d, block size: %d",
391
+ len(token_ids),
392
+ num_blocks,
393
+ block_size,
394
+ )
395
+
396
+ block_ids = torch.tensor(tracker.allocated_block_ids, dtype=torch.long)
397
+ block_offsets = torch.arange(0, block_size, dtype=torch.long)
398
+ slot_mapping = (
399
+ block_offsets.reshape((1, block_size))
400
+ + block_ids.reshape((num_blocks, 1)) * block_size
401
+ )
402
+
403
+ slot_mapping = slot_mapping.flatten()[: len(token_ids)]
404
+ assert slot_mapping.dtype == torch.long # TODO: this could be removed
405
+
406
+ # For load operation: log if the request is scheduled to load
407
+ if load_spec is not None and load_spec.can_load:
408
+ logger.debug(
409
+ "Scheduled to load %d tokens (%d cached in vLLM) for request %s",
410
+ load_spec.lmcache_cached_tokens,
411
+ load_spec.vllm_cached_tokens,
412
+ tracker.req_id,
413
+ )
414
+
415
+ # Note: We keep load_spec even when can_load=False to pass metrics to worker
416
+ return ReqMeta(
417
+ req_id=tracker.req_id,
418
+ token_ids=token_ids,
419
+ slot_mapping=slot_mapping,
420
+ is_last_prefill=is_last_prefill,
421
+ save_spec=save_spec,
422
+ load_spec=load_spec,
423
+ disagg_spec=tracker.disagg_spec,
424
+ request_configs=tracker.request_configs,
425
+ )
426
+
427
+
428
+ @dataclass
429
+ class LMCacheConnectorMetadata(KVConnectorMetadata):
430
+ requests: list[ReqMeta] = field(default_factory=list)
431
+
432
+ @_lmcache_nvtx_annotate
433
+ def add_request(self, req_meta: ReqMeta) -> None:
434
+ """Add a request to the metadata.
435
+
436
+ Args:
437
+ req_meta (ReqMeta): the request metadata.
438
+ """
439
+ self.requests.append(req_meta)
440
+
441
+
442
+ class LMCacheConnectorV1Impl:
443
+ def __init__(
444
+ self,
445
+ vllm_config: "VllmConfig",
446
+ role: KVConnectorRole,
447
+ parent: KVConnectorBase_V1,
448
+ ):
449
+ self._parent = parent
450
+ self._vllm_config = vllm_config
451
+ self._role = role
452
+ self.device = vllm_config.device_config.device
453
+ self.kv_role = vllm_config.kv_transfer_config.kv_role
454
+ self.worker_count = vllm_config.parallel_config.tensor_parallel_size
455
+
456
+ # Load and configure LMCache config
457
+ config = lmcache_get_or_create_config()
458
+ assert isinstance(config, LMCacheEngineConfig), (
459
+ "LMCache v1 configuration is should be passed for vLLM v1."
460
+ )
461
+ self._apply_extra_config(config, vllm_config)
462
+ self.config = config
463
+
464
+ service_factory = VllmServiceFactory(config, vllm_config, role.name.lower())
465
+ self._manager = LMCacheManager(config, service_factory, connector=self)
466
+
467
+ # Start services managed by LMCacheManager
468
+ self._manager.start_services()
469
+
470
+ # Initialize connector-specific state
471
+ self._init_connector_state(role, vllm_config, config)
472
+
473
+ # Setup metrics for monitoring data structures
474
+ self._setup_metrics()
475
+
476
+ logger.info(
477
+ "LMCache initialized for role %s with version %s, "
478
+ "vllm version %s, lmcache cache_engine metadata: %s",
479
+ role,
480
+ utils.get_version(),
481
+ VLLM_VERSION,
482
+ getattr(self.lmcache_engine, "metadata", None),
483
+ )
484
+
485
+ def _apply_extra_config(
486
+ self, config: LMCacheEngineConfig, vllm_config: "VllmConfig"
487
+ ) -> None:
488
+ """Apply extra config from vLLM to LMCache config."""
489
+ kv_connector_extra_config = (
490
+ vllm_config.kv_transfer_config.kv_connector_extra_config
491
+ )
492
+ if kv_connector_extra_config:
493
+ for key, value in kv_connector_extra_config.items():
494
+ if key.startswith("lmcache."):
495
+ config_key = key[8:] # Remove "lmcache." prefix
496
+ if validate_and_set_config_value(config, config_key, value):
497
+ logger.info(
498
+ "Updated config %s from vLLM extra config",
499
+ config_key,
500
+ )
501
+
502
+ def _init_connector_state(
503
+ self,
504
+ role: KVConnectorRole,
505
+ vllm_config: "VllmConfig",
506
+ config: LMCacheEngineConfig,
507
+ ) -> None:
508
+ """Initialize connector-specific state variables."""
509
+ self.async_loading = config.enable_async_loading
510
+ self.layerwise_retrievers: list[
511
+ Generator[Optional[torch.Tensor], None, None]
512
+ ] = []
513
+ self._layerwise_save_storers: dict[
514
+ str, Generator[Optional[torch.Tensor], None, None]
515
+ ] = {}
516
+ self._stats_monitor = LMCStatsMonitor.GetOrCreate()
517
+
518
+ # Role-specific initialization
519
+ if role == KVConnectorRole.SCHEDULER:
520
+ self._unfinished_requests: dict[str, "Request"] = {}
521
+ else:
522
+ self.use_layerwise = config.use_layerwise
523
+ self.enable_blending = config.enable_blending
524
+
525
+ if self.enable_blending:
526
+ assert self.lmcache_engine is not None
527
+ assert self.lmcache_engine.gpu_connector is not None, (
528
+ "GPU connector must be available for blending"
529
+ )
530
+ self.blender = LMCBlenderBuilder.get_or_create(
531
+ ENGINE_NAME,
532
+ self.lmcache_engine,
533
+ self.lmcache_engine.gpu_connector,
534
+ config,
535
+ )
536
+
537
+ # Legacy compatibility check
538
+ self._check_legacy_register_kv_caches()
539
+
540
+ self.kv_caches: dict[str, torch.Tensor] = {}
541
+ self._block_size = vllm_config.cache_config.block_size
542
+ self.load_specs: dict[str, LoadSpec] = {}
543
+ self.kv_cache_manager: Optional["KVCacheManager"] = None
544
+ self._request_trackers: dict[str, RequestTracker] = {}
545
+
546
+ self._discard_partial_chunks = (
547
+ vllm_config.kv_transfer_config.get_from_extra_config(
548
+ "discard_partial_chunks", False
549
+ )
550
+ or not config.save_unfull_chunk
551
+ )
552
+
553
+ self._lmcache_chunk_size = config.chunk_size
554
+
555
+ self.skip_last_n_tokens = vllm_config.kv_transfer_config.get_from_extra_config(
556
+ "skip_last_n_tokens", 0
557
+ )
558
+
559
+ self.num_layers = vllm_config.model_config.get_num_layers(
560
+ vllm_config.parallel_config
561
+ )
562
+ self.current_layer = 0
563
+
564
+ self.force_skip_save = bool(os.environ.get("LMCACHE_FORCE_SKIP_SAVE", False))
565
+ self._requests_priority: dict[str, int] = {}
566
+ self._invalid_block_ids: set[int] = set()
567
+
568
+ def _check_legacy_register_kv_caches(self) -> None:
569
+ """Check for legacy connector without register_kv_caches implementation."""
570
+ if self.lmcache_engine is None:
571
+ return
572
+
573
+ child_class = self._parent.__class__
574
+ parent_class = KVConnectorBase_V1
575
+ child_method = getattr(child_class, "register_kv_caches", None)
576
+ parent_method = getattr(parent_class, "register_kv_caches", None)
577
+
578
+ if child_method is None or parent_method is None:
579
+ implements = False
580
+ else:
581
+ implements = child_method is not parent_method
582
+
583
+ if not implements:
584
+ logger.warning(
585
+ "Please use the latest lmcache connector, otherwise some "
586
+ "features may not work, such as DSA"
587
+ )
588
+ self._manager.post_init()
589
+
590
+ # ==================== Property Accessors ====================
591
+
592
+ @property
593
+ def lmcache_engine(self) -> Optional[LMCacheEngine]:
594
+ """Get the LMCache engine instance from manager."""
595
+ return self._manager.lmcache_engine
596
+
597
+ @property
598
+ def lmcache_engine_metadata(self):
599
+ """Get the LMCache engine metadata from manager."""
600
+ return self._manager.lmcache_engine_metadata
601
+
602
+ @property
603
+ def lookup_client(self) -> Optional["LookupClientInterface"]:
604
+ """Get the lookup client from manager."""
605
+ return self._manager.lookup_client
606
+
607
+ @property
608
+ def lookup_server(self):
609
+ """Get the lookup server from manager."""
610
+ return self._manager.lookup_server
611
+
612
+ def _setup_metrics(self):
613
+ """Setup metrics for monitoring data structures in the connector."""
614
+ prometheus_logger = PrometheusLogger.GetInstanceOrNone()
615
+ if prometheus_logger is None:
616
+ logger.warning(
617
+ "PrometheusLogger is not initialized, "
618
+ "connector metrics will not be collected"
619
+ )
620
+ return
621
+
622
+ # Set up metrics for scheduler-specific and general data structures
623
+ metrics_map = {
624
+ "_unfinished_requests": "scheduler_unfinished_requests_count",
625
+ "load_specs": "connector_load_specs_count",
626
+ "_request_trackers": "connector_request_trackers_count",
627
+ "kv_caches": "connector_kv_caches_count",
628
+ "layerwise_retrievers": "connector_layerwise_retrievers_count",
629
+ "_invalid_block_ids": "connector_invalid_block_ids_count",
630
+ "_requests_priority": "connector_requests_priority_count",
631
+ }
632
+
633
+ for attr_name, metric_name in metrics_map.items():
634
+ if hasattr(self, attr_name):
635
+ metric = getattr(prometheus_logger, metric_name)
636
+ # Use a default argument in the lambda to capture
637
+ # the current value of `attr_name`
638
+ # to avoid issues with late binding in closures.
639
+ metric.set_function(lambda name=attr_name: len(getattr(self, name)))
640
+
641
+ def get_inference_info(self) -> dict:
642
+ """Get inference information including vLLM config and related details.
643
+
644
+ Returns:
645
+ dict: Dictionary containing inference information
646
+ """
647
+ # Get vLLM config information
648
+ vllm_config = self._vllm_config
649
+
650
+ # Use vLLM config's string representation and add specific configs
651
+ inference_info = {
652
+ "vllm_version": VLLM_VERSION,
653
+ "lmcache_version": utils.get_version(),
654
+ "vllm_config": str(vllm_config),
655
+ "model_config": {
656
+ "model": getattr(vllm_config.model_config, "model", None),
657
+ "dtype": str(getattr(vllm_config.model_config, "dtype", None)),
658
+ "max_model_len": getattr(
659
+ vllm_config.model_config, "max_model_len", None
660
+ ),
661
+ "vocab_size": getattr(vllm_config.model_config, "vocab_size", None),
662
+ "num_layers": getattr(
663
+ vllm_config.model_config, "get_num_layers", lambda _: None
664
+ )(vllm_config.parallel_config),
665
+ "num_attention_heads": getattr(
666
+ vllm_config.model_config, "get_num_attention_heads", lambda _: None
667
+ )(vllm_config.parallel_config),
668
+ "num_kv_heads": getattr(
669
+ vllm_config.model_config, "get_num_kv_heads", lambda _: None
670
+ )(vllm_config.parallel_config),
671
+ "head_size": getattr(
672
+ vllm_config.model_config, "get_head_size", lambda: None
673
+ )(),
674
+ },
675
+ "cache_config": {
676
+ "block_size": getattr(vllm_config.cache_config, "block_size", None),
677
+ "cache_dtype": str(
678
+ getattr(vllm_config.cache_config, "cache_dtype", None)
679
+ ),
680
+ "gpu_memory_utilization": getattr(
681
+ vllm_config.cache_config, "gpu_memory_utilization", None
682
+ ),
683
+ "swap_space": getattr(vllm_config.cache_config, "swap_space", None),
684
+ "enable_prefix_caching": getattr(
685
+ vllm_config.cache_config, "enable_prefix_caching", None
686
+ ),
687
+ },
688
+ }
689
+
690
+ return inference_info
691
+
692
+ def get_inference_version(self) -> str:
693
+ """Get vLLM version information.
694
+
695
+ Returns:
696
+ str: vLLM version string
697
+ """
698
+ return VLLM_VERSION
699
+
700
+ def _build_kv_layer_groups(self):
701
+ # Build KV layer groups structure if not already built
702
+ if self.lmcache_engine is not None:
703
+ assert len(self.kv_caches) > 0
704
+ kv_layer_groups_manager = (
705
+ self.lmcache_engine.metadata.kv_layer_groups_manager
706
+ )
707
+ kv_layer_groups_manager.build_kv_layer_groups(self.kv_caches)
708
+
709
+ # TODO(chunxiaozheng): in the latest lmcache_connector, we use `register_kv_caches`
710
+ # to init self.kv_caches, we keep it in order to be compatible with old versions
711
+ # and will be removed in the future.
712
+ @_lmcache_nvtx_annotate
713
+ def _init_kv_caches_from_forward_context(self, forward_context: "ForwardContext"):
714
+ for layer_name in forward_context.no_compile_layers:
715
+ attn_layer = forward_context.no_compile_layers[layer_name]
716
+ if not hasattr(attn_layer, "kv_cache"):
717
+ logger.debug("The layer %s does not have kv_cache, skip it", layer_name)
718
+ continue
719
+
720
+ if layer_name not in self.kv_caches:
721
+ self.kv_caches[layer_name] = attn_layer.kv_cache[
722
+ forward_context.virtual_engine
723
+ ]
724
+
725
+ self._build_kv_layer_groups()
726
+
727
+ ####################
728
+ # Worker side APIs
729
+ ####################
730
+ @_lmcache_nvtx_annotate
731
+ def register_kv_caches(self, kv_caches: dict[str, torch.Tensor]):
732
+ logger.info("Registering KV caches")
733
+ # TODO(chunxiaozheng): `_init_kv_caches_from_forward_context` is
734
+ # not called, we should consider removing it.
735
+ assert len(self.kv_caches) == 0 and len(kv_caches) > 0
736
+ self.kv_caches = kv_caches
737
+ self._build_kv_layer_groups()
738
+ self._manager.post_init()
739
+
740
+ @_lmcache_nvtx_annotate
741
+ def start_load_kv(self, forward_context: "ForwardContext", **kwargs) -> None:
742
+ """Start loading the KV cache from the connector buffer to vLLM's
743
+ paged KV buffer.
744
+
745
+ Args:
746
+ forward_context (ForwardContext): the forward context.
747
+ **kwargs: additional arguments for the load operation
748
+
749
+ Note:
750
+ The number of elements in kv_caches and layer_names should be
751
+ the same.
752
+ """
753
+ self.current_layer = 0
754
+
755
+ if len(self.kv_caches) == 0:
756
+ logger.warning(
757
+ "Please update LMCacheConnector, "
758
+ "use register_kv_caches to init kv_caches"
759
+ )
760
+ self._init_kv_caches_from_forward_context(forward_context)
761
+
762
+ metadata = self._parent._get_connector_metadata()
763
+ assert isinstance(metadata, LMCacheConnectorMetadata)
764
+
765
+ assert len(self.kv_caches) > 0
766
+ kvcaches = list(self.kv_caches.values())
767
+
768
+ attn_metadata = forward_context.attn_metadata
769
+ if attn_metadata is None:
770
+ logger.debug("In connector.start_load_kv, but the attn_metadata is None")
771
+ return
772
+
773
+ assert self.lmcache_engine is not None
774
+
775
+ self.layerwise_retrievers = []
776
+
777
+ for idx, request in enumerate(metadata.requests):
778
+ if request.load_spec is None or not request.load_spec.can_load:
779
+ continue
780
+ last_idx = idx
781
+
782
+ for idx, request in enumerate(metadata.requests):
783
+ # Update metrics for all requests that have a load_spec
784
+ if request.load_spec is not None:
785
+ self._stats_monitor.update_interval_vllm_hit_tokens(
786
+ request.load_spec.vllm_cached_tokens
787
+ )
788
+ self._stats_monitor.update_interval_prompt_tokens(
789
+ len(request.token_ids)
790
+ )
791
+
792
+ if request.load_spec is None or not request.load_spec.can_load:
793
+ continue
794
+
795
+ tokens = request.token_ids
796
+ # TODO: have a pre-allocated buffer to hold the slot_mappings
797
+ slot_mapping = request.slot_mapping.to(self.device)
798
+ assert len(tokens) == len(slot_mapping)
799
+
800
+ token_mask = torch.ones(len(tokens), dtype=torch.bool)
801
+ masked_token_count = (
802
+ request.load_spec.vllm_cached_tokens
803
+ // self._lmcache_chunk_size
804
+ * self._lmcache_chunk_size
805
+ )
806
+ token_mask[:masked_token_count] = False
807
+
808
+ lmcache_cached_tokens = request.load_spec.lmcache_cached_tokens
809
+ if self.use_layerwise:
810
+ if idx == last_idx:
811
+ sync = True
812
+ else:
813
+ sync = False
814
+ # NOTE(Jiayi): Perform blending before layerwise prefix caching
815
+ if self.enable_blending:
816
+ # TODO(Jiayi): Need to make prefix caching and blending compatible
817
+ self.blender.blend(
818
+ tokens[:lmcache_cached_tokens],
819
+ token_mask[:lmcache_cached_tokens],
820
+ kvcaches=kvcaches,
821
+ slot_mapping=slot_mapping[:lmcache_cached_tokens],
822
+ vllm_cached_tokens=request.load_spec.vllm_cached_tokens,
823
+ )
824
+ else:
825
+ layerwise_retriever = self.lmcache_engine.retrieve_layer(
826
+ tokens[:lmcache_cached_tokens],
827
+ token_mask[:lmcache_cached_tokens],
828
+ kvcaches=kvcaches,
829
+ slot_mapping=slot_mapping[:lmcache_cached_tokens],
830
+ vllm_cached_tokens=request.load_spec.vllm_cached_tokens,
831
+ sync=sync,
832
+ )
833
+ # NOTE: retrieve for two layers at the first layer
834
+ next(layerwise_retriever)
835
+ next(layerwise_retriever)
836
+ self.layerwise_retrievers.append(layerwise_retriever)
837
+ else:
838
+ ret_token_mask = self.lmcache_engine.retrieve(
839
+ tokens[:lmcache_cached_tokens],
840
+ token_mask[:lmcache_cached_tokens],
841
+ kvcaches=kvcaches,
842
+ slot_mapping=slot_mapping[:lmcache_cached_tokens],
843
+ vllm_cached_tokens=request.load_spec.vllm_cached_tokens,
844
+ request_configs=request.request_configs,
845
+ req_id=request.req_id,
846
+ )
847
+
848
+ # Check the result
849
+ num_retrieved_tokens = ret_token_mask.sum().item()
850
+ num_expected_tokens = (
851
+ lmcache_cached_tokens - request.load_spec.vllm_cached_tokens
852
+ )
853
+ if num_retrieved_tokens < num_expected_tokens:
854
+ logger.error(
855
+ "Request %s"
856
+ "The number of retrieved tokens is less than the "
857
+ "expected number of tokens! This should not happen!",
858
+ request.req_id,
859
+ )
860
+ logger.error(
861
+ "Num retrieved tokens: %d, num expected tokens: %d",
862
+ num_retrieved_tokens,
863
+ num_expected_tokens,
864
+ )
865
+ """
866
+ Report failed block IDs in case of partial failure.
867
+ """
868
+ missing_blocks = self.record_failed_blocks(
869
+ request.req_id,
870
+ token_mask[:lmcache_cached_tokens],
871
+ ret_token_mask,
872
+ slot_mapping[:lmcache_cached_tokens],
873
+ )
874
+ self._invalid_block_ids.update(missing_blocks)
875
+
876
+ def record_failed_blocks(
877
+ self,
878
+ request_id: str,
879
+ expected_mask: torch.Tensor,
880
+ ret_mask: torch.Tensor,
881
+ slot_mapping: torch.Tensor,
882
+ ) -> set[int]:
883
+ """Record block IDs associated with failed load attempts.
884
+
885
+ Args:
886
+ request_id: request id from vLLM.
887
+ expected_mask: Boolean tensor indicating which tokens were expected to
888
+ be loaded from LMCache. True means the token should be loaded,
889
+ False means the token is already cached in vLLM and does not need
890
+ to be loaded from LMCache.
891
+ ret_mask: Boolean tensor indicating which tokens were actually
892
+ successfully retrieved from LMCache. True means the token was
893
+ successfully loaded. For example, if 256 tokens are expected to be
894
+ loaded, but only 192 tokens are successfully loaded, then the
895
+ ret_mask will be a tensor of 256 items like [T, T, ..., F, F, ...]
896
+ where the first 192 elements are True and the last 64 elements
897
+ are False.
898
+ slot_mapping: Tensor indicating slot IDs for each token. The block
899
+ ID is computed by dividing the slot ID by the block size.
900
+
901
+ Example:
902
+ expected_mask = [F, T, T, T] meaning the 1st is in vLLM cache
903
+ ret_mask = [F, T, F, F] meaning failure from loading the 3rd
904
+ missing_mask = expected_mask & ~ret_mask = [F, F, T, T]
905
+ missing_indices = [2, 3]
906
+ then missing_blocks is calculated from slot_mapping and missing_indices
907
+
908
+ Returns:
909
+ set[int]: Set of block IDs that failed to load.
910
+ """
911
+
912
+ if expected_mask.numel() == 0:
913
+ return set()
914
+
915
+ expected_mask_cpu = expected_mask.to(device="cpu", dtype=torch.bool)
916
+ ret_mask_cpu = ret_mask.to(device="cpu", dtype=torch.bool)
917
+
918
+ if ret_mask_cpu.shape[0] != expected_mask_cpu.shape[0]:
919
+ logger.debug("expected_mask_cpu.shape[0] != ret_mask_cpu.shape[0]")
920
+ return set()
921
+
922
+ missing_mask = expected_mask_cpu & ~ret_mask_cpu
923
+ if not torch.any(missing_mask):
924
+ return set()
925
+
926
+ missing_indices = torch.nonzero(missing_mask, as_tuple=False).view(-1)
927
+ if missing_indices.numel() == 0:
928
+ return set()
929
+
930
+ slot_mapping_cpu = slot_mapping.to(device="cpu", dtype=torch.long)
931
+ if slot_mapping_cpu.shape[0] > missing_mask.shape[0]:
932
+ slot_mapping_cpu = slot_mapping_cpu[: missing_mask.shape[0]]
933
+
934
+ missing_blocks_tensor = torch.unique(
935
+ slot_mapping_cpu[missing_indices] // self._block_size
936
+ )
937
+ missing_blocks = {int(block.item()) for block in missing_blocks_tensor}
938
+
939
+ if not missing_blocks:
940
+ return set()
941
+
942
+ logger.warning(
943
+ "Request %s failed to load %d tokens across %d blocks",
944
+ request_id,
945
+ missing_indices.numel(),
946
+ len(missing_blocks),
947
+ )
948
+ return missing_blocks
949
+
950
+ @_lmcache_nvtx_annotate
951
+ def wait_for_layer_load(self, layer_name: str) -> None:
952
+ """Blocking until the KV for a specific layer is loaded into vLLM's
953
+ paged buffer.
954
+
955
+ This interface will be useful for layer-by-layer pipelining.
956
+
957
+ Args:
958
+ layer_name: the name of that layer
959
+ """
960
+ if self.layerwise_retrievers:
961
+ logger.debug(f"Waiting for layer {self.current_layer} to be loaded")
962
+
963
+ # Wait for the layer to be loaded
964
+ for layerwise_retriever in self.layerwise_retrievers:
965
+ ret_token_mask = next(layerwise_retriever)
966
+
967
+ if self.current_layer == self.num_layers - 1:
968
+ assert ret_token_mask is not None
969
+ num_retrieved_tokens = ret_token_mask.sum().item()
970
+ logger.info(f"Retrieved {num_retrieved_tokens} tokens")
971
+
972
+ if self.layerwise_retrievers:
973
+ self.current_layer += 1
974
+
975
+ return
976
+
977
+ @_lmcache_nvtx_annotate
978
+ def save_kv_layer(
979
+ self,
980
+ layer_name: str,
981
+ kv_layer: torch.Tensor,
982
+ attn_metadata: "AttentionMetadata",
983
+ **kwargs,
984
+ ) -> None:
985
+ """Start saving the a layer of KV cache from vLLM's paged buffer
986
+ to the connector.
987
+
988
+ Args:
989
+ layer_name (str): the name of the layer.
990
+ kv_layer (torch.Tensor): the paged KV buffer of the current
991
+ layer in vLLM.
992
+ attn_metadata (AttentionMetadata): the attention metadata.
993
+ **kwargs: additional arguments for the save operation.
994
+ """
995
+ assert self.lmcache_engine is not None
996
+
997
+ if not self.use_layerwise:
998
+ return
999
+
1000
+ if self.kv_role == "kv_consumer":
1001
+ # Don't do save if the role is kv_consumer
1002
+ return
1003
+ if self._parent._connector_metadata is None:
1004
+ logger.warning(
1005
+ "In connector.save_kv_layer, but the connector metadata is None"
1006
+ )
1007
+ return
1008
+ connector_metadata = self._parent._get_connector_metadata()
1009
+ assert isinstance(connector_metadata, LMCacheConnectorMetadata)
1010
+
1011
+ assert len(self.kv_caches) > 0
1012
+
1013
+ kvcaches = list(self.kv_caches.values())
1014
+ is_first = True
1015
+
1016
+ for request in connector_metadata.requests:
1017
+ save_spec = request.save_spec
1018
+ if (
1019
+ save_spec is None or not save_spec.can_save
1020
+ ) and self.kv_role != "kv_producer":
1021
+ continue
1022
+
1023
+ layerwise_storer = self._layerwise_save_storers.get(request.req_id)
1024
+ if layerwise_storer is None:
1025
+ token_ids = request.token_ids
1026
+ assert isinstance(token_ids, list)
1027
+
1028
+ slot_mapping = request.slot_mapping
1029
+ assert isinstance(slot_mapping, torch.Tensor)
1030
+ assert len(slot_mapping) == len(token_ids)
1031
+
1032
+ # TODO: have a pre-allocated buffer to hold the slot_mappings
1033
+ slot_mapping = slot_mapping.to(self.device)
1034
+
1035
+ if self.kv_role == "kv_producer":
1036
+ skip_leading_tokens = 0
1037
+ else:
1038
+ assert save_spec is not None
1039
+ skip_leading_tokens = save_spec.skip_leading_tokens
1040
+
1041
+ if skip_leading_tokens == len(token_ids):
1042
+ continue # skip this request
1043
+ # Align to lmcache chunk size
1044
+ skip_leading_tokens = (
1045
+ skip_leading_tokens
1046
+ // self._lmcache_chunk_size
1047
+ * self._lmcache_chunk_size
1048
+ )
1049
+
1050
+ store_mask = torch.ones(len(token_ids), dtype=torch.bool)
1051
+ store_mask[:skip_leading_tokens] = False
1052
+
1053
+ logger.debug(
1054
+ "Storing KV cache for %d out of %d tokens "
1055
+ "(skip_leading_tokens=%d) for request %s",
1056
+ len(token_ids) - skip_leading_tokens,
1057
+ len(token_ids),
1058
+ skip_leading_tokens,
1059
+ request.req_id,
1060
+ )
1061
+
1062
+ # TODO (Jiayi): need to make layerwise storing
1063
+ # compatible with disagg spec
1064
+ layerwise_storer = self.lmcache_engine.store_layer(
1065
+ token_ids,
1066
+ mask=store_mask,
1067
+ kvcaches=kvcaches,
1068
+ slot_mapping=slot_mapping,
1069
+ offset=skip_leading_tokens,
1070
+ sync=is_first,
1071
+ req_id=request.req_id,
1072
+ )
1073
+ self._layerwise_save_storers[request.req_id] = layerwise_storer
1074
+ if is_first:
1075
+ is_first = False
1076
+
1077
+ next(layerwise_storer)
1078
+
1079
+ @_lmcache_nvtx_annotate
1080
+ def wait_for_save(self):
1081
+ """Blocking until the KV cache is saved to the connector buffer."""
1082
+
1083
+ connector_metadata = self._parent._get_connector_metadata()
1084
+ assert isinstance(connector_metadata, LMCacheConnectorMetadata)
1085
+
1086
+ if self.kv_role == "kv_consumer":
1087
+ # Don't do save if the role is kv_consumer
1088
+ # But still need to unpin the kv caches according to req_id
1089
+ # to balance the pin count from contains()
1090
+ assert self.lmcache_engine is not None, (
1091
+ "LMCacheEngine must be initialized to unpin requests."
1092
+ )
1093
+ for request in connector_metadata.requests:
1094
+ self.lmcache_engine.lookup_unpin(request.req_id)
1095
+
1096
+ return
1097
+
1098
+ if self.use_layerwise:
1099
+ for request in connector_metadata.requests:
1100
+ layerwise_storer = self._layerwise_save_storers.pop(
1101
+ request.req_id, None
1102
+ )
1103
+ if layerwise_storer is not None:
1104
+ next(layerwise_storer)
1105
+ # unpin the kv caches according to req_id
1106
+ self.lmcache_engine.lookup_unpin(request.req_id)
1107
+ return
1108
+
1109
+ assert len(self.kv_caches) > 0
1110
+ kvcaches = list(self.kv_caches.values())
1111
+
1112
+ assert self.lmcache_engine is not None
1113
+
1114
+ for request in connector_metadata.requests:
1115
+ # unpin the kv caches according to req_id
1116
+ self.lmcache_engine.lookup_unpin(request.req_id)
1117
+
1118
+ save_spec = request.save_spec
1119
+ if (
1120
+ save_spec is None or not save_spec.can_save
1121
+ ) and self.kv_role != "kv_producer":
1122
+ continue
1123
+
1124
+ token_ids = request.token_ids
1125
+
1126
+ slot_mapping = request.slot_mapping
1127
+ assert isinstance(slot_mapping, torch.Tensor)
1128
+ assert len(slot_mapping) == len(token_ids)
1129
+
1130
+ # TODO: have a pre-allocated buffer to hold the slot_mappings
1131
+ slot_mapping = slot_mapping.to(self.device)
1132
+
1133
+ skip_leading_tokens = save_spec.skip_leading_tokens
1134
+ # shared storage disaggregation will not have a disagg_spec passed in
1135
+ if self.kv_role == "kv_producer" and request.disagg_spec:
1136
+ skip_leading_tokens = min(
1137
+ skip_leading_tokens, request.disagg_spec.num_transferred_tokens
1138
+ )
1139
+
1140
+ if skip_leading_tokens == len(token_ids):
1141
+ continue # skip this request
1142
+ # Align to lmcache chunk size
1143
+ skip_leading_tokens = (
1144
+ skip_leading_tokens
1145
+ // self._lmcache_chunk_size
1146
+ * self._lmcache_chunk_size
1147
+ )
1148
+
1149
+ store_mask = torch.ones(len(token_ids), dtype=torch.bool)
1150
+ store_mask[:skip_leading_tokens] = False
1151
+
1152
+ logger.debug(
1153
+ "Storing KV cache for %d out of %d tokens "
1154
+ "(skip_leading_tokens=%d) for request %s",
1155
+ len(token_ids) - skip_leading_tokens,
1156
+ len(token_ids),
1157
+ skip_leading_tokens,
1158
+ request.req_id,
1159
+ )
1160
+
1161
+ is_last_prefill = request.is_last_prefill
1162
+ if is_last_prefill:
1163
+ if request.disagg_spec:
1164
+ request.disagg_spec.is_last_prefill = True
1165
+ else:
1166
+ if not self.enable_blending:
1167
+ token_len = len(token_ids)
1168
+ aligned_token_len = (
1169
+ token_len // self._lmcache_chunk_size * self._lmcache_chunk_size
1170
+ )
1171
+ token_ids = token_ids[:aligned_token_len]
1172
+ store_mask = store_mask[:aligned_token_len]
1173
+ slot_mapping = slot_mapping[:aligned_token_len]
1174
+
1175
+ self.lmcache_engine.store(
1176
+ token_ids,
1177
+ mask=store_mask,
1178
+ kvcaches=kvcaches,
1179
+ slot_mapping=slot_mapping,
1180
+ offset=skip_leading_tokens,
1181
+ transfer_spec=request.disagg_spec,
1182
+ request_configs=request.request_configs,
1183
+ req_id=request.req_id,
1184
+ )
1185
+
1186
+ # Update skip_leading_tokens only on last rank to ensure
1187
+ # each PP stage stores its own KV cache
1188
+ if get_pp_group().is_last_rank:
1189
+ # NOTE(Jiayi): We assume all tokens are saved
1190
+ save_spec.skip_leading_tokens = len(token_ids)
1191
+ if request.disagg_spec:
1192
+ request.disagg_spec.num_transferred_tokens = len(token_ids)
1193
+
1194
+ @_lmcache_nvtx_annotate
1195
+ def get_finished(
1196
+ self, finished_req_ids: set[str]
1197
+ ) -> tuple[Optional[set[str]], Optional[set[str]]]:
1198
+ return None, None
1199
+
1200
+ def get_block_ids_with_load_errors(self) -> set[int]:
1201
+ invalid_blocks = self._invalid_block_ids.copy()
1202
+ self._invalid_block_ids.clear()
1203
+ return invalid_blocks
1204
+
1205
+ @_lmcache_nvtx_annotate
1206
+ def shutdown(self):
1207
+ """Shutdown the connector by delegating to LMCacheManager."""
1208
+ logger.info("Starting LMCacheConnector shutdown...")
1209
+ self._manager.stop_services()
1210
+
1211
+ ###################
1212
+ # Scheduler side APIs
1213
+ ####################
1214
+
1215
+ @_lmcache_nvtx_annotate
1216
+ def get_num_new_matched_tokens(
1217
+ self,
1218
+ request: "Request",
1219
+ num_computed_tokens: int,
1220
+ ) -> Optional[int]:
1221
+ """
1222
+ Check for external KV cache hit.
1223
+
1224
+ Args:
1225
+ request (Request): the request object.
1226
+ num_computed_tokens (int): the number of locally
1227
+ computed tokens for this request
1228
+
1229
+ Returns:
1230
+ the number of tokens that can be loaded from the
1231
+ external KV cache beyond what is already computed.
1232
+ """
1233
+ # Ignore DP attention mock requests
1234
+ if request.request_id.startswith("mock_req"):
1235
+ return 0
1236
+ # to handle preempted requests, we want `get_num_new_matched_tokens` to be
1237
+ # idempotent under the condition that `update_state_after_alloc` is NOT called
1238
+ # then the two side-effects that must be idempotent are:
1239
+ # 1. lookup_client caches a result
1240
+ # uncached in `update_state_after_alloc` if this request can be scheduled
1241
+ # 2. cache engine will pin the KV caches for the request
1242
+ # unpinned in `wait_for_save` if this request can be scheduled
1243
+ if self.kv_role == "kv_producer" and not hasattr(
1244
+ self.lookup_client, "supports_producer_reuse"
1245
+ ):
1246
+ return 0
1247
+
1248
+ req_id = request.request_id
1249
+
1250
+ # lookup_client is always initialized for scheduler role
1251
+ assert self.lookup_client is not None
1252
+
1253
+ if (
1254
+ num_external_hit_tokens := self.lookup_client.lookup_cache(lookup_id=req_id)
1255
+ ) != -1:
1256
+ # -1 means no result cached
1257
+ # None or int means ongoing (async) or cached result
1258
+ logger.debug(
1259
+ f"Found {num_external_hit_tokens} hit tokens for request"
1260
+ f" {req_id} in the lookup cache."
1261
+ )
1262
+ else:
1263
+ logger.debug(f"Looking up cache for the first time for request {req_id}!")
1264
+ self._requests_priority[req_id] = getattr(request, "priority", 0)
1265
+
1266
+ # token_ids = request.prompt_token_ids
1267
+ # all token ids covers the preemption case
1268
+ token_ids = request.all_token_ids
1269
+
1270
+ # If the request has multimodal hashes, apply them to the token ids
1271
+ mm_hashes, mm_positions = extract_mm_features(request)
1272
+ if mm_hashes and mm_positions:
1273
+ # TODO(Jiayi): Optimize this
1274
+ token_ids = torch.tensor(request.prompt_token_ids)
1275
+ apply_mm_hashes_to_token_ids(token_ids, mm_hashes, mm_positions)
1276
+ token_ids = token_ids.tolist()
1277
+
1278
+ request_configs = extract_request_configs(request.sampling_params)
1279
+ if self.skip_last_n_tokens > 0:
1280
+ token_ids = token_ids[: -self.skip_last_n_tokens]
1281
+
1282
+ num_external_hit_tokens = self.lookup_client.lookup(
1283
+ token_ids,
1284
+ lookup_id=req_id,
1285
+ request_configs=request_configs,
1286
+ )
1287
+
1288
+ if num_external_hit_tokens is None:
1289
+ logger.debug(
1290
+ "Reqid: %s, Total tokens %d, Inference Engine computed tokens: %d, "
1291
+ "LMCache hit tokens: None.",
1292
+ req_id,
1293
+ request.num_tokens,
1294
+ num_computed_tokens,
1295
+ )
1296
+ return None
1297
+
1298
+ # When prompt length is divisible by the block size and all
1299
+ # blocks are cached, we need to recompute the last token.
1300
+ # This will be removed in the future if vLLM's scheduler provides
1301
+ # a better support for this case.
1302
+ need_to_allocate = num_external_hit_tokens - num_computed_tokens
1303
+
1304
+ # In, full-prompt-hit case, we need to recompute the last token
1305
+ if num_external_hit_tokens == request.num_tokens:
1306
+ need_to_allocate -= 1
1307
+
1308
+ # Check if hit tokens meet the minimum for retrieve
1309
+ # If below minimum, skip retrieve but still record hit tokens
1310
+ # for skip_leading_tokens to avoid re-storing existing chunks
1311
+ min_retrieve = self.config.min_retrieve_tokens
1312
+ below_min_retrieve = min_retrieve > 0 and need_to_allocate < min_retrieve
1313
+
1314
+ if below_min_retrieve:
1315
+ logger.info(
1316
+ "Reqid: %s, Total tokens %d, Inference Engine computed tokens: %d, "
1317
+ "LMCache hit tokens: %d, but need to load: %d < min_retrieve %d, "
1318
+ "skip retrieve but record for save skip",
1319
+ req_id,
1320
+ request.num_tokens,
1321
+ num_computed_tokens,
1322
+ num_external_hit_tokens,
1323
+ max(need_to_allocate, 0),
1324
+ min_retrieve,
1325
+ )
1326
+ else:
1327
+ logger.info(
1328
+ "Reqid: %s, Total tokens %d, Inference Engine computed tokens: %d, "
1329
+ "LMCache hit tokens: %d, need to load: %d",
1330
+ req_id,
1331
+ request.num_tokens,
1332
+ num_computed_tokens,
1333
+ num_external_hit_tokens,
1334
+ max(need_to_allocate, 0),
1335
+ )
1336
+
1337
+ self.load_specs[req_id] = LoadSpec(
1338
+ vllm_cached_tokens=num_computed_tokens,
1339
+ lmcache_cached_tokens=num_external_hit_tokens,
1340
+ can_load=False,
1341
+ )
1342
+
1343
+ if below_min_retrieve or need_to_allocate <= 0:
1344
+ return 0
1345
+
1346
+ # TODO: Align to vLLM block size. Should test whether it can be removed
1347
+ # need_to_allocate = need_to_allocate // self._block_size * \
1348
+ # self._block_size
1349
+
1350
+ return need_to_allocate
1351
+
1352
+ @_lmcache_nvtx_annotate
1353
+ def update_state_after_alloc(self, request: "Request", num_external_tokens: int):
1354
+ """
1355
+ Update KVConnector state after temporary buffer alloc.
1356
+
1357
+ For SharedStorageConnector, update _request_needs_load
1358
+ if the CacheManager this allocated blocks for us.
1359
+ """
1360
+
1361
+ # Clear local status in lookup client when a new request is
1362
+ # successfully scheduled.
1363
+ assert self.lookup_client is not None
1364
+ self.lookup_client.clear_lookup_status(request.request_id)
1365
+
1366
+ kv_transfer_params = (
1367
+ request.kv_transfer_params
1368
+ if hasattr(request, "kv_transfer_params")
1369
+ else None
1370
+ )
1371
+
1372
+ if kv_transfer_params is not None and "disagg_spec" in kv_transfer_params:
1373
+ req_disagg_spec = kv_transfer_params["disagg_spec"]
1374
+
1375
+ receiver_id = req_disagg_spec["receiver_host"] + str(
1376
+ req_disagg_spec["receiver_init_port"]
1377
+ )
1378
+
1379
+ disagg_spec = DisaggSpec(
1380
+ req_id=req_disagg_spec["req_id"],
1381
+ receiver_id=receiver_id,
1382
+ receiver_host=req_disagg_spec["receiver_host"],
1383
+ receiver_init_port=req_disagg_spec["receiver_init_port"],
1384
+ receiver_alloc_port=req_disagg_spec["receiver_alloc_port"],
1385
+ )
1386
+
1387
+ tmp_disagg_tracker[request.request_id] = disagg_spec
1388
+ self._unfinished_requests[request.request_id] = request
1389
+
1390
+ if request.request_id not in self.load_specs:
1391
+ # No KV tokens from external KV cache, return
1392
+ return
1393
+
1394
+ if num_external_tokens == 0:
1395
+ # No need to load anything
1396
+ self.load_specs[request.request_id].can_load = False
1397
+ return
1398
+
1399
+ recalc_last = (
1400
+ 1
1401
+ if (
1402
+ self.load_specs[request.request_id].lmcache_cached_tokens
1403
+ == request.num_tokens
1404
+ )
1405
+ else 0
1406
+ )
1407
+ assert (
1408
+ num_external_tokens
1409
+ == self.load_specs[request.request_id].lmcache_cached_tokens
1410
+ - self.load_specs[request.request_id].vllm_cached_tokens
1411
+ - recalc_last
1412
+ ), (
1413
+ f"Mismatch in tokens to load: {num_external_tokens} vs "
1414
+ f"{self.load_specs[request.request_id].lmcache_cached_tokens} "
1415
+ "(tokens in lmcache) - "
1416
+ f"{self.load_specs[request.request_id].vllm_cached_tokens} "
1417
+ "(tokens in vllm) - "
1418
+ f"{recalc_last} "
1419
+ "(full lmcache hits subtracts last token to recalculate logits)"
1420
+ f" for request {request.request_id}"
1421
+ )
1422
+
1423
+ self.load_specs[request.request_id].can_load = True
1424
+
1425
+ @_lmcache_nvtx_annotate
1426
+ def build_connector_meta(
1427
+ self, scheduler_output: SchedulerOutput
1428
+ ) -> KVConnectorMetadata:
1429
+ """Attach the connector metadata to the request object.
1430
+
1431
+ This function should NOT modify other fields in the scheduler_output
1432
+ except the `kv_connector_metadata` field.
1433
+ Also, calling this function will reset the state of the connector.
1434
+
1435
+ Args:
1436
+ scheduler_output (SchedulerOutput): the scheduler output object.
1437
+ """
1438
+
1439
+ force_skip_save = self.kv_role == "kv_consumer" or self.force_skip_save
1440
+
1441
+ meta = LMCacheConnectorMetadata()
1442
+
1443
+ for finished_req_id in scheduler_output.finished_req_ids:
1444
+ self._request_trackers.pop(finished_req_id, None)
1445
+ self._unfinished_requests.pop(finished_req_id, None)
1446
+
1447
+ # We should load KV for:
1448
+ # 1. new requests
1449
+ # 2. preempted requests (once per recovery)
1450
+ # can_load will only be True if `update_state_after_alloc` has been called
1451
+ # which only happens when vLLM's KV manager has space to receive KV from LMCache
1452
+ for request in scheduler_output.scheduled_new_reqs:
1453
+ # Ignore DP attention mock requests
1454
+ if request.req_id.startswith("mock_req"):
1455
+ continue
1456
+ load_spec = self.load_specs.pop(request.req_id, None)
1457
+ num_tokens_to_compute = (
1458
+ request.num_computed_tokens
1459
+ + scheduler_output.num_scheduled_tokens[request.req_id]
1460
+ )
1461
+ lmcache_cached_tokens = 0
1462
+ if load_spec is not None:
1463
+ lmcache_cached_tokens = load_spec.lmcache_cached_tokens
1464
+ request_priority = self._requests_priority.pop(request.req_id, 0)
1465
+
1466
+ skip_save = force_skip_save or (
1467
+ self.config.priority_limit is not None
1468
+ and request_priority > self.config.priority_limit
1469
+ )
1470
+
1471
+ request_tracker = RequestTracker.from_new_request(
1472
+ self.config,
1473
+ request,
1474
+ num_tokens_to_compute,
1475
+ lmcache_cached_tokens,
1476
+ skip_save,
1477
+ )
1478
+ self._request_trackers[request.req_id] = request_tracker
1479
+
1480
+ req_meta = ReqMeta.from_request_tracker(
1481
+ request_tracker,
1482
+ self._block_size,
1483
+ self._lmcache_chunk_size,
1484
+ load_spec=load_spec,
1485
+ discard_partial_chunks=self._discard_partial_chunks,
1486
+ save_decode_cache=self.config.save_decode_cache,
1487
+ )
1488
+ if req_meta is not None:
1489
+ meta.add_request(req_meta)
1490
+
1491
+ cached_reqs = scheduler_output.scheduled_cached_reqs
1492
+
1493
+ # NOTE: For backward compatibility with vllm version < 0.9.2,
1494
+ # In the latest vllm version, the type of scheduled_cached_reqs has
1495
+ # changed from list to object `CachedRequestData`
1496
+ if isinstance(cached_reqs, list):
1497
+ for i, req in enumerate(cached_reqs):
1498
+ load_spec = self.load_specs.pop(req.req_id, None)
1499
+ lmcache_cached_tokens = 0
1500
+ vllm_cached_tokens = 0
1501
+ if load_spec is not None:
1502
+ lmcache_cached_tokens = load_spec.lmcache_cached_tokens
1503
+ vllm_cached_tokens = load_spec.vllm_cached_tokens
1504
+ request_tracker = self._request_trackers[req.req_id]
1505
+
1506
+ # Pass all_token_ids for preempted requests to restore
1507
+ # token_ids correctly for chunk key computation
1508
+ all_token_ids = None
1509
+ if req.resumed_from_preemption:
1510
+ vllm_request = self._unfinished_requests.get(req.req_id)
1511
+ assert vllm_request is not None, (
1512
+ f"Preempted request {req.req_id} not found "
1513
+ "in _unfinished_requests"
1514
+ )
1515
+ all_token_ids = list(vllm_request.all_token_ids)
1516
+
1517
+ request_tracker.update(
1518
+ req.new_token_ids,
1519
+ req.new_block_ids,
1520
+ req.resumed_from_preemption,
1521
+ lmcache_cached_tokens=lmcache_cached_tokens,
1522
+ vllm_cached_tokens=vllm_cached_tokens,
1523
+ all_token_ids=all_token_ids,
1524
+ )
1525
+
1526
+ req_meta = ReqMeta.from_request_tracker(
1527
+ request_tracker,
1528
+ self._block_size,
1529
+ self._lmcache_chunk_size,
1530
+ load_spec=load_spec,
1531
+ discard_partial_chunks=self._discard_partial_chunks,
1532
+ save_decode_cache=self.config.save_decode_cache,
1533
+ )
1534
+ if req_meta is not None:
1535
+ meta.add_request(req_meta)
1536
+ return meta
1537
+
1538
+ for i, req_id in enumerate(cached_reqs.req_ids):
1539
+ request_tracker = self._request_trackers[req_id]
1540
+ num_new_tokens = scheduler_output.num_scheduled_tokens[req_id]
1541
+ # TODO: this is a dangerous reference to the request object inside vllm
1542
+ if request := self._unfinished_requests.get(req_id):
1543
+ num_current_tokens = request.num_computed_tokens
1544
+ # tracker_len < num_computed_tokens during decode
1545
+ # (important for save_decode_cache).
1546
+ # num_computed_tokens < tracker_len after preemption.
1547
+ tracker_len = len(request_tracker.token_ids)
1548
+ slice_base = min(num_current_tokens, tracker_len)
1549
+ new_token_ids = request.all_token_ids[
1550
+ slice_base : slice_base + num_new_tokens
1551
+ ]
1552
+ else:
1553
+ raise ValueError(
1554
+ f"Request {req_id} is not in _unfinished_requests, "
1555
+ f"but it is scheduled to be cached"
1556
+ )
1557
+ new_block_ids = cached_reqs.new_block_ids[i]
1558
+
1559
+ load_spec = self.load_specs.pop(req_id, None)
1560
+ lmcache_cached_tokens = 0
1561
+ vllm_cached_tokens = 0
1562
+ if load_spec is not None:
1563
+ lmcache_cached_tokens = load_spec.lmcache_cached_tokens
1564
+ vllm_cached_tokens = load_spec.vllm_cached_tokens
1565
+
1566
+ # Handle both old and new versions of CachedRequestData
1567
+ if hasattr(cached_reqs, "resumed_req_ids"):
1568
+ # New version with resumed_req_ids
1569
+ preempted = req_id in cached_reqs.resumed_req_ids
1570
+ elif hasattr(cached_reqs, "resumed_from_preemption"):
1571
+ # Old version with resumed_from_preemption
1572
+ preempted = cached_reqs.resumed_from_preemption[i]
1573
+ else:
1574
+ # This case should not be reached with supported vLLM versions.
1575
+ # Raising an error is safer than assuming not preempted.
1576
+ raise AttributeError(
1577
+ f"Unable to determine preemption status for request {req_id}. "
1578
+ f"This might be due to an unsupported vLLM version."
1579
+ )
1580
+ if preempted:
1581
+ assert load_spec is not None, (
1582
+ f"Request {req_id} is preempted but was not given a load spec"
1583
+ )
1584
+ # num_computed_tokens should be reset to 0 during preemption
1585
+ # and then set to the number of already cached tokens (maxxing
1586
+ # prefix caching and lmcache)
1587
+ # this assumption is crucial for the update() call of RequestTracker
1588
+ # On full cache hit, get_num_new_matched_tokens subtracts 1
1589
+ # to force last-token recomputation. This only affects
1590
+ # num_computed_tokens when lmcache has all tokens AND
1591
+ # provides more than vLLM's local cache.
1592
+ expected = max(lmcache_cached_tokens, load_spec.vllm_cached_tokens)
1593
+ full_hit_adj = (
1594
+ lmcache_cached_tokens == len(request.all_token_ids)
1595
+ and lmcache_cached_tokens > load_spec.vllm_cached_tokens
1596
+ )
1597
+ if full_hit_adj:
1598
+ expected -= 1
1599
+ assert request.num_computed_tokens == expected, (
1600
+ f"Preempted request {req_id} has "
1601
+ f"num_computed_tokens {request.num_computed_tokens} "
1602
+ f"but expected {expected} "
1603
+ f"(full_hit_adj={full_hit_adj})"
1604
+ )
1605
+
1606
+ # When retrieve fail, vllm will call _handle_invalid_blocks to
1607
+ # reset request.num_computed_tokens, this will lead to
1608
+ # request_tracker.token_ids being not matched with vllm
1609
+ if num_current_tokens < len(request_tracker.token_ids):
1610
+ logger.warning(
1611
+ "Request %s rolled back from %d to %d tokens; "
1612
+ "truncating tracker state.",
1613
+ req_id,
1614
+ len(request_tracker.token_ids),
1615
+ num_current_tokens,
1616
+ )
1617
+ num_token_slots = (
1618
+ len(request_tracker.allocated_block_ids) * self._block_size
1619
+ )
1620
+ tokens_to_keep = num_current_tokens
1621
+ if num_token_slots < num_current_tokens:
1622
+ logger.warning(
1623
+ "Request %s tracker has %d token slots but %d tokens; "
1624
+ "capping token_ids to slot capacity.",
1625
+ req_id,
1626
+ num_token_slots,
1627
+ num_current_tokens,
1628
+ )
1629
+ tokens_to_keep = num_token_slots
1630
+
1631
+ request_tracker.token_ids = list(request.all_token_ids[:tokens_to_keep])
1632
+ request_tracker.num_saved_tokens = min(
1633
+ request_tracker.num_saved_tokens, tokens_to_keep
1634
+ )
1635
+
1636
+ # Pass all_token_ids for preempted requests to restore
1637
+ # token_ids correctly for chunk key computation
1638
+ all_token_ids = list(request.all_token_ids) if preempted else None
1639
+
1640
+ request_tracker.update(
1641
+ new_token_ids,
1642
+ new_block_ids,
1643
+ preempted=preempted,
1644
+ lmcache_cached_tokens=lmcache_cached_tokens,
1645
+ vllm_cached_tokens=vllm_cached_tokens,
1646
+ all_token_ids=all_token_ids,
1647
+ )
1648
+
1649
+ req_meta = ReqMeta.from_request_tracker(
1650
+ request_tracker,
1651
+ self._block_size,
1652
+ self._lmcache_chunk_size,
1653
+ load_spec=load_spec,
1654
+ discard_partial_chunks=self._discard_partial_chunks,
1655
+ save_decode_cache=self.config.save_decode_cache,
1656
+ )
1657
+ if req_meta is not None:
1658
+ meta.add_request(req_meta)
1659
+
1660
+ return meta
1661
+
1662
+ @_lmcache_nvtx_annotate
1663
+ def request_finished(
1664
+ self,
1665
+ request: "Request",
1666
+ block_ids: list[int],
1667
+ ) -> tuple[bool, Optional[dict[str, Any]]]:
1668
+ # Layerwise save uses request-scoped generators. If request finishes
1669
+ # without entering wait_for_save (abort/error/evict path), make sure
1670
+ # we release the generator entry to avoid leaking state.
1671
+ if getattr(self, "use_layerwise", False) and hasattr(
1672
+ self, "_layerwise_save_storers"
1673
+ ):
1674
+ self._layerwise_save_storers.pop(request.request_id, None)
1675
+
1676
+ # Cleanup if request was aborted
1677
+ if request.status == RequestStatus.FINISHED_ABORTED and self.async_loading:
1678
+ # Cancel any ongoing async lookup and prefetch tasks on workers
1679
+ lookup_id = request.request_id
1680
+ assert self.lookup_client is not None
1681
+ self.lookup_client.cancel_lookup(lookup_id) # type: ignore[attr-defined]
1682
+
1683
+ params = (
1684
+ request.kv_transfer_params
1685
+ if hasattr(request, "kv_transfer_params")
1686
+ else None
1687
+ )
1688
+ return_params = None
1689
+
1690
+ # NOTE: Used to stream back the first token
1691
+ # for disagg prefill
1692
+ if params is not None and "ret_first_tok" in params:
1693
+ return_params = {
1694
+ "first_tok": request._output_token_ids[0],
1695
+ }
1696
+
1697
+ if self.config.get_extra_config_value(
1698
+ "enable_cache_usage_details_in_response", False
1699
+ ):
1700
+ request_tracker = self._request_trackers.get(request.request_id)
1701
+ if request_tracker:
1702
+ return_params = return_params or {}
1703
+ return_params["num_lmcache_cached_tokens"] = (
1704
+ request_tracker.num_lmcache_cached_tokens
1705
+ )
1706
+
1707
+ return False, return_params
1708
+
1709
+ @_lmcache_nvtx_annotate
1710
+ def get_kv_events(self) -> Iterable[CacheStoreEvent]:
1711
+ if self.lmcache_engine is not None:
1712
+ return self.lmcache_engine.get_kv_events()
1713
+ return []