lmcache-cli 0.5.2.dev0__py3-none-any.whl → 0.5.2rc2.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 (173) hide show
  1. lmcache/__init__.py +14 -129
  2. lmcache/_version.py +3 -3
  3. lmcache/cli/commands/bench/l2_adapter_bench/__init__.py +26 -1
  4. lmcache/cli/commands/bench/l2_adapter_bench/command.py +126 -0
  5. lmcache/cli/commands/bench/server_bench/__init__.py +26 -1
  6. lmcache/cli/commands/bench/server_bench/command.py +150 -0
  7. lmcache/cli/commands/coordinator.py +16 -4
  8. lmcache/cli/commands/tool/flamegraph.py +160 -0
  9. lmcache/cli/profiling.py +738 -0
  10. lmcache/integration/vllm/lmcache_mp_connector.py +64 -6
  11. lmcache/integration/vllm/lmcache_mp_connector_0201.py +28 -55
  12. lmcache/integration/vllm/vllm_multi_process_adapter.py +27 -2
  13. lmcache/logging.py +14 -3
  14. lmcache/observability.py +1 -1
  15. lmcache/python_ops_fallback.py +145 -21
  16. lmcache/sdk/batch.py +321 -0
  17. lmcache/sdk/stream.py +326 -0
  18. lmcache/usage_context.py +21 -405
  19. lmcache/usage_telemetry/README.md +149 -0
  20. lmcache/usage_telemetry/__init__.py +72 -0
  21. lmcache/usage_telemetry/context.py +176 -0
  22. lmcache/usage_telemetry/continuous.py +212 -0
  23. lmcache/usage_telemetry/env_probe.py +139 -0
  24. lmcache/usage_telemetry/guard.py +49 -0
  25. lmcache/usage_telemetry/identity.py +107 -0
  26. lmcache/usage_telemetry/messages.py +301 -0
  27. lmcache/usage_telemetry/metric_specs.py +78 -0
  28. lmcache/usage_telemetry/mp.py +98 -0
  29. lmcache/usage_telemetry/mp_continuous.py +242 -0
  30. lmcache/usage_telemetry/transport.py +103 -0
  31. lmcache/utils.py +21 -0
  32. lmcache/v1/cache_controller/config.py +7 -4
  33. lmcache/v1/cache_controller/utils.py +1 -1
  34. lmcache/v1/cache_controller/worker.py +6 -6
  35. lmcache/v1/cache_engine.py +12 -7
  36. lmcache/v1/distributed/config.py +10 -2
  37. lmcache/v1/distributed/l1_manager.py +12 -4
  38. lmcache/v1/distributed/l2_adapters/bigtable_key_encoder.py +53 -0
  39. lmcache/v1/distributed/l2_adapters/bigtable_l2_adapter.py +1224 -0
  40. lmcache/v1/distributed/l2_adapters/fault_inject_l2_adapter.py +4 -4
  41. lmcache/v1/distributed/l2_adapters/hfbucket_l2_adapter.py +3 -2
  42. lmcache/v1/distributed/l2_adapters/mock_l2_adapter.py +2 -1
  43. lmcache/v1/distributed/l2_adapters/nixl_store_dynamic_l2_adapter.py +41 -6
  44. lmcache/v1/distributed/l2_adapters/sagemaker_hyperpod_client.py +515 -0
  45. lmcache/v1/distributed/l2_adapters/sagemaker_hyperpod_l2_adapter.py +825 -0
  46. lmcache/v1/distributed/l2_adapters/valkey_l2_adapter.py +1117 -0
  47. lmcache/v1/distributed/memory_manager/devdax_l1_memory_manager.py +1 -1
  48. lmcache/v1/distributed/memory_manager/l1_memory_manager.py +2 -2
  49. lmcache/v1/distributed/quota_manager.py +59 -4
  50. lmcache/v1/distributed/serde/factory.py +8 -5
  51. lmcache/v1/distributed/storage_controllers/eviction_controller.py +17 -0
  52. lmcache/v1/distributed/storage_controllers/prefetch_controller.py +6 -3
  53. lmcache/v1/distributed/storage_manager.py +20 -0
  54. lmcache/v1/gpu_connector/gpu_connectors.py +1 -1
  55. lmcache/v1/gpu_connector/gpu_ops.py +1 -1
  56. lmcache/v1/gpu_connector/kv_format/detectors/vllm.py +16 -12
  57. lmcache/v1/gpu_connector/kv_format/specs/base.py +6 -0
  58. lmcache/v1/gpu_connector/kv_format/specs/nb_nl_two_bs_nh_hs.py +3 -0
  59. lmcache/v1/gpu_connector/kv_format/specs/nb_nl_two_nh_bs_hs.py +3 -0
  60. lmcache/v1/gpu_connector/kv_format/specs/nl_x_nb_bs_hs.py +3 -0
  61. lmcache/v1/gpu_connector/kv_format/specs/nl_x_nb_bs_nh_two_hs.py +77 -0
  62. lmcache/v1/gpu_connector/kv_format/specs/nl_x_nb_nh_bs_two_hs.py +16 -1
  63. lmcache/v1/gpu_connector/kv_format/specs/nl_x_nb_two_bs_nh_hs.py +3 -0
  64. lmcache/v1/gpu_connector/kv_format/specs/nl_x_nb_two_nh_bs_hs.py +3 -0
  65. lmcache/v1/gpu_connector/kv_format/specs/nl_x_nbbs_one_hs.py +3 -0
  66. lmcache/v1/gpu_connector/kv_format/specs/nl_x_two_nb_bs_nh_hs.py +3 -0
  67. lmcache/v1/gpu_connector/kv_format/specs/nl_x_two_nb_nh_bs_hs.py +3 -0
  68. lmcache/v1/gpu_connector/kv_format/specs/two_x_nl_x_nb_bs_nh_hs.py +3 -0
  69. lmcache/v1/gpu_connector/kv_format/specs/two_x_nl_x_nbbs_nh_hs.py +3 -0
  70. lmcache/v1/gpu_connector/musa_connectors.py +1 -1
  71. lmcache/v1/gpu_connector/utils.py +11 -3
  72. lmcache/v1/gpu_connector/xpu_connectors.py +1 -1
  73. lmcache/v1/health_monitor/checks/remote_backend_check.py +5 -1
  74. lmcache/v1/hidden_state_store.py +1 -1
  75. lmcache/v1/kv_layer_groups.py +19 -0
  76. lmcache/v1/lookup_client/chunk_statistics_lookup_client.py +14 -12
  77. lmcache/v1/lookup_client/factory.py +0 -5
  78. lmcache/v1/lookup_client/hit_limit_lookup_client.py +10 -7
  79. lmcache/v1/lookup_client/lmcache_lookup_client.py +17 -14
  80. lmcache/v1/lookup_client/lmcache_lookup_client_bypass.py +12 -9
  81. lmcache/v1/memory_allocators/__init__.py +45 -0
  82. lmcache/v1/memory_allocators/ad_hoc_memory_allocator.py +152 -0
  83. lmcache/v1/memory_allocators/buffer_allocator.py +112 -0
  84. lmcache/v1/memory_allocators/cu_file_memory_allocator.py +38 -0
  85. lmcache/v1/memory_allocators/devdax_memory_allocator.py +501 -0
  86. lmcache/v1/memory_allocators/gpu_memory_allocator.py +148 -0
  87. lmcache/v1/memory_allocators/hip_file_memory_allocator.py +34 -0
  88. lmcache/v1/memory_allocators/host_memory_allocator.py +135 -0
  89. lmcache/v1/{lazy_memory_allocator.py → memory_allocators/lazy_memory_allocator.py} +51 -12
  90. lmcache/v1/memory_allocators/mixed_memory_allocator.py +276 -0
  91. lmcache/v1/memory_allocators/paged_cpu_gpu_memory_allocator.py +205 -0
  92. lmcache/v1/memory_allocators/paged_tensor_memory_allocator.py +345 -0
  93. lmcache/v1/memory_allocators/pin_memory_allocator.py +146 -0
  94. lmcache/v1/memory_allocators/tensor_memory_allocator.py +328 -0
  95. lmcache/v1/memory_management.py +17 -1739
  96. lmcache/v1/metadata.py +6 -3
  97. lmcache/v1/mp_coordinator/app.py +14 -2
  98. lmcache/v1/mp_coordinator/cache_control/eviction_manager.py +76 -20
  99. lmcache/v1/mp_coordinator/config.py +16 -7
  100. lmcache/v1/mp_coordinator/http_apis/cache_api.py +181 -0
  101. lmcache/v1/mp_coordinator/http_apis/dependencies.py +6 -1
  102. lmcache/v1/mp_coordinator/http_apis/quota_api.py +45 -0
  103. lmcache/v1/mp_coordinator/schemas.py +106 -0
  104. lmcache/v1/mp_coordinator/utils/__init__.py +1 -0
  105. lmcache/v1/mp_observability/README.md +6 -0
  106. lmcache/v1/mp_observability/config.py +50 -0
  107. lmcache/v1/mp_observability/subscribers/logging/__init__.py +4 -0
  108. lmcache/v1/mp_observability/subscribers/logging/extra_stats.py +182 -0
  109. lmcache/v1/multiprocess/cache_control/key_resolver.py +45 -53
  110. lmcache/v1/multiprocess/cache_control/object_service.py +40 -19
  111. lmcache/v1/multiprocess/cache_control/prefetch_service.py +19 -12
  112. lmcache/v1/multiprocess/engine_context.py +7 -0
  113. lmcache/v1/multiprocess/http_apis/cache_api.py +13 -8
  114. lmcache/v1/multiprocess/http_apis/dependencies.py +1 -1
  115. lmcache/v1/multiprocess/http_apis/schemas.py +7 -3
  116. lmcache/v1/multiprocess/modules/blend_v3.py +207 -67
  117. lmcache/v1/multiprocess/modules/lmcache_driven_transfer.py +51 -18
  118. lmcache/v1/multiprocess/protocols/blend_v3.py +8 -5
  119. lmcache/v1/multiprocess/server.py +10 -1
  120. lmcache/v1/multiprocess/transfer_context/__init__.py +2 -0
  121. lmcache/v1/multiprocess/transfer_context/async_engine_driven.py +369 -0
  122. lmcache/v1/multiprocess/transfer_context/base.py +37 -16
  123. lmcache/v1/multiprocess/transfer_context/shm.py +3 -2
  124. lmcache/v1/multiprocess/transfer_context/worker_transfer.py +93 -7
  125. lmcache/v1/platform/__init__.py +221 -19
  126. lmcache/v1/platform/base_device_spec.py +165 -0
  127. lmcache/v1/platform/base_ipc_wrapper.py +6 -5
  128. lmcache/v1/platform/base_pin_memory.py +1 -0
  129. lmcache/v1/platform/cache_context.py +6 -0
  130. lmcache/v1/platform/cpu/__init__.py +56 -4
  131. lmcache/v1/platform/cpu/cache_context.py +6 -0
  132. lmcache/v1/platform/cpu/shm.py +6 -9
  133. lmcache/v1/platform/cuda/__init__.py +47 -17
  134. lmcache/v1/platform/cuda/cache_context.py +5 -0
  135. lmcache/v1/platform/cuda/ipc_wrapper.py +12 -15
  136. lmcache/v1/platform/cuda/pin_memory.py +2 -1
  137. lmcache/v1/platform/hpu/__init__.py +35 -0
  138. lmcache/v1/platform/musa/__init__.py +51 -14
  139. lmcache/v1/platform/musa/{ipc.py → ipc_wrapper.py} +4 -5
  140. lmcache/v1/platform/xpu/__init__.py +35 -0
  141. lmcache/v1/protocol.py +7 -3
  142. lmcache/v1/storage_backend/audit_backend.py +34 -17
  143. lmcache/v1/storage_backend/connector/azure_adapter.py +71 -0
  144. lmcache/v1/storage_backend/connector/azure_connector.py +322 -0
  145. lmcache/v1/storage_backend/connector/bigtable_config.py +12 -2
  146. lmcache/v1/storage_backend/connector/bigtable_connector.py +403 -186
  147. lmcache/v1/storage_backend/connector/bigtable_schema.py +25 -9
  148. lmcache/v1/storage_backend/connector/bigtable_sharder.py +137 -0
  149. lmcache/v1/storage_backend/connector/eic_connector.py +2 -1
  150. lmcache/v1/storage_backend/connector/fs_connector.py +18 -12
  151. lmcache/v1/storage_backend/connector/hfbucket_connector.py +13 -8
  152. lmcache/v1/storage_backend/connector/s3_connector.py +40 -26
  153. lmcache/v1/storage_backend/connector/valkey_connector.py +30 -317
  154. lmcache/v1/storage_backend/gds_backend.py +54 -26
  155. lmcache/v1/storage_backend/local_cpu_backend.py +4 -2
  156. lmcache/v1/storage_backend/local_disk_backend.py +123 -16
  157. lmcache/v1/storage_backend/nixl_storage_backend.py +4 -2
  158. lmcache/v1/storage_backend/p2p_backend.py +3 -1
  159. lmcache/v1/storage_backend/pd_backend.py +3 -1
  160. lmcache/v1/storage_backend/pd_backend_async.py +3 -1
  161. lmcache/v1/storage_backend/valkey/__init__.py +15 -0
  162. lmcache/v1/storage_backend/valkey/worker_pool.py +633 -0
  163. {lmcache_cli-0.5.2.dev0.dist-info → lmcache_cli-0.5.2rc2.dev0.dist-info}/METADATA +4 -1
  164. {lmcache_cli-0.5.2.dev0.dist-info → lmcache_cli-0.5.2rc2.dev0.dist-info}/RECORD +170 -126
  165. {lmcache_cli-0.5.2.dev0.dist-info → lmcache_cli-0.5.2rc2.dev0.dist-info}/scm_file_list.json +1944 -1849
  166. lmcache_cli-0.5.2rc2.dev0.dist-info/scm_version.json +8 -0
  167. lmcache/v1/platform/_registry.py +0 -262
  168. lmcache/v1/platform/device_ext.py +0 -91
  169. lmcache_cli-0.5.2.dev0.dist-info/scm_version.json +0 -8
  170. {lmcache_cli-0.5.2.dev0.dist-info → lmcache_cli-0.5.2rc2.dev0.dist-info}/WHEEL +0 -0
  171. {lmcache_cli-0.5.2.dev0.dist-info → lmcache_cli-0.5.2rc2.dev0.dist-info}/entry_points.txt +0 -0
  172. {lmcache_cli-0.5.2.dev0.dist-info → lmcache_cli-0.5.2rc2.dev0.dist-info}/licenses/LICENSE +0 -0
  173. {lmcache_cli-0.5.2.dev0.dist-info → lmcache_cli-0.5.2rc2.dev0.dist-info}/top_level.txt +0 -0
