lmcache-cli 0.4.5.dev0__py3-none-any.whl → 0.4.6.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 (176) hide show
  1. lmcache/__init__.py +37 -1
  2. lmcache/_version.py +3 -3
  3. lmcache/cli/commands/bench/__init__.py +101 -2
  4. lmcache/cli/commands/bench/engine_bench/config.py +8 -4
  5. lmcache/cli/commands/bench/engine_bench/interactive/schema.py +41 -0
  6. lmcache/cli/commands/bench/engine_bench/request_sender.py +9 -0
  7. lmcache/cli/commands/bench/engine_bench/workloads/__init__.py +23 -0
  8. lmcache/cli/commands/bench/engine_bench/workloads/prefix_suffix_tuner.py +637 -0
  9. lmcache/cli/commands/describe.py +20 -13
  10. lmcache/cli/commands/kvcache.py +2 -2
  11. lmcache/cli/commands/ping.py +1 -1
  12. lmcache/cli/commands/server.py +5 -5
  13. lmcache/cli/commands/test_cache.py +997 -0
  14. lmcache/integration/sglang/sglang_adapter.py +24 -12
  15. lmcache/integration/sglang/utils.py +2 -0
  16. lmcache/integration/tensorrt_llm/__init__.py +78 -0
  17. lmcache/integration/tensorrt_llm/tensorrt_adapter.py +377 -0
  18. lmcache/integration/tensorrt_llm/tensorrt_mp_adapter.py +487 -0
  19. lmcache/integration/tensorrt_llm/utils.py +94 -0
  20. lmcache/integration/vllm/lmcache_connector_v1.py +0 -5
  21. lmcache/integration/vllm/lmcache_connector_v1_085.py +0 -5
  22. lmcache/integration/vllm/lmcache_mp_connector.py +1202 -0
  23. lmcache/integration/vllm/lmcache_mp_connector_0180.py +11 -10
  24. lmcache/integration/vllm/lmcache_mp_connector_0201.py +1195 -0
  25. lmcache/integration/vllm/utils.py +13 -27
  26. lmcache/integration/vllm/vllm_ec_adapter.py +194 -0
  27. lmcache/integration/vllm/vllm_multi_process_adapter.py +230 -59
  28. lmcache/integration/vllm/vllm_v1_adapter.py +128 -21
  29. lmcache/non_cuda_equivalents.py +56 -7
  30. lmcache/storage_backend/serde/cachegen_decoder.py +11 -6
  31. lmcache/storage_backend/serde/cachegen_encoder.py +9 -6
  32. lmcache/tools/mp_status_viewer/__main__.py +1 -1
  33. lmcache/usage_context.py +5 -14
  34. lmcache/utils.py +51 -0
  35. lmcache/v1/cache_engine.py +59 -28
  36. lmcache/v1/check/check_mode_test_l2_adapter.py +50 -7
  37. lmcache/v1/check/utils.py +1 -6
  38. lmcache/v1/compute/attention/__init__.py +1 -0
  39. lmcache/v1/compute/attention/flash_attn.py +3 -2
  40. lmcache/v1/compute/attention/flash_infer_sparse.py +3 -2
  41. lmcache/v1/compute/attention/metadata.py +92 -1
  42. lmcache/v1/compute/attention/triton_kernels/__init__.py +13 -0
  43. lmcache/v1/compute/attention/triton_kernels/block_sparse_attention.py +559 -0
  44. lmcache/v1/compute/attention/triton_sparse.py +145 -0
  45. lmcache/v1/compute/attention/utils.py +74 -7
  46. lmcache/v1/compute/blend/blender.py +2 -1
  47. lmcache/v1/compute/models/base.py +17 -2
  48. lmcache/v1/compute/positional_encoding.py +9 -4
  49. lmcache/v1/config.py +220 -1
  50. lmcache/v1/distributed/config.py +5 -3
  51. lmcache/v1/distributed/eviction_policy/__init__.py +4 -0
  52. lmcache/v1/distributed/eviction_policy/factory.py +5 -0
  53. lmcache/v1/distributed/eviction_policy/isolated_lru.py +191 -0
  54. lmcache/v1/distributed/internal_api.py +8 -0
  55. lmcache/v1/distributed/l1_manager.py +42 -0
  56. lmcache/v1/distributed/l2_adapters/__init__.py +2 -1
  57. lmcache/v1/distributed/l2_adapters/base.py +206 -21
  58. lmcache/v1/distributed/l2_adapters/config.py +105 -2
  59. lmcache/v1/distributed/l2_adapters/dax_l2_adapter.py +583 -0
  60. lmcache/v1/distributed/l2_adapters/fs_l2_adapter.py +32 -15
  61. lmcache/v1/distributed/l2_adapters/fs_native_l2_adapter.py +9 -1
  62. lmcache/v1/distributed/l2_adapters/mock_l2_adapter.py +41 -26
  63. lmcache/v1/distributed/l2_adapters/mooncake_store_l2_adapter.py +98 -19
  64. lmcache/v1/distributed/l2_adapters/native_connector_l2_adapter.py +107 -40
  65. lmcache/v1/distributed/l2_adapters/nixl_store_dynamic_l2_adapter.py +39 -27
  66. lmcache/v1/distributed/l2_adapters/nixl_store_l2_adapter.py +73 -34
  67. lmcache/v1/distributed/l2_adapters/plugin_l2_adapter.py +10 -6
  68. lmcache/v1/distributed/l2_adapters/raw_block_l2_adapter.py +770 -0
  69. lmcache/v1/distributed/l2_adapters/s3_l2_adapter.py +1057 -0
  70. lmcache/v1/distributed/l2_adapters/serde_wrapper.py +649 -0
  71. lmcache/v1/distributed/quota_manager.py +94 -0
  72. lmcache/v1/distributed/serde/__init__.py +39 -0
  73. lmcache/v1/distributed/serde/async_processor.py +218 -0
  74. lmcache/v1/distributed/serde/base.py +215 -0
  75. lmcache/v1/distributed/serde/factory.py +69 -0
  76. lmcache/v1/distributed/serde/fp8.py +103 -0
  77. lmcache/v1/distributed/serde/utils.py +54 -0
  78. lmcache/v1/distributed/storage_controllers/eviction_controller.py +133 -6
  79. lmcache/v1/distributed/storage_controllers/prefetch_controller.py +225 -54
  80. lmcache/v1/distributed/storage_controllers/store_controller.py +270 -83
  81. lmcache/v1/distributed/storage_manager.py +126 -17
  82. lmcache/v1/ec_engine.py +202 -0
  83. lmcache/v1/gpu_connector/__init__.py +17 -9
  84. lmcache/v1/gpu_connector/gpu_connectors.py +382 -78
  85. lmcache/v1/gpu_connector/hpu_connector.py +4 -2
  86. lmcache/v1/gpu_connector/utils.py +682 -193
  87. lmcache/v1/gpu_connector/xpu_connectors.py +4 -2
  88. lmcache/v1/internal_api_server/api_registry.py +5 -12
  89. lmcache/v1/internal_api_server/api_server.py +17 -6
  90. lmcache/v1/kv_layer_groups.py +497 -187
  91. lmcache/v1/lazy_memory_allocator.py +12 -3
  92. lmcache/v1/lookup_client/lmcache_lookup_client.py +9 -1
  93. lmcache/v1/lookup_client/mooncake_lookup_client.py +17 -8
  94. lmcache/v1/memory_management.py +50 -16
  95. lmcache/v1/metadata.py +35 -31
  96. lmcache/v1/mp_observability/README.md +5 -0
  97. lmcache/v1/mp_observability/config.py +74 -3
  98. lmcache/v1/mp_observability/event.py +33 -1
  99. lmcache/v1/mp_observability/event_bus.py +53 -1
  100. lmcache/v1/mp_observability/otel_init.py +91 -17
  101. lmcache/v1/mp_observability/subscribers/logging/__init__.py +5 -1
  102. lmcache/v1/mp_observability/subscribers/logging/cb_server.py +119 -0
  103. lmcache/v1/mp_observability/subscribers/metrics/__init__.py +40 -0
  104. lmcache/v1/mp_observability/subscribers/metrics/cb_server.py +186 -0
  105. lmcache/v1/mp_observability/subscribers/metrics/engine.py +69 -0
  106. lmcache/v1/mp_observability/subscribers/metrics/event_bus.py +79 -0
  107. lmcache/v1/mp_observability/subscribers/metrics/l0_l1_throughput.py +148 -0
  108. lmcache/v1/mp_observability/subscribers/metrics/l1_eviction_loop.py +48 -0
  109. lmcache/v1/mp_observability/subscribers/metrics/l1_failures.py +86 -0
  110. lmcache/v1/mp_observability/subscribers/metrics/l2.py +35 -2
  111. lmcache/v1/mp_observability/subscribers/metrics/l2_failures.py +59 -0
  112. lmcache/v1/mp_observability/subscribers/metrics/l2_throughput.py +193 -0
  113. lmcache/v1/mp_observability/subscribers/metrics/lookup.py +93 -0
  114. lmcache/v1/mp_observability/subscribers/metrics/sm_lifecycle.py +166 -0
  115. lmcache/v1/mp_observability/subscribers/tracing/__init__.py +9 -1
  116. lmcache/v1/mp_observability/subscribers/tracing/cb_server.py +369 -0
  117. lmcache/v1/mp_observability/subscribers/tracing/mp_server.py +22 -9
  118. lmcache/v1/multiprocess/blend_server_v2.py +578 -122
  119. lmcache/v1/multiprocess/custom_types.py +108 -8
  120. lmcache/v1/multiprocess/futures.py +18 -10
  121. lmcache/v1/multiprocess/gpu_context.py +133 -104
  122. lmcache/v1/multiprocess/http_api_registry.py +44 -0
  123. lmcache/v1/multiprocess/http_apis/__init__.py +1 -0
  124. lmcache/v1/multiprocess/http_apis/cache_api.py +263 -0
  125. lmcache/v1/multiprocess/http_apis/common_api.py +46 -0
  126. lmcache/v1/multiprocess/http_apis/conf_api.py +59 -0
  127. lmcache/v1/multiprocess/http_apis/healthcheck_api.py +31 -0
  128. lmcache/v1/multiprocess/http_apis/quota_api.py +173 -0
  129. lmcache/v1/multiprocess/http_apis/root_api.py +15 -0
  130. lmcache/v1/multiprocess/http_apis/status_api.py +25 -0
  131. lmcache/v1/multiprocess/http_apis/version_api.py +8 -0
  132. lmcache/v1/multiprocess/http_server.py +12 -63
  133. lmcache/v1/multiprocess/mp_runtime_plugin_launcher.py +4 -25
  134. lmcache/v1/multiprocess/mq.py +13 -11
  135. lmcache/v1/multiprocess/protocols/engine.py +5 -2
  136. lmcache/v1/multiprocess/server.py +162 -30
  137. lmcache/v1/platform/__init__.py +19 -0
  138. lmcache/v1/platform/event_notifier.py +242 -0
  139. lmcache/v1/standalone/__main__.py +2 -1
  140. lmcache/v1/storage_backend/__init__.py +15 -12
  141. lmcache/v1/storage_backend/abstract_backend.py +17 -2
  142. lmcache/v1/storage_backend/connector/hf3fs_adapter.py +275 -0
  143. lmcache/v1/storage_backend/connector/hf3fs_connector.py +622 -0
  144. lmcache/v1/storage_backend/connector/hfbucket_adapter.py +55 -0
  145. lmcache/v1/storage_backend/connector/hfbucket_connector.py +965 -0
  146. lmcache/v1/storage_backend/connector/mooncakestore_connector.py +281 -65
  147. lmcache/v1/storage_backend/dax/__init__.py +9 -0
  148. lmcache/v1/storage_backend/dax/core.py +865 -0
  149. lmcache/v1/storage_backend/gds_backend.py +31 -3
  150. lmcache/v1/storage_backend/local_cpu_backend.py +81 -5
  151. lmcache/v1/storage_backend/local_disk_backend.py +35 -18
  152. lmcache/v1/storage_backend/maru_backend.py +2 -1
  153. lmcache/v1/storage_backend/naive_serde/cachegen_decoder.py +11 -6
  154. lmcache/v1/storage_backend/naive_serde/cachegen_encoder.py +6 -5
  155. lmcache/v1/storage_backend/nixl_storage_backend.py +264 -30
  156. lmcache/v1/storage_backend/path_sharder.py +5 -7
  157. lmcache/v1/storage_backend/pd_backend.py +479 -40
  158. lmcache/v1/storage_backend/pd_backend_async.py +1664 -0
  159. lmcache/v1/storage_backend/plugins/dax_backend.py +278 -804
  160. lmcache/v1/storage_backend/plugins/rust_raw_block_backend.py +346 -1122
  161. lmcache/v1/storage_backend/raw_block/__init__.py +42 -0
  162. lmcache/v1/storage_backend/raw_block/core.py +1476 -0
  163. lmcache/v1/storage_backend/raw_block/key_codec.py +168 -0
  164. lmcache/v1/storage_backend/remote_backend.py +2 -1
  165. lmcache/v1/storage_backend/storage_manager.py +26 -7
  166. lmcache/v1/system_detection.py +2 -2
  167. lmcache/v1/transfer_channel/nixl_channel.py +51 -2
  168. lmcache/v1/transfer_channel/transfer_utils.py +4 -6
  169. lmcache/v1/utils/json_utils.py +56 -0
  170. lmcache/v1/utils/router_discovery.py +54 -0
  171. {lmcache_cli-0.4.5.dev0.dist-info → lmcache_cli-0.4.6.dev0.dist-info}/METADATA +2 -1
  172. {lmcache_cli-0.4.5.dev0.dist-info → lmcache_cli-0.4.6.dev0.dist-info}/RECORD +176 -115
  173. {lmcache_cli-0.4.5.dev0.dist-info → lmcache_cli-0.4.6.dev0.dist-info}/WHEEL +0 -0
  174. {lmcache_cli-0.4.5.dev0.dist-info → lmcache_cli-0.4.6.dev0.dist-info}/entry_points.txt +0 -0
  175. {lmcache_cli-0.4.5.dev0.dist-info → lmcache_cli-0.4.6.dev0.dist-info}/licenses/LICENSE +0 -0
  176. {lmcache_cli-0.4.5.dev0.dist-info → lmcache_cli-0.4.6.dev0.dist-info}/top_level.txt +0 -0
