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