lmcache/__init__.py CHANGED
@@ -1,14 +1,17 @@
1
1
  # SPDX-License-Identifier: Apache-2.0
2
2
 
3
3
  # Standard
4
- from typing import Any
5
- import importlib
6
4
  import sys
7
- import types
8
5
 
9
6
  # First Party
10
7
  from lmcache.logging import init_logger
11
- from lmcache.v1.platform.device_ext import DeviceExt
8
+
9
+ # --------------------------
10
+ # Backend instance & Device detection
11
+ # --------------------------
12
+ from lmcache.v1.platform import get_backend
13
+ from lmcache.v1.platform import torch_dev as torch_dev
14
+ from lmcache.v1.platform import torch_device_type as torch_device_type
12
15
 
13
16
  try:
14
17
  # First Party
@@ -17,135 +20,17 @@ except ImportError:
17
20
  __version__ = "unknown"
18
21
 
19
22
  logger = init_logger(__name__)
20
- # Standard
21
23
 
22
24
  __all__ = ["__version__", "torch_dev", "torch_device_type"]
23
25
 
24
-
25
- # --------------------------
26
- # Device detection
27
- # --------------------------
28
- def _detect_device() -> tuple[Any, str]:
29
- """
30
- Detect the available accelerator and return the corresponding torch
31
- device module and device type string.
32
-
33
- Returns:
34
- tuple[Any, str]: A tuple of (torch_device_module, device_type_string),
35
- e.g. ``(torch.cuda, "cuda")``, ``(torch.musa, "musa")``, or
36
- ``(torch.xpu, "xpu")``.
37
- """
38
- try:
39
- # Third Party
40
- import torch
41
- except ImportError:
42
- return None, "cpu" # fallback,CLI-only
43
-
44
- if hasattr(torch, "musa") and torch.musa.is_available(): # type: ignore[attr-defined]
45
- logger.info("MUSA device is available. Using MUSA for LMCache engine.")
46
- return torch.musa, "musa" # type: ignore[attr-defined]
47
- elif hasattr(torch, "xpu") and torch.xpu.is_available():
48
- return torch.xpu, "xpu"
49
- elif hasattr(torch, "hpu") and torch.hpu.is_available():
50
- return torch.hpu, "hpu"
51
- elif torch.cuda.is_available():
52
- return torch.cuda, "cuda"
53
- else:
54
- # First Party
55
- from lmcache.v1.platform.cpu.stub_cpu_device import StubCPUDevice
56
-
57
- # Fallback: always return torch, cpu as stub
58
- return StubCPUDevice("cpu"), "cpu"
59
-
60
-
61
- torch_dev, torch_device_type = _detect_device()
62
-
63
- logger.info(" torch_dev=%s, torch_device_type=%s", torch_dev, torch_device_type)
64
-
65
-
66
- # Attach the DeviceExt instance as ``torch_dev.ext``. This monkey-patches a
67
- # standard torch module (e.g. ``torch.cuda``) with a custom attribute that does
68
- # not exist in the original module. The ``# type: ignore[attr-defined]`` suppresses
69
- # the expected mypy/pyright "attr-defined" error from this intentional extension.
70
- if torch_dev is not None:
71
- torch_dev.ext = DeviceExt(torch_device_type) # type: ignore[attr-defined]
72
- else:
73
- logger.warning("torch_dev is None, skipping DeviceExt initialization.")
74
- pass
75
-
76
-
77
- # --------------------------
78
- # Dynamic backend selection
79
- # --------------------------
80
- def _get_backend() -> Any:
81
- """
82
- Try backends in order, first successful import wins.
83
- """
84
- default_module = importlib.import_module("lmcache.python_ops_fallback")
85
- # Third Party
86
- import torch
87
-
88
- backend_candidates = [
89
- # Keep backend priority aligned with _detect_device().
90
- # MUSA currently uses a Python adapter under the platform package,
91
- # unlike the compiled XPU/CUDA extension modules.
92
- (
93
- "lmcache.v1.platform.musa.ops",
94
- "musa_ops",
95
- lambda: hasattr(torch, "musa") and torch.musa.is_available(), # type: ignore[attr-defined]
96
- ),
97
- (
98
- "lmcache.xpu_ops",
99
- "xpu_ops",
100
- lambda: torch.xpu.is_available(),
101
- ),
102
- (
103
- "lmcache.c_ops",
104
- "cuda_ops",
105
- lambda: torch.cuda.is_available(),
106
- ),
107
- # should extend to more HWs..
108
- ]
109
-
110
- for module_name, backend_name, predicate in backend_candidates:
111
- # 1 Check whether the backend is available before importing
112
- try:
113
- if not predicate():
114
- logger.info(
115
- "Skipping backend %s: predicate returned False",
116
- module_name,
117
- )
118
- continue
119
- except Exception as e:
120
- logger.warning(
121
- "Skipping backend %s: predicate raised error: %s",
122
- module_name,
123
- e,
124
- )
125
- continue
126
- # 2 Run availability check for the backend
127
- try:
128
- backend_module = importlib.import_module(module_name)
129
- merged_module = types.ModuleType("lmcache.c_ops")
130
- merged_module.__dict__.update(default_module.__dict__)
131
- merged_module.__dict__.update(backend_module.__dict__)
132
- logger.info("Using backend: %s", module_name)
133
- return merged_module
134
- except Exception as e:
135
- logger.warning("Failed to import backend %s: %s", module_name, e)
136
-
137
- return default_module
138
-
139
-
140
- # --------------------------
141
- # Backend instance
142
- # --------------------------
143
- try:
144
- _ops = _get_backend()
145
- # override lmcache.c_ops with merged module,
26
+ _ops = get_backend(torch_device_type)
27
+ if _ops is not None:
28
+ # Override lmcache.c_ops with merged module,
146
29
  # in which:
147
30
  # python_ops_fallback as base,
148
31
  # use backend implementation if exists
149
32
  sys.modules["lmcache.c_ops"] = _ops
150
- except (ImportError, ModuleNotFoundError):
151
- logger.debug("No compute backend loaded; CLI-only mode (torch/numba not installed)")
33
+ else:
34
+ logger.warning(
35
+ "No compute backend loaded; CLI-only mode (torch/numba not installed)"
36
+ )
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.2.dev0'
22
- __version_tuple__ = version_tuple = (0, 5, 2, 'dev0')
21
+ __version__ = version = '0.5.2rc2.dev0'
22
+ __version_tuple__ = version_tuple = (0, 5, 2, 'rc2', 'dev0')
23
23
 
24
- __commit_id__ = commit_id = 'g979719d7a'
24
+ __commit_id__ = commit_id = 'gcd2c0d6a6'
@@ -30,9 +30,34 @@ class L2AdapterBenchCommand(BaseCommand):
30
30
  add_l2_arguments(parser)
31
31
 
32
32
  def execute(self, args: argparse.Namespace) -> None:
33
+ # Standard
34
+ import copy
35
+ import sys
36
+
33
37
  # First Party
34
38
  from lmcache.cli.commands.bench.l2_adapter_bench.command import (
35
39
  run_l2_adapter_bench,
36
40
  )