lmcache/__init__.py CHANGED
@@ -15,7 +15,43 @@ except ImportError:
15
15
  __version__ = "unknown"
16
16
 
17
17
  logger = init_logger(__name__)
18
- __all__ = ["__version__"]
18
+ # Standard
19
+
20
+ __all__ = ["__version__", "torch_dev", "torch_device_type"]
21
+
22
+
23
+ # --------------------------
24
+ # Device detection
25
+ # --------------------------
26
+ def _detect_device() -> tuple[Any, str]:
27
+ """
28
+ Detect the available accelerator and return the corresponding torch
29
+ device module and device type string.
30
+
31
+ Returns:
32
+ tuple[Any, str]: A tuple of (torch_device_module, device_type_string),
33
+ e.g. ``(torch.cuda, "cuda")`` or ``(torch.xpu, "xpu")``.
34
+
35
+ Raises:
36
+ RuntimeError: If no supported accelerator is found (checked CUDA, XPU, HPU).
37
+ """
38
+ try:
39
+ # Third Party
40
+ import torch
41
+ except ImportError:
42
+ return None, "cpu" # fallback,CLI-only
43
+
44
+ if hasattr(torch, "xpu") and torch.xpu.is_available():
45
+ return torch.xpu, "xpu"
46
+ elif hasattr(torch, "hpu") and torch.hpu.is_available():
47
+ return torch.hpu, "hpu"
48
+ else:
49
+ # Fallback: always return torch.cuda for backward compatibility
50
+ # with existing tests and code paths that assume CUDA is the default.
51
+ return torch.cuda, "cuda"
52
+
53
+
54
+ torch_dev, torch_device_type = _detect_device()
19
55
 
