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.
- lmcache/_version.py +3 -3
- lmcache/cli/commands/bench/server_bench/__init__.py +21 -5
- lmcache/cli/commands/bench/server_bench/allocator.py +78 -0
- lmcache/cli/commands/bench/server_bench/arguments.py +226 -0
- lmcache/cli/commands/bench/server_bench/buffers.py +46 -30
- lmcache/cli/commands/bench/server_bench/cases/baseline.py +48 -41
- lmcache/cli/commands/bench/server_bench/client.py +143 -144
- lmcache/cli/commands/bench/server_bench/command.py +14 -240
- lmcache/cli/commands/bench/server_bench/config.py +8 -1
- lmcache/cli/commands/bench/server_bench/helpers.py +6 -68
- lmcache/cli/commands/server.py +1 -1
- lmcache/inprocess/v1/gpu_connector/musa_connectors.py +2 -1
- lmcache/integration/atom/multi_process_adapter.py +5 -4
- lmcache/integration/sglang/unified_lmcache_mp_connector.py +3 -2
- lmcache/integration/tensorrt_llm/tensorrt_mp_adapter.py +4 -4
- lmcache/integration/vllm/lazy_offload_manager.py +14 -11
- lmcache/integration/vllm/lmcache_mp_connector.py +4 -4
- lmcache/integration/vllm/lmcache_mp_metadata.py +59 -6
- lmcache/integration/vllm/mp_lookup_state.py +2 -1
- lmcache/integration/vllm/utils.py +4 -2
- lmcache/integration/vllm/vllm_multi_process_adapter.py +35 -25
- lmcache/sdk/context.py +2 -2
- lmcache/sdk/qringbuffer.py +3 -3
- lmcache/security/__init__.py +2 -0
- lmcache/security/api_key.py +207 -0
- lmcache/security/http.py +99 -0
- lmcache/v1/distributed/l2_adapters/bigtable_l2_adapter.py +6 -6
- lmcache/v1/distributed/l2_adapters/dax_l2_adapter.py +5 -5
- lmcache/v1/distributed/l2_adapters/hfbucket_client.py +193 -0
- lmcache/v1/distributed/l2_adapters/hfbucket_l2_adapter.py +2 -2
- lmcache/v1/{storage_backend → distributed/l2_adapters}/raw_block/__init__.py +2 -2
- lmcache/v1/{storage_backend → distributed/l2_adapters}/raw_block/core.py +2 -2
- lmcache/v1/distributed/l2_adapters/raw_block_l2_adapter.py +3 -3
- lmcache/v1/{storage_backend → distributed/l2_adapters}/valkey/__init__.py +1 -1
- lmcache/v1/distributed/l2_adapters/valkey_l2_adapter.py +1 -1
- lmcache/v1/gpu_connector/kv_format/specs/base.py +68 -2
- lmcache/v1/kv_layer_groups.py +23 -0
- lmcache/v1/mp_observability/README.md +1 -1
- lmcache/v1/multiprocess/cache_control/key_resolver.py +2 -2
- lmcache/v1/multiprocess/config.py +15 -15
- lmcache/v1/multiprocess/custom_types.py +99 -9
- lmcache/v1/multiprocess/engine_context.py +1 -1
- lmcache/v1/multiprocess/http_apis/common/env_api.py +1 -0
- lmcache/v1/multiprocess/http_auth.py +35 -123
- lmcache/v1/multiprocess/http_server.py +1 -1
- lmcache/v1/multiprocess/modules/blend/lookup.py +15 -10
- lmcache/v1/multiprocess/modules/blend/module.py +3 -2
- lmcache/v1/multiprocess/modules/blend/store.py +6 -3
- lmcache/v1/multiprocess/modules/engine_driven_transfer.py +145 -6
- lmcache/v1/multiprocess/modules/lmcache_driven_transfer.py +2 -1
- lmcache/v1/multiprocess/modules/lookup.py +10 -9
- lmcache/v1/multiprocess/modules/p2p_controller.py +9 -2
- lmcache/v1/multiprocess/modules/server_transfer.py +282 -100
- lmcache/v1/multiprocess/session.py +23 -15
- lmcache/v1/multiprocess/token_codec.py +90 -0
- lmcache/v1/multiprocess/token_hasher.py +96 -9
- lmcache/v1/multiprocess/transfer_context/async_engine_driven.py +125 -0
- lmcache/v1/multiprocess/transfer_context/base.py +96 -17
- lmcache/v1/multiprocess/transfer_context/inline.py +53 -2
- lmcache/v1/multiprocess/transfer_context/shm.py +68 -0
- lmcache/v1/multiprocess/transfer_context/tensor_wire.py +164 -5
- lmcache/v1/multiprocess/transfer_context/worker_transfer.py +583 -24
- lmcache/v1/multiprocess/transport/auth.py +50 -0
- lmcache/v1/multiprocess/transport/base.py +4 -1
- lmcache/v1/multiprocess/transport/grpc_impl/_proto_gen/common_pb2.pyi +4 -4
- lmcache/v1/multiprocess/transport/grpc_impl/native_server.py +417 -0
- lmcache/v1/multiprocess/transport/grpc_impl/protos/common.proto +2 -1
- lmcache/v1/multiprocess/transport/grpc_impl/protos/engine_driven_service.proto +22 -0
- lmcache/v1/multiprocess/transport/grpc_impl/server_base.py +4 -2
- lmcache/v1/multiprocess/transport/grpc_impl/server_builder.py +7 -2
- lmcache/v1/platform/devices/mlu/__init__.py +76 -0
- lmcache/v1/platform/devices/mlu/device_ops.py +277 -0
- lmcache/v1/platform/devices/musa/cache_context.py +9 -4
- lmcache/v1/platform/devices/musa/device_ops.py +35 -33
- lmcache/v1/platform/devices/musa/format_capabilities.py +93 -0
- lmcache/v1/platform/devices/musa/native_kv_transfer.py +9 -25
- lmcache/v1/platform/native_block_transfer.py +6 -4
- lmcache/v1/storage_backend/connector/bigtable_connector.py +4 -2
- lmcache/v1/storage_backend/connector/hfbucket_connector.py +9 -199
- lmcache/v1/storage_backend/connector/valkey_connector.py +6 -6
- lmcache/v1/storage_backend/plugins/dax_backend.py +1 -1
- lmcache/v1/storage_backend/plugins/rust_raw_block_backend.py +5 -5
- {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc3.dev0.dist-info}/METADATA +1 -1
- {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc3.dev0.dist-info}/RECORD +94 -82
- /lmcache/v1/{storage_backend/connector → distributed/l2_adapters}/bigtable_config.py +0 -0
- /lmcache/v1/{storage_backend/connector → distributed/l2_adapters}/bigtable_sharder.py +0 -0
- /lmcache/v1/{storage_backend → distributed/l2_adapters}/dax/__init__.py +0 -0
- /lmcache/v1/{storage_backend → distributed/l2_adapters}/dax/core.py +0 -0
- /lmcache/v1/{storage_backend → distributed/l2_adapters}/raw_block/key_codec.py +0 -0
- /lmcache/v1/{storage_backend → distributed/l2_adapters}/valkey/worker_pool.py +0 -0
- {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc3.dev0.dist-info}/WHEEL +0 -0
- {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc3.dev0.dist-info}/entry_points.txt +0 -0
- {lmcache_cli-0.5.7rc2.dev0.dist-info → lmcache_cli-0.5.7rc3.dev0.dist-info}/licenses/LICENSE +0 -0
- {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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 5, 7, '
|
|
21
|
+
__version__ = version = '0.5.7rc3.dev0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 5, 7, 'rc3', 'dev0')
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
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.
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
65
|
-
self.kv_caches[
|
|
66
|
-
self._block_dims[
|
|
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(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
74
|
-
"""
|
|
75
|
-
for view in self.block_views(
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
"""
|
|
85
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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=
|
|
260
|
-
token_count=
|
|
261
|
-
operation="retrieve",
|
|
258
|
+
start_token=hit_tokens,
|
|
259
|
+
token_count=miss_tokens,
|
|
262
260
|
)
|
|
263
261
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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)
|