37
41
 
38
- run_l2_adapter_bench(self, args)
42
+ # A comma-separated --flamegraph-mode profiles one benchmark run per
43
+ # mode (the modes cannot share a recording window), each rendered to
44
+ # its own default path.
45
+ modes = [args.flamegraph_mode]
46
+ if getattr(args, "flamegraph", "off") == "on" and "," in args.flamegraph_mode:
47
+ modes = list(
48
+ dict.fromkeys(
49
+ m.strip() for m in args.flamegraph_mode.split(",") if m.strip()
50
+ )
51
+ )
52
+ if args.flamegraph_output:
53
+ print(
54
+ "Error: --flamegraph-output takes a single path; with "
55
+ "several modes each graph uses its own default path.",
56
+ file=sys.stderr,
57
+ )
58
+ sys.exit(2)
59
+
60
+ for mode in modes:
61
+ run_args = copy.copy(args)
62
+ run_args.flamegraph_mode = mode
63
+ run_l2_adapter_bench(self, run_args)
@@ -122,6 +122,46 @@ def add_l2_arguments(parser: argparse.ArgumentParser) -> None:
122
122
  default=None,
123
123
  help="Run only the specified operation (default: run all).",
124
124
  )
125
+ parser.add_argument(
126
+ "--flamegraph",
127
+ choices=["on", "off"],
128
+ default="off",
129
+ help=(
130
+ "Capture a flame graph of the measured phases if turned on. The benchmark "
131
+ "profiles itself and renders an SVG"
132
+ ),
133
+ )
134
+ parser.add_argument(
135
+ "--flamegraph-mode",
136
+ default="on-cpu",
137
+ metavar="MODE[,MODE...]",
138
+ help=(
139
+ "What to sample when profiling is on (default: on-cpu). Pass "
140
+ "several comma-separated to profile one benchmark run per mode. "
141
+ "Modes: on-cpu, off-cpu, wakeup, offwake (perf/bcc), wall, gil "
142
+ "(py-spy). See the 'lmcache tool flamegraph' docs for detail."
143
+ ),
144
+ )
145
+ parser.add_argument(
146
+ "--flamegraph-output",
147
+ default="",
148
+ metavar="PATH",
149
+ help=(
150
+ "SVG output path for '--flamegraph on'. Default: "
151
+ "/tmp/lmcache_bench_flames/<adapter>.<mode>.svg."
152
+ ),
153
+ )
154
+ parser.add_argument(
155
+ "--flamegraph-scripts-dir",
156
+ default="",
157
+ metavar="DIR",
158
+ help=(
159
+ "Directory with the FlameGraph scripts (flamegraph.pl, "
160
+ "stackcollapse-perf.pl); default ~/FlameGraph (cloned there on "
161
+ "first use). Unused by --flamegraph-mode wall / gil, which "
162
+ "render their own SVG."
163
+ ),
164
+ )
125
165
 
