lmcache-cli 0.5.7rc2.dev0__py3-none-any.whl → 0.5.7rc4.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 (329) hide show
  1. lmcache/_version.py +3 -3
  2. lmcache/cli/commands/bench/engine_bench/quality/alignment.py +328 -21
  3. lmcache/cli/commands/bench/engine_bench/quality/prompts.py +48 -3
  4. lmcache/cli/commands/bench/engine_bench/quality/scoring.py +10 -3
  5. lmcache/cli/commands/bench/quality_bench/__init__.py +34 -0
  6. lmcache/cli/commands/bench/quality_bench/analyze_command.py +44 -0
  7. lmcache/cli/commands/bench/quality_bench/core/__init__.py +7 -0
  8. lmcache/cli/commands/bench/quality_bench/core/analyze_stage.py +285 -0
  9. lmcache/cli/commands/bench/quality_bench/core/blend_counters.py +214 -0
  10. lmcache/cli/commands/bench/quality_bench/core/config.py +343 -0
  11. lmcache/cli/commands/bench/quality_bench/core/judge.py +492 -0
  12. lmcache/cli/commands/bench/quality_bench/core/plan.py +920 -0
  13. lmcache/cli/commands/bench/quality_bench/core/report.py +322 -0
  14. lmcache/cli/commands/bench/quality_bench/core/run_files.py +589 -0
  15. lmcache/cli/commands/bench/quality_bench/core/run_stage.py +518 -0
  16. lmcache/cli/commands/bench/quality_bench/core/runner.py +237 -0
  17. lmcache/cli/commands/bench/quality_bench/core/task.py +48 -0
  18. lmcache/cli/commands/bench/quality_bench/core/verdict.py +875 -0
  19. lmcache/cli/commands/bench/quality_bench/run_command.py +46 -0
  20. lmcache/cli/commands/bench/server_bench/__init__.py +21 -5
  21. lmcache/cli/commands/bench/server_bench/allocator.py +78 -0
  22. lmcache/cli/commands/bench/server_bench/arguments.py +236 -0
  23. lmcache/cli/commands/bench/server_bench/buffers.py +95 -32
  24. lmcache/cli/commands/bench/server_bench/cases/baseline.py +63 -41
  25. lmcache/cli/commands/bench/server_bench/client.py +326 -187
  26. lmcache/cli/commands/bench/server_bench/command.py +26 -240
  27. lmcache/cli/commands/bench/server_bench/config.py +14 -1
  28. lmcache/cli/commands/bench/server_bench/helpers.py +42 -130
  29. lmcache/cli/commands/server.py +1 -1
  30. lmcache/cli/commands/trace/_dispatch.py +50 -9
  31. lmcache/inprocess/integration/base_service_factory.py +2 -2
  32. lmcache/inprocess/integration/tensorrt_llm/utils.py +2 -2
  33. lmcache/inprocess/integration/vllm/utils.py +160 -0
  34. lmcache/inprocess/integration/vllm/vllm_ec_adapter.py +2 -2
  35. lmcache/inprocess/integration/vllm/vllm_service_factory.py +8 -8
  36. lmcache/inprocess/integration/vllm/vllm_v1_adapter.py +9 -9
  37. lmcache/inprocess/standalone/__main__.py +7 -4
  38. lmcache/inprocess/standalone/manager.py +2 -2
  39. lmcache/inprocess/standalone/standalone_service_factory.py +2 -2
  40. lmcache/inprocess/v1/api_server/__main__.py +1 -1
  41. lmcache/inprocess/v1/cache_controller/config.py +2 -2
  42. lmcache/inprocess/v1/cache_controller/full_sync_sender.py +2 -2
  43. lmcache/inprocess/v1/cache_controller/worker.py +2 -2
  44. lmcache/inprocess/v1/cache_engine.py +9 -10
  45. lmcache/inprocess/v1/check/check_mode_test_remote.py +5 -5
  46. lmcache/inprocess/v1/check/utils.py +4 -4
  47. lmcache/{v1 → inprocess/v1}/compute/attention/abstract.py +1 -1
  48. lmcache/{v1 → inprocess/v1}/compute/attention/flash_attn.py +4 -4
  49. lmcache/{v1 → inprocess/v1}/compute/attention/flash_infer_sparse.py +3 -3
  50. lmcache/{v1 → inprocess/v1}/compute/attention/metadata.py +1 -1
  51. lmcache/{v1 → inprocess/v1}/compute/attention/triton_sparse.py +4 -4
  52. lmcache/{v1 → inprocess/v1}/compute/attention/utils.py +1 -1
  53. lmcache/{v1 → inprocess/v1}/compute/blend/__init__.py +1 -1
  54. lmcache/{v1 → inprocess/v1}/compute/blend/blender.py +7 -4
  55. lmcache/{v1 → inprocess/v1}/compute/blend/utils.py +3 -3
  56. lmcache/{v1 → inprocess/v1}/compute/models/base.py +2 -2
  57. lmcache/{v1 → inprocess/v1}/compute/models/llama.py +1 -1
  58. lmcache/{v1 → inprocess/v1}/compute/models/qwen3.py +1 -1
  59. lmcache/{v1 → inprocess/v1}/compute/models/utils.py +3 -3
  60. lmcache/{v1 → inprocess/v1}/config.py +2 -23
  61. lmcache/inprocess/v1/ec_engine.py +3 -3
  62. lmcache/inprocess/v1/gpu_connector/__init__.py +2 -2
  63. lmcache/inprocess/v1/gpu_connector/gpu_connectors.py +2 -2
  64. lmcache/inprocess/v1/gpu_connector/hpu_connector.py +1 -1
  65. lmcache/inprocess/v1/gpu_connector/musa_connectors.py +3 -2
  66. lmcache/inprocess/v1/gpu_connector/utils.py +1 -1
  67. lmcache/inprocess/v1/gpu_connector/xpu_connectors.py +2 -2
  68. lmcache/inprocess/v1/health_monitor/base.py +5 -3
  69. lmcache/inprocess/v1/health_monitor/checks/remote_backend_check.py +6 -4
  70. lmcache/inprocess/v1/hidden_state_store.py +2 -2
  71. lmcache/inprocess/v1/internal_api_server/vllm/conf_api.py +2 -2
  72. lmcache/inprocess/v1/internal_api_server/vllm/load_fs_chunks_api.py +2 -2
  73. lmcache/inprocess/v1/lookup_client/chunk_statistics_lookup_client.py +2 -2
  74. lmcache/inprocess/v1/lookup_client/factory.py +2 -2
  75. lmcache/inprocess/v1/lookup_client/hit_limit_lookup_client.py +1 -1
  76. lmcache/inprocess/v1/lookup_client/lmcache_async_lookup_client.py +2 -2
  77. lmcache/inprocess/v1/lookup_client/lmcache_lookup_client.py +2 -2
  78. lmcache/inprocess/v1/lookup_client/lmcache_lookup_client_bypass.py +2 -2
  79. lmcache/inprocess/v1/lookup_client/mooncake_lookup_client.py +5 -5
  80. lmcache/inprocess/v1/manager.py +2 -2
  81. lmcache/inprocess/v1/memory_allocators/__init__.py +2 -0
  82. lmcache/{v1 → inprocess/v1}/storage_backend/__init__.py +19 -13
  83. lmcache/{v1 → inprocess/v1}/storage_backend/abstract_backend.py +3 -3
  84. lmcache/{v1 → inprocess/v1}/storage_backend/audit_backend.py +3 -1
  85. lmcache/{v1 → inprocess/v1}/storage_backend/batched_message_sender.py +2 -2
  86. lmcache/{v1 → inprocess/v1}/storage_backend/cache_policy/__init__.py +7 -5
  87. lmcache/{v1 → inprocess/v1}/storage_backend/cache_policy/fifo.py +4 -1
  88. lmcache/{v1 → inprocess/v1}/storage_backend/cache_policy/lfu.py +4 -1
  89. lmcache/{v1 → inprocess/v1}/storage_backend/cache_policy/lru.py +4 -1
  90. lmcache/{v1 → inprocess/v1}/storage_backend/cache_policy/mru.py +4 -1
  91. lmcache/{v1 → inprocess/v1}/storage_backend/connector/__init__.py +9 -7
  92. lmcache/{v1 → inprocess/v1}/storage_backend/connector/audit_adapter.py +5 -3
  93. lmcache/{v1 → inprocess/v1}/storage_backend/connector/audit_connector.py +4 -2
  94. lmcache/{v1 → inprocess/v1}/storage_backend/connector/azure_adapter.py +5 -3
  95. lmcache/{v1 → inprocess/v1}/storage_backend/connector/azure_connector.py +4 -2
  96. lmcache/{v1 → inprocess/v1}/storage_backend/connector/base_connector.py +2 -2
  97. lmcache/{v1 → inprocess/v1}/storage_backend/connector/bigtable_adapter.py +7 -2
  98. lmcache/{v1 → inprocess/v1}/storage_backend/connector/bigtable_connector.py +12 -6
  99. lmcache/{v1 → inprocess/v1}/storage_backend/connector/blackhole_adapter.py +7 -2
  100. lmcache/{v1 → inprocess/v1}/storage_backend/connector/blackhole_connector.py +4 -3
  101. lmcache/{v1 → inprocess/v1}/storage_backend/connector/eic_adapter.py +5 -3
  102. lmcache/{v1 → inprocess/v1}/storage_backend/connector/eic_connector.py +7 -3
  103. lmcache/{v1 → inprocess/v1}/storage_backend/connector/external_adapter.py +5 -3
  104. lmcache/{v1 → inprocess/v1}/storage_backend/connector/fs_adapter.py +3 -3
  105. lmcache/{v1 → inprocess/v1}/storage_backend/connector/fs_connector.py +5 -3
  106. lmcache/{v1 → inprocess/v1}/storage_backend/connector/hf3fs_adapter.py +5 -3
  107. lmcache/{v1 → inprocess/v1}/storage_backend/connector/hf3fs_connector.py +2 -2
  108. lmcache/{v1 → inprocess/v1}/storage_backend/connector/hfbucket_adapter.py +6 -4
  109. lmcache/{v1 → inprocess/v1}/storage_backend/connector/hfbucket_connector.py +15 -203
  110. lmcache/{v1 → inprocess/v1}/storage_backend/connector/infinistore_adapter.py +5 -3
  111. lmcache/{v1 → inprocess/v1}/storage_backend/connector/infinistore_connector.py +4 -2
  112. lmcache/{v1 → inprocess/v1}/storage_backend/connector/instrumented_connector.py +3 -1
  113. lmcache/{v1 → inprocess/v1}/storage_backend/connector/mock_adapter.py +5 -3
  114. lmcache/{v1 → inprocess/v1}/storage_backend/connector/mock_connector.py +7 -3
  115. lmcache/{v1 → inprocess/v1}/storage_backend/connector/mooncakestore_adapter.py +3 -3
  116. lmcache/{v1 → inprocess/v1}/storage_backend/connector/mooncakestore_connector.py +8 -7
  117. lmcache/{v1 → inprocess/v1}/storage_backend/connector/redis_adapter.py +5 -3
  118. lmcache/{v1 → inprocess/v1}/storage_backend/connector/redis_connector.py +8 -4
  119. lmcache/{v1 → inprocess/v1}/storage_backend/connector/s3_adapter.py +5 -3
  120. lmcache/{v1 → inprocess/v1}/storage_backend/connector/s3_connector.py +7 -3
  121. lmcache/{v1 → inprocess/v1}/storage_backend/connector/sagemaker_hyperpod_adapter.py +5 -3
  122. lmcache/{v1 → inprocess/v1}/storage_backend/connector/sagemaker_hyperpod_connector.py +7 -3
  123. lmcache/{v1 → inprocess/v1}/storage_backend/connector/valkey_adapter.py +6 -4
  124. lmcache/{v1 → inprocess/v1}/storage_backend/connector/valkey_connector.py +10 -6
  125. lmcache/{v1 → inprocess/v1}/storage_backend/gds_backend.py +13 -9
  126. lmcache/{v1 → inprocess/v1}/storage_backend/job_executor/pq_executor.py +3 -1
  127. lmcache/{storage_backend/serde → inprocess/v1/storage_backend/legacy_serde}/cachegen_decoder.py +6 -6
  128. lmcache/{storage_backend/serde → inprocess/v1/storage_backend/legacy_serde}/cachegen_encoder.py +6 -6
  129. lmcache/{v1 → inprocess/v1}/storage_backend/local_cpu_backend.py +13 -9
  130. lmcache/{v1 → inprocess/v1}/storage_backend/local_disk_backend.py +14 -10
  131. lmcache/{v1 → inprocess/v1}/storage_backend/maru_backend.py +5 -3
  132. lmcache/{v1 → inprocess/v1}/storage_backend/naive_serde/__init__.py +14 -7
  133. lmcache/{v1 → inprocess/v1}/storage_backend/naive_serde/cachegen_basics.py +3 -1
  134. lmcache/{v1 → inprocess/v1}/storage_backend/naive_serde/cachegen_decoder.py +9 -7
  135. lmcache/{v1 → inprocess/v1}/storage_backend/naive_serde/cachegen_encoder.py +9 -5
  136. lmcache/{v1 → inprocess/v1}/storage_backend/naive_serde/kivi_serde.py +4 -1
  137. lmcache/{v1 → inprocess/v1}/storage_backend/naive_serde/naive_serde.py +4 -1
  138. lmcache/{v1 → inprocess/v1}/storage_backend/nixl_storage_backend.py +8 -6
  139. lmcache/{v1 → inprocess/v1}/storage_backend/p2p_backend.py +9 -7
  140. lmcache/{v1 → inprocess/v1}/storage_backend/pd_backend.py +8 -6
  141. lmcache/{v1 → inprocess/v1}/storage_backend/pd_backend_async.py +12 -8
  142. lmcache/{v1 → inprocess/v1}/storage_backend/plugins/dax_backend.py +5 -5
  143. lmcache/{v1 → inprocess/v1}/storage_backend/plugins/rust_raw_block_backend.py +3 -3
  144. lmcache/{v1 → inprocess/v1}/storage_backend/remote_backend.py +11 -7
  145. lmcache/{v1 → inprocess/v1}/storage_backend/resp_client.py +1 -1
  146. lmcache/{v1 → inprocess/v1}/storage_backend/storage_backend_listener.py +3 -1
  147. lmcache/{v1 → inprocess/v1}/storage_backend/storage_manager.py +8 -8
  148. lmcache/inprocess/v1/system_detection.py +116 -0
  149. lmcache/inprocess/v1/token_database.py +2 -2
  150. lmcache/integration/atom/multi_process_adapter.py +5 -4
  151. lmcache/integration/sglang/unified_lmcache_mp_connector.py +11 -10
  152. lmcache/integration/tensorrt_llm/tensorrt_mp_adapter.py +4 -4
  153. lmcache/integration/vllm/experimental/token_drop.py +18 -1
  154. lmcache/integration/vllm/experimental/token_drop_kv.py +55 -0
  155. lmcache/integration/vllm/lazy_offload_manager.py +14 -11
  156. lmcache/integration/vllm/lmcache_mp_connector.py +4 -4
  157. lmcache/integration/vllm/lmcache_mp_metadata.py +59 -6
  158. lmcache/integration/vllm/mp_lookup_state.py +2 -1
  159. lmcache/integration/vllm/mp_server_launcher.py +31 -15
  160. lmcache/integration/vllm/utils.py +5 -178
  161. lmcache/integration/vllm/vllm_multi_process_adapter.py +70 -90
  162. lmcache/observability.py +5 -29
  163. lmcache/sdk/context.py +2 -2
  164. lmcache/sdk/qringbuffer.py +3 -3
  165. lmcache/security/__init__.py +2 -0
  166. lmcache/security/api_key.py +207 -0
  167. lmcache/security/http.py +99 -0
  168. lmcache/tools/cache_simulator/trace_analyze.py +62 -0
  169. lmcache/usage_telemetry/context.py +2 -2
  170. lmcache/usage_telemetry/continuous.py +1 -1
  171. lmcache/usage_telemetry/messages.py +13 -12
  172. lmcache/v1/distributed/api.py +106 -51
  173. lmcache/v1/distributed/config.py +58 -149
  174. lmcache/v1/distributed/internal_api.py +14 -42
  175. lmcache/v1/distributed/key_codec.py +104 -0
  176. lmcache/v1/distributed/l1_manager/__init__.py +39 -0
  177. lmcache/v1/distributed/{l1_manager.py → l1_manager/base.py} +95 -267
  178. lmcache/v1/distributed/l1_manager/devdax.py +203 -0
  179. lmcache/v1/distributed/l1_manager/dram.py +39 -0
  180. lmcache/v1/distributed/l1_manager/factory.py +75 -0
  181. lmcache/v1/distributed/l1_manager/gds.py +73 -0
  182. lmcache/v1/distributed/l2_adapters/bigtable_key_encoder.py +5 -20
  183. lmcache/v1/distributed/l2_adapters/bigtable_l2_adapter.py +9 -21
  184. lmcache/v1/distributed/l2_adapters/dax_l2_adapter.py +5 -5
  185. lmcache/v1/distributed/l2_adapters/fs_l2_adapter.py +154 -124
  186. lmcache/v1/distributed/l2_adapters/hfbucket_client.py +193 -0
  187. lmcache/v1/distributed/l2_adapters/hfbucket_l2_adapter.py +5 -16
  188. lmcache/v1/distributed/l2_adapters/native_connector_l2_adapter.py +3 -24
  189. lmcache/v1/distributed/l2_adapters/nixl_store_agents/dynamic_nixl_store_agent.py +3 -6
  190. lmcache/v1/{storage_backend → distributed/l2_adapters}/raw_block/__init__.py +2 -2
  191. lmcache/v1/{storage_backend → distributed/l2_adapters}/raw_block/core.py +8 -3
  192. lmcache/v1/{storage_backend → distributed/l2_adapters}/raw_block/key_codec.py +5 -27
  193. lmcache/v1/distributed/l2_adapters/raw_block_l2_adapter.py +3 -3
  194. lmcache/v1/distributed/l2_adapters/s3_l2_adapter.py +80 -74
  195. lmcache/v1/distributed/l2_adapters/sagemaker_hyperpod_l2_adapter.py +3 -15
  196. lmcache/v1/{storage_backend → distributed/l2_adapters}/valkey/__init__.py +1 -1
  197. lmcache/v1/distributed/l2_adapters/valkey_l2_adapter.py +1 -1
  198. lmcache/v1/distributed/memory_manager/__init__.py +5 -3
  199. lmcache/v1/distributed/memory_manager/devdax_l1_memory_manager.py +12 -43
  200. lmcache/v1/distributed/memory_manager/gds_l1_memory_manager.py +2 -13
  201. lmcache/v1/distributed/memory_manager/l1_manager_protocol.py +3 -7
  202. lmcache/v1/distributed/memory_manager/l1_memory_manager.py +1 -12
  203. lmcache/v1/distributed/serde/turboquant/store_kernel.py +3 -0
  204. lmcache/v1/distributed/storage_controllers/prefetch_controller.py +38 -10
  205. lmcache/v1/distributed/storage_controllers/utils.py +2 -30
  206. lmcache/v1/distributed/storage_controllers/write_policy.py +104 -11
  207. lmcache/v1/distributed/storage_manager.py +302 -245
  208. lmcache/v1/distributed/transfer_channel/impl/mooncake_te_impl.py +16 -3
  209. lmcache/v1/gpu_connector/kv_format/specs/base.py +68 -2
  210. lmcache/v1/kv_layer_groups.py +23 -0
  211. lmcache/v1/memory_allocators/devdax_memory_allocator.py +26 -148
  212. lmcache/v1/memory_management.py +0 -19
  213. lmcache/v1/memory_orchestrator/_codec.py +15 -17
  214. lmcache/v1/mp_coordinator/app.py +27 -20
  215. lmcache/v1/mp_coordinator/background_loops.py +4 -2
  216. lmcache/v1/mp_coordinator/controllers/eviction_controller.py +5 -1
  217. lmcache/v1/mp_coordinator/controllers/eviction_http_api.py +56 -9
  218. lmcache/v1/mp_coordinator/controllers/prefetch_manager.py +192 -8
  219. lmcache/v1/mp_coordinator/http_apis/cache_api.py +1 -0
  220. lmcache/v1/mp_coordinator/http_apis/directory_api.py +53 -4
  221. lmcache/v1/mp_coordinator/ingest/kafka_event_source.py +13 -8
  222. lmcache/v1/mp_coordinator/ingest/stream_position.py +2 -3
  223. lmcache/v1/mp_coordinator/observability.py +327 -5
  224. lmcache/v1/mp_coordinator/persistence/checkpoint.py +172 -9
  225. lmcache/v1/mp_coordinator/persistence/metadata.py +47 -4
  226. lmcache/v1/mp_coordinator/utils/histogram.py +99 -0
  227. lmcache/v1/mp_coordinator/views/instance_registry.py +55 -4
  228. lmcache/v1/mp_coordinator/views/key_directory.py +73 -2
  229. lmcache/v1/mp_coordinator/views/usage_manager.py +27 -0
  230. lmcache/v1/mp_observability/README.md +1 -1
  231. lmcache/v1/mp_observability/otel_init.py +153 -5
  232. lmcache/v1/mp_observability/subscribers/logging/extra_stats.py +10 -4
  233. lmcache/v1/mp_observability/subscribers/metrics/l0_l1_throughput.py +15 -7
  234. lmcache/v1/mp_observability/subscribers/metrics/l1_eviction_loop.py +1 -1
  235. lmcache/v1/mp_observability/subscribers/tracing/mp_server.py +20 -8
  236. lmcache/v1/mp_observability/subscribers/tracing/span_registry.py +46 -17
  237. lmcache/v1/mp_observability/subscribers/tracing/transfer_phase.py +4 -2
  238. lmcache/v1/mp_observability/trace/format.py +4 -4
  239. lmcache/v1/mp_observability/trace/reader.py +8 -1
  240. lmcache/v1/multiprocess/cache_control/key_resolver.py +2 -2
  241. lmcache/v1/multiprocess/client_config.py +50 -0
  242. lmcache/v1/multiprocess/config.py +15 -15
  243. lmcache/v1/multiprocess/custom_types.py +103 -9
  244. lmcache/v1/multiprocess/engine_context.py +1 -1
  245. lmcache/v1/multiprocess/http_apis/common/env_api.py +1 -0
  246. lmcache/v1/multiprocess/http_auth.py +35 -123
  247. lmcache/v1/multiprocess/http_server.py +1 -1
  248. lmcache/v1/multiprocess/kv_event_subscriber.py +150 -0
  249. lmcache/v1/multiprocess/modules/blend/lookup.py +15 -10
  250. lmcache/v1/multiprocess/modules/blend/module.py +3 -2
  251. lmcache/v1/multiprocess/modules/blend/store.py +6 -3
  252. lmcache/v1/multiprocess/modules/engine_driven_transfer.py +388 -30
  253. lmcache/v1/multiprocess/modules/lmcache_driven_transfer.py +18 -16
  254. lmcache/v1/multiprocess/modules/lookup.py +10 -9
  255. lmcache/v1/multiprocess/modules/p2p_controller.py +28 -3
  256. lmcache/v1/multiprocess/modules/server_transfer.py +300 -112
  257. lmcache/v1/multiprocess/plugin_modules/qstore.py +4 -5
  258. lmcache/v1/multiprocess/session.py +23 -15
  259. lmcache/v1/multiprocess/token_codec.py +90 -0
  260. lmcache/v1/multiprocess/token_hasher.py +96 -9
  261. lmcache/v1/multiprocess/transfer_context/async_engine_driven.py +125 -0
  262. lmcache/v1/multiprocess/transfer_context/base.py +114 -25
  263. lmcache/v1/multiprocess/transfer_context/inline.py +53 -2
  264. lmcache/v1/multiprocess/transfer_context/shm.py +68 -0
  265. lmcache/v1/multiprocess/transfer_context/tensor_wire.py +164 -5
  266. lmcache/v1/multiprocess/transfer_context/worker_transfer.py +575 -24
  267. lmcache/v1/multiprocess/transport/auth.py +50 -0
  268. lmcache/v1/multiprocess/transport/base.py +4 -1
  269. lmcache/v1/multiprocess/transport/errors.py +39 -0
  270. lmcache/v1/multiprocess/transport/grpc_impl/_proto_gen/common_pb2.pyi +20 -6
  271. lmcache/v1/multiprocess/transport/grpc_impl/_proto_gen/p2p_service_pb2.pyi +5 -12
  272. lmcache/v1/multiprocess/transport/grpc_impl/client.py +143 -10
  273. lmcache/v1/multiprocess/transport/grpc_impl/native_metrics.py +115 -0
  274. lmcache/v1/multiprocess/transport/grpc_impl/native_server.py +458 -0
  275. lmcache/v1/multiprocess/transport/grpc_impl/protos/common.proto +10 -1
  276. lmcache/v1/multiprocess/transport/grpc_impl/protos/engine_driven_service.proto +23 -0
  277. lmcache/v1/multiprocess/transport/grpc_impl/protos/p2p_service.proto +3 -5
  278. lmcache/v1/multiprocess/transport/grpc_impl/server.py +6 -2
  279. lmcache/v1/multiprocess/transport/grpc_impl/server_base.py +4 -2
  280. lmcache/v1/multiprocess/transport/grpc_impl/server_builder.py +7 -2
  281. lmcache/v1/multiprocess/transport/grpc_impl/server_handlers.py +3 -0
  282. lmcache/v1/multiprocess/transport/zmq_impl/mq.py +38 -1
  283. lmcache/v1/multiprocess/transport/zmq_impl/wire.py +45 -0
  284. lmcache/v1/periodic_thread.py +1 -1
  285. lmcache/v1/platform/devices/mlu/__init__.py +76 -0
  286. lmcache/v1/platform/devices/mlu/device_ops.py +277 -0
  287. lmcache/v1/platform/devices/musa/cache_context.py +9 -4
  288. lmcache/v1/platform/devices/musa/device_ops.py +35 -33
  289. lmcache/v1/platform/devices/musa/format_capabilities.py +93 -0
  290. lmcache/v1/platform/devices/musa/native_kv_transfer.py +9 -25
  291. lmcache/v1/platform/native_block_transfer.py +6 -4
  292. lmcache/v1/system_detection.py +4 -113
  293. {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc4.dev0.dist-info}/METADATA +3 -2
  294. {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc4.dev0.dist-info}/RECORD +328 -287
  295. lmcache/inprocess/v1/pin_monitor.py +0 -260
  296. /lmcache/{v1 → inprocess/v1}/compute/__init__.py +0 -0
  297. /lmcache/{storage_backend/serde → inprocess/v1/compute/attention}/__init__.py +0 -0
  298. /lmcache/{v1 → inprocess/v1}/compute/attention/triton_kernels/__init__.py +0 -0
  299. /lmcache/{v1 → inprocess/v1}/compute/attention/triton_kernels/block_sparse_attention.py +0 -0
  300. /lmcache/{v1 → inprocess/v1}/compute/blend/metadata.py +0 -0
  301. /lmcache/{v1 → inprocess/v1}/compute/models/__init__.py +0 -0
  302. /lmcache/{v1 → inprocess/v1}/compute/positional_encoding.py +0 -0
  303. /lmcache/{v1 → inprocess/v1}/config_base.py +0 -0
  304. /lmcache/{v1 → inprocess/v1}/memory_allocators/cu_file_memory_allocator.py +0 -0
  305. /lmcache/{v1 → inprocess/v1}/memory_allocators/hip_file_memory_allocator.py +0 -0
  306. /lmcache/{v1 → inprocess/v1}/memory_allocators/paged_cpu_gpu_memory_allocator.py +0 -0
  307. /lmcache/{v1 → inprocess/v1}/metadata.py +0 -0
  308. /lmcache/{v1 → inprocess/v1}/storage_backend/cache_policy/base_policy.py +0 -0
  309. /lmcache/{v1 → inprocess/v1}/storage_backend/connector/bigtable_schema.py +0 -0
  310. /lmcache/{v1 → inprocess/v1}/storage_backend/hipfile_shim.py +0 -0
  311. /lmcache/{v1 → inprocess/v1}/storage_backend/job_executor/__init__.py +0 -0
  312. /lmcache/{v1 → inprocess/v1}/storage_backend/job_executor/base_executor.py +0 -0
  313. /lmcache/{v1/compute/attention → inprocess/v1/storage_backend/legacy_serde}/__init__.py +0 -0
  314. /lmcache/{storage_backend/serde → inprocess/v1/storage_backend/legacy_serde}/cachegen_basics.py +0 -0
  315. /lmcache/{storage_backend/serde → inprocess/v1/storage_backend/legacy_serde}/serde.py +0 -0
  316. /lmcache/{v1 → inprocess/v1}/storage_backend/naive_serde/serde.py +0 -0
  317. /lmcache/{v1 → inprocess/v1}/storage_backend/native_clients/aerospike_client.py +0 -0
  318. /lmcache/{v1 → inprocess/v1}/storage_backend/native_clients/connector_client_base.py +0 -0
  319. /lmcache/{v1 → inprocess/v1}/storage_backend/native_clients/resp_client.py +0 -0
  320. /lmcache/{v1 → inprocess/v1}/storage_backend/path_sharder.py +0 -0
  321. /lmcache/v1/{storage_backend/connector → distributed/l2_adapters}/bigtable_config.py +0 -0
  322. /lmcache/v1/{storage_backend/connector → distributed/l2_adapters}/bigtable_sharder.py +0 -0
  323. /lmcache/v1/{storage_backend → distributed/l2_adapters}/dax/__init__.py +0 -0
  324. /lmcache/v1/{storage_backend → distributed/l2_adapters}/dax/core.py +0 -0
  325. /lmcache/v1/{storage_backend → distributed/l2_adapters}/valkey/worker_pool.py +0 -0
  326. {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc4.dev0.dist-info}/WHEEL +0 -0
  327. {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc4.dev0.dist-info}/entry_points.txt +0 -0
  328. {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc4.dev0.dist-info}/licenses/LICENSE +0 -0
  329. {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc4.dev0.dist-info}/top_level.txt +0 -0
