lmcache-cli 0.5.7rc2.dev0__py3-none-any.whl → 0.5.7rc3.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 (94) hide show
  1. lmcache/_version.py +3 -3
  2. lmcache/cli/commands/bench/server_bench/__init__.py +21 -5
  3. lmcache/cli/commands/bench/server_bench/allocator.py +78 -0
  4. lmcache/cli/commands/bench/server_bench/arguments.py +226 -0
  5. lmcache/cli/commands/bench/server_bench/buffers.py +46 -30
  6. lmcache/cli/commands/bench/server_bench/cases/baseline.py +48 -41
  7. lmcache/cli/commands/bench/server_bench/client.py +143 -144
  8. lmcache/cli/commands/bench/server_bench/command.py +14 -240
  9. lmcache/cli/commands/bench/server_bench/config.py +8 -1
  10. lmcache/cli/commands/bench/server_bench/helpers.py +6 -68
  11. lmcache/cli/commands/server.py +1 -1
  12. lmcache/inprocess/v1/gpu_connector/musa_connectors.py +2 -1
  13. lmcache/integration/atom/multi_process_adapter.py +5 -4
  14. lmcache/integration/sglang/unified_lmcache_mp_connector.py +3 -2
  15. lmcache/integration/tensorrt_llm/tensorrt_mp_adapter.py +4 -4
  16. lmcache/integration/vllm/lazy_offload_manager.py +14 -11
  17. lmcache/integration/vllm/lmcache_mp_connector.py +4 -4
  18. lmcache/integration/vllm/lmcache_mp_metadata.py +59 -6
  19. lmcache/integration/vllm/mp_lookup_state.py +2 -1
  20. lmcache/integration/vllm/utils.py +4 -2
  21. lmcache/integration/vllm/vllm_multi_process_adapter.py +35 -25
  22. lmcache/sdk/context.py +2 -2
  23. lmcache/sdk/qringbuffer.py +3 -3
  24. lmcache/security/__init__.py +2 -0
  25. lmcache/security/api_key.py +207 -0
  26. lmcache/security/http.py +99 -0
  27. lmcache/v1/distributed/l2_adapters/bigtable_l2_adapter.py +6 -6
  28. lmcache/v1/distributed/l2_adapters/dax_l2_adapter.py +5 -5
  29. lmcache/v1/distributed/l2_adapters/hfbucket_client.py +193 -0
  30. lmcache/v1/distributed/l2_adapters/hfbucket_l2_adapter.py +2 -2
  31. lmcache/v1/{storage_backend → distributed/l2_adapters}/raw_block/__init__.py +2 -2
  32. lmcache/v1/{storage_backend → distributed/l2_adapters}/raw_block/core.py +2 -2
  33. lmcache/v1/distributed/l2_adapters/raw_block_l2_adapter.py +3 -3
  34. lmcache/v1/{storage_backend → distributed/l2_adapters}/valkey/__init__.py +1 -1
  35. lmcache/v1/distributed/l2_adapters/valkey_l2_adapter.py +1 -1
  36. lmcache/v1/gpu_connector/kv_format/specs/base.py +68 -2
  37. lmcache/v1/kv_layer_groups.py +23 -0
  38. lmcache/v1/mp_observability/README.md +1 -1
  39. lmcache/v1/multiprocess/cache_control/key_resolver.py +2 -2
  40. lmcache/v1/multiprocess/config.py +15 -15
  41. lmcache/v1/multiprocess/custom_types.py +99 -9
  42. lmcache/v1/multiprocess/engine_context.py +1 -1
  43. lmcache/v1/multiprocess/http_apis/common/env_api.py +1 -0
  44. lmcache/v1/multiprocess/http_auth.py +35 -123
  45. lmcache/v1/multiprocess/http_server.py +1 -1
  46. lmcache/v1/multiprocess/modules/blend/lookup.py +15 -10
  47. lmcache/v1/multiprocess/modules/blend/module.py +3 -2
  48. lmcache/v1/multiprocess/modules/blend/store.py +6 -3
  49. lmcache/v1/multiprocess/modules/engine_driven_transfer.py +145 -6
  50. lmcache/v1/multiprocess/modules/lmcache_driven_transfer.py +2 -1
  51. lmcache/v1/multiprocess/modules/lookup.py +10 -9
  52. lmcache/v1/multiprocess/modules/p2p_controller.py +9 -2
  53. lmcache/v1/multiprocess/modules/server_transfer.py +282 -100
  54. lmcache/v1/multiprocess/session.py +23 -15
  55. lmcache/v1/multiprocess/token_codec.py +90 -0
  56. lmcache/v1/multiprocess/token_hasher.py +96 -9
  57. lmcache/v1/multiprocess/transfer_context/async_engine_driven.py +125 -0
  58. lmcache/v1/multiprocess/transfer_context/base.py +96 -17
  59. lmcache/v1/multiprocess/transfer_context/inline.py +53 -2
  60. lmcache/v1/multiprocess/transfer_context/shm.py +68 -0
  61. lmcache/v1/multiprocess/transfer_context/tensor_wire.py +164 -5
  62. lmcache/v1/multiprocess/transfer_context/worker_transfer.py +583 -24
  63. lmcache/v1/multiprocess/transport/auth.py +50 -0
  64. lmcache/v1/multiprocess/transport/base.py +4 -1
  65. lmcache/v1/multiprocess/transport/grpc_impl/_proto_gen/common_pb2.pyi +4 -4
  66. lmcache/v1/multiprocess/transport/grpc_impl/native_server.py +417 -0
  67. lmcache/v1/multiprocess/transport/grpc_impl/protos/common.proto +2 -1
  68. lmcache/v1/multiprocess/transport/grpc_impl/protos/engine_driven_service.proto +22 -0
  69. lmcache/v1/multiprocess/transport/grpc_impl/server_base.py +4 -2
  70. lmcache/v1/multiprocess/transport/grpc_impl/server_builder.py +7 -2
  71. lmcache/v1/platform/devices/mlu/__init__.py +76 -0
  72. lmcache/v1/platform/devices/mlu/device_ops.py +277 -0
  73. lmcache/v1/platform/devices/musa/cache_context.py +9 -4
  74. lmcache/v1/platform/devices/musa/device_ops.py +35 -33
  75. lmcache/v1/platform/devices/musa/format_capabilities.py +93 -0
  76. lmcache/v1/platform/devices/musa/native_kv_transfer.py +9 -25
  77. lmcache/v1/platform/native_block_transfer.py +6 -4
  78. lmcache/v1/storage_backend/connector/bigtable_connector.py +4 -2
  79. lmcache/v1/storage_backend/connector/hfbucket_connector.py +9 -199
  80. lmcache/v1/storage_backend/connector/valkey_connector.py +6 -6
  81. lmcache/v1/storage_backend/plugins/dax_backend.py +1 -1
  82. lmcache/v1/storage_backend/plugins/rust_raw_block_backend.py +5 -5
  83. {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc3.dev0.dist-info}/METADATA +1 -1
  84. {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc3.dev0.dist-info}/RECORD +94 -82
  85. /lmcache/v1/{storage_backend/connector → distributed/l2_adapters}/bigtable_config.py +0 -0
  86. /lmcache/v1/{storage_backend/connector → distributed/l2_adapters}/bigtable_sharder.py +0 -0
  87. /lmcache/v1/{storage_backend → distributed/l2_adapters}/dax/__init__.py +0 -0
  88. /lmcache/v1/{storage_backend → distributed/l2_adapters}/dax/core.py +0 -0
  89. /lmcache/v1/{storage_backend → distributed/l2_adapters}/raw_block/key_codec.py +0 -0
  90. /lmcache/v1/{storage_backend → distributed/l2_adapters}/valkey/worker_pool.py +0 -0
  91. {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc3.dev0.dist-info}/WHEEL +0 -0
  92. {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc3.dev0.dist-info}/entry_points.txt +0 -0
  93. {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc3.dev0.dist-info}/licenses/LICENSE +0 -0
  94. {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc3.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.7rc3.dev0'
22
+ __version_tuple__ = version_tuple = (0, 5, 7, 'rc3', 'dev0')
23
23
 
24
- __commit_id__ = commit_id = 'gaa12d34e3'
24
+ __commit_id__ = commit_id = 'gbf85b41fa'
@@ -22,22 +22,38 @@ class ServerBenchCommand(BaseCommand):
22
22
  return "End-to-end test for LMCache MP cache server (GPU mode)."
23
23
 
24
24
  def add_arguments(self, parser: argparse.ArgumentParser) -> None:
25
+ """Register benchmark arguments even without the full runtime.
26
+
27
+ Args:
28
+ parser: The argument parser for the server benchmark subcommand.
29
+ """
25
30
  # First Party
26
- from lmcache.cli.commands.bench.server_bench.command import (
31
+ from lmcache.cli.commands.bench.server_bench.arguments import (
27
32
  add_server_arguments,
28
33
  )
29
34
 
30
35
  add_server_arguments(parser)
31
36
 
32
37
  def execute(self, args: argparse.Namespace) -> None:
38
+ """Load and execute the benchmark, reporting runtime import failures."""
33
39
  # Standard
34
40
  import copy
35
41
  import sys
36
42
 
37
- # First Party
38
- from lmcache.cli.commands.bench.server_bench.command import (
39
- run_server_bench,
40
- )
43
+ try:
44
+ # First Party
45
+ from lmcache.cli.commands.bench.server_bench.command import (
46
+ run_server_bench,
47
+ )
48
+ except ImportError as exc:
49
+ print(
50
+ "ERROR: Unable to load `lmcache bench server`. "
51
+ "This command requires the full LMCache package.\n"
52
+ " Install with: pip install lmcache\n"
53
+ f" Original import error: {exc}",
54
+ file=sys.stderr,
55
+ )
56
+ sys.exit(2)
41
57
 
42
58
  # A comma-separated --flamegraph-mode drives the load once per mode
43
59
  # (the modes cannot share a recording window), each rendered to its
@@ -0,0 +1,78 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Request-exclusive block allocation shared by all TP workers."""
3
+
4
+ # Standard
5
+ from dataclasses import dataclass
6
+ from typing import Literal
7
+ import random
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class GroupBlocks:
12
+ """Physical block IDs in logical order for one engine group."""
13
+
14
+ group_id: int
15
+ block_ids: tuple[int, ...]
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class RequestBlocks:
20
+ """Immutable group allocations shared by every TP worker."""
21
+
22
+ groups: tuple[GroupBlocks, ...]
23
+
24
+ def as_block_ids(self) -> list[list[int]]:
25
+ """Return group-ordered IDs for buffer and transfer APIs."""
26
+ return [list(group.block_ids) for group in self.groups]
27
+
28
+
29
+ class BlockAllocator:
30
+ """Single-threaded, request-exclusive allocation without prefix sharing."""
31
+
32
+ def __init__(
33
+ self,
34
+ groups: int,
35
+ capacity: int,
36
+ *,
37
+ allocation_order: Literal["sequential", "random"] = "sequential",
38
+ seed: int = 0,
39
+ ) -> None:
40
+ """Create one free pool per group with a reproducible allocation order."""
41
+ if groups < 0 or capacity < 0:
42
+ raise ValueError("groups and capacity must be non-negative")
43
+ if allocation_order not in ("sequential", "random"):
44
+ raise ValueError(f"unsupported allocation order: {allocation_order}")
45
+ self._free = [set(range(capacity)) for _ in range(groups)]
46
+ self._allocation_order = allocation_order
47
+ self._random = random.Random(seed)
48
+
49
+ def allocate(self, count: int) -> RequestBlocks:
50
+ """Atomically reserve count > 0 exclusive blocks in every group."""
51
+ if count <= 0:
52
+ raise ValueError("block count must be positive")
53
+ for group_id, free in enumerate(self._free):
54
+ if len(free) < count:
55
+ raise RuntimeError(
56
+ f"group {group_id} needs {count} blocks, only {len(free)} free"
57
+ )
58
+ allocated = RequestBlocks(
59
+ tuple(
60
+ GroupBlocks(
61
+ group_id,
62
+ tuple(
63
+ self._random.sample(sorted(free), count)
64
+ if self._allocation_order == "random"
65
+ else sorted(free)[:count]
66
+ ),
67
+ )
68
+ for group_id, free in enumerate(self._free)
69
+ )
70
+ )
71
+ for group in allocated.groups:
72
+ self._free[group.group_id].difference_update(group.block_ids)
73
+ return allocated
74
+
75
+ def release(self, allocated: RequestBlocks) -> None:
76
+ """Return owned blocks after all transfers using them have finished."""
77
+ for group in allocated.groups:
78
+ self._free[group.group_id].update(group.block_ids)
@@ -0,0 +1,226 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Runtime-independent argument registration for ``lmcache bench server``.
3
+
4
+ Keep this module free of torch, zmq, and benchmark runtime imports so the
5
+ lightweight CLI can show help and parse options before reporting missing
6
+ runtime dependencies. The dtype help is checked against DTYPE_MAP in tests.
7
+ """
8
+
9
+ # Standard
10
+ import argparse
11
+
12
+ _DEFAULT_SHAPE_SPEC = "(2,1024,16,8,128):float16:32"
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Parser registration
17
+ # ---------------------------------------------------------------------------
18
+
19
+
20
+ def add_server_arguments(parser: argparse.ArgumentParser) -> None:
21
+ """Add ``lmcache bench server`` arguments without loading runtime dependencies.
22
+
23
+ Args:
24
+ parser: The ``ArgumentParser`` for the server bench subcommand.
25
+ """
26
+
27
+ parser.add_argument(
28
+ "--rpc-url",
29
+ default="tcp://localhost:5555",
30
+ help=("MP request endpoint (default: tcp://localhost:5555)"),
31
+ )
32
+ parser.add_argument(
33
+ "--mode",
34
+ choices=["cpu", "gpu"],
35
+ default="gpu",
36
+ help=(
37
+ "Run mode (default: gpu). In cpu mode the client allocates "
38
+ "regular CPU KV tensors; the selected TransferContext owns "
39
+ "any SHM migration or staging."
40
+ ),
41
+ )
42
+ parser.add_argument(
43
+ "--transfer-mode",
44
+ choices=["auto", "engine_driven", "lmcache_driven"],
45
+ default="auto",
46
+ help=(
47
+ "Transport routing for STORE/RETRIEVE (default: auto). "
48
+ "`lmcache_driven` forces the server-driven handle path "
49
+ "(REGISTER_KV_CACHE + STORE/RETRIEVE), which supports "
50
+ "both CUDA IPC and CPU SHM for zero-copy transfers. "
51
+ "`engine_driven` forces the worker-side gather/scatter "
52
+ "data path (REGISTER_KV_CACHE_ENGINE_DRIVEN_CONTEXT + "
53
+ "PREPARE/COMMIT). "
54
+ "`auto` keeps the historical mapping: "
55
+ "gpu->lmcache_driven, cpu->engine_driven."
56
+ ),
57
+ )
58
+ parser.add_argument(
59
+ "--tp-size",
60
+ type=int,
61
+ default=1,
62
+ help=(
63
+ "Simulated tensor-parallel world size (default: 1). Each "
64
+ "rank registers its own KV cache under a distinct "
65
+ "instance_id, and STORE / RETRIEVE fan out per rank the "
66
+ "same way LMCacheMPWorkerAdapter routes them in a real "
67
+ "vLLM deployment (MLA -> only rank 0 stores; non-MLA -> "
68
+ "every rank stores; every rank always retrieves)."
69
+ ),
70
+ )
71
+ parser.add_argument(
72
+ "--use-mla",
73
+ action="store_true",
74
+ default=False,
75
+ help=(
76
+ "MLA mode: fold all TP ranks into a single kv_worker "
77
+ "so only rank 0 writes KV and every rank retrieves "
78
+ "the shared KV object (default: False). "
79
+ "Also implied when --kvcache-shape-spec declares "
80
+ "kv_size=1."
81
+ ),
82
+ )
83
+ parser.add_argument(
84
+ "--num-tokens",
85
+ type=int,
86
+ default=512,
87
+ help="Tokens per request (default: 512)",
88
+ )
89
+
90
+ parser.add_argument(
91
+ "--block-allocation",
92
+ choices=["sequential", "random"],
93
+ default="sequential",
94
+ help="Block allocation order (default: sequential).",
95
+ )
96
+
97
+ # -- KV cache shape --
98
+ kv = parser.add_argument_group("KV cache shape")
99
+ kv.add_argument(
100
+ "--kvcache-shape-spec",
101
+ type=str,
102
+ default=_DEFAULT_SHAPE_SPEC,
103
+ help=(
104
+ "KV shape spec. Describes one or more KV layer groups "
105
+ "separated by ';'. "
106
+ "Grammar: "
107
+ "'(kv_size,NB,BS,NH,HS):dtype:layers[;(...):dtype:layers...]'. "
108
+ "Fields: kv_size=2 for classical K/V or 1 for MLA, "
109
+ "NB=num_blocks, BS=block_size (tokens/block), "
110
+ "NH=num_heads, HS=head_size (elements). "
111
+ "dtype is the element dtype (supported: float16, float32, bfloat16, "
112
+ "uint8, int8); 'uint8' "
113
+ "is used for FP8-quantized KV. 'layers' is the number "
114
+ "of consecutive layers sharing this group's geometry. "
115
+ "Multi-group example (MLA + classical attention): "
116
+ "'(1,1024,16,1,128):float16:4;"
117
+ "(2,1024,16,8,128):float16:28'. "
118
+ "All groups must share the same NB and BS. "
119
+ "See lmcache.v1.kv_layer_groups.parse_kvcache_shape_spec "
120
+ "for the authoritative parser. Default: '%s'" % _DEFAULT_SHAPE_SPEC
121
+ ),
122
+ )
123
+ kv.add_argument(
124
+ "--num-blocks",
125
+ type=int,
126
+ default=1024,
127
+ help="Paged blocks (default: 1024)",
128
+ )
129
+ kv.add_argument(
130
+ "--block-size",
131
+ type=int,
132
+ default=16,
133
+ help="Tokens per block (default: 16)",
134
+ )
135
+
136
+ parser.add_argument(
137
+ "--start",
138
+ type=int,
139
+ default=0,
140
+ help="Starting sequence number (default: 0)",
141
+ )
142
+ parser.add_argument(
143
+ "--end",
144
+ type=int,
145
+ default=None,
146
+ help=("Ending sequence number (exclusive). If not set, runs forever."),
147
+ )
148
+ parser.add_argument(
149
+ "--interval",
150
+ type=float,
151
+ default=0.5,
152
+ help=("Seconds between requests (default: 0.5)"),
153
+ )
154
+ parser.add_argument(
155
+ "--concurrency",
156
+ type=int,
157
+ default=1,
158
+ help=(
159
+ "Number of clients sending requests at the same time "
160
+ "(default: 1). Each client opens its own connection, allocates "
161
+ "its own KV tensors, and registers its Workers under distinct "
162
+ "instance IDs; sequences are dealt round-robin across clients."
163
+ ),
164
+ )
165
+ parser.add_argument(
166
+ "--url",
167
+ default="http://localhost:8080",
168
+ help=("Legacy HTTP URL; checksums are computed from local worker buffers"),
169
+ )
170
+
171
+ prof = parser.add_argument_group(
172
+ "server profiling",
173
+ "Flame-graph the MP server process while this benchmark drives "
174
+ "load into it. The server's store path (hashing, allocation, "
175
+ "gather, D2H) runs in its own process, not in this client, so "
176
+ "profiling attaches to --profile-server-pid rather than to the "
177
+ "benchmark. See 'lmcache tool flamegraph' for the standalone form.",
178
+ )
179
+ prof.add_argument(
180
+ "--flamegraph",
181
+ choices=["on", "off"],
182
+ default="off",
183
+ help="Record a flame graph of the server during the run (default: off).",
184
+ )
185
+ prof.add_argument(
186
+ "--profile-server-pid",
187
+ type=int,
188
+ default=0,
189
+ metavar="PID",
190
+ help=(
191
+ "Server process to profile, e.g. $(pgrep -f 'lmcache server'). "
192
+ "Required when --flamegraph on."
193
+ ),
194
+ )
195
+ prof.add_argument(
196
+ "--flamegraph-mode",
197
+ default="gil",
198
+ metavar="MODE[,MODE...]",
199
+ help=(
200
+ "What to sample in the server (default: gil). Pass several "
201
+ "comma-separated to profile one load run per mode. Modes: on-cpu, "
202
+ "off-cpu, wakeup, offwake (perf/bcc), wall, gil (py-spy). perf/bcc "
203
+ "name Python functions only when the server was launched with "
204
+ "PYTHONPERFSUPPORT=1. See the 'lmcache tool flamegraph' docs."
205
+ ),
206
+ )
207
+ prof.add_argument(
208
+ "--flamegraph-output",
209
+ default="",
210
+ metavar="PATH",
211
+ help=(
212
+ "SVG output path. Default: "
213
+ "/tmp/lmcache_bench_flames/server-pid<PID>.<mode>.svg."
214
+ ),
215
+ )
216
+ prof.add_argument(
217
+ "--flamegraph-scripts-dir",
218
+ default="",
219
+ metavar="DIR",
220
+ help=(
221
+ "Directory with the FlameGraph scripts (flamegraph.pl, "
222
+ "stackcollapse-perf.pl); default ~/FlameGraph (cloned there on "
223
+ "first use). Unused by --flamegraph-mode wall / gil, which "
224
+ "render their own SVG."
225
+ ),
226
+ )
@@ -12,6 +12,14 @@ import torch
12
12
  from lmcache import torch_dev, torch_device_type
13
13
  from lmcache.v1.kv_layer_groups import KVLayerGroupInfo
14
14
  from lmcache.v1.multiprocess.group_view import EngineGroupInfo
15
+ from lmcache.v1.platform import synchronize_device
16
+
17
+
18
+ def _block_bytes(tensor: torch.Tensor, dim: int, block_ids: Sequence[int]) -> bytes:
19
+ """Gather block bytes in logical order."""
20
+ indices = torch.tensor(block_ids, dtype=torch.long, device=tensor.device)
21
+ selected = tensor.index_select(dim, indices)
22
+ return selected.contiguous().view(torch.uint8).cpu().numpy().tobytes()
15
23
 
16
24
 
17
25
  class KVCacheBuffers:
@@ -61,43 +69,51 @@ class KVCacheBuffers:
61
69
  tensor = torch.randint(
62
70
  bounds.min, bounds.max + 1, shape, dtype=group.dtype, device=dev
63
71
  )
64
- name = f"layer.{len(self.kv_caches)}"
65
- self.kv_caches[name] = tensor
66
- self._block_dims[name] = block_dim
72
+ tensor_name = f"tensor.{len(self.kv_caches)}"
73
+ self.kv_caches[tensor_name] = tensor
74
+ self._block_dims[tensor_name] = block_dim
67
75
 
68
- def block_views(self, block_offset: int, num_blocks: int) -> Iterator[torch.Tensor]:
69
- """Yield writable block-range views in layer order."""
70
- for name, tensor in self.kv_caches.items():
71
- yield tensor.narrow(self._block_dims[name], block_offset, num_blocks)
76
+ def block_views(
77
+ self, block_ids_per_group: Sequence[Sequence[int]]
78
+ ) -> Iterator[torch.Tensor]:
79
+ """Yield validated block views in group/tensor/logical-block order."""
80
+ for _, tensor, dim, block_ids in self._iter_group_tensors(block_ids_per_group):
81
+ for block_id in block_ids:
82
+ yield tensor.narrow(dim, block_id, 1)
72
83
 
73
- def zero_blocks(self, block_offset: int, num_blocks: int) -> None:
74
- """Zero the selected block range in every layer."""
75
- for view in self.block_views(block_offset, num_blocks):
84
+ def set_blocks_to_zero(self, block_ids_per_group: Sequence[Sequence[int]]) -> None:
85
+ """Set bound blocks to zero."""
86
+ for view in self.block_views(block_ids_per_group):
76
87
  view.zero_()
77
88
 
78
- def checksums(
79
- self, block_offset: int, num_blocks: int, chunk_size: int
80
- ) -> list[str]:
81
- """Hash selected CPU blocks per chunk across layers.
89
+ def checksum(self, block_ids_per_group: Sequence[Sequence[int]]) -> str:
90
+ """Hash selected blocks across tensors in logical order into one MD5 digest."""
91
+ digest = hashlib.md5()
92
+ for _, tensor, dim, block_ids in self._iter_group_tensors(block_ids_per_group):
93
+ digest.update(_block_bytes(tensor, dim, block_ids))
94
+ return digest.hexdigest()
82
95
 
83
- ``chunk_size`` counts tokens; incomplete trailing chunks are ignored.
84
- """
85
- if chunk_size % self.block_size != 0:
86
- raise ValueError(
87
- "chunk_size %d must be a multiple of block_size %d"
88
- % (chunk_size, self.block_size)
89
- )
90
- blocks_per_chunk = chunk_size // self.block_size
91
- checksums: list[str] = []
92
- for chunk in range(num_blocks // blocks_per_chunk):
93
- start = block_offset + chunk * blocks_per_chunk
94
- digest = hashlib.md5()
95
- for view in self.block_views(start, blocks_per_chunk):
96
- digest.update(view.contiguous().view(torch.uint8).numpy().tobytes())
97
- checksums.append(digest.hexdigest())
98
- return checksums
96
+ def synchronize(self) -> None:
97
+ """Wait for buffer preparation outside transfer timing."""
98
+ synchronize_device(next(iter(self.kv_caches.values())).device)
99
99
 
100
100
  def release(self) -> None:
101
101
  """Drop owned references without synchronizing or unregistering."""
102
102
  self.kv_caches.clear()
103
103
  self._block_dims.clear()
104
+
105
+ def _iter_group_tensors(
106
+ self, block_ids_per_group: Sequence[Sequence[int]]
107
+ ) -> Iterator[tuple[int, torch.Tensor, int, Sequence[int]]]:
108
+ """Yield registered tensor indices, tensors, block axes, and bound block IDs."""
109
+ for group, block_ids in zip(
110
+ self.engine_group_infos, block_ids_per_group, strict=True
111
+ ):
112
+ for tensor_index in group.layer_indices:
113
+ tensor_name = f"tensor.{tensor_index}"
114
+ yield (
115
+ tensor_index,
116
+ self.kv_caches[tensor_name],
117
+ self._block_dims[tensor_name],
118
+ block_ids,
119
+ )
@@ -49,6 +49,7 @@ class BaselineBenchCase:
49
49
 
50
50
  sequence_count: int | None
51
51
  interval_seconds: float
52
+ num_tokens: int = field(default=512, kw_only=True)
52
53
  sequence_id_offset: int = 0
53
54
  sequence_id_stride: int = 1
54
55
  stop: threading.Event | None = field(default=None, compare=False)
@@ -206,65 +207,71 @@ class BaselineBenchCase:
206
207
  )
207
208
  )
208
209
 
209
- @staticmethod
210
210
  def _execute_request(
211
+ self,
211
212
  client: ServerBenchClient,
212
213
  sequence_id: int,
213
214
  request_kind: Literal["cold", "warm"],
214
215
  ) -> _RequestOutcome | None:
216
+ # First Party
217
+ from lmcache.cli.commands.bench.server_bench.helpers import _build_token_ids
218
+
215
219
  request = client.create_request(
216
220
  sequence_id,
217
221
  request_id="req-%d-%s" % (sequence_id, request_kind),
218
222
  request_kind=request_kind,
223
+ token_ids=_build_token_ids(sequence_id, self.num_tokens),
219
224
  )
220
225
  if request is None:
221
226
  return None
222
227
 
223
- lookup = client.lookup(request)
224
- if not lookup.succeeded:
225
- return _RequestOutcome(lookup=lookup)
226
-
227
- hit_tokens = lookup.hit_chunks * request.chunk_size
228
- miss_tokens = request.num_full_tokens - hit_tokens
228
+ try:
229
+ lookup = client.lookup(request)
230
+ if not lookup.succeeded:
231
+ return _RequestOutcome(lookup=lookup)
232
+
233
+ hit_tokens = lookup.hit_chunks * request.chunk_size
234
+ miss_tokens = request.num_full_tokens - hit_tokens
235
+
236
+ checksums: dict[int, list[str]] | None = None
237
+ if request_kind == "cold" and miss_tokens > 0:
238
+ checksums = client.compute_checksums(
239
+ request,
240
+ start_token=hit_tokens,
241
+ token_count=miss_tokens,
242
+ operation="store",
243
+ )
244
+ if request_kind == "warm" and hit_tokens > 0:
245
+ client.zero_destination(
246
+ request,
247
+ start_token=0,
248
+ token_count=hit_tokens,
249
+ )
229
250
 
230
- checksums: dict[int, list[str]] | None = None
231
- if request_kind == "cold" and miss_tokens > 0:
232
- checksums = client.compute_checksums(
233
- request,
234
- start_token=hit_tokens,
235
- token_count=miss_tokens,
236
- operation="store",
237
- )
238
- if request_kind == "warm" and hit_tokens > 0:
239
- client.zero_destination(
251
+ retrieve = client.retrieve(
240
252
  request,
241
253
  start_token=0,
242
254
  token_count=hit_tokens,
243
255
  )
244
-
245
- retrieve = client.retrieve(
246
- request,
247
- start_token=0,
248
- token_count=hit_tokens,
249
- )
250
- store = client.store(
251
- request,
252
- start_token=hit_tokens,
253
- token_count=miss_tokens,
254
- )
255
-
256
- if request_kind == "warm" and hit_tokens > 0:
257
- checksums = client.compute_checksums(
256
+ store = client.store(
258
257
  request,
259
- start_token=0,
260
- token_count=hit_tokens,
261
- operation="retrieve",
258
+ start_token=hit_tokens,
259
+ token_count=miss_tokens,
262
260
  )
263
261
 
264
- client.end_session(request)
265
- return _RequestOutcome(
266
- lookup=lookup,
267
- checksums=checksums,
268
- retrieve=retrieve,
269
- store=store,
270
- )
262
+ if request_kind == "warm" and hit_tokens > 0:
263
+ checksums = client.compute_checksums(
264
+ request,
265
+ start_token=0,
266
+ token_count=hit_tokens,
267
+ operation="retrieve",
268
+ )
269
+
270
+ return _RequestOutcome(
271
+ lookup=lookup,
272
+ checksums=checksums,
273
+ retrieve=retrieve,
274
+ store=store,
275
+ )
276
+ finally:
277
+ client.release_request(request)