126
166
 
127
167
  # ---------------------------------------------------------------------------
@@ -152,6 +192,14 @@ def run_l2_adapter_bench(command: "BaseCommand", args: argparse.Namespace) -> No
152
192
  bench_lookup,
153
193
  bench_store,
154
194
  )
195
+ from lmcache.cli.profiling import (
196
+ PY_SPY_MODES,
197
+ FlameProfiler,
198
+ ProfileError,
199
+ check_profiling_deps,
200
+ default_output_path,
201
+ resolve_flamegraph_dir,
202
+ )
155
203
  from lmcache.v1.distributed.l2_adapters import create_l2_adapter
156
204
  from lmcache.v1.distributed.l2_adapters.config import (
157
205
  parse_args_to_l2_adapters_config,
@@ -224,6 +272,31 @@ def run_l2_adapter_bench(command: "BaseCommand", args: argparse.Namespace) -> No
224
272
  l1_buffer = make_aligned_tensor(2 * keys_per_round * data_size, l1_align_bytes)
225
273
  l1_memory_desc = create_l1_memory_desc(l1_buffer, align_bytes=l1_align_bytes)
226
274
 
275
+ # Resolve and validate the flame-graph toolchain up front, before any
276
+ # adapter (and its worker threads) is created. A user who explicitly
277
+ # passes ``--flamegraph on`` gets a fast, actionable failure if a
278
+ # required tool is missing, rather than a benchmark that silently runs
279
+ # unprofiled. When ``--flamegraph`` is off (default), none of this runs
280
+ # and the benchmark behaves exactly as before.
281
+ flamegraph_on = getattr(args, "flamegraph", "off") == "on"
282
+ flamegraph_dir = ""
283
+ if flamegraph_on:
284
+ try:
285
+ check_profiling_deps(args.flamegraph_mode)
286
+ # py-spy renders its own SVG; only the perf/bcc modes need
287
+ # the FlameGraph scripts, so do not clone them otherwise.
288
+ if args.flamegraph_mode not in PY_SPY_MODES:
289
+ flamegraph_dir = resolve_flamegraph_dir(
290
+ args.flamegraph_scripts_dir, log
291
+ )
292
+ except ProfileError as e:
293
+ print(
294
+ "Error: --flamegraph on was requested but the profiling "
295
+ f"toolchain is unavailable:\n {e}",
296
+ file=sys.stderr,
297
+ )
298
+ sys.exit(2)
299
+
227
300
  log("\n[Init] Creating adapter...")
228
301
  try:
229
302
  adapter = create_l2_adapter(adapter_cfg, l1_memory_desc=l1_memory_desc)
@@ -232,6 +305,38 @@ def run_l2_adapter_bench(command: "BaseCommand", args: argparse.Namespace) -> No
232
305
  print(f"[Init] Failed to create adapter: {e}", file=sys.stderr)
233
306
  sys.exit(1)
234
307
 
308
+ # Optional self-profiling: record a flame graph of the measured
309
+ # phases. Built here, after the adapter (and its worker threads)
310
+ # exist, so the recorder attaches to a fully-started process. The
311
+ # toolchain was already validated above, so a ProfileError here is a
312
+ # late failure (e.g. the output directory is not writable); since the
313
+ # user explicitly requested a flame graph we fail loudly rather than
314
+ # downgrade, closing the adapter we just opened first.
315
+ profiler: FlameProfiler | None = None
316
+ if flamegraph_on:
317
+ adapter_name = type(adapter).__name__
318
+ try:
319
+ profiler = FlameProfiler(
320
+ mode=args.flamegraph_mode,
321
+ output=(
322
+ args.flamegraph_output
323
+ or default_output_path(adapter_name, args.flamegraph_mode)
324
+ ),
325
+ flamegraph_dir=flamegraph_dir,
326
+ pid=os.getpid(),
327
+ title=f"{args.flamegraph_mode} ({adapter_name})",
328
+ )
329
+ except ProfileError as e:
330
+ print(
331
+ f"Error: cannot start flame-graph profiling: {e}",
332
+ file=sys.stderr,
333
+ )
334
+ try:
335
+ adapter.close()
336
+ except Exception:
337
+ pass
338
+ sys.exit(2)
339
+
235
340
  # ------------------------------------------------------------------
236
341
  # Idx layout
237
342
  # ------------------------------------------------------------------
@@ -347,6 +452,18 @@ def run_l2_adapter_bench(command: "BaseCommand", args: argparse.Namespace) -> No
347
452
  last_load_round_keys: list[list] | None = None
348
453
 
349
454
  try:
455
+ if profiler is not None:
456
+ # Pre-build the payload buffers before the recorder starts so
457
+ # the one-time tensor allocation + fill (benchmark harness work,
458
+ # not the adapter) is kept out of the flame graph. The batches
459
+ # are reused across rounds, so this is the only build; warming
460
+ # it here keeps the recording focused on adapter I/O.
461
+ if args.only is None or args.only == "store":
462
+ _store_objs(0)
463
+ if args.only is None or args.only == "load":
464
+ _load_objs(0)
465
+ profiler.start(log)
466
+
350
467
  # ---- Store ----
351
468
  if args.only is None or args.only == "store":
352
469
  log(f"[Store] Running {warmup} warmup + {rounds} measurement rounds...")
@@ -398,6 +515,11 @@ def run_l2_adapter_bench(command: "BaseCommand", args: argparse.Namespace) -> No
398
515
  last_load_round_keys = _build_round_keys(total_rounds - 1)
399
516
  log("")
400
517
 
518
+ # Stop profiling before verification / summary so the flame
519
+ # graph reflects only the measured store/lookup/load work.
520
+ if profiler is not None:
521
+ profiler.stop(log)
522
+
401
523
  # ---- Round-trip verification (last measured round only) ----
402
524
  if (
403
525
  not args.skip_verify
@@ -433,6 +555,10 @@ def run_l2_adapter_bench(command: "BaseCommand", args: argparse.Namespace) -> No
433
555
  results=results,
434
556
  )
435
557
  finally:
558
+ # Idempotent: a no-op if profiling already stopped on the normal
559
+ # path; tears the recorder down if a phase raised.
560
+ if profiler is not None:
561
+ profiler.stop(log)
436
562
  log("[Cleanup] Closing adapter...")
437
563
  try:
438
564
  adapter.close()
@@ -30,9 +30,34 @@ class ServerBenchCommand(BaseCommand):
30
30
  add_server_arguments(parser)
31
31
 
32
32
  def execute(self, args: argparse.Namespace) -> None:
33
+ # Standard
34
+ import copy
35
+ import sys
36
+
33
37
  # First Party
34
38
  from lmcache.cli.commands.bench.server_bench.command import (
35
39
  run_server_bench,
36
40
  )
37
41
 
38
- run_server_bench(self, args)
42
+ # A comma-separated --flamegraph-mode drives the load once per mode
43
+ # (the modes cannot share a recording window), each rendered to its
44
+ # own default path.
45
+ modes = [args.flamegraph_mode]
46
+ if getattr(args, "flamegraph", "off") == "on" and "," in args.flamegraph_mode:
47
+ modes = list(
48
+ dict.fromkeys(
49
+ m.strip() for m in args.flamegraph_mode.split(",") if m.strip()
50
+ )
51
+ )
52
+ if args.flamegraph_output:
53
+ print(
54
+ "Error: --flamegraph-output takes a single path; with "
55
+ "several modes each graph uses its own default path.",
56
+ file=sys.stderr,
57
+ )
58
+ sys.exit(2)
59
+
60
+ for mode in modes:
61
+ run_args = copy.copy(args)
62
+ run_args.flamegraph_mode = mode
63
+ run_server_bench(self, run_args)
@@ -63,8 +63,12 @@ from lmcache.cli.commands.bench.server_bench.helpers import (
63
63
  )
64
64
 
65
65
  if TYPE_CHECKING:
66
+ # Standard
67
+ from collections.abc import Callable
68
+
66
69
  # First Party
67
70
  from lmcache.cli.commands.base import BaseCommand
71
+ from lmcache.cli.profiling import FlameProfiler
68
72
  from lmcache.v1.multiprocess.custom_types import KVCache
69
73
 
70
74
 
@@ -192,12 +196,145 @@ def add_server_arguments(parser: argparse.ArgumentParser) -> None:
192
196
  help=("HTTP base URL for checksum API (default: http://localhost:8080)"),
193
197
  )