20
56
 
21
57
  # --------------------------
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.4.5.dev0'
22
- __version_tuple__ = version_tuple = (0, 4, 5, 'dev0')
21
+ __version__ = version = '0.4.6.dev0'
22
+ __version_tuple__ = version_tuple = (0, 4, 6, 'dev0')
23
23
 
24
- __commit_id__ = commit_id = 'g6fbec463e'
24
+ __commit_id__ = commit_id = 'g168bbb976'
@@ -28,12 +28,28 @@ from lmcache.cli.commands.bench.engine_bench.stats import (
28
28
  from lmcache.cli.commands.bench.engine_bench.workloads import create_workload
29
29
  from lmcache.logging import init_logger
30
30
 
31
+ # Gated for slim install — TestCacheCommand pulls torch/MP runtime.
32
+ _TEST_CACHE_IMPORT_ERROR: ImportError | None = None
33
+ try:
34
+ # First Party
35
+ from lmcache.cli.commands.test_cache import TestCacheCommand
36
+ except ImportError as _exc:
37
+ _TEST_CACHE_IMPORT_ERROR = _exc
38
+ TestCacheCommand = None # type: ignore[assignment,misc]
39
+
31
40
  logger = init_logger(__name__)
32
41
 
33
42
 
34
43
  class BenchCommand(BaseCommand):
35
44
  """CLI command for sustained performance benchmarking."""
36
45
 
46
+ def __init__(self) -> None:
47
+ super().__init__()
48
+ # None on slim install; _register_kvcache registers a stub instead.
49
+ self._kvcache_delegate = (
50
+ TestCacheCommand() if TestCacheCommand is not None else None
51
+ )
52
+
37
53
  def name(self) -> str:
38
54
  return "bench"
39
55
 
@@ -52,9 +68,10 @@ class BenchCommand(BaseCommand):
52
68
  inner = parser.add_subparsers(
53
69
  dest="bench_target",
54
70
  required=True,
55
- metavar="{engine}",
71
+ metavar="{engine,kvcache}",
56
72
  )
57
73
  self._register_engine(inner)
74
+ self._register_kvcache(inner)
58
75
 
59
76
  def _register_engine(
60
77
  self,
@@ -99,6 +116,7 @@ class BenchCommand(BaseCommand):
99
116
  "long-doc-permutator",
100
117
  "long-doc-qa",
101
118
  "multi-round-chat",
119
+ "prefix-suffix-tuner",
102
120
  "random-prefill",
103
121
  ],
104
122
  help="Workload type.",
@@ -262,6 +280,36 @@ class BenchCommand(BaseCommand):
262
280
  help="Benchmark duration in seconds (default: 60).",
263
281
  )
