lmcache-cli 0.5.1.dev0__py3-none-any.whl → 0.5.1rc3.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/__init__.py +12 -0
- lmcache/_version.py +3 -3
- lmcache/banner.py +6 -2
- lmcache/cli/commands/bench/engine_bench/interactive/__init__.py +175 -68
- lmcache/cli/commands/bench/engine_bench/interactive/schema.py +6 -2
- lmcache/cli/commands/bench/engine_bench/interactive/state.py +4 -0
- lmcache/cli/commands/bench/engine_bench/interactive/terminal.py +71 -13
- lmcache/cli/commands/bench/server_bench/command.py +2 -2
- lmcache/cli/commands/bench/server_bench/helpers.py +16 -24
- lmcache/cli/commands/coordinator.py +17 -1
- lmcache/cli/commands/describe.py +154 -5
- lmcache/cli/commands/kvcache.py +2 -2
- lmcache/cli/commands/query/_request.py +19 -8
- lmcache/cli/commands/query/engine_command.py +2 -1
- lmcache/integration/sglang/multi_process_adapter.py +6 -2
- lmcache/integration/tensorrt_llm/tensorrt_mp_adapter.py +1 -1
- lmcache/integration/vllm/kv_cache_groups.py +16 -14
- lmcache/integration/vllm/lmcache_mp_connector.py +26 -1
- lmcache/integration/vllm/vllm_multi_process_adapter.py +4 -5
- lmcache/integration/vllm/vllm_v1_adapter.py +66 -14
- lmcache/python_ops_fallback.py +145 -11
- lmcache/sdk/__init__.py +10 -0
- lmcache/sdk/kvcache.py +539 -0
- lmcache/sdk/wrapper/__init__.py +9 -0
- lmcache/sdk/wrapper/contiguous.py +75 -0
- lmcache/v1/cache_controller/full_sync_sender.py +2 -2
- lmcache/v1/cache_engine.py +56 -39
- lmcache/v1/config.py +80 -0
- lmcache/v1/distributed/api.py +65 -2
- lmcache/v1/distributed/config.py +14 -12
- lmcache/v1/distributed/l1_manager.py +8 -3
- lmcache/v1/distributed/l2_adapters/base.py +3 -3
- lmcache/v1/distributed/l2_adapters/config.py +3 -1
- lmcache/v1/distributed/l2_adapters/fs_l2_adapter.py +2 -1
- lmcache/v1/distributed/l2_adapters/mooncake_store_l2_adapter.py +4 -3
- lmcache/v1/distributed/l2_adapters/s3_l2_adapter.py +4 -7
- lmcache/v1/distributed/memory_manager/__init__.py +5 -0
- lmcache/v1/distributed/memory_manager/devdax_l1_memory_manager.py +76 -0
- lmcache/v1/distributed/memory_manager/l1_memory_manager.py +1 -23
- lmcache/v1/distributed/serde/__init__.py +8 -0
- lmcache/v1/distributed/serde/turboquant/__init__.py +15 -0
- lmcache/v1/distributed/serde/turboquant/decode_kernel.py +173 -0
- lmcache/v1/distributed/serde/turboquant/store_kernel.py +445 -0
- lmcache/v1/distributed/serde/turboquant/turboquant.py +896 -0
- lmcache/v1/distributed/storage_controllers/prefetch_controller.py +54 -12
- lmcache/v1/distributed/storage_manager.py +57 -3
- lmcache/v1/distributed/tiers.py +23 -0
- lmcache/v1/distributed/transfer_channel/impl/nixl_impl.py +2 -1
- lmcache/v1/gpu_connector/_cufile_async.py +26 -9
- lmcache/v1/gpu_connector/_gds_async.py +46 -0
- lmcache/v1/gpu_connector/_hipfile_async.py +532 -0
- lmcache/v1/gpu_connector/gds_context.py +39 -37
- lmcache/v1/gpu_connector/gpu_connectors.py +6 -4
- lmcache/v1/gpu_connector/gpu_ops.py +54 -0
- lmcache/v1/gpu_connector/kv_format/contiguity.py +0 -3
- lmcache/v1/gpu_connector/kv_format/specs/base.py +12 -10
- lmcache/v1/gpu_connector/kv_format/specs/nb_nl_two_bs_nh_hs.py +0 -1
- lmcache/v1/gpu_connector/kv_format/specs/nb_nl_two_nh_bs_hs.py +0 -2
- lmcache/v1/gpu_connector/kv_format/specs/nl_x_nb_bs_hs.py +0 -1
- lmcache/v1/gpu_connector/kv_format/specs/nl_x_nb_nh_bs_two_hs.py +0 -1
- lmcache/v1/gpu_connector/kv_format/specs/nl_x_nb_two_nh_bs_hs.py +0 -1
- lmcache/v1/gpu_connector/kv_format/specs/nl_x_nbbs_one_hs.py +0 -1
- lmcache/v1/gpu_connector/kv_format/specs/nl_x_two_nb_nh_bs_hs.py +0 -1
- lmcache/v1/gpu_connector/utils.py +74 -26
- lmcache/v1/hidden_state_store.py +364 -0
- lmcache/v1/kv_layer_groups.py +141 -105
- lmcache/v1/lazy_memory_allocator.py +13 -11
- lmcache/v1/memory_management.py +61 -53
- lmcache/v1/mp_coordinator/__main__.py +7 -1
- lmcache/v1/mp_coordinator/app.py +26 -9
- lmcache/v1/mp_coordinator/{l2 → cache_control}/event_listener.py +1 -1
- lmcache/v1/mp_coordinator/{l2 → cache_control}/eviction_manager.py +6 -6
- lmcache/v1/mp_coordinator/cache_control/prefetch_manager.py +98 -0
- lmcache/v1/mp_coordinator/{l2 → cache_control}/resync_manager.py +5 -5
- lmcache/v1/mp_coordinator/config.py +11 -2
- lmcache/v1/mp_coordinator/http_apis/cache_api.py +128 -0
- lmcache/v1/mp_coordinator/http_apis/dependencies.py +83 -0
- lmcache/v1/mp_coordinator/http_apis/instances_api.py +8 -26
- lmcache/v1/mp_coordinator/http_apis/{l2_api.py → quota_api.py} +92 -90
- lmcache/v1/mp_coordinator/schemas.py +60 -11
- lmcache/v1/mp_observability/config.py +6 -0
- lmcache/v1/mp_observability/errors.py +66 -0
- lmcache/v1/mp_observability/event.py +5 -0
- lmcache/v1/mp_observability/subscribers/logging/__init__.py +4 -0
- lmcache/v1/mp_observability/subscribers/logging/timeout.py +28 -0
- lmcache/v1/mp_observability/subscribers/metrics/__init__.py +4 -0
- lmcache/v1/mp_observability/subscribers/metrics/timeout.py +31 -0
- lmcache/v1/mp_observability/subscribers/tracing/__init__.py +4 -0
- lmcache/v1/mp_observability/subscribers/tracing/timeout.py +70 -0
- lmcache/v1/mp_observability/trace/codecs.py +26 -0
- lmcache/v1/multiprocess/affinity_pool.py +59 -7
- lmcache/v1/multiprocess/cache_control/__init__.py +1 -0
- lmcache/v1/multiprocess/cache_control/errors.py +22 -0
- lmcache/v1/multiprocess/cache_control/key_resolver.py +91 -0
- lmcache/v1/multiprocess/cache_control/object_service.py +180 -0
- lmcache/v1/multiprocess/cache_control/prefetch_service.py +108 -0
- lmcache/v1/multiprocess/config.py +13 -0
- lmcache/v1/multiprocess/custom_types.py +3 -227
- lmcache/v1/multiprocess/engine_context.py +46 -1
- lmcache/v1/multiprocess/futures.py +3 -2
- lmcache/v1/multiprocess/group_view.py +25 -0
- lmcache/v1/multiprocess/http_apis/cache_api.py +254 -166
- lmcache/v1/multiprocess/http_apis/{conf_api.py → config_api.py} +30 -2
- lmcache/v1/multiprocess/http_apis/dependencies.py +78 -0
- lmcache/v1/multiprocess/http_apis/error_handlers.py +55 -0
- lmcache/v1/multiprocess/http_apis/info_api.py +76 -0
- lmcache/v1/multiprocess/http_apis/reconfigure_api.py +0 -37
- lmcache/v1/multiprocess/http_apis/schemas.py +73 -0
- lmcache/v1/multiprocess/http_server.py +10 -1
- lmcache/v1/multiprocess/modules/blend_v3.py +1 -1
- lmcache/v1/multiprocess/modules/lmcache_driven_transfer.py +203 -5
- lmcache/v1/multiprocess/modules/lookup.py +106 -6
- lmcache/v1/multiprocess/server.py +3 -2
- lmcache/v1/multiprocess/session.py +45 -2
- lmcache/v1/multiprocess/transfer_context/shm.py +63 -6
- lmcache/v1/multiprocess/transfer_context/worker_transfer.py +19 -0
- lmcache/v1/multiprocess/warm_prefetch.py +140 -0
- lmcache/v1/platform/__init__.py +10 -14
- lmcache/v1/platform/_registry.py +148 -16
- lmcache/v1/platform/base_cache_context.py +45 -39
- lmcache/v1/platform/base_ipc_wrapper.py +160 -0
- lmcache/v1/platform/base_pin_memory.py +48 -0
- lmcache/v1/platform/cache_context.py +5 -0
- lmcache/v1/platform/cpu/__init__.py +4 -31
- lmcache/v1/platform/cpu/cache_context.py +13 -23
- lmcache/v1/platform/cpu/shm.py +28 -1
- lmcache/v1/platform/cuda/__init__.py +11 -27
- lmcache/v1/platform/cuda/cache_context.py +14 -22
- lmcache/v1/platform/cuda/ipc_wrapper.py +189 -0
- lmcache/v1/platform/cuda/pin_memory.py +170 -0
- lmcache/v1/platform/device_ext.py +91 -0
- lmcache/v1/platform/musa/__init__.py +22 -1
- lmcache/v1/platform/musa/ipc.py +318 -0
- lmcache/v1/storage_backend/connector/bigtable_schema.py +5 -4
- lmcache/v1/storage_backend/connector/s3_connector.py +8 -8
- lmcache/v1/storage_backend/connector/valkey_adapter.py +64 -2
- lmcache/v1/storage_backend/connector/valkey_connector.py +72 -4
- lmcache/v1/storage_backend/gds_backend.py +8 -3
- lmcache/v1/storage_backend/local_cpu_backend.py +61 -42
- lmcache/v1/storage_backend/local_disk_backend.py +30 -28
- lmcache/v1/storage_backend/nixl_storage_backend.py +43 -10
- lmcache/v1/storage_backend/plugins/rust_raw_block_backend.py +31 -16
- lmcache/v1/storage_backend/remote_backend.py +21 -19
- lmcache/v1/storage_backend/storage_manager.py +18 -14
- lmcache/v1/system_detection.py +3 -3
- lmcache/v1/token_database.py +5 -4
- {lmcache_cli-0.5.1.dev0.dist-info → lmcache_cli-0.5.1rc3.dev0.dist-info}/METADATA +3 -2
- {lmcache_cli-0.5.1.dev0.dist-info → lmcache_cli-0.5.1rc3.dev0.dist-info}/RECORD +156 -125
- {lmcache_cli-0.5.1.dev0.dist-info → lmcache_cli-0.5.1rc3.dev0.dist-info}/scm_file_list.json +93 -22
- lmcache_cli-0.5.1rc3.dev0.dist-info/scm_version.json +8 -0
- lmcache/v1/multiprocess/http_apis/healthcheck_api.py +0 -31
- lmcache/v1/multiprocess/http_apis/l2_api.py +0 -241
- lmcache/v1/multiprocess/http_apis/root_api.py +0 -15
- lmcache/v1/multiprocess/http_apis/status_api.py +0 -25
- lmcache/v1/multiprocess/http_apis/version_api.py +0 -8
- lmcache_cli-0.5.1.dev0.dist-info/scm_version.json +0 -8
- /lmcache/v1/mp_coordinator/{l2 → cache_control}/__init__.py +0 -0
- /lmcache/v1/mp_coordinator/{l2 → cache_control}/usage_manager.py +0 -0
- {lmcache_cli-0.5.1.dev0.dist-info → lmcache_cli-0.5.1rc3.dev0.dist-info}/WHEEL +0 -0
- {lmcache_cli-0.5.1.dev0.dist-info → lmcache_cli-0.5.1rc3.dev0.dist-info}/entry_points.txt +0 -0
- {lmcache_cli-0.5.1.dev0.dist-info → lmcache_cli-0.5.1rc3.dev0.dist-info}/licenses/LICENSE +0 -0
- {lmcache_cli-0.5.1.dev0.dist-info → lmcache_cli-0.5.1rc3.dev0.dist-info}/top_level.txt +0 -0
lmcache/__init__.py
CHANGED
|
@@ -8,6 +8,7 @@ import types
|
|
|
8
8
|
|
|
9
9
|
# First Party
|
|
10
10
|
from lmcache.logging import init_logger
|
|
11
|
+
from lmcache.v1.platform.device_ext import DeviceExt
|
|
11
12
|
|
|
12
13
|
try:
|
|
13
14
|
# First Party
|
|
@@ -62,6 +63,17 @@ torch_dev, torch_device_type = _detect_device()
|
|
|
62
63
|
logger.info(" torch_dev=%s, torch_device_type=%s", torch_dev, torch_device_type)
|
|
63
64
|
|
|
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
|
+
|
|
65
77
|
# --------------------------
|
|
66
78
|
# Dynamic backend selection
|
|
67
79
|
# --------------------------
|
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, 1, 'dev0')
|
|
21
|
+
__version__ = version = '0.5.1rc3.dev0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 5, 1, 'rc3', 'dev0')
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g979719d7a'
|
lmcache/banner.py
CHANGED
|
@@ -8,9 +8,13 @@ only, so tensor-parallel deployments print a single banner). Setting the
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
# Standard
|
|
11
|
-
from typing import
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
12
|
import os
|
|
13
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
# Standard
|
|
16
|
+
from typing import TextIO
|
|
17
|
+
|
|
14
18
|
try:
|
|
15
19
|
# First Party
|
|
16
20
|
from lmcache import _version
|
|
@@ -97,7 +101,7 @@ def _render_banner(colored: bool) -> str:
|
|
|
97
101
|
return "\n".join(lines)
|
|
98
102
|
|
|
99
103
|
|
|
100
|
-
def print_banner_once(stream: TextIO) -> None:
|
|
104
|
+
def print_banner_once(stream: "TextIO") -> None:
|
|
101
105
|
"""Print the LMCache startup banner to ``stream`` at most once.
|
|
102
106
|
|
|
103
107
|
The banner shows the LMCache logo, version, and website, followed by
|
|
@@ -7,6 +7,7 @@ returns a complete ``argparse.Namespace`` ready for the orchestrator.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
# Standard
|
|
10
|
+
from typing import cast
|
|
10
11
|
import argparse
|
|
11
12
|
import sys
|
|
12
13
|
|
|
@@ -20,6 +21,7 @@ from lmcache.cli.commands.bench.engine_bench.interactive.state import (
|
|
|
20
21
|
from lmcache.cli.commands.bench.engine_bench.interactive.terminal import (
|
|
21
22
|
BOLD,
|
|
22
23
|
CYAN,
|
|
24
|
+
GO_BACK,
|
|
23
25
|
RESET,
|
|
24
26
|
YELLOW,
|
|
25
27
|
prompt_bool,
|
|
@@ -31,18 +33,43 @@ from lmcache.cli.commands.bench.engine_bench.interactive.terminal import (
|
|
|
31
33
|
__all__ = ["run_interactive"]
|
|
32
34
|
|
|
33
35
|
|
|
36
|
+
# Config items whose text input is a URL and accepts port/host shorthand.
|
|
37
|
+
_URL_KEYS = {"engine_url", "lmcache_url"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _normalize_url(value: str) -> str:
|
|
41
|
+
"""Expand shorthand URL input into a fully-qualified URL.
|
|
42
|
+
|
|
43
|
+
A bare port (``8000``) becomes ``http://localhost:8000``; a bare host or
|
|
44
|
+
``host:port`` (``localhost:8000``) gains an ``http://`` scheme. Values
|
|
45
|
+
that already carry a scheme (``://``) and the empty string are returned
|
|
46
|
+
unchanged.
|
|
47
|
+
"""
|
|
48
|
+
value = value.strip()
|
|
49
|
+
if not value or "://" in value:
|
|
50
|
+
return value
|
|
51
|
+
if value.isdigit():
|
|
52
|
+
return f"http://localhost:{value}"
|
|
53
|
+
return f"http://{value}"
|
|
54
|
+
|
|
55
|
+
|
|
34
56
|
# ---------------------------------------------------------------------------
|
|
35
57
|
# Prompt dispatcher
|
|
36
58
|
# ---------------------------------------------------------------------------
|
|
37
59
|
|
|
38
60
|
|
|
39
|
-
def _prompt_for_item(item: ConfigItem) -> object:
|
|
40
|
-
"""Prompt the user for a single config item based on its type.
|
|
61
|
+
def _prompt_for_item(item: ConfigItem, allow_back: bool) -> object:
|
|
62
|
+
"""Prompt the user for a single config item based on its type.
|
|
63
|
+
|
|
64
|
+
Returns the entered value, or :data:`GO_BACK` when ``allow_back`` is True
|
|
65
|
+
and the user asks to step back to the previous question.
|
|
66
|
+
"""
|
|
41
67
|
if item.input_type == "text":
|
|
42
68
|
return prompt_text(
|
|
43
69
|
item.display_name,
|
|
44
70
|
item.description,
|
|
45
71
|
default=item.default if item.default is not None else "",
|
|
72
|
+
allow_back=allow_back,
|
|
46
73
|
)
|
|
47
74
|
if item.input_type == "int":
|
|
48
75
|
return prompt_number(
|
|
@@ -50,6 +77,7 @@ def _prompt_for_item(item: ConfigItem) -> object:
|
|
|
50
77
|
item.description,
|
|
51
78
|
default=item.default,
|
|
52
79
|
number_type=int,
|
|
80
|
+
allow_back=allow_back,
|
|
53
81
|
)
|
|
54
82
|
if item.input_type == "float":
|
|
55
83
|
return prompt_number(
|
|
@@ -57,12 +85,14 @@ def _prompt_for_item(item: ConfigItem) -> object:
|
|
|
57
85
|
item.description,
|
|
58
86
|
default=item.default,
|
|
59
87
|
number_type=float,
|
|
88
|
+
allow_back=allow_back,
|
|
60
89
|
)
|
|
61
90
|
if item.input_type == "bool":
|
|
62
91
|
return prompt_bool(
|
|
63
92
|
item.display_name,
|
|
64
93
|
item.description,
|
|
65
94
|
default=bool(item.default) if item.default is not None else True,
|
|
95
|
+
allow_back=allow_back,
|
|
66
96
|
)
|
|
67
97
|
if item.input_type == "choice":
|
|
68
98
|
return prompt_choice(
|
|
@@ -70,6 +100,7 @@ def _prompt_for_item(item: ConfigItem) -> object:
|
|
|
70
100
|
item.description,
|
|
71
101
|
choices=item.choices,
|
|
72
102
|
default=item.default if item.default is not None else "",
|
|
103
|
+
allow_back=allow_back,
|
|
73
104
|
)
|
|
74
105
|
raise ValueError(f"Unknown input_type {item.input_type!r} for {item.key}")
|
|
75
106
|
|
|
@@ -79,23 +110,20 @@ def _prompt_for_item(item: ConfigItem) -> object:
|
|
|
79
110
|
# ---------------------------------------------------------------------------
|
|
80
111
|
|
|
81
112
|
|
|
82
|
-
def _prompt_gate(section_name: str, detail: str) ->
|
|
113
|
+
def _prompt_gate(section_name: str, detail: str, allow_back: bool) -> object:
|
|
83
114
|
"""Ask the user whether to configure a section or skip with defaults.
|
|
84
115
|
|
|
85
|
-
Returns
|
|
116
|
+
Returns ``"configure"``, ``"use defaults"``, or :data:`GO_BACK`.
|
|
86
117
|
"""
|
|
87
|
-
return (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
default="use defaults",
|
|
97
|
-
)
|
|
98
|
-
== "configure"
|
|
118
|
+
return prompt_choice(
|
|
119
|
+
section_name,
|
|
120
|
+
f"Would you like to configure {detail}?\n Defaults will be used if you skip.",
|
|
121
|
+
choices=[
|
|
122
|
+
("use defaults", "Skip, use defaults"),
|
|
123
|
+
("configure", "Yes, configure"),
|
|
124
|
+
],
|
|
125
|
+
default="use defaults",
|
|
126
|
+
allow_back=allow_back,
|
|
99
127
|
)
|
|
100
128
|
|
|
101
129
|
|
|
@@ -121,10 +149,10 @@ def _print_summary(state: InteractiveState) -> None:
|
|
|
121
149
|
# ---------------------------------------------------------------------------
|
|
122
150
|
|
|
123
151
|
|
|
124
|
-
def _prompt_action() ->
|
|
125
|
-
"""Ask the user to start the benchmark
|
|
152
|
+
def _prompt_action(allow_back: bool) -> object:
|
|
153
|
+
"""Ask the user to start the benchmark, export config, or quit.
|
|
126
154
|
|
|
127
|
-
Returns ``"start"
|
|
155
|
+
Returns ``"start"``, ``"export"``, ``"quit"``, or :data:`GO_BACK`.
|
|
128
156
|
"""
|
|
129
157
|
return prompt_choice(
|
|
130
158
|
"What would you like to do?",
|
|
@@ -132,8 +160,10 @@ def _prompt_action() -> str:
|
|
|
132
160
|
choices=[
|
|
133
161
|
("start", "Start benchmark"),
|
|
134
162
|
("export", "Export configuration for later use and exit"),
|
|
163
|
+
("quit", "Quit without running"),
|
|
135
164
|
],
|
|
136
165
|
default="start",
|
|
166
|
+
allow_back=allow_back,
|
|
137
167
|
)
|
|
138
168
|
|
|
139
169
|
|
|
@@ -177,10 +207,14 @@ def _resolve_before_export(state: InteractiveState) -> None:
|
|
|
177
207
|
def _handle_export(state: InteractiveState) -> None:
|
|
178
208
|
"""Prompt for filename, resolve values, save JSON, and exit."""
|
|
179
209
|
_resolve_before_export(state)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
210
|
+
# No allow_back here, so the return is always a plain string.
|
|
211
|
+
filename = cast(
|
|
212
|
+
str,
|
|
213
|
+
prompt_text(
|
|
214
|
+
"Export filename",
|
|
215
|
+
"",
|
|
216
|
+
default="bench_config.json",
|
|
217
|
+
),
|
|
184
218
|
)
|
|
185
219
|
state.save_json(filename)
|
|
186
220
|
print()
|
|
@@ -199,12 +233,116 @@ def _handle_export(state: InteractiveState) -> None:
|
|
|
199
233
|
# ---------------------------------------------------------------------------
|
|
200
234
|
|
|
201
235
|
|
|
236
|
+
# Heading text for each section gate, keyed by section name.
|
|
237
|
+
_GATE_TEXT = {
|
|
238
|
+
"general": ("General settings", "general settings (model, KV cache volume, etc.)"),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _next_question(
|
|
243
|
+
state: InteractiveState, gates: dict[str, str]
|
|
244
|
+
) -> ConfigItem | str | None:
|
|
245
|
+
"""Determine the next question to ask.
|
|
246
|
+
|
|
247
|
+
Returns a ``ConfigItem`` to prompt for a value, a section name
|
|
248
|
+
(``"general"`` / ``"workload"``) to prompt the section gate, or None when
|
|
249
|
+
configuration is complete. The result is derived purely from current
|
|
250
|
+
state plus the gate decisions already made, so stepping back (which
|
|
251
|
+
un-sets the most recent answer) naturally re-presents the question that
|
|
252
|
+
produced it.
|
|
253
|
+
"""
|
|
254
|
+
missing = state.get_missing_required()
|
|
255
|
+
if missing:
|
|
256
|
+
return missing[0]
|
|
257
|
+
|
|
258
|
+
if state.has_unconfigured_general() and "general" not in gates:
|
|
259
|
+
return "general"
|
|
260
|
+
if gates.get("general") == "configure":
|
|
261
|
+
general = state.get_general_items()
|
|
262
|
+
if general:
|
|
263
|
+
return general[0]
|
|
264
|
+
|
|
265
|
+
workload = [i for i in state.get_workload_items() if not state.is_set(i.key)]
|
|
266
|
+
if workload and "workload" not in gates:
|
|
267
|
+
return "workload"
|
|
268
|
+
if gates.get("workload") == "configure" and workload:
|
|
269
|
+
return workload[0]
|
|
270
|
+
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _gate_text(section: str, state: InteractiveState) -> tuple[str, str]:
|
|
275
|
+
"""Return the ``(section_name, detail)`` heading for a gate prompt."""
|
|
276
|
+
if section == "workload":
|
|
277
|
+
return (
|
|
278
|
+
f"Workload settings ({state.get('workload', 'workload')})",
|
|
279
|
+
"workload-specific settings",
|
|
280
|
+
)
|
|
281
|
+
return _GATE_TEXT[section]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _gather_config(state: InteractiveState) -> bool:
|
|
285
|
+
"""Drive the question loop until the user starts, exports, or quits.
|
|
286
|
+
|
|
287
|
+
Walks required items, the general/workload gates, and the summary, while
|
|
288
|
+
letting the user step back to revise any earlier answer. Gate decisions
|
|
289
|
+
are tracked outside ``state`` so they never leak into the exported config.
|
|
290
|
+
|
|
291
|
+
Returns True to start the benchmark, False to quit. Exits the process
|
|
292
|
+
directly on export.
|
|
293
|
+
"""
|
|
294
|
+
gates: dict[str, str] = {}
|
|
295
|
+
# Trail of committed answers, for stepping back. Item keys are stored as
|
|
296
|
+
# the key itself; gate decisions as ``"gate:<section>"``.
|
|
297
|
+
trail: list[str] = []
|
|
298
|
+
|
|
299
|
+
def step_back() -> None:
|
|
300
|
+
marker = trail.pop()
|
|
301
|
+
if marker.startswith("gate:"):
|
|
302
|
+
gates.pop(marker[len("gate:") :], None)
|
|
303
|
+
else:
|
|
304
|
+
state.unset(marker)
|
|
305
|
+
|
|
306
|
+
while True:
|
|
307
|
+
question = _next_question(state, gates)
|
|
308
|
+
|
|
309
|
+
if question is None:
|
|
310
|
+
_print_summary(state)
|
|
311
|
+
action = _prompt_action(allow_back=bool(trail))
|
|
312
|
+
if action is GO_BACK:
|
|
313
|
+
step_back()
|
|
314
|
+
continue
|
|
315
|
+
if action == "export":
|
|
316
|
+
_handle_export(state) # exits
|
|
317
|
+
return action == "start"
|
|
318
|
+
|
|
319
|
+
if isinstance(question, ConfigItem):
|
|
320
|
+
value = _prompt_for_item(question, allow_back=bool(trail))
|
|
321
|
+
if value is GO_BACK:
|
|
322
|
+
step_back()
|
|
323
|
+
continue
|
|
324
|
+
if question.key in _URL_KEYS and isinstance(value, str):
|
|
325
|
+
value = _normalize_url(value)
|
|
326
|
+
state.set(question.key, value)
|
|
327
|
+
trail.append(question.key)
|
|
328
|
+
else:
|
|
329
|
+
name, detail = _gate_text(question, state)
|
|
330
|
+
choice = _prompt_gate(name, detail, allow_back=bool(trail))
|
|
331
|
+
if choice is GO_BACK:
|
|
332
|
+
step_back()
|
|
333
|
+
continue
|
|
334
|
+
gates[question] = str(choice)
|
|
335
|
+
trail.append(f"gate:{question}")
|
|
336
|
+
|
|
337
|
+
|
|
202
338
|
def run_interactive(args: argparse.Namespace) -> argparse.Namespace:
|
|
203
339
|
"""Run the interactive configuration flow.
|
|
204
340
|
|
|
205
341
|
Walks the user through missing required items, offers gates for
|
|
206
342
|
general and workload-specific settings, shows a summary, and
|
|
207
|
-
returns a complete ``argparse.Namespace``.
|
|
343
|
+
returns a complete ``argparse.Namespace``. At any prompt the user can
|
|
344
|
+
step back to the previous question (``<``) or exit (Ctrl-C / Ctrl-D, or
|
|
345
|
+
the Quit action).
|
|
208
346
|
|
|
209
347
|
Args:
|
|
210
348
|
args: Partially-populated CLI args (some may be None).
|
|
@@ -219,50 +357,19 @@ def run_interactive(args: argparse.Namespace) -> argparse.Namespace:
|
|
|
219
357
|
print(f"{BOLD}{'═' * 50}{RESET}")
|
|
220
358
|
print(f"{BOLD} lmcache bench engine — Interactive Setup{RESET}")
|
|
221
359
|
print(f"{BOLD}{'═' * 50}{RESET}")
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
# ── Phase 2: General settings gate ────────────────────────────────
|
|
237
|
-
if state.has_unconfigured_general():
|
|
238
|
-
if _prompt_gate(
|
|
239
|
-
"General settings",
|
|
240
|
-
"general settings (model, KV cache volume, etc.)",
|
|
241
|
-
):
|
|
242
|
-
for item in state.get_general_items():
|
|
243
|
-
value = _prompt_for_item(item)
|
|
244
|
-
state.set(item.key, value)
|
|
245
|
-
|
|
246
|
-
# ── Phase 3: Workload settings gate (always shown) ────────────────
|
|
247
|
-
if state.has_workload_items():
|
|
248
|
-
workload = state.get("workload", "workload")
|
|
249
|
-
if _prompt_gate(
|
|
250
|
-
f"Workload settings ({workload})",
|
|
251
|
-
"workload-specific settings",
|
|
252
|
-
):
|
|
253
|
-
for item in state.get_workload_items():
|
|
254
|
-
value = _prompt_for_item(item)
|
|
255
|
-
state.set(item.key, value)
|
|
256
|
-
|
|
257
|
-
# Fill all remaining defaults
|
|
258
|
-
state.fill_defaults()
|
|
259
|
-
|
|
260
|
-
# ── Phase 4: Summary + action ─────────────────────────────────────
|
|
261
|
-
_print_summary(state)
|
|
262
|
-
action = _prompt_action()
|
|
263
|
-
|
|
264
|
-
if action == "export":
|
|
265
|
-
_handle_export(state)
|
|
360
|
+
print(f" {YELLOW}Type < to go back · Ctrl-C to exit{RESET}")
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
start = _gather_config(state)
|
|
364
|
+
except (KeyboardInterrupt, EOFError):
|
|
365
|
+
print()
|
|
366
|
+
print(f" {YELLOW}Setup cancelled.{RESET}")
|
|
367
|
+
sys.exit(0)
|
|
368
|
+
|
|
369
|
+
if not start:
|
|
370
|
+
print()
|
|
371
|
+
print(f" {YELLOW}Quit without running.{RESET}")
|
|
372
|
+
sys.exit(0)
|
|
266
373
|
|
|
267
374
|
# Carry over output settings from original CLI args
|
|
268
375
|
ns = state.to_namespace()
|
|
@@ -90,7 +90,8 @@ ALL_ITEMS: list[ConfigItem] = [
|
|
|
90
90
|
key="engine_url",
|
|
91
91
|
display_name="Engine URL",
|
|
92
92
|
description=(
|
|
93
|
-
"URL of the inference engine. "
|
|
93
|
+
"URL of the inference engine. Enter just a port (e.g. 8000) to "
|
|
94
|
+
"use http://localhost:8000. "
|
|
94
95
|
"Set OPENAI_API_KEY env var if authentication is needed."
|
|
95
96
|
),
|
|
96
97
|
input_type="text",
|
|
@@ -135,7 +136,10 @@ ALL_ITEMS: list[ConfigItem] = [
|
|
|
135
136
|
ConfigItem(
|
|
136
137
|
key="lmcache_url",
|
|
137
138
|
display_name="LMCache Server URL",
|
|
138
|
-
description=
|
|
139
|
+
description=(
|
|
140
|
+
"URL of the running LMCache HTTP server. Enter just a port "
|
|
141
|
+
"(e.g. 8080) to use http://localhost:8080."
|
|
142
|
+
),
|
|
139
143
|
input_type="text",
|
|
140
144
|
default="http://localhost:8080",
|
|
141
145
|
required=False,
|
|
@@ -74,6 +74,10 @@ class InteractiveState:
|
|
|
74
74
|
def set(self, key: str, value: Any) -> None:
|
|
75
75
|
self._values[key] = value
|
|
76
76
|
|
|
77
|
+
def unset(self, key: str) -> None:
|
|
78
|
+
"""Remove a key so it counts as unset again (used when stepping back)."""
|
|
79
|
+
self._values.pop(key, None)
|
|
80
|
+
|
|
77
81
|
@property
|
|
78
82
|
def values(self) -> dict[str, Any]:
|
|
79
83
|
return dict(self._values)
|
|
@@ -7,11 +7,28 @@ Provides simple prompt functions for collecting user input:
|
|
|
7
7
|
- ``prompt_number`` — numeric input with type validation
|
|
8
8
|
- ``prompt_bool`` — Y/N confirmation
|
|
9
9
|
- ``prompt_choice`` — arrow-key selection (falls back to numbered list on non-TTY)
|
|
10
|
+
|
|
11
|
+
Every prompt accepts ``allow_back``. When enabled, the user steps back to the
|
|
12
|
+
previous question by typing ``<`` — in every prompt, typed or arrow-key — in
|
|
13
|
+
which case the prompt returns the :data:`GO_BACK` sentinel instead of a value.
|
|
10
14
|
"""
|
|
11
15
|
|
|
12
16
|
# Standard
|
|
13
17
|
import sys
|
|
14
18
|
|
|
19
|
+
|
|
20
|
+
class _GoBack:
|
|
21
|
+
"""Sentinel type returned by prompts when the user asks to step back."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Returned by any prompt (when ``allow_back=True``) to mean "go to the
|
|
25
|
+
# previous question". Compare with ``is`` — there is only one instance.
|
|
26
|
+
GO_BACK = _GoBack()
|
|
27
|
+
|
|
28
|
+
# Token the user types to go back, in any text/number/bool prompt and in the
|
|
29
|
+
# non-TTY choice fallback.
|
|
30
|
+
BACK_TOKEN = "<"
|
|
31
|
+
|
|
15
32
|
# ---------------------------------------------------------------------------
|
|
16
33
|
# ANSI helpers
|
|
17
34
|
# ---------------------------------------------------------------------------
|
|
@@ -30,6 +47,11 @@ def _is_tty() -> bool:
|
|
|
30
47
|
return hasattr(sys.stdin, "fileno") and sys.stdin.isatty()
|
|
31
48
|
|
|
32
49
|
|
|
50
|
+
def _print_back_hint() -> None:
|
|
51
|
+
"""Print the dim 'type < to go back' hint used by typed prompts."""
|
|
52
|
+
print(f" {DIM}(type {BACK_TOKEN} to go back){RESET}")
|
|
53
|
+
|
|
54
|
+
|
|
33
55
|
# ---------------------------------------------------------------------------
|
|
34
56
|
# prompt_text
|
|
35
57
|
# ---------------------------------------------------------------------------
|
|
@@ -39,26 +61,33 @@ def prompt_text(
|
|
|
39
61
|
label: str,
|
|
40
62
|
description: str = "",
|
|
41
63
|
default: str = "",
|
|
42
|
-
|
|
64
|
+
allow_back: bool = False,
|
|
65
|
+
) -> str | _GoBack:
|
|
43
66
|
"""Prompt for free-form text input.
|
|
44
67
|
|
|
45
68
|
Args:
|
|
46
69
|
label: The config item name shown as a heading.
|
|
47
70
|
description: One-line explanation shown below the label.
|
|
48
71
|
default: Default value; shown in brackets, returned on empty Enter.
|
|
72
|
+
allow_back: If True, typing ``<`` returns :data:`GO_BACK`.
|
|
49
73
|
|
|
50
74
|
Returns:
|
|
51
|
-
The user's input,
|
|
75
|
+
The user's input, the default if they pressed Enter, or
|
|
76
|
+
:data:`GO_BACK` if they asked to step back.
|
|
52
77
|
"""
|
|
53
78
|
print()
|
|
54
79
|
print(f"{BOLD}{label}{RESET}")
|
|
55
80
|
if description:
|
|
56
81
|
print(f" {description}")
|
|
82
|
+
if allow_back:
|
|
83
|
+
_print_back_hint()
|
|
57
84
|
if default:
|
|
58
85
|
prompt = f" {DIM}[default: {default}]{RESET} {GREEN}>{RESET} "
|
|
59
86
|
else:
|
|
60
87
|
prompt = f" {GREEN}>{RESET} "
|
|
61
88
|
value = input(prompt).strip()
|
|
89
|
+
if allow_back and value == BACK_TOKEN:
|
|
90
|
+
return GO_BACK
|
|
62
91
|
return value if value else default
|
|
63
92
|
|
|
64
93
|
|
|
@@ -72,7 +101,8 @@ def prompt_number(
|
|
|
72
101
|
description: str = "",
|
|
73
102
|
default: float | int | None = None,
|
|
74
103
|
number_type: type = int,
|
|
75
|
-
|
|
104
|
+
allow_back: bool = False,
|
|
105
|
+
) -> float | int | _GoBack:
|
|
76
106
|
"""Prompt for a numeric value with validation.
|
|
77
107
|
|
|
78
108
|
Args:
|
|
@@ -80,14 +110,17 @@ def prompt_number(
|
|
|
80
110
|
description: One-line explanation.
|
|
81
111
|
default: Default value; returned on empty Enter. None means required.
|
|
82
112
|
number_type: ``int`` or ``float``.
|
|
113
|
+
allow_back: If True, typing ``<`` returns :data:`GO_BACK`.
|
|
83
114
|
|
|
84
115
|
Returns:
|
|
85
|
-
The parsed number.
|
|
116
|
+
The parsed number, or :data:`GO_BACK` if the user asked to step back.
|
|
86
117
|
"""
|
|
87
118
|
print()
|
|
88
119
|
print(f"{BOLD}{label}{RESET}")
|
|
89
120
|
if description:
|
|
90
121
|
print(f" {description}")
|
|
122
|
+
if allow_back:
|
|
123
|
+
_print_back_hint()
|
|
91
124
|
|
|
92
125
|
while True:
|
|
93
126
|
if default is not None:
|
|
@@ -95,6 +128,8 @@ def prompt_number(
|
|
|
95
128
|
else:
|
|
96
129
|
prompt = f" {GREEN}>{RESET} "
|
|
97
130
|
raw = input(prompt).strip()
|
|
131
|
+
if allow_back and raw == BACK_TOKEN:
|
|
132
|
+
return GO_BACK
|
|
98
133
|
if not raw and default is not None:
|
|
99
134
|
return default
|
|
100
135
|
try:
|
|
@@ -113,7 +148,8 @@ def prompt_bool(
|
|
|
113
148
|
label: str,
|
|
114
149
|
description: str = "",
|
|
115
150
|
default: bool = True,
|
|
116
|
-
|
|
151
|
+
allow_back: bool = False,
|
|
152
|
+
) -> bool | _GoBack:
|
|
117
153
|
"""Prompt for a yes/no confirmation.
|
|
118
154
|
|
|
119
155
|
Shows ``[Y/n]`` or ``[y/N]`` depending on the default.
|
|
@@ -123,18 +159,24 @@ def prompt_bool(
|
|
|
123
159
|
label: The config item name.
|
|
124
160
|
description: One-line explanation.
|
|
125
161
|
default: Default value when user presses Enter.
|
|
162
|
+
allow_back: If True, typing ``<`` returns :data:`GO_BACK`.
|
|
126
163
|
|
|
127
164
|
Returns:
|
|
128
|
-
True for yes, False for no
|
|
165
|
+
True for yes, False for no, or :data:`GO_BACK` if the user asked to
|
|
166
|
+
step back.
|
|
129
167
|
"""
|
|
130
168
|
print()
|
|
131
169
|
print(f"{BOLD}{label}{RESET}")
|
|
132
170
|
if description:
|
|
133
171
|
print(f" {description}")
|
|
172
|
+
if allow_back:
|
|
173
|
+
_print_back_hint()
|
|
134
174
|
|
|
135
175
|
hint = "[default: Y] (Y/n)" if default else "[default: N] (y/N)"
|
|
136
176
|
while True:
|
|
137
177
|
raw = input(f" {DIM}{hint}{RESET} {GREEN}>{RESET} ").strip().lower()
|
|
178
|
+
if allow_back and raw == BACK_TOKEN:
|
|
179
|
+
return GO_BACK
|
|
138
180
|
if not raw:
|
|
139
181
|
return default
|
|
140
182
|
if raw in ("y", "yes"):
|
|
@@ -164,6 +206,12 @@ def _read_key() -> str:
|
|
|
164
206
|
try:
|
|
165
207
|
tty.setraw(fd)
|
|
166
208
|
ch = sys.stdin.read(1)
|
|
209
|
+
# Raw mode disables signal generation, so translate Ctrl-C / Ctrl-D
|
|
210
|
+
# into the usual exceptions the caller already expects.
|
|
211
|
+
if ch == "\x03":
|
|
212
|
+
raise KeyboardInterrupt
|
|
213
|
+
if ch == "\x04":
|
|
214
|
+
raise EOFError
|
|
167
215
|
if ch == "\r" or ch == "\n":
|
|
168
216
|
return "enter"
|
|
169
217
|
if ch == "\x1b":
|
|
@@ -201,7 +249,8 @@ def prompt_choice(
|
|
|
201
249
|
description: str = "",
|
|
202
250
|
choices: list[tuple[str, str]] = [], # noqa: B006
|
|
203
251
|
default: str = "",
|
|
204
|
-
|
|
252
|
+
allow_back: bool = False,
|
|
253
|
+
) -> str | _GoBack:
|
|
205
254
|
"""Prompt user to select from a list using arrow keys.
|
|
206
255
|
|
|
207
256
|
Each choice is a ``(value, description)`` tuple. The description is
|
|
@@ -214,9 +263,11 @@ def prompt_choice(
|
|
|
214
263
|
description: One-line explanation.
|
|
215
264
|
choices: List of ``(value, one_line_description)`` tuples.
|
|
216
265
|
default: Pre-selected value. Defaults to the first choice.
|
|
266
|
+
allow_back: If True, pressing ``<`` returns :data:`GO_BACK`.
|
|
217
267
|
|
|
218
268
|
Returns:
|
|
219
|
-
The selected value string
|
|
269
|
+
The selected value string, or :data:`GO_BACK` if the user asked to
|
|
270
|
+
step back.
|
|
220
271
|
"""
|
|
221
272
|
if not choices:
|
|
222
273
|
raise ValueError("choices must not be empty")
|
|
@@ -235,9 +286,11 @@ def prompt_choice(
|
|
|
235
286
|
|
|
236
287
|
# Non-TTY fallback: numbered list
|
|
237
288
|
if not _is_tty():
|
|
238
|
-
return _prompt_choice_fallback(choices, selected)
|
|
289
|
+
return _prompt_choice_fallback(choices, selected, allow_back)
|
|
239
290
|
|
|
240
|
-
|
|
291
|
+
nav = "Use ↑↓ to navigate, Enter to select"
|
|
292
|
+
nav += f", {BACK_TOKEN} to go back." if allow_back else "."
|
|
293
|
+
print(f" {DIM}{nav}{RESET}")
|
|
241
294
|
print()
|
|
242
295
|
|
|
243
296
|
# Initial render
|
|
@@ -251,14 +304,14 @@ def prompt_choice(
|
|
|
251
304
|
selected = (selected - 1) % len(choices)
|
|
252
305
|
elif key == "down":
|
|
253
306
|
selected = (selected + 1) % len(choices)
|
|
254
|
-
elif key == "enter":
|
|
307
|
+
elif key == "enter" or (key == BACK_TOKEN and allow_back):
|
|
255
308
|
# Clear and re-render final state
|
|
256
309
|
num_lines = len(choices)
|
|
257
310
|
sys.stdout.write(f"\r{CURSOR_UP * num_lines}")
|
|
258
311
|
lines = _render_choices(choices, selected)
|
|
259
312
|
for line in lines:
|
|
260
313
|
print(f"{CLEAR_LINE}{line}")
|
|
261
|
-
return choices[selected][0]
|
|
314
|
+
return GO_BACK if key == BACK_TOKEN else choices[selected][0]
|
|
262
315
|
else:
|
|
263
316
|
continue
|
|
264
317
|
|
|
@@ -273,13 +326,18 @@ def prompt_choice(
|
|
|
273
326
|
def _prompt_choice_fallback(
|
|
274
327
|
choices: list[tuple[str, str]],
|
|
275
328
|
default_index: int,
|
|
276
|
-
|
|
329
|
+
allow_back: bool = False,
|
|
330
|
+
) -> str | _GoBack:
|
|
277
331
|
"""Numbered fallback for non-TTY environments."""
|
|
278
332
|
for i, (val, desc) in enumerate(choices):
|
|
279
333
|
marker = "*" if i == default_index else " "
|
|
280
334
|
print(f" {marker} {i + 1}) {val} — {desc}")
|
|
335
|
+
if allow_back:
|
|
336
|
+
_print_back_hint()
|
|
281
337
|
while True:
|
|
282
338
|
raw = input(f" [default: {default_index + 1}] {GREEN}>{RESET} ").strip()
|
|
339
|
+
if allow_back and raw == BACK_TOKEN:
|
|
340
|
+
return GO_BACK
|
|
283
341
|
if not raw:
|
|
284
342
|
return choices[default_index][0]
|
|
285
343
|
try:
|