194
198
 
199
+ prof = parser.add_argument_group(
200
+ "server profiling",
201
+ "Flame-graph the MP server process while this benchmark drives "
202
+ "load into it. The server's store path (hashing, allocation, "
203
+ "gather, D2H) runs in its own process, not in this client, so "
204
+ "profiling attaches to --profile-server-pid rather than to the "
205
+ "benchmark. See 'lmcache tool flamegraph' for the standalone form.",
206
+ )
207
+ prof.add_argument(
208
+ "--flamegraph",
209
+ choices=["on", "off"],
210
+ default="off",
211
+ help="Record a flame graph of the server during the run (default: off).",
212
+ )
213
+ prof.add_argument(
214
+ "--profile-server-pid",
215
+ type=int,
216
+ default=0,
217
+ metavar="PID",
218
+ help=(
219
+ "Server process to profile, e.g. $(pgrep -f 'lmcache server'). "
220
+ "Required when --flamegraph on."
221
+ ),
222
+ )
223
+ prof.add_argument(
224
+ "--flamegraph-mode",
225
+ default="gil",
226
+ metavar="MODE[,MODE...]",
227
+ help=(
228
+ "What to sample in the server (default: gil). Pass several "
229
+ "comma-separated to profile one load run per mode. Modes: on-cpu, "
230
+ "off-cpu, wakeup, offwake (perf/bcc), wall, gil (py-spy). perf/bcc "
231
+ "name Python functions only when the server was launched with "
232
+ "PYTHONPERFSUPPORT=1. See the 'lmcache tool flamegraph' docs."
233
+ ),
234
+ )
235
+ prof.add_argument(
236
+ "--flamegraph-output",
237
+ default="",
238
+ metavar="PATH",
239
+ help=(
240
+ "SVG output path. Default: "
241
+ "/tmp/lmcache_bench_flames/server-pid<PID>.<mode>.svg."
242
+ ),
243
+ )
244
+ prof.add_argument(
245
+ "--flamegraph-scripts-dir",
246
+ default="",
247
+ metavar="DIR",
248
+ help=(
249
+ "Directory with the FlameGraph scripts (flamegraph.pl, "
250
+ "stackcollapse-perf.pl); default ~/FlameGraph (cloned there on "
251
+ "first use). Unused by --flamegraph-mode wall / gil, which "
252
+ "render their own SVG."
253
+ ),
254
+ )
255
+
195
256
 