lmcache/_version.py CHANGED
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.5.7rc2.dev0'
22
- __version_tuple__ = version_tuple = (0, 5, 7, 'rc2', 'dev0')
21
+ __version__ = version = '0.5.7rc4.dev0'
22
+ __version_tuple__ = version_tuple = (0, 5, 7, 'rc4', 'dev0')
23
23
 
24
- __commit_id__ = commit_id = 'gaa12d34e3'
24
+ __commit_id__ = commit_id = 'g45a0bb420'
@@ -5,11 +5,27 @@ A passage cached on its own matches inside a longer prompt only if it starts
5
5
  on a cache-chunk boundary in both places: chunks are content-addressed, so a
6
6
  passage that lands off-phase shares no chunk with its cached copy and nothing
7
7
  is reused, silently. :class:`ChunkAligner` pads the shared system block and
8
- each passage so every block occupies whole chunks.
8
+ each passage so every block occupies whole chunks, and
9
+ :meth:`ChunkAligner.place_blocks` checks, on the rendered chat prompt, that
10
+ they really do.
11
+
12
+ What fills the padding is the :class:`Filler`:
13
+
14
+ * ``words`` (legacy, and ``rag-qa-quality``'s) appends random single-token
15
+ words.
16
+ * ``dots`` / ``dashes`` wrap each passage as
17
+ ``Passage <id>:\\n<text>\\n\\n . . … .\\nend of passage <id>`` -- a repeated
18
+ low-information token the model can read as a divider. The block starts
19
+ with a letter and ends with a digit, so no tokenizer merges it with its
20
+ neighbours, and no chat template trims it (see :func:`block_id`).
9
21
  """
10
22
 
11
23
  # Standard
24
+ from collections.abc import Mapping
25
+ from dataclasses import dataclass
12
26
  from typing import Any
27
+ import enum
28
+ import hashlib
13
29
  import math
14
30
  import random
15
31
 
@@ -31,14 +47,79 @@ _TEMPLATE_SENTINEL = "██SENTINEL██"
31
47
  # Re-encode attempts per block; a merged boundary token shifts the estimate.
32
48
  _PAD_ATTEMPTS = 8
33
49
 
50
+ # Chunk targets tried per block by the unit fillers before giving up: a
51
+ # boundary merge can make a target unreachable, never the next one too.
52
+ _PAD_TARGETS = 3
53
+
54
+
55
+ class Filler(enum.Enum):
56
+ """What pads a block to a whole number of chunks."""
57
+
58
+ WORDS = "words"
59
+ """Random single-token words (legacy; ``rag-qa-quality`` uses it).
60
+ Noisy for the model to read."""
61
+
62
+ DOTS = "dots"
63
+ """Repeated ``" ."`` inside a ``Passage <id>: … end of passage <id>``
64
+ frame."""
65
+
66
+ DASHES = "dashes"
67
+ """Repeated ``" -"`` inside the same frame as ``dots``."""
68
+
69
+
70
+ _PAD_UNITS = {Filler.DOTS: " .", Filler.DASHES: " -"}
71
+
72
+ # Ends the padded system block in the framed layouts: a fixed digit, so the
73
+ # first passage's "Passage" never merges into it.
74
+ _SYSTEM_TAIL = "\nend of instructions 0"
75
+
76
+
77
+ def block_id(text: str) -> str:
78
+ """Return the id a framed block is labelled with.
79
+
80
+ Six hex characters of the text's digest plus one decimal digit, so the
81
+ id -- and the block, which ends with it -- always ends in a digit. Every
82
+ tokenizer checked splits a digit from a following letter or newline, while
83
+ a final letter merges into the next block's first word on some, a final
84
+ punctuation mark glues to the newlines after it on others, and final
85
+ whitespace is merged with its neighbours or stripped by chat templates.
86
+
87
+ Args:
88
+ text: The raw passage text.
89
+
90
+ Returns:
91
+ The id, e.g. ``"4f7a1c9"``. Identical for identical text.
92
+ """
93
+ digest = hashlib.sha256(text.encode()).hexdigest()
94
+ return digest[:6] + str(int(digest, 16) % 10)
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class BlockPlacement:
99
+ """Where one block sits in a rendered prompt.
100
+
101
+ Attributes:
102
+ block: The block text.
103
+ start_token: Its first token's index in the tokenized prompt, or
104
+ ``-1`` when the block was not found verbatim.
105
+ tokens: Its token ids in this prompt.
106
+ problem: Why it cannot be reused, or ``""`` when it starts and ends
107
+ on chunk boundaries with no token straddling either.
108
+ """
109
+
110
+ block: str
111
+ start_token: int
112
+ tokens: tuple[int, ...]
113
+ problem: str
114
+
34
115
 
35
116
  class ChunkAligner:
36
117
  """Pads prompt blocks so each occupies a whole number of cache chunks.
37
118
 
38
- Padding is deterministic: the same tokenizer, chunk size, seed and text
39
- always yield the same padded block, so two runs build byte-identical
40
- prompts. Passage padding is seeded from the passage text, so a passage
41
- shared by several samples is padded once, identically.
119
+ Padding is deterministic: the same tokenizer, chunk size, filler and text
120
+ always yield the same padded block (and, for ``words``, the same seed),
121
+ so two runs build byte-identical prompts. A passage shared by several
122
+ samples is padded once, identically.
42
123
  """
43
124
 
44
125
  def __init__(
@@ -47,19 +128,25 @@ class ChunkAligner:
47
128
  model_name: str,
48
129
  align_tokens: int,
49
130
  seed: int,
131
+ filler: Filler = Filler.WORDS,
132
+ template_kwargs: Mapping[str, Any] | None = None,
50
133
  ) -> None:
51
- """Build the filler pool for *tokenizer*.
134
+ """Build the aligner (and, for ``words``, the filler pool).
52
135
 
53
136
  Args:
54
137
  tokenizer: A HuggingFace tokenizer for the served model.
55
138
  model_name: The model's name, used only in log messages.
56
139
  align_tokens: Chunk size to align to. Should equal the
57
140
  deployment's LMCache chunk size.
58
- seed: Seed for filler selection.
141
+ seed: Seed for filler selection (``words`` only).
142
+ filler: What pads each block.
143
+ template_kwargs: Chat-template variables the engine renders
144
+ with; they can change the template's length ahead of the
145
+ content.
59
146
 
60
147
  Raises:
61
148
  ValueError: If *align_tokens* is not positive, or the tokenizer
62
- cannot supply a single-token filler pool.
149
+ cannot supply a single-token filler pool for ``words``.
63
150
  """
64
151
  if align_tokens < 1:
65
152
  raise ValueError(f"align_tokens must be >= 1, got {align_tokens}")
@@ -67,8 +154,25 @@ class ChunkAligner:
67
154
  self._model_name = model_name
68
155
  self._align_tokens = align_tokens
69
156
  self._seed = seed
70
- self._pool = build_single_token_pool(tokenizer, _FILLER_VOCAB_SIZE, seed=seed)
71
- self._passage_blocks: dict[str, str] = {}
157
+ self._filler = filler
158
+ self._template_kwargs = dict(template_kwargs or {})
159
+ self._pool = (
160
+ build_single_token_pool(tokenizer, _FILLER_VOCAB_SIZE, seed=seed)
161
+ if filler is Filler.WORDS
162
+ else None
163
+ )
164
+ self._passage_blocks: dict[str, tuple[str, int]] = {}
165
+ self._system_pad_tokens = 0
166
+
167
+ @property
168
+ def filler(self) -> Filler:
169
+ """What pads each block."""
170
+ return self._filler
171
+
172
+ @property
173
+ def system_pad_tokens(self) -> int:
174
+ """Padding tokens in the last block :meth:`system_block` returned."""
175
+ return self._system_pad_tokens
72
176
 
73
177
  def token_length(self, text: str) -> int:
74
178
  """Return the token length of *text*, without special tokens.
@@ -95,9 +199,16 @@ class ChunkAligner:
95
199
  The padded system block.
96
200
  """
97
201
  prefix_tokens = self._chat_prefix_tokens()
98
- block = self._pad_to_multiple(
99
- system_prompt, prefix_tokens, random.Random(self._seed)
100
- )
202
+ if self._filler is Filler.WORDS:
203
+ block = self._pad_to_multiple(
204
+ system_prompt, prefix_tokens, random.Random(self._seed)
205
+ )
206
+ pad = self.token_length(block) - self.token_length(system_prompt)
207
+ else:
208
+ block, pad = self._pad_framed(
209
+ "", system_prompt, _SYSTEM_TAIL, prefix_tokens
210
+ )
211
+ self._system_pad_tokens = pad
101
212
  logger.info(
102
213
  "System block: %d tokens after a %d-token chat-template prefix",
103
214
  self.token_length(block),
@@ -114,12 +225,161 @@ class ChunkAligner:
114
225
  Returns:
115
226
  The padded passage; identical across calls for the same text.
116
227
  """
117
- block = self._passage_blocks.get(passage)
118
- if block is None:
119
- rng = random.Random(f"{self._seed}:{passage}")
120
- block = self._pad_to_multiple(passage, 0, rng)
121
- self._passage_blocks[passage] = block
122
- return block
228
+ return self._padded_passage(passage)[0]
229
+
230
+ def pad_tokens(self, passage: str) -> int:
231
+ """Return how many padding tokens :meth:`passage_block` adds.
232
+
233
+ Args:
234
+ passage: Raw passage text.
235
+
236
+ Returns:
237
+ Tokens of filler (``words``: the words; framed fillers: the
238
+ repeated unit), excluding the frame around the text.
239
+ """
240
+ return self._padded_passage(passage)[1]
241
+
242
+ def framed(self, passage: str) -> str:
243
+ """Return *passage* in the framed layout without padding.
244
+
245
+ ``Passage <id>:\n<text>\n\nend of passage <id>``: the same label
246
+ and end marker as the ``dots`` / ``dashes`` blocks, for prompts that
247
+ are not chunk-aligned (``bench quality``'s ``concatenated``
248
+ scenario).
249
+
250
+ Args:
251
+ passage: Raw passage text.
252
+
253
+ Returns:
254
+ The framed passage.
255
+ """
256
+ ident = block_id(passage)
257
+ return f"Passage {ident}:\n{passage}\n\nend of passage {ident}"
258
+
259
+ def render_and_encode(
260
+ self, messages: list[dict[str, str]]
261
+ ) -> tuple[str, list[int]]:
262
+ """Render *messages* with the chat template and tokenize the result.
263
+
264
+ Args:
265
+ messages: Chat messages, as sent to the engine.
266
+
267
+ Returns:
268
+ The rendered prompt and its token ids, as the engine sees them.
269
+ """
270
+ rendered = self._render(messages)
271
+ return rendered, self._encode(rendered)
272
+
273
+ def token_boundary(
274
+ self, rendered: str, char_index: int, ids: list[int]
275
+ ) -> tuple[int, bool]:
276
+ """Map a character position in a rendered prompt to a token index.
277
+
278
+ Args:
279
+ rendered: A prompt from :meth:`render_and_encode`.
280
+ char_index: A character position in it.
281
+ ids: Its token ids.
282
+
283
+ Returns:
284
+ The number of tokens before *char_index*, and whether a token
285
+ boundary falls exactly there (the text before it tokenizes to a
286
+ prefix of the whole prompt's tokens).
287
+ """
288
+ head = self._encode(rendered[:char_index])
289
+ return len(head), ids[: len(head)] == head
290
+
291
+ def place_blocks(
292
+ self, messages: list[dict[str, str]], blocks: list[str]
293
+ ) -> tuple[int, list[BlockPlacement]]:
294
+ """Locate *blocks* in the prompt the engine will tokenize.
295
+
296
+ Renders *messages* with the chat template (as the engine does),
297
+ tokenizes the result, and checks each block: it must appear
298
+ verbatim, in order, start and end on chunk boundaries, and no token
299
+ may straddle either boundary. Only then do its chunks hash the same
300
+ here as in any other prompt holding it -- which callers check by
301
+ comparing ``tokens`` across prompts.
302
+
303
+ Args:
304
+ messages: The chat messages, as sent to the engine.
305
+ blocks: The padded blocks the messages contain, in order.
306
+
307
+ Returns:
308
+ The rendered prompt's token count, and one placement per block.
309
+ """
310
+ rendered, ids = self.render_and_encode(messages)
311
+ placements: list[BlockPlacement] = []
312
+ cursor = 0
313
+ for block in blocks:
314
+ start = rendered.find(block, cursor)
315
+ if start < 0:
316
+ placements.append(
317
+ BlockPlacement(
318
+ block,
319
+ -1,
320
+ (),
321
+ "not found verbatim in the rendered prompt (the chat "
322
+ "template changed it, e.g. trimmed whitespace)",
323
+ )
324
+ )
325
+ continue
326
+ end = start + len(block)
327
+ cursor = end
328
+ first, clean_start = self.token_boundary(rendered, start, ids)
329
+ last, clean_end = self.token_boundary(rendered, end, ids)
330
+ problems = []
331
+ if not clean_start:
332
+ problems.append("a token straddles its start")
333
+ if not clean_end:
334
+ problems.append("a token straddles its end")
335
+ if clean_start and first % self._align_tokens:
336
+ problems.append(
337
+ f"starts at token {first}, not on a "
338
+ f"{self._align_tokens}-token chunk boundary"
339
+ )
340
+ if clean_start and clean_end and (last - first) % self._align_tokens:
341
+ problems.append(
342
+ f"spans {last - first} tokens, not a whole number of chunks"
343
+ )
344
+ placements.append(
345
+ BlockPlacement(
346
+ block, first, tuple(ids[first:last]), "; ".join(problems)
347
+ )
348
+ )
349
+ return len(ids), placements
350
+
351
+ def _padded_passage(self, passage: str) -> tuple[str, int]:
352
+ """Pad *passage* once and cache the block with its padding count."""
353
+ cached = self._passage_blocks.get(passage)
354
+ if cached is None:
355
+ if self._filler is Filler.WORDS:
356
+ rng = random.Random(f"{self._seed}:{passage}")
357
+ block = self._pad_to_multiple(passage, 0, rng)
358
+ cached = (block, self.token_length(block) - self.token_length(passage))
359
+ else:
360
+ ident = block_id(passage)
361
+ cached = self._pad_framed(
362
+ f"Passage {ident}:\n", passage, f"\nend of passage {ident}", 0
363
+ )
364
+ self._passage_blocks[passage] = cached
365
+ return cached
366
+
367
+ def _render(self, messages: list[dict[str, str]]) -> str:
368
+ """Render *messages* as the engine will, or their content without one."""
369
+ try:
370
+ return str(
371
+ self._tokenizer.apply_chat_template(
372
+ messages,
373
+ tokenize=False,
374
+ add_generation_prompt=True,
375
+ **self._template_kwargs,
376
+ )
377
+ )
378
+ except Exception: # noqa: BLE001 - no template: content only, as below
379
+ return "".join(m["content"] for m in messages)
380
+
381
+ def _encode(self, text: str) -> list[int]:
382
+ return list(self._tokenizer.encode(text, add_special_tokens=False))
123
383
 
124
384
  def _chat_prefix_tokens(self) -> int:
125
385
  """Count the tokens the chat template inserts before the content.
@@ -133,6 +393,7 @@ class ChunkAligner:
133
393
  [{"role": "user", "content": _TEMPLATE_SENTINEL}],
134
394
  tokenize=False,
135
395
  add_generation_prompt=True,
396
+ **self._template_kwargs,
136
397
  )
137
398
  except Exception as e: # noqa: BLE001 - a template failure is non-fatal
138
399
  logger.warning(
@@ -146,8 +407,54 @@ class ChunkAligner:
146
407
  head = str(rendered).split(_TEMPLATE_SENTINEL)[0]
147
408
  return self.token_length(head)
148
409
 
410
+ def _pad_framed(
411
+ self, head: str, text: str, tail: str, offset: int
412
+ ) -> tuple[str, int]:
413
+ """Pad ``head + text + "\\n\\n" + unit*n + tail`` to a chunk boundary.
414
+
415
+ Args:
416
+ head: Text before *text* (the passage label, or ``""``).
417
+ text: The passage or system prompt.
418
+ tail: Text after the padding (the end marker).
419
+ offset: Tokens already ahead of the block.
420
+
421
+ Returns:
422
+ The padded block and its padding token count. When no count
423
+ lands on a boundary the closest attempt is returned; the
424
+ in-context check (:meth:`place_blocks`) then reports it.
425
+ """
426
+ unit = _PAD_UNITS[self._filler]
427
+ align = self._align_tokens
428
+
429
+ def build(n: int) -> str:
430
+ return head + text + "\n\n" + unit * n + tail
431
+
432
+ base = self.token_length(build(0))
433
+ target = math.ceil((offset + base) / align) * align
434
+ block = build(0)
435
+ for _ in range(_PAD_TARGETS):
436
+ n = target - offset - base
437
+ for _ in range(_PAD_ATTEMPTS):
438
+ n = max(n, 0)
439
+ block = build(n)
440
+ actual = offset + self.token_length(block)
441
+ if actual == target:
442
+ return block, actual - offset - base
443
+ n += target - actual
444
+ target += align
445
+ logger.warning(
446
+ "Could not pad a block to a %d-token boundary with %r; cache "
447
+ "reuse will be partial",
448
+ align,
449
+ unit,
450
+ )
451
+ return block, self.token_length(block) - base
452
+
149
453
  def _pad_to_multiple(self, text: str, offset: int, rng: random.Random) -> str:
150
454
  """Pad *text* so ``offset + len(text)`` is a whole chunk count."""
455
+ pool = self._pool
456
+ if pool is None:
457
+ raise RuntimeError("word padding needs the words filler")
151
458
  align = self._align_tokens
152
459
  total = offset + self.token_length(text)
153
460
  target = math.ceil(total / align) * align
@@ -159,8 +466,8 @@ class ChunkAligner:
159
466
  # Correct against a re-encode: a merged boundary token shifts the
160
467
  # estimate, and an off-by-one phase error costs a whole chunk.
161
468
  for _ in range(_PAD_ATTEMPTS):
162
- words = [rng.choice(self._pool.words) for _ in range(max(num_words, 0))]
163
- padded = text + "\n" + self._pool.join(words)
469
+ words = [rng.choice(pool.words) for _ in range(max(num_words, 0))]
470
+ padded = text + "\n" + pool.join(words)
164
471
  actual = offset + self.token_length(padded)
165
472
  if actual == target:
166
473
  return padded
@@ -4,8 +4,19 @@
4
4
  Kept in one module so every benchmark that scores answers over the same
5
5
  passages asks byte-identical questions: a prompt difference between two runs
6
6
  would read as a quality difference.
7
+
8
+ Two versions exist. :data:`ANSWER_TAG_PROMPT` (``bench quality``) asks for
9
+ ``<answer>`` tags. :data:`QA_SYSTEM_PROMPT` / :data:`QA_QUESTION_TEMPLATE`
10
+ (``rag-qa-quality``) ask for ``<final_answer>`` tags, which gpt-oss trips
11
+ over: its response format names the answer channel ``final``, and a prompt
12
+ full of ``final_answer`` leads it to write ``<|channel|>final_answer>…``,
13
+ dropping the opening tag. ``extract_final_answer`` accepts both tags and that
14
+ form.
7
15
  """
8
16
 
17
+ # Standard
18
+ from dataclasses import dataclass
19
+
9
20
  QA_SYSTEM_PROMPT = (
10
21
  "Answer the question using only the given passages. You may reason "
11
22
  "through the problem, but put only the concise final answer between "
@@ -20,16 +31,50 @@ QA_QUESTION_TEMPLATE = (
20
31
  )
21
32
 
22
33
 
34
+ @dataclass(frozen=True)
35
+ class QAPrompt:
36
+ """The instructions around a question's passages.
37
+
38
+ Attributes:
39
+ system: Placed before the passages (then chunk-aligned).
40
+ question_template: Placed after them; ``{question}`` is filled in.
41
+ """
42
+
43
+ system: str
44
+ question_template: str
45
+
46
+
47
+ ANSWER_TAG_PROMPT = QAPrompt(
48
+ system=(
49
+ "Answer the question using only the given passages. You may reason "
50
+ "through the problem, but put only the concise answer between "
51
+ "<answer> and </answer>. Always emit both tags.\n\n"
52
+ "The following are the given passages.\n"
53
+ ),
54
+ question_template=(
55
+ "\n\nAnswer the question using only the passages above. End with "
56
+ "exactly one concise answer in this form: <answer>answer</answer>.\n\n"
57
+ "Question: {question}\nAnswer:"
58
+ ),
59
+ )
60
+ """The prompt with ``<answer>`` tags, which no checked chat format uses."""
61
+
62
+
23
63
  def compose_qa_messages(
24
- system_block: str, passage_blocks: list[str], question: str
64
+ system_block: str,
65
+ passage_blocks: list[str],
66
+ question: str,
67
+ question_template: str = QA_QUESTION_TEMPLATE,
25
68
  ) -> list[dict[str, str]]:
26
69
  """Build the chat messages for one question over its passages.
27
70
 
28
71
  Args:
29
- system_block: :data:`QA_SYSTEM_PROMPT`, chunk-aligned by
72
+ system_block: A :class:`QAPrompt` system text, chunk-aligned by
30
73
  :meth:`ChunkAligner.system_block`.
31
74
  passage_blocks: Chunk-aligned passages, in the order to present them.
32
75
  question: The question text.
76
+ question_template: The text after the passages, with a
77
+ ``{question}`` field; the system block's prompt version.
33
78
 
34
79
  Returns:
35
80
  A single user message:
@@ -38,7 +83,7 @@ def compose_qa_messages(
38
83
  content = (
39
84
  system_block
40
85
  + "".join(passage_blocks)
41
- + QA_QUESTION_TEMPLATE.format(question=question)
86
+ + question_template.format(question=question)
42
87
  )
43
88
  return [{"role": "user", "content": content}]
44
89
 
@@ -17,8 +17,12 @@ _PUNCTUATION = str.maketrans("", "", string.punctuation)
17
17
 
18
18
  # Lazy match, last region wins: reasoning models may echo an example answer
19
19
  # before their own. Include an unfinished last region to avoid scoring the example.
20
+ # Opening tags: <final_answer> or <answer>, or a bare "final_answer>" -- gpt-oss
21
+ # opens its answer channel with "<|channel|>final" and, primed by the prompt,
22
+ # continues "_answer>" instead of "<|message|>", so the "<" never appears.
20
23
  _FINAL_ANSWER = re.compile(
21
- r"<final_answer>\s*(.*?)\s*(</final_answer>|$)",
24
+ r"(?:<(?:final_answer|answer)>|(?<![<\w/])final_answer>)"
25
+ r"\s*(.*?)\s*(</(?:final_answer|answer)>|$)",
22
26
  re.IGNORECASE | re.DOTALL,
23
27
  )
24
28
 
@@ -106,8 +110,11 @@ _ABSTENTION_ANSWERS = frozenset({"none", "n/a", "na", "null", "unknown"})
106
110
  def extract_final_answer(output: str) -> str:
107
111
  """Extract the model's delimited final answer from a response.
108
112
 
109
- An unterminated region counts as no answer: generation was cut off, so
110
- scoring the reasoning before it would report an answer never produced.
113
+ Accepts ``<final_answer>…</final_answer>`` and ``<answer>…</answer>``
114
+ (either prompt version), and gpt-oss's ``final_answer>…</final_answer>``
115
+ with the opening ``<`` lost to its channel header. An unterminated region
116
+ counts as no answer: generation was cut off, so scoring the reasoning
117
+ before it would report an answer never produced.
111
118
 
112
119
  Args:
113
120
  output: The model's full response text.
@@ -0,0 +1,34 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """``lmcache bench quality`` command group.
3
+
4
+ Measures whether a serving engine change -- e.g. quantization, KV cache
5
+ compression, post-training, or another engine or model -- changes the
6
+ answers compared with a reference, in two stages:
7
+
8
+ * ``run`` sends byte-identical prompts to one or more arms (a reference, a
9
+ candidate under test, and optionally a control) and
10
+ saves their answers to a results directory.
11
+ * ``analyze`` scores the saved answers and reports per-question rows and a
12
+ verdict.
13
+
14
+ Arms can be run one at a time into the same directory, so they never need
15
+ to be served at once. The subcommands are auto-discovered from the
16
+ ``*_command`` modules in this package; the runtime lives in ``core`` so that
17
+ discovery imports only these lightweight wrappers.
18
+ """
19
+
20
+ # First Party
21
+ from lmcache.cli.commands.base import CompositeCommand
22
+
23
+
24
+ class QualityBenchCommand(CompositeCommand):
25
+ """Compare a candidate serving stack's answers with a reference."""
26
+
27
+ def name(self) -> str:
28
+ return "quality"
29
+
30
+ def help(self) -> str:
31
+ return (
32
+ "Compare a candidate serving stack's answers with a reference "
33
+ "(e.g. a quantized model vs the original)."
34
+ )
@@ -0,0 +1,44 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """``lmcache bench quality analyze`` subcommand."""
3
+
4
+ # Standard
5
+ import argparse
6
+
7
+ # First Party
8
+ from lmcache.cli.commands.base import BaseCommand
9
+
10
+
11
+ class QualityAnalyzeCommand(BaseCommand):
12
+ """Score saved arm answers and reach the quality verdict."""
13
+
14
+ def name(self) -> str:
15
+ return "analyze"
16
+
17
+ def help(self) -> str:
18
+ return "Score the saved answers in a results directory and print a verdict."
19
+
20
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
21
+ # First Party
22
+ from lmcache.cli.commands.bench.quality_bench.core.analyze_stage import (
23
+ add_analyze_arguments,
24
+ )
25
+
26
+ add_analyze_arguments(parser)
27
+
28
+ def register(self, subparsers: argparse._SubParsersAction) -> None:
29
+ """Register without the common output args (--format/--output).
30
+
31
+ Results are written into the results directory, so the generic
32
+ output flags do not apply.
33
+ """
34
+ parser = subparsers.add_parser(self.name(), help=self.help())
35
+ self.add_arguments(parser)
36
+ parser.set_defaults(func=self.execute)
37
+
38
+ def execute(self, args: argparse.Namespace) -> None:
39
+ # First Party
40
+ from lmcache.cli.commands.bench.quality_bench.core.analyze_stage import (
41
+ run_quality_analyze,
42
+ )
43
+
44
+ run_quality_analyze(args)
@@ -0,0 +1,7 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Runtime for ``lmcache bench quality``: plans, arm runs, scoring, verdict.
3
+
4
+ Kept in a sub-package so that command discovery, which imports every
5
+ module directly under ``quality_bench``, imports only the lightweight
6
+ command wrappers.
7
+ """