264
282
 
283
+ # --- Prefix-suffix-tuner workload args ---
284
+ psf_group = parser.add_argument_group(
285
+ "prefix-suffix-tuner workload options",
286
+ )
287
+ psf_group.add_argument(
288
+ "--psf-context-length",
289
+ type=int,
290
+ default=8000,
291
+ help="Total tokens per request (prefix + breaker + suffix) "
292
+ "(default: 8000).",
293
+ )
294
+ psf_group.add_argument(
295
+ "--psf-prefix-ratio",
296
+ type=float,
297
+ default=0.8,
298
+ help="Fraction of context-length used by the prefix (default: 0.8). "
299
+ "Must be in (0.0, 1.0). The remainder (minus a 32-token breaker) is "
300
+ "the shared suffix.",
301
+ )
302
+ psf_group.add_argument(
303
+ "--psf-thrash",
304
+ type=float,
305
+ default=20.0,
306
+ help="Size in GB of the KV-cache tier to overflow (default: 20.0). "
307
+ "The workload sizes its prefix pool to slightly more than this, "
308
+ "so every pass-2 request misses that tier and falls through to "
309
+ "the next one. Use the L0 (HBM) size for vanilla vLLM baselines, "
310
+ "or the L1 (LMCache DRAM) size for tiered baselines.",
311
+ )
312
+
265
313
  # --- Random-prefill workload args ---
