lmcache-cli 0.4.8.dev24__py3-none-any.whl → 0.4.8.dev38__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/engine_bench/command.py +38 -2
- lmcache/cli/commands/bench/engine_bench/config.py +2 -0
- lmcache/cli/commands/bench/engine_bench/interactive/schema.py +21 -0
- lmcache/cli/commands/bench/engine_bench/request_sender.py +16 -1
- lmcache/cli/commands/bench/engine_bench/workloads/__init__.py +30 -0
- lmcache/cli/commands/bench/engine_bench/workloads/long_doc_qa.py +9 -0
- lmcache/cli/commands/coordinator.py +20 -0
- lmcache/cli/commands/tool/__init__.py +3 -2
- lmcache/cli/commands/tool/transfer_channel_benchmark.py +53 -0
- lmcache/integration/vllm/vllm_multi_process_adapter.py +18 -3
- lmcache/tools/transfer_channel_benchmark/README.md +128 -0
- lmcache/tools/transfer_channel_benchmark/__init__.py +1 -0
- lmcache/tools/transfer_channel_benchmark/__main__.py +39 -0
- lmcache/tools/transfer_channel_benchmark/benchmark.py +385 -0
- lmcache/tools/transfer_channel_benchmark/config.py +252 -0
- lmcache/v1/config.py +6 -0
- lmcache/v1/distributed/api.py +64 -0
- lmcache/v1/distributed/config.py +200 -9
- lmcache/v1/distributed/l2_adapters/base.py +25 -1
- lmcache/v1/distributed/l2_adapters/config.py +22 -0
- lmcache/v1/distributed/l2_adapters/fault_inject_l2_adapter.py +462 -0
- lmcache/v1/distributed/l2_adapters/s3_l2_adapter.py +235 -4
- lmcache/v1/distributed/l2_adapters/serde_wrapper.py +13 -1
- lmcache/v1/distributed/memory_manager/l1_memory_manager.py +30 -8
- lmcache/v1/distributed/storage_manager.py +23 -0
- lmcache/v1/distributed/transfer_channel/__init__.py +105 -0
- lmcache/v1/distributed/transfer_channel/abstract.py +149 -0
- lmcache/v1/distributed/transfer_channel/api.py +47 -0
- lmcache/v1/distributed/transfer_channel/factory.py +100 -0
- lmcache/v1/distributed/transfer_channel/impl/__init__.py +9 -0
- lmcache/v1/distributed/transfer_channel/impl/nixl_impl.py +591 -0
- lmcache/v1/memory_management.py +455 -0
- lmcache/v1/mp_coordinator/app.py +33 -5
- lmcache/v1/mp_coordinator/blend_client.py +2 -2
- lmcache/v1/mp_coordinator/config.py +33 -0
- lmcache/v1/mp_coordinator/http_apis/l2_api.py +19 -75
- lmcache/v1/mp_coordinator/l2/event_listener.py +22 -45
- lmcache/v1/mp_coordinator/l2/eviction_manager.py +117 -79
- lmcache/v1/mp_coordinator/l2/resync_manager.py +149 -0
- lmcache/v1/mp_coordinator/l2/usage_manager.py +57 -70
- lmcache/v1/mp_coordinator/registry.py +10 -0
- lmcache/v1/mp_coordinator/schemas.py +9 -28
- lmcache/v1/mp_observability/event.py +10 -3
- lmcache/v1/mp_observability/subscribers/metrics/cb_server.py +11 -0
- lmcache/v1/mp_observability/subscribers/tracing/cb_server.py +30 -9
- lmcache/v1/multiprocess/config.py +65 -0
- lmcache/v1/multiprocess/custom_types.py +23 -1
- lmcache/v1/multiprocess/engine_context.py +1 -1
- lmcache/v1/multiprocess/engine_module.py +65 -0
- lmcache/v1/multiprocess/http_apis/l2_api.py +241 -0
- lmcache/v1/multiprocess/modules/blend_v3.py +265 -35
- lmcache/v1/multiprocess/modules/engine_driven_transfer.py +165 -74
- lmcache/v1/multiprocess/modules/lmcache_driven_transfer.py +150 -29
- lmcache/v1/multiprocess/modules/management.py +87 -7
- lmcache/v1/multiprocess/modules/p2p_controller.py +245 -0
- lmcache/v1/multiprocess/mq.py +5 -0
- lmcache/v1/multiprocess/protocols/__init__.py +2 -0
- lmcache/v1/multiprocess/protocols/base.py +5 -0
- lmcache/v1/multiprocess/protocols/controller.py +6 -2
- lmcache/v1/multiprocess/protocols/p2p.py +61 -0
- lmcache/v1/multiprocess/server.py +48 -20
- lmcache/v1/storage_backend/nixl_storage_backend.py +28 -4
- {lmcache_cli-0.4.8.dev24.dist-info → lmcache_cli-0.4.8.dev38.dist-info}/METADATA +1 -1
- {lmcache_cli-0.4.8.dev24.dist-info → lmcache_cli-0.4.8.dev38.dist-info}/RECORD +69 -52
- {lmcache_cli-0.4.8.dev24.dist-info → lmcache_cli-0.4.8.dev38.dist-info}/WHEEL +0 -0
- {lmcache_cli-0.4.8.dev24.dist-info → lmcache_cli-0.4.8.dev38.dist-info}/entry_points.txt +0 -0
- {lmcache_cli-0.4.8.dev24.dist-info → lmcache_cli-0.4.8.dev38.dist-info}/licenses/LICENSE +0 -0
- {lmcache_cli-0.4.8.dev24.dist-info → lmcache_cli-0.4.8.dev38.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.4.8.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 4, 8, '
|
|
21
|
+
__version__ = version = '0.4.8.dev38'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 4, 8, 'dev38')
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'ge1fdbc634'
|
|
@@ -33,7 +33,10 @@ from lmcache.cli.commands.bench.engine_bench.stats import (
|
|
|
33
33
|
FinalStats,
|
|
34
34
|
StatsCollector,
|
|
35
35
|
)
|
|
36
|
-
from lmcache.cli.commands.bench.engine_bench.workloads import
|
|
36
|
+
from lmcache.cli.commands.bench.engine_bench.workloads import (
|
|
37
|
+
create_workload,
|
|
38
|
+
validate_max_output_length_supported,
|
|
39
|
+
)
|
|
37
40
|
from lmcache.logging import init_logger
|
|
38
41
|
|
|
39
42
|
if TYPE_CHECKING:
|
|
@@ -42,6 +45,10 @@ if TYPE_CHECKING:
|
|
|
42
45
|
|
|
43
46
|
logger = init_logger(__name__)
|
|
44
47
|
|
|
48
|
+
# Default for --ldqa-max-output-length; centralized so the "max output length
|
|
49
|
+
# explicitly set" check stays in sync with the parser.
|
|
50
|
+
_LDQA_MAX_OUTPUT_LENGTH_DEFAULT = 128
|
|
51
|
+
|
|
45
52
|
|
|
46
53
|
# ---------------------------------------------------------------------------
|
|
47
54
|
# Parser registration
|
|
@@ -147,6 +154,15 @@ def register_engine_parser(
|
|
|
147
154
|
action="store_true",
|
|
148
155
|
help="Suppress real-time progress display.",
|
|
149
156
|
)
|
|
157
|
+
parser.add_argument(
|
|
158
|
+
"--ignore-eos",
|
|
159
|
+
action="store_true",
|
|
160
|
+
help=(
|
|
161
|
+
"Force generation to run for the full output length by ignoring "
|
|
162
|
+
"the model's EOS token (vLLM sampling extension). Makes decode "
|
|
163
|
+
"throughput reproducible regardless of when the model would stop."
|
|
164
|
+
),
|
|
165
|
+
)
|
|
150
166
|
parser.add_argument(
|
|
151
167
|
"--no-interactive",
|
|
152
168
|
action="store_true",
|
|
@@ -223,6 +239,16 @@ def register_engine_parser(
|
|
|
223
239
|
default=3,
|
|
224
240
|
help="Max concurrent in-flight requests (default: 3).",
|
|
225
241
|
)
|
|
242
|
+
group.add_argument(
|
|
243
|
+
"--ldqa-max-output-length",
|
|
244
|
+
type=int,
|
|
245
|
+
default=_LDQA_MAX_OUTPUT_LENGTH_DEFAULT,
|
|
246
|
+
help=(
|
|
247
|
+
f"Max tokens to generate per benchmark query "
|
|
248
|
+
f"(default: {_LDQA_MAX_OUTPUT_LENGTH_DEFAULT}). Combine with "
|
|
249
|
+
"--ignore-eos for a reproducible decode phase."
|
|
250
|
+
),
|
|
251
|
+
)
|
|
226
252
|
|
|
227
253
|
# --- Multi-round-chat workload args ---
|
|
228
254
|
mrc_group = parser.add_argument_group(
|
|
@@ -410,6 +436,7 @@ def _export_config(
|
|
|
410
436
|
state.set("workload", config.workload)
|
|
411
437
|
state.set("kv_cache_volume", config.kv_cache_volume_gb)
|
|
412
438
|
state.set("tokens_per_gb_kvcache", config.tokens_per_gb_kvcache)
|
|
439
|
+
state.set("ignore_eos", config.ignore_eos)
|
|
413
440
|
|
|
414
441
|
# Workload-specific args from namespace
|
|
415
442
|
for item in state.get_workload_items():
|
|
@@ -524,6 +551,11 @@ def run_engine_bench(command: "BaseCommand", args: argparse.Namespace) -> None:
|
|
|
524
551
|
# 1. Parse config
|
|
525
552
|
config = parse_args_to_config(args)
|
|
526
553
|
|
|
554
|
+
# 1a. A max output length can only be set for workloads that have a
|
|
555
|
+
# max-output-length parameter; reject it for any other workload.
|
|
556
|
+
if args.ldqa_max_output_length != _LDQA_MAX_OUTPUT_LENGTH_DEFAULT:
|
|
557
|
+
validate_max_output_length_supported(config.workload)
|
|
558
|
+
|
|
527
559
|
# 1b. --export-config: save resolved config and exit
|
|
528
560
|
export_path = getattr(args, "export_config", None)
|
|
529
561
|
if export_path:
|
|
@@ -546,7 +578,11 @@ def run_engine_bench(command: "BaseCommand", args: argparse.Namespace) -> None:
|
|
|
546
578
|
)
|
|
547
579
|
|
|
548
580
|
# 3. Create request sender (callbacks wired after workload creation)
|
|
549
|
-
request_sender = RequestSender(
|
|
581
|
+
request_sender = RequestSender(
|
|
582
|
+
config.engine_url,
|
|
583
|
+
config.model,
|
|
584
|
+
ignore_eos=config.ignore_eos,
|
|
585
|
+
)
|
|
550
586
|
|
|
551
587
|
# 4. Create workload
|
|
552
588
|
workload = create_workload(
|
|
@@ -39,6 +39,7 @@ class EngineBenchConfig:
|
|
|
39
39
|
export_csv: bool
|
|
40
40
|
export_json: bool
|
|
41
41
|
quiet: bool
|
|
42
|
+
ignore_eos: bool = False
|
|
42
43
|
|
|
43
44
|
def __post_init__(self) -> None:
|
|
44
45
|
if not self.engine_url:
|
|
@@ -246,4 +247,5 @@ def parse_args_to_config(args: argparse.Namespace) -> EngineBenchConfig:
|
|
|
246
247
|
export_csv=not args.no_csv,
|
|
247
248
|
export_json=args.json,
|
|
248
249
|
quiet=args.quiet,
|
|
250
|
+
ignore_eos=args.ignore_eos,
|
|
249
251
|
)
|
|
@@ -179,6 +179,18 @@ ALL_ITEMS: list[ConfigItem] = [
|
|
|
179
179
|
default=100.0,
|
|
180
180
|
phase=PHASE_GENERAL,
|
|
181
181
|
),
|
|
182
|
+
ConfigItem(
|
|
183
|
+
key="ignore_eos",
|
|
184
|
+
display_name="Ignore EOS",
|
|
185
|
+
description=(
|
|
186
|
+
"Force generation to run for the full output length by ignoring "
|
|
187
|
+
"the model's EOS token (vLLM extension). Makes decode throughput "
|
|
188
|
+
"reproducible."
|
|
189
|
+
),
|
|
190
|
+
input_type="bool",
|
|
191
|
+
default=False,
|
|
192
|
+
phase=PHASE_GENERAL,
|
|
193
|
+
),
|
|
182
194
|
# ── Phase 3: long-doc-permutator ─────────────────────────────────
|
|
183
195
|
ConfigItem(
|
|
184
196
|
key="ldp_num_contexts",
|
|
@@ -266,6 +278,15 @@ ALL_ITEMS: list[ConfigItem] = [
|
|
|
266
278
|
condition=_workload_is("long-doc-qa"),
|
|
267
279
|
phase=PHASE_WORKLOAD,
|
|
268
280
|
),
|
|
281
|
+
ConfigItem(
|
|
282
|
+
key="ldqa_max_output_length",
|
|
283
|
+
display_name="Max output length (tokens)",
|
|
284
|
+
description="Max tokens to generate per benchmark query.",
|
|
285
|
+
input_type="int",
|
|
286
|
+
default=128,
|
|
287
|
+
condition=_workload_is("long-doc-qa"),
|
|
288
|
+
phase=PHASE_WORKLOAD,
|
|
289
|
+
),
|
|
269
290
|
# ── Phase 3: multi-round-chat ─────────────────────────────────────
|
|
270
291
|
ConfigItem(
|
|
271
292
|
key="mrc_shared_prompt_length",
|
|
@@ -73,10 +73,12 @@ class RequestSender:
|
|
|
73
73
|
model: str,
|
|
74
74
|
completions_mode: bool = False,
|
|
75
75
|
on_finished: list[OnFinishedCallback] = [], # noqa: B006
|
|
76
|
+
ignore_eos: bool = False,
|
|
76
77
|
) -> None:
|
|
77
78
|
self._model = model
|
|
78
79
|
self._completions_mode = completions_mode
|
|
79
80
|
self._on_finished = list(on_finished)
|
|
81
|
+
self._ignore_eos = ignore_eos
|
|
80
82
|
|
|
81
83
|
base_url = _normalize_url(engine_url)
|
|
82
84
|
api_key = os.getenv("OPENAI_API_KEY", "")
|
|
@@ -220,7 +222,18 @@ class RequestSender:
|
|
|
220
222
|
messages: list[dict[str, str]],
|
|
221
223
|
max_tokens: int,
|
|
222
224
|
) -> collections.abc.AsyncIterator:
|
|
223
|
-
"""Dispatch the streaming API call (chat or completions).
|
|
225
|
+
"""Dispatch the streaming API call (chat or completions).
|
|
226
|
+
|
|
227
|
+
When ``ignore_eos`` is set on the sender, ``{"ignore_eos": true}`` is
|
|
228
|
+
added to the request body (a vLLM sampling extension) so generation
|
|
229
|
+
always runs for the full ``max_tokens`` instead of stopping at the
|
|
230
|
+
model's EOS token. This makes decode-throughput numbers reproducible.
|
|
231
|
+
"""
|
|
232
|
+
# Attach extra_body only when ignore_eos is set; otherwise send the
|
|
233
|
+
# plain request so no vLLM-specific field reaches non-vLLM backends.
|
|
234
|
+
extra: dict[str, dict[str, bool]] = {}
|
|
235
|
+
if self._ignore_eos:
|
|
236
|
+
extra["extra_body"] = {"ignore_eos": True}
|
|
224
237
|
if self._completions_mode:
|
|
225
238
|
prompt = messages[0]["content"] if messages else ""
|
|
226
239
|
return await self._client.completions.create(
|
|
@@ -230,6 +243,7 @@ class RequestSender:
|
|
|
230
243
|
max_tokens=max_tokens,
|
|
231
244
|
temperature=0.0,
|
|
232
245
|
stream_options={"include_usage": True},
|
|
246
|
+
**extra,
|
|
233
247
|
)
|
|
234
248
|
return await self._client.chat.completions.create(
|
|
235
249
|
model=self._model,
|
|
@@ -238,4 +252,5 @@ class RequestSender:
|
|
|
238
252
|
max_tokens=max_tokens,
|
|
239
253
|
temperature=0.0,
|
|
240
254
|
stream_options={"include_usage": True},
|
|
255
|
+
**extra,
|
|
241
256
|
)
|
|
@@ -52,6 +52,7 @@ __all__ = [
|
|
|
52
52
|
"RandomPrefillConfig",
|
|
53
53
|
"RandomPrefillWorkload",
|
|
54
54
|
"create_workload",
|
|
55
|
+
"validate_max_output_length_supported",
|
|
55
56
|
]
|
|
56
57
|
|
|
57
58
|
_WORKLOAD_NAMES = (
|
|
@@ -62,6 +63,34 @@ _WORKLOAD_NAMES = (
|
|
|
62
63
|
"random-prefill",
|
|
63
64
|
)
|
|
64
65
|
|
|
66
|
+
# Workloads that expose a user-configurable max output length, via a
|
|
67
|
+
# ``max_output_length`` arg (``--ldqa-max-output-length`` / ``--mrc-output-length``).
|
|
68
|
+
_WORKLOADS_WITH_MAX_OUTPUT_LENGTH: frozenset[str] = frozenset(
|
|
69
|
+
{"long-doc-qa", "multi-round-chat"}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def validate_max_output_length_supported(workload: str) -> None:
|
|
74
|
+
"""Validate that a max output length can be specified for ``workload``.
|
|
75
|
+
|
|
76
|
+
Only workloads with a max-output-length parameter (``long-doc-qa``,
|
|
77
|
+
``multi-round-chat``) support setting it; every other workload fixes its
|
|
78
|
+
generation length internally, so requesting one is rejected.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
workload: The selected workload name (``EngineBenchConfig.workload``).
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ValueError: If ``workload`` has no max-output-length parameter.
|
|
85
|
+
"""
|
|
86
|
+
if workload in _WORKLOADS_WITH_MAX_OUTPUT_LENGTH:
|
|
87
|
+
return
|
|
88
|
+
supported = ", ".join(sorted(_WORKLOADS_WITH_MAX_OUTPUT_LENGTH))
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"max output length cannot be specified for the {workload!r} workload: "
|
|
91
|
+
f"it has no max-output-length parameter. Supported workloads: {supported}."
|
|
92
|
+
)
|
|
93
|
+
|
|
65
94
|
|
|
66
95
|
def create_workload(
|
|
67
96
|
config: EngineBenchConfig,
|
|
@@ -114,6 +143,7 @@ def create_workload(
|
|
|
114
143
|
query_per_document=args.ldqa_query_per_document,
|
|
115
144
|
shuffle_policy=args.ldqa_shuffle_policy,
|
|
116
145
|
num_inflight_requests=args.ldqa_num_inflight_requests,
|
|
146
|
+
max_output_length=args.ldqa_max_output_length,
|
|
117
147
|
)
|
|
118
148
|
return LongDocQAWorkload(
|
|
119
149
|
config=ld_workload_config,
|
|
@@ -27,6 +27,7 @@ class LongDocQAConfig:
|
|
|
27
27
|
num_documents: int = 1
|
|
28
28
|
shuffle_policy: str = "random"
|
|
29
29
|
num_inflight_requests: int = 3
|
|
30
|
+
max_output_length: int = 128
|
|
30
31
|
|
|
31
32
|
def __post_init__(self) -> None:
|
|
32
33
|
if self.document_length <= 0:
|
|
@@ -37,6 +38,10 @@ class LongDocQAConfig:
|
|
|
37
38
|
raise ValueError(
|
|
38
39
|
f"query_per_document must be >= 1, got {self.query_per_document}"
|
|
39
40
|
)
|
|
41
|
+
if self.max_output_length < 1:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"max_output_length must be >= 1, got {self.max_output_length}"
|
|
44
|
+
)
|
|
40
45
|
if self.num_inflight_requests < 1:
|
|
41
46
|
raise ValueError(
|
|
42
47
|
f"num_inflight_requests must be >= 1, got {self.num_inflight_requests}"
|
|
@@ -56,6 +61,7 @@ class LongDocQAConfig:
|
|
|
56
61
|
query_per_document: int = 2,
|
|
57
62
|
shuffle_policy: str = "random",
|
|
58
63
|
num_inflight_requests: int = 3,
|
|
64
|
+
max_output_length: int = 128,
|
|
59
65
|
) -> "LongDocQAConfig":
|
|
60
66
|
"""Create a config with ``num_documents`` computed from KV cache budget.
|
|
61
67
|
|
|
@@ -66,6 +72,7 @@ class LongDocQAConfig:
|
|
|
66
72
|
query_per_document: Number of questions per document.
|
|
67
73
|
shuffle_policy: Request ordering — ``"random"`` or ``"tile"``.
|
|
68
74
|
num_inflight_requests: Max concurrent in-flight requests.
|
|
75
|
+
max_output_length: Max tokens to generate per benchmark query.
|
|
69
76
|
|
|
70
77
|
Returns:
|
|
71
78
|
A fully-resolved LongDocQAConfig with computed num_documents.
|
|
@@ -88,6 +95,7 @@ class LongDocQAConfig:
|
|
|
88
95
|
num_documents=num_documents,
|
|
89
96
|
shuffle_policy=shuffle_policy,
|
|
90
97
|
num_inflight_requests=num_inflight_requests,
|
|
98
|
+
max_output_length=max_output_length,
|
|
91
99
|
)
|
|
92
100
|
|
|
93
101
|
|
|
@@ -265,6 +273,7 @@ class LongDocQAWorkload(BaseWorkload):
|
|
|
265
273
|
await self._request_sender.send_request(
|
|
266
274
|
request_id,
|
|
267
275
|
messages,
|
|
276
|
+
max_tokens=self._config.max_output_length,
|
|
268
277
|
)
|
|
269
278
|
finally:
|
|
270
279
|
self._semaphore.release()
|
|
@@ -101,6 +101,24 @@ class CoordinatorCommand(BaseCommand):
|
|
|
101
101
|
"quota, 0.0 (exclusive) to 1.0 (default: 1.0)."
|
|
102
102
|
),
|
|
103
103
|
)
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"--blend-chunk-size",
|
|
106
|
+
type=int,
|
|
107
|
+
default=None,
|
|
108
|
+
help=(
|
|
109
|
+
"Tokens per chunk for the global CacheBlend directory; must "
|
|
110
|
+
"equal the LMCache chunk size the blend servers use (default: 256)."
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--blend-probe-stride",
|
|
115
|
+
type=int,
|
|
116
|
+
default=None,
|
|
117
|
+
help=(
|
|
118
|
+
"Positions between CacheBlend match probes; 1 probes every "
|
|
119
|
+
"offset for full recall (default: 1)."
|
|
120
|
+
),
|
|
121
|
+
)
|
|
104
122
|
|
|
105
123
|
def execute(self, args: argparse.Namespace) -> None:
|
|
106
124
|
"""Build the coordinator config and serve the app with uvicorn.
|
|
@@ -145,6 +163,8 @@ class CoordinatorCommand(BaseCommand):
|
|
|
145
163
|
("eviction_check_interval", args.eviction_check_interval),
|
|
146
164
|
("eviction_ratio", args.eviction_ratio),
|
|
147
165
|
("trigger_watermark", args.trigger_watermark),
|
|
166
|
+
("blend_chunk_size", args.blend_chunk_size),
|
|
167
|
+
("blend_probe_stride", args.blend_probe_stride),
|
|
148
168
|
)
|
|
149
169
|
if value is not None
|
|
150
170
|
}
|
|
@@ -15,7 +15,7 @@ import sys
|
|
|
15
15
|
|
|
16
16
|
# First Party
|
|
17
17
|
from lmcache.cli.commands.base import BaseCommand
|
|
18
|
-
from lmcache.cli.commands.tool import cache_simulator
|
|
18
|
+
from lmcache.cli.commands.tool import cache_simulator, transfer_channel_benchmark
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class ToolCommand(BaseCommand):
|
|
@@ -46,9 +46,10 @@ class ToolCommand(BaseCommand):
|
|
|
46
46
|
inner = parser.add_subparsers(
|
|
47
47
|
dest="tool_name",
|
|
48
48
|
required=True,
|
|
49
|
-
metavar="{cache-simulator}",
|
|
49
|
+
metavar="{cache-simulator,transfer-channel-benchmark}",
|
|
50
50
|
)
|
|
51
51
|
cache_simulator.register(inner)
|
|
52
|
+
transfer_channel_benchmark.register(inner)
|
|
52
53
|
|
|
53
54
|
def execute(self, args: argparse.Namespace) -> None:
|
|
54
55
|
"""Dispatch is handled per-tool via parser.set_defaults(func=...).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""``lmcache tool transfer-channel-benchmark`` sub-subcommand wiring.
|
|
3
|
+
|
|
4
|
+
Argument definitions and execution logic live in the benchmark module:
|
|
5
|
+
|
|
6
|
+
* :func:`~lmcache.tools.transfer_channel_benchmark.benchmark.add_benchmark_arguments`
|
|
7
|
+
* :func:`~lmcache.tools.transfer_channel_benchmark.benchmark.run_benchmark`
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Standard
|
|
11
|
+
import argparse
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register(subparsers: argparse._SubParsersAction) -> None:
|
|
16
|
+
"""Register the ``transfer-channel-benchmark`` sub-subcommand.
|
|
17
|
+
|
|
18
|
+
Only the torch-free ``config`` module is imported here, so registering this
|
|
19
|
+
tool (which happens on every ``lmcache`` invocation) does not require torch
|
|
20
|
+
or the distributed runtime. Those are imported in ``execute``.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
subparsers: The subparsers action from the ``lmcache tool`` parser.
|
|
24
|
+
"""
|
|
25
|
+
# First Party
|
|
26
|
+
from lmcache.tools.transfer_channel_benchmark.config import (
|
|
27
|
+
add_benchmark_arguments,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
parser = subparsers.add_parser(
|
|
31
|
+
"transfer-channel-benchmark",
|
|
32
|
+
help="Benchmark transfer channel read throughput (server/client).",
|
|
33
|
+
description=(
|
|
34
|
+
"Throughput benchmark for the LMCache transfer channel. Run one "
|
|
35
|
+
"process with --role server and another with --role client."
|
|
36
|
+
),
|
|
37
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
38
|
+
)
|
|
39
|
+
add_benchmark_arguments(parser)
|
|
40
|
+
parser.set_defaults(func=execute)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def execute(args: argparse.Namespace) -> None:
|
|
44
|
+
"""Run the benchmark and exit non-zero on failure.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
args: Parsed CLI arguments.
|
|
48
|
+
"""
|
|
49
|
+
# First Party
|
|
50
|
+
from lmcache.tools.transfer_channel_benchmark.benchmark import run_benchmark
|
|
51
|
+
|
|
52
|
+
if not run_benchmark(args):
|
|
53
|
+
sys.exit(1)
|
|
@@ -271,14 +271,21 @@ def _raise_server_unreachable(server_url: str, timeout: float) -> NoReturn:
|
|
|
271
271
|
def send_ping(
|
|
272
272
|
mq_client: MessageQueueClient,
|
|
273
273
|
timeout: float,
|
|
274
|
+
instance_id: int | None = None,
|
|
274
275
|
) -> bool:
|
|
275
276
|
"""Send a PING request and return the result.
|
|
276
277
|
|
|
278
|
+
Args:
|
|
279
|
+
mq_client: The message queue client.
|
|
280
|
+
timeout: Seconds to wait for the server's response.
|
|
281
|
+
instance_id: The worker's instance ID so the server can refresh its
|
|
282
|
+
liveness, or None for an untracked prober (scheduler adapter).
|
|
283
|
+
|
|
277
284
|
Returns:
|
|
278
285
|
True if server is healthy, False on timeout or error.
|
|
279
286
|
"""
|
|
280
287
|
try:
|
|
281
|
-
future = send_lmcache_request(mq_client, RequestType.PING, [])
|
|
288
|
+
future = send_lmcache_request(mq_client, RequestType.PING, [instance_id])
|
|
282
289
|
return future.result(timeout=timeout)
|
|
283
290
|
except TimeoutError:
|
|
284
291
|
return False
|
|
@@ -389,6 +396,7 @@ class HeartbeatThread(PeriodicThread):
|
|
|
389
396
|
mq_client: MessageQueueClient,
|
|
390
397
|
health_event: threading.Event,
|
|
391
398
|
interval: float = DEFAULT_HEARTBEAT_INTERVAL,
|
|
399
|
+
instance_id: int | None = None,
|
|
392
400
|
):
|
|
393
401
|
"""
|
|
394
402
|
Args:
|
|
@@ -398,6 +406,9 @@ class HeartbeatThread(PeriodicThread):
|
|
|
398
406
|
Adapters check this event to decide whether to proceed
|
|
399
407
|
with operations or enter degraded mode.
|
|
400
408
|
interval: Seconds between heartbeat pings and ping timeout.
|
|
409
|
+
instance_id: The worker's instance ID sent with each PING so the
|
|
410
|
+
server can refresh its liveness, or None for an untracked
|
|
411
|
+
prober (the scheduler adapter).
|
|
401
412
|
"""
|
|
402
413
|
super().__init__(
|
|
403
414
|
name="lmcache-heartbeat",
|
|
@@ -407,6 +418,7 @@ class HeartbeatThread(PeriodicThread):
|
|
|
407
418
|
self._mq_client = mq_client
|
|
408
419
|
self._health_event = health_event
|
|
409
420
|
self._interval = interval
|
|
421
|
+
self._instance_id = instance_id
|
|
410
422
|
|
|
411
423
|
# Optional callback invoked on the unhealthy->healthy edge,
|
|
412
424
|
# before the health event is set. See register_recover_callback.
|
|
@@ -445,7 +457,9 @@ class HeartbeatThread(PeriodicThread):
|
|
|
445
457
|
UNREGISTER must not re-register a ghost context.
|
|
446
458
|
"""
|
|
447
459
|
was_healthy = self._health_event.is_set()
|
|
448
|
-
healthy = send_ping(
|
|
460
|
+
healthy = send_ping(
|
|
461
|
+
self._mq_client, timeout=self._interval, instance_id=self._instance_id
|
|
462
|
+
)
|
|
449
463
|
|
|
450
464
|
if self.stop_requested:
|
|
451
465
|
return ThreadRunSummary(
|
|
@@ -1170,7 +1184,7 @@ class LMCacheMPWorkerAdapter:
|
|
|
1170
1184
|
``last_seen``, so it is never reaped while alive -- no re-registration
|
|
1171
1185
|
is needed at startup, and the first store/retrieve is not gated. The
|
|
1172
1186
|
recover callback still re-registers on a genuine unhealthy->healthy
|
|
1173
|
-
edge (server restart)
|
|
1187
|
+
edge (server restart).
|
|
1174
1188
|
"""
|
|
1175
1189
|
if self._heartbeat is not None:
|
|
1176
1190
|
return
|
|
@@ -1181,6 +1195,7 @@ class LMCacheMPWorkerAdapter:
|
|
|
1181
1195
|
mq_client=self.mq_client,
|
|
1182
1196
|
health_event=self._health_event,
|
|
1183
1197
|
interval=self._heartbeat_interval,
|
|
1198
|
+
instance_id=self.instance_id,
|
|
1184
1199
|
)
|
|
1185
1200
|
heartbeat.register_recover_callback(self._reregister_kv_caches_callback)
|
|
1186
1201
|
heartbeat.start()
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Transfer Channel Throughput Benchmark
|
|
2
|
+
|
|
3
|
+
Measures read throughput (GB/s) of the LMCache **transfer channel**
|
|
4
|
+
(`lmcache/v1/distributed/transfer_channel/`) for batched peer-to-peer reads.
|
|
5
|
+
|
|
6
|
+
Unlike a raw-tensor microbenchmark, this tool uses LMCache's `L1MemoryManager`
|
|
7
|
+
to initialize the registered memory region and to **allocate the transferred
|
|
8
|
+
objects** on both sides, so it exercises the same memory path production uses.
|
|
9
|
+
|
|
10
|
+
## How it works
|
|
11
|
+
|
|
12
|
+
The benchmark runs as **two separate processes**:
|
|
13
|
+
|
|
14
|
+
- **server** — uses an `L1MemoryManager` to allocate a registered L1 buffer and
|
|
15
|
+
a pool of source memory objects, registers the buffer with the transfer
|
|
16
|
+
channel, and publishes the source object catalog (`offset`/`size` per object)
|
|
17
|
+
over a small ZMQ side-channel.
|
|
18
|
+
- **client** — fetches the catalog, allocates its own destination objects via an
|
|
19
|
+
`L1MemoryManager`, connects the transfer channel, and repeatedly reads a random
|
|
20
|
+
`--num-objects` subset of the source objects, then reports throughput.
|
|
21
|
+
|
|
22
|
+
A side-channel is needed because the transfer channel handshake only exchanges
|
|
23
|
+
the whole-buffer registration, not per-object offsets.
|
|
24
|
+
|
|
25
|
+
> Only the `nixl` transfer channel type is registered today. The tool is generic
|
|
26
|
+
> over `--transfer-channel-type`; an unknown type raises a clear error.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
Run via the module or the `lmcache` CLI. Start the server first.
|
|
31
|
+
|
|
32
|
+
### `python -m`
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# terminal 1 — server
|
|
36
|
+
python -m lmcache.tools.transfer_channel_benchmark \
|
|
37
|
+
--role server --transfer-channel-type nixl \
|
|
38
|
+
--url 0.0.0.0:7600 --control-url 0.0.0.0:7610 \
|
|
39
|
+
--buffer-size 2GB --page-size 512KB --object-size 10MB
|
|
40
|
+
|
|
41
|
+
# terminal 2 — client
|
|
42
|
+
python -m lmcache.tools.transfer_channel_benchmark \
|
|
43
|
+
--role client --transfer-channel-type nixl \
|
|
44
|
+
--url 127.0.0.1:7600 --control-url 127.0.0.1:7610 \
|
|
45
|
+
--listen-url 0.0.0.0:7601 \
|
|
46
|
+
--page-size 512KB --object-size 10MB \
|
|
47
|
+
--num-objects 10 --iters 3 --warmup 1
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### `lmcache tool`
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
lmcache tool transfer-channel-benchmark --role server --url 0.0.0.0:7600 \
|
|
54
|
+
--control-url 0.0.0.0:7610 --buffer-size 2GB --object-size 10MB
|
|
55
|
+
lmcache tool transfer-channel-benchmark --role client --url 127.0.0.1:7600 \
|
|
56
|
+
--control-url 127.0.0.1:7610 --object-size 10MB --num-objects 10
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The server always writes a deterministic per-object byte pattern (object index
|
|
60
|
+
mod 256) into its source objects, so adding `--verify` on the **client** checks
|
|
61
|
+
the transferred bytes against that pattern (no `--verify` needed on the server).
|
|
62
|
+
|
|
63
|
+
## Key options
|
|
64
|
+
|
|
65
|
+
| Option | Role | Meaning |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| `--role {server,client}` | both | Which side to run (required). |
|
|
68
|
+
| `--transfer-channel-type` | both | Implementation to benchmark (default `nixl`). |
|
|
69
|
+
| `--nixl-backend` | both | nixl backend, e.g. `UCX` (nixl-specific). |
|
|
70
|
+
| `--url` | both | Server binds its transfer-channel server here; client dials it. |
|
|
71
|
+
| `--listen-url` | client | Client's own (mandatory) transfer-channel server bind. |
|
|
72
|
+
| `--control-url` | both | Catalog side-channel: server binds, client connects. |
|
|
73
|
+
| `--buffer-size` | server | Registered L1 source buffer size (e.g. `8GB`). |
|
|
74
|
+
| `--page-size` | both | Page / alignment size; **must match** on both sides. |
|
|
75
|
+
| `--object-size` | both | Per-object size; multiple of `--page-size`. |
|
|
76
|
+
| `--num-objects` | client | Objects transferred per read. |
|
|
77
|
+
| `--num-source-objects` | server | Source pool size (default `5 * --num-objects`). |
|
|
78
|
+
| `--iters` / `--warmup` | client | Measured / warmup read iterations. |
|
|
79
|
+
| `--seed` | client | RNG seed for the read subset. |
|
|
80
|
+
| `--verify` | client | Verify transferred bytes against the server's known pattern. |
|
|
81
|
+
| `--use-lazy` | both | Use the lazy L1 allocator (experimental for registration). |
|
|
82
|
+
| `--server-timeout` | server | Seconds to serve catalog requests before exiting. |
|
|
83
|
+
|
|
84
|
+
## Notes
|
|
85
|
+
|
|
86
|
+
- `--page-size` must be identical on server and client (enforced via the catalog
|
|
87
|
+
handshake) so remote page-index math lines up.
|
|
88
|
+
- With the default (non-lazy) allocator the whole `--buffer-size` is allocated up
|
|
89
|
+
front and may be CUDA-pinned; keep it within available host memory.
|
|
90
|
+
- Requires a working transfer channel runtime (for `nixl`, a UCX backend).
|
|
91
|
+
|
|
92
|
+
## Performance: NUMA placement
|
|
93
|
+
|
|
94
|
+
On a multi-NUMA host with NICs spread across nodes (e.g. an 8-NIC, 2-socket
|
|
95
|
+
box), run **both** the server and client under `numactl --interleave=all`:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
numactl --interleave=all \
|
|
99
|
+
python -m lmcache.tools.transfer_channel_benchmark --role server ...
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Without it the registered buffer is allocated on a single NUMA node, so only the
|
|
103
|
+
rails local to that node reach full bandwidth and the rest are throttled by the
|
|
104
|
+
cross-socket link — roughly halving throughput. In testing on a 2-NUMA / 8-rail
|
|
105
|
+
host this was the difference between **~110 GB/s** (no interleave) and
|
|
106
|
+
**~210 GB/s** (`--interleave=all`), with everything else identical. Page size
|
|
107
|
+
only matters in the small regime (descriptor-bound below ~64KB); from ~128KB up
|
|
108
|
+
the transfer is bandwidth-bound and flat.
|
|
109
|
+
|
|
110
|
+
## Troubleshooting
|
|
111
|
+
|
|
112
|
+
- **Client hangs after "connected" or during connect, then raises
|
|
113
|
+
`TimeoutError` after ~60s.** The transfer-channel handshake could not reach the
|
|
114
|
+
server. Check that `--url`/`--control-url` on the client point at the *server's*
|
|
115
|
+
reachable address (a common mistake is a typo such as a trailing dot:
|
|
116
|
+
`10.0.0.5.:7600`), that the server is running, and that the ports are open
|
|
117
|
+
between hosts:
|
|
118
|
+
```bash
|
|
119
|
+
nc -vz <server-host> 7600 # transfer channel
|
|
120
|
+
nc -vz <server-host> 7610 # catalog side-channel
|
|
121
|
+
```
|
|
122
|
+
The handshake has a fixed 60s timeout, so a misconfiguration fails with a clear
|
|
123
|
+
error instead of hanging forever.
|
|
124
|
+
- **`page_size`/`object_size` mismatch error.** The client validates these against
|
|
125
|
+
the server's catalog; pass the same `--page-size` and `--object-size` on both.
|
|
126
|
+
- **Allocation `RuntimeError` on the server.** The source pool
|
|
127
|
+
(`--num-source-objects` × `--object-size`) must fit in `--buffer-size`; increase
|
|
128
|
+
the buffer or reduce the pool/object size.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""``python -m lmcache.tools.transfer_channel_benchmark`` entry point."""
|
|
3
|
+
|
|
4
|
+
# Standard
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
# First Party
|
|
9
|
+
# Only the torch-free config module is imported eagerly so that `-h` works
|
|
10
|
+
# without torch installed. The runtime (which needs torch) is imported after
|
|
11
|
+
# arguments are parsed.
|
|
12
|
+
from lmcache.tools.transfer_channel_benchmark.config import add_benchmark_arguments
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> int:
|
|
16
|
+
"""Parse arguments and run the transfer channel benchmark.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Process exit code (0 on success, 1 on failure).
|
|
20
|
+
"""
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
prog="python -m lmcache.tools.transfer_channel_benchmark",
|
|
23
|
+
description="Throughput benchmark for the LMCache transfer channel.",
|
|
24
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
25
|
+
)
|
|
26
|
+
add_benchmark_arguments(parser)
|
|
27
|
+
args = parser.parse_args()
|
|
28
|
+
|
|
29
|
+
# Imported here (not at module load) so this entry point can show --help
|
|
30
|
+
# without requiring torch; importing it raises a clear error if torch is
|
|
31
|
+
# missing.
|
|
32
|
+
# First Party
|
|
33
|
+
from lmcache.tools.transfer_channel_benchmark.benchmark import run_benchmark
|
|
34
|
+
|
|
35
|
+
return 0 if run_benchmark(args) else 1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
sys.exit(main())
|