196
257
  # ---------------------------------------------------------------------------
197
258
  # Public entry point
198
259
  # ---------------------------------------------------------------------------
199
260
 
200
261
 
262
+ def _build_server_profiler(
263
+ args: argparse.Namespace,
264
+ log: "Callable[[str], None]",
265
+ ) -> "FlameProfiler | None":
266
+ """Build a profiler attached to the server, or ``None`` if disabled.
267
+
268
+ Validates the target pid and toolchain eagerly so a misconfigured run
269
+ fails before any load is sent. The returned profiler is not started;
270
+ the caller wraps the load loop with ``start`` / ``stop``.
271
+
272
+ Args:
273
+ args: Parsed CLI arguments for ``lmcache bench server``.
274
+ log: Progress logger.
275
+
276
+ Returns:
277
+ A ready :class:`FlameProfiler` targeting ``--profile-server-pid``,
278
+ or ``None`` when ``--flamegraph`` is off.
279
+ """
280
+ if getattr(args, "flamegraph", "off") != "on":
281
+ return None
282
+
283
+ # First Party
284
+ from lmcache.cli.profiling import (
285
+ PY_SPY_MODES,
286
+ FlameProfiler,
287
+ ProfileError,
288
+ check_profiling_deps,
289
+ default_output_path,
290
+ resolve_flamegraph_dir,
291
+ )
292
+
293
+ pid = args.profile_server_pid
294
+ if pid <= 0:
295
+ print(
296
+ "Error: --flamegraph on requires --profile-server-pid "
297
+ "(the pid of the running 'lmcache server').",
298
+ file=sys.stderr,
299
+ )
300
+ sys.exit(2)
301
+ try:
302
+ os.kill(pid, 0)
303
+ except ProcessLookupError:
304
+ print(f"Error: no such process: --profile-server-pid {pid}", file=sys.stderr)
305
+ sys.exit(2)
306
+ except PermissionError:
307
+ print(
308
+ f"Error: server pid {pid} belongs to another user; "
309
+ "profiling it needs root.",
310
+ file=sys.stderr,
311
+ )
312
+ sys.exit(2)
313
+
314
+ try:
315
+ check_profiling_deps(args.flamegraph_mode)
316
+ flamegraph_dir = ""
317
+ if args.flamegraph_mode not in PY_SPY_MODES:
318
+ flamegraph_dir = resolve_flamegraph_dir(args.flamegraph_scripts_dir, log)
319
+ output = args.flamegraph_output or default_output_path(
320
+ f"server-pid{pid}", args.flamegraph_mode
321
+ )
322
+ return FlameProfiler(
323
+ mode=args.flamegraph_mode,
324
+ output=output,
325
+ flamegraph_dir=flamegraph_dir,
326
+ pid=pid,
327
+ title=f"{args.flamegraph_mode} (server pid {pid})",
328
+ )
329
+ except ProfileError as e:
330
+ print(
331
+ "Error: --flamegraph on was requested but profiling is "
332
+ f"unavailable:\n {e}",
333
+ file=sys.stderr,
334
+ )
335
+ sys.exit(2)
336
+
337
+
201
338
  def run_server_bench(
202
339
  command: "BaseCommand",
203
340
  args: argparse.Namespace,
@@ -252,6 +389,12 @@ def run_server_bench(
252
389
  "using REGISTER_KV_CACHE + STORE/RETRIEVE over POSIX SHM"
253
390
  )
254
391
 
392
+ # The profiler targets the server process (--profile-server-pid), not
393
+ # this benchmark client. Build it before opening any connection so a
394
+ # bad pid or a missing toolchain fails immediately, not after a full
395
+ # benchmark has already run. ``None`` when --flamegraph is off.
396
+ profiler = _build_server_profiler(args, log)
397
+
255
398
  total_requests = 0
256
399
  total_checksum_ok = 0
257
400
  total_checksum_fail = 0
@@ -475,6 +618,10 @@ def run_server_bench(
475
618
  # ``/cache/checksums`` path.
476
619
  client_tensors = None if use_handle else client_kv_tensors
477
620
 
621
+ # Record only the steady-state load, not the one-time registration.
622
+ if profiler is not None:
623
+ profiler.start(log)
624
+
478
625
  for seq_no in seq_iter:
479
626
  log("=== Request seq=%d ===" % seq_no)
480
627
 
@@ -557,6 +704,9 @@ def run_server_bench(
557
704
  except KeyboardInterrupt:
558
705
  log("\nStopping...")
559
706
  finally:
707
+ # Stop recording once load ends, before teardown
708
+ if profiler is not None:
709
+ profiler.stop(log)
560
710
  # Deregister our context from the server before tearing down the
561
711
  # client. Otherwise the server keeps the registration (and the
562
712
  # CUDA-IPC / POSIX-SHM mappings it holds) alive forever, leaking
@@ -102,12 +102,23 @@ class CoordinatorCommand(BaseCommand):
102
102
  ),
103
103
  )
104
104
  parser.add_argument(
105
- "--blend-chunk-size",
105
+ "--chunk-size",
106
106
  type=int,
107
107
  default=None,
108
108
  help=(
109
- "Tokens per chunk for the global CacheBlend directory; must "
110
- "equal the LMCache chunk size the blend servers use (default: 256)."
109
+ "Tokens per KV chunk: the CacheBlend match unit and the unit used "
110
+ "to resolve pin token_ids to keys. Must equal the MP servers' "
111
+ "--chunk-size (default: 256)."
112
+ ),
113
+ )
114
+ parser.add_argument(
115
+ "--hash-algorithm",
116
+ type=str,
117
+ default=None,
118
+ help=(
119
+ "Token hash algorithm for pin key resolution; must equal the MP "
120
+ "servers' --hash-algorithm. 'blake3' (default) is self-contained; "
121
+ "other algorithms require vLLM importable in the coordinator."
111
122
  ),
112
123
  )
113
124
  parser.add_argument(
@@ -172,7 +183,8 @@ class CoordinatorCommand(BaseCommand):
172
183
  ("eviction_check_interval", args.eviction_check_interval),
173
184
  ("eviction_ratio", args.eviction_ratio),
174
185
  ("trigger_watermark", args.trigger_watermark),
175
- ("blend_chunk_size", args.blend_chunk_size),
186
+ ("chunk_size", args.chunk_size),
187
+ ("hash_algorithm", args.hash_algorithm),
176
188
  ("blend_probe_stride", args.blend_probe_stride),
177
189
  ("timeout_keep_alive", args.timeout_keep_alive),
178
190
  )