266
314
  rp_group = parser.add_argument_group(
267
315
  "random-prefill workload options",
@@ -281,8 +329,59 @@ class BenchCommand(BaseCommand):
281
329
 
282
330
  parser.set_defaults(func=self.execute)
283
331
 
332
+ # ------------------------------------------------------------------
333
+ # kvcache bench target — end-to-end MP cache sanity test
334
+ # ------------------------------------------------------------------
335
+
336
+ def _register_kvcache(
337
+ self,
338
+ subparsers: argparse._SubParsersAction,
339
+ ) -> None:
340
+ """Register ``lmcache bench kvcache``. Delegates to
341
+ :class:`TestCacheCommand`, or registers a stub on slim install.
342
+ """
343
+ if self._kvcache_delegate is None:
344
+ subparsers.add_parser(
345
+ "kvcache",
346
+ help="(requires full lmcache install)",
347
+ description=(
348
+ "End-to-end sanity test for the LMCache MP cache server. "
349
+ "Requires the full `lmcache` package; not available in "
350
+ "the `lmcache-cli` install."
351
+ ),
352
+ ).set_defaults(func=self.execute)
353
+ return
354
+ parser = subparsers.add_parser(
355
+ "kvcache",
356
+ help=self._kvcache_delegate.help(),
357
+ description=(
358
+ "End-to-end sanity test for the LMCache MP cache server: "
359
+ "runs LOOKUP / STORE / RETRIEVE against a live MP server "
360
+ "and verifies KV cache checksums."
361
+ ),
362
+ )
363
+ self._kvcache_delegate.add_arguments(parser)
364
+ parser.set_defaults(func=self.execute)
365
+
366
+ def _bench_kvcache(self, args: argparse.Namespace) -> None:
367
+ """Dispatch ``lmcache bench kvcache`` to ``TestCacheCommand``."""
368
+ if self._kvcache_delegate is None:
369
+ print(
370
+ "ERROR: `lmcache bench kvcache` needs the full LMCache "
371
+ "package, but only the `lmcache-cli` shell is installed.\n"
372
+ " Install the full package with `pip install lmcache` "
373
+ "and try again.\n"
374
+ f" Original import error: {_TEST_CACHE_IMPORT_ERROR}",
375
+ file=sys.stderr,
376
+ )
377
+ sys.exit(1)
378
+ self._kvcache_delegate.execute(args)
379
+
284
380
  def execute(self, args: argparse.Namespace) -> None:
285
- handlers = {"engine": self._bench_engine}
381
+ handlers = {
382
+ "engine": self._bench_engine,
383
+ "kvcache": self._bench_kvcache,
384
+ }
286
385
  handler = handlers.get(args.bench_target)
287
386
  if handler is None:
288
387
  print(
@@ -93,7 +93,7 @@ def auto_detect_model(engine_url: str) -> str:
93
93
 
94
94
 
95
95
  def _fetch_lmcache_status(lmcache_url: str) -> dict:
96
- """Fetch ``/api/status`` from the LMCache HTTP server.
96
+ """Fetch ``/status`` from the LMCache HTTP server.
97
97
 
98
98
  Returns:
99
99
  Parsed JSON response.
@@ -104,7 +104,7 @@ def _fetch_lmcache_status(lmcache_url: str) -> dict:
104
104
  url = lmcache_url.rstrip("/")
105
105
  if not url.startswith(("http://", "https://")):
106
106
  url = f"http://{url}"
107
- status_url = f"{url}/api/status"
107
+ status_url = f"{url}/status"
108
108
 
109
109
  logger.debug("Fetching LMCache status from %s", status_url)
110
110
 
@@ -125,7 +125,7 @@ def _find_model_meta(
125
125
  """Find the GPU metadata entry matching *model_name*.
126
126
 
127
127
  Args:
128
- gpu_meta: The ``gpu_context_meta`` dict from ``/api/status``.
128
+ gpu_meta: The ``gpu_context_meta`` dict from ``/status``.
129
129
  model_name: Model name to match.
130
130
 
131
131
  Returns:
@@ -148,7 +148,7 @@ def _find_model_meta(
148
148
  def resolve_tokens_per_gb(lmcache_url: str, model_name: str) -> int:
149
149
  """Query the LMCache server and compute tokens per GB of KV cache.
150
150
 
151
- Fetches ``/api/status``, finds the model entry matching
151
+ Fetches ``/status``, finds the model entry matching
152
152
  *model_name*, and computes::
153
153
 
154
154
  global_bytes_per_token = cache_size_per_token * world_size
@@ -172,6 +172,10 @@ def resolve_tokens_per_gb(lmcache_url: str, model_name: str) -> int:
172
172
  data = _fetch_lmcache_status(lmcache_url)
173
173
 
174
174
  gpu_meta = data.get("gpu_context_meta", {})
175
+ if not gpu_meta:
176
+ # CB-only deployments (engine_type="blend") populate
177
+ # cb_gpu_context_meta instead of gpu_context_meta.
178
+ gpu_meta = data.get("cb_gpu_context_meta", {})
175
179
  if not gpu_meta:
176
180
  raise RuntimeError(
177
181
  "No model info returned by LMCache server; "
@@ -112,6 +112,10 @@ ALL_ITEMS: list[ConfigItem] = [
112
112
  ),
113
113
  ("long-doc-qa", "Repeated Q&A over long documents (tests KV cache reuse)"),
114
114
  ("multi-round-chat", "Multi-turn chat with stateful sessions"),
115
+ (
116
+ "prefix-suffix-tuner",
117
+ "Two-pass sequential workload demonstrating tiered KV cache reuse",
118
+ ),
115
119
  ("random-prefill", "Prefill-only requests fired simultaneously"),
116
120
  ],
117
121
  phase=PHASE_REQUIRED,
@@ -317,6 +321,43 @@ ALL_ITEMS: list[ConfigItem] = [
317
321
  condition=_workload_is("multi-round-chat"),
318
322
  phase=PHASE_WORKLOAD,
319
323
  ),
324
+ # ── Phase 3: prefix-suffix-tuner ──────────────────────────────────
325
+ ConfigItem(
326
+ key="psf_context_length",
327
+ display_name="Context length (tokens)",
328
+ description="Total tokens per request (prefix + breaker + suffix).",
329
+ input_type="int",
330
+ default=8000,
331
+ condition=_workload_is("prefix-suffix-tuner"),
332
+ phase=PHASE_WORKLOAD,
333
+ ),
334
+ ConfigItem(
335
+ key="psf_prefix_ratio",
336
+ display_name="Prefix ratio",
337
+ description=(
338
+ "Fraction of context-length used by the prefix. Must be in "
339
+ "(0.0, 1.0). The remainder (minus a 32-token breaker) is the "
340
+ "shared suffix."
341
+ ),
342
+ input_type="float",
343
+ default=0.8,
344
+ condition=_workload_is("prefix-suffix-tuner"),
345
+ phase=PHASE_WORKLOAD,
346
+ ),
347
+ ConfigItem(
348
+ key="psf_thrash",
349
+ display_name="Target tier size (GB)",
350
+ description=(
351
+ "Size in GB of the KV-cache tier to overflow. The prefix pool "
352
+ "is sized to slightly more than this, so every pass-2 request "
353
+ "misses the targeted tier. Use the L0 (HBM) size for vanilla "
354
+ "vLLM, or the L1 (LMCache DRAM) size for tiered baselines."
355
+ ),
356
+ input_type="float",
357
+ default=20.0,
358
+ condition=_workload_is("prefix-suffix-tuner"),
359
+ phase=PHASE_WORKLOAD,
360
+ ),
320
361
  # ── Phase 3: random-prefill ───────────────────────────────────────
321
362
  ConfigItem(
322
363
  key="rp_request_length",
@@ -111,6 +111,7 @@ class RequestSender:
111
111
  """
112
112
  submit_time = time.time()
113
113
  first_token_time = 0.0
114
+ first_chunk_time = 0.0
114
115
  tokens: list[str] = []
115
116
  num_input_tokens = 0
116
117
  num_output_tokens = 0
@@ -119,6 +120,9 @@ class RequestSender:
119
120
  response = await self._create_stream(messages, max_tokens)
120
121
 
121
122
  async for chunk in response:
123
+ if not first_chunk_time:
124
+ first_chunk_time = time.time()
125
+
122
126
  # Extract usage from final chunk
123
127
  usage = getattr(chunk, "usage", None)
124
128
  if usage is not None:
@@ -136,6 +140,11 @@ class RequestSender:
136
140
  tokens.append(content)
137
141
 
138
142
  finish_time = time.time()
143
+ if first_token_time == 0.0 and num_output_tokens > 0:
144
+ # Empty-content stream (common with max_tokens=1, e.g. the
145
+ # single token is EOS): use first chunk arrival as TTFT —
146
+ # closer to engine prefill completion than finish_time.
147
+ first_token_time = first_chunk_time or finish_time
139
148
  successful = first_token_time > 0.0
140
149
  ttft = (first_token_time - submit_time) if successful else -1.0
141
150
  request_latency = finish_time - submit_time
@@ -30,6 +30,10 @@ from lmcache.cli.commands.bench.engine_bench.workloads.multi_round_chat import (
30
30
  MultiRoundChatConfig,
31
31
  MultiRoundChatWorkload,
32
32
  )
33
+ from lmcache.cli.commands.bench.engine_bench.workloads.prefix_suffix_tuner import (
34
+ PrefixSuffixTunerConfig,
35
+ PrefixSuffixTunerWorkload,
36
+ )
33
37
  from lmcache.cli.commands.bench.engine_bench.workloads.random_prefill import (
34
38
  RandomPrefillConfig,
35
39
  RandomPrefillWorkload,
@@ -43,6 +47,8 @@ __all__ = [
43
47
  "LongDocQAWorkload",
44
48
  "MultiRoundChatConfig",
45
49
  "MultiRoundChatWorkload",
50
+ "PrefixSuffixTunerConfig",
51
+ "PrefixSuffixTunerWorkload",
46
52
  "RandomPrefillConfig",
47
53
  "RandomPrefillWorkload",
48
54
  "create_workload",
@@ -52,6 +58,7 @@ _WORKLOAD_NAMES = (
52
58
  "long-doc-permutator",
53
59
  "long-doc-qa",
54
60
  "multi-round-chat",
61
+ "prefix-suffix-tuner",
55
62
  "random-prefill",
56
63
  )
57
64
 
@@ -135,6 +142,22 @@ def create_workload(
135
142
  seed=config.seed,
136
143
  )
137
144
 
145
+ if config.workload == "prefix-suffix-tuner":
146
+ psf_workload_config = PrefixSuffixTunerConfig.resolve(
147
+ tokens_per_gb_kvcache=config.tokens_per_gb_kvcache,
148
+ context_length=args.psf_context_length,
149
+ prefix_ratio=args.psf_prefix_ratio,
150
+ thrash=args.psf_thrash,
151
+ )
152
+ return PrefixSuffixTunerWorkload(
153
+ config=psf_workload_config,
154
+ request_sender=request_sender,
155
+ stats_collector=stats_collector,
156
+ progress_monitor=progress_monitor,
157
+ seed=config.seed,
158
+ model_name=config.model,
159
+ )
160
+
138
161
  if config.workload == "random-prefill":
139
162
  rp_workload_config = RandomPrefillConfig.resolve(
140
163
  request_length=args.rp_request_length,