A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 05:30 UTC
[Core][Hybrid allocator + kv connector 1/n] Enable hybrid allocator + KV cache connector (#25712)
Signed-off-by: KuntaiDu <kuntai@uchicago.edu> Signed-off-by: Kuntai Du <kuntai@uchicago.edu>
Kuntai Du committed
Oct 24, 2025 at 23:34 UTC
b853540388bf8c796e1c88dabceaeebaab73621e
15 files changed
+113
-18
tests/v1/core/test_scheduler.py
+7
@@ -899,6 +899,7 @@ def test_kv_connector_basic():
899
scheduler = create_scheduler(
900
enable_prefix_caching=True,
901
use_kv_connector=True,
902
+ disable_hybrid_kv_cache_manager=True,
903
)
904
NUM_TOTAL_BLOCKS = scheduler.kv_cache_manager.block_pool.get_num_free_blocks()
905
BLOCK_SIZE = scheduler.cache_config.block_size
@@ -1024,6 +1025,7 @@ def test_external_prefix_cache_metrics():
1025
scheduler = create_scheduler(
1026
enable_prefix_caching=False,
1027
use_kv_connector=True,
1028
+ disable_hybrid_kv_cache_manager=True,
1029
)
1030
1031
# Mock connector to simulate a partial external cache hit
@@ -1088,6 +1090,7 @@ def test_kv_connector_unable_to_allocate():
1090
use_kv_connector=True,
1091
block_size=BLOCK_SIZE,
1092
num_blocks=NUM_BLOCKS,
1093
+ disable_hybrid_kv_cache_manager=True,
1094
)
1095
NUM_MATCHED_NEW_TOKENS = BLOCK_SIZE * 2
1096
scheduler.connector.get_num_new_matched_tokens = Mock(name="method")
@@ -1171,6 +1174,7 @@ def test_kv_connector_handles_preemption():
1174
use_kv_connector=True,
1175
block_size=BLOCK_SIZE,
1176
num_blocks=NUM_BLOCKS,
1177
+ disable_hybrid_kv_cache_manager=True,
1178
)
1179
1180
NUM_MATCHED_NEW_TOKENS = BLOCK_SIZE
@@ -1387,6 +1391,7 @@ def create_scheduler_with_priority(
1391
block_size: int = 16,
1392
max_model_len: int | None = None,
1393
num_speculative_tokens: int | None = None,
1394
+ disable_hybrid_kv_cache_manager: bool = False,
1395
) -> Scheduler:
1396
"""Create scheduler with priority policy enabled.
1397
@@ -1411,6 +1416,7 @@ def create_scheduler_with_priority(
1416
disable_chunked_mm_input=disable_chunked_mm_input,
1417
enable_chunked_prefill=True,
1418
policy="priority", # Enable priority scheduling
1419
+ disable_hybrid_kv_cache_manager=disable_hybrid_kv_cache_manager,
1420
)
1421
model_config = ModelConfig(
1422
model=model,
@@ -2018,6 +2024,7 @@ def test_priority_scheduling_preemption_and_resumption_when_out_of_kv():
2024
num_blocks=5, # Can hold 64 tokens (first block is null)
2025
block_size=16, # Standard block size
2026
use_kv_connector=True,
2027
+ disable_hybrid_kv_cache_manager=True,
2028
)
2029
2030
# Create a request and schedule it
tests/v1/core/utils.py
+2
@@ -46,6 +46,7 @@ def create_scheduler(
46
num_speculative_tokens: int | None = None,
47
skip_tokenizer_init: bool = False,
48
async_scheduling: bool = False,
49
+ disable_hybrid_kv_cache_manager: bool = False,
50
) -> Scheduler | AsyncScheduler:
51
"""Create scheduler under test.
52
@@ -70,6 +71,7 @@ def create_scheduler(
71
disable_chunked_mm_input=disable_chunked_mm_input,
72
enable_chunked_prefill=True,
73
async_scheduling=async_scheduling,
74
+ disable_hybrid_kv_cache_manager=disable_hybrid_kv_cache_manager,
75
)
76
model_config = ModelConfig(
77
model=model,
tests/v1/kv_connector/nixl_integration/run_accuracy_test.sh
+2
@@ -136,6 +136,7 @@ run_tests_for_model() {
136
vllm serve $model_name \
137
--port $PORT \
138
--enforce-eager \
139
+ --disable-hybrid-kv-cache-manager \
140
--gpu-memory-utilization $GPU_MEMORY_UTILIZATION \
141
--tensor-parallel-size $PREFILLER_TP_SIZE \
142
--kv-transfer-config '$KV_CONFIG'"
@@ -178,6 +179,7 @@ run_tests_for_model() {
179
--port $PORT \
180
--enforce-eager \
181
--gpu-memory-utilization $GPU_MEMORY_UTILIZATION \
182
+ --disable-hybrid-kv-cache-manager \
183
--kv-transfer-config '$KV_CONFIG'"
184
185
# DP-EP attention mode
tests/v1/kv_connector/nixl_integration/run_edge_case_test.sh
+2
@@ -85,6 +85,7 @@ run_tests_for_model() {
85
--port $PREFILL_PORT \
86
--enforce-eager \
87
--gpu-memory-utilization 0.2 \
88
+ --disable-hybrid-kv-cache-manager \
89
--kv-transfer-config '$KV_CONFIG'"
90
91
if [ -n "$model_args" ]; then
@@ -103,6 +104,7 @@ run_tests_for_model() {
104
--port $DECODE_PORT \
105
--enforce-eager \
106
--gpu-memory-utilization 0.2 \
107
+ --disable-hybrid-kv-cache-manager \
108
--kv-transfer-config '$KV_CONFIG'"
109
110
if [ -n "$model_args" ]; then
tests/v1/kv_connector/unit/test_multi_connector.py
+1
@@ -114,6 +114,7 @@ def test_multi_shared_storage_connector_consistency():
114
enforce_eager=True,
115
gpu_memory_utilization=0.5,
116
kv_transfer_config=kv_transfer_config,
117
+ disable_hybrid_kv_cache_manager=True,
118
)
119
# Run generation - this should trigger saving KV cache
120
_ = llm.generate(PROMPTS, SAMPLING_PARAMS)
tests/v1/kv_connector/unit/test_nixl_connector.py
+1
@@ -932,6 +932,7 @@ def test_abort_timeout_on_prefiller(monkeypatch, distributed_executor_backend):
932
"gpu_memory_utilization": 0.5,
933
"kv_transfer_config": kv_transfer_config,
934
"distributed_executor_backend": distributed_executor_backend,
935
+ "disable_hybrid_kv_cache_manager": True,
936
}
937
938
timeout = 6
tests/v1/kv_connector/unit/test_shared_storage_connector.py
+1
@@ -132,6 +132,7 @@ def test_shared_storage_connector_hashes(tmp_path):
132
enforce_eager=True,
133
kv_transfer_config=kv_transfer_config,
134
limit_mm_per_prompt={"image": 2},
135
+ disable_hybrid_kv_cache_manager=True,
136
)
137
138
# don't put this import at the top level
tests/v1/kv_connector/unit/utils.py
+3
@@ -91,6 +91,9 @@ def create_vllm_config(
91
max_num_batched_tokens=max_num_batched_tokens,
92
max_model_len=max_model_len,
93
enable_chunked_prefill=enable_chunked_prefill,
94
+ # Disable hybrid KV cache manager for testing
95
+ # Should be removed after we support hybrid KV cache manager-based testing.
96
+ disable_hybrid_kv_cache_manager=True,
97
)
98
model_config = ModelConfig(
99
model=model,
tests/v1/kv_offload/test_cpu_offloading.py
+1
@@ -27,6 +27,7 @@ def test_cpu_offloading(cpu_block_size: int) -> None:
27
model="meta-llama/Llama-3.2-1B-Instruct",
28
gpu_memory_utilization=0.5,
29
kv_transfer_config=kv_transfer_config,
30
+ disable_hybrid_kv_cache_manager=True,
31
)
32
33
prompts = ["Hi " * 100]
vllm/config/vllm.py
+3
-3
@@ -40,11 +40,14 @@ if TYPE_CHECKING:
40
from transformers import PretrainedConfig
41
42
from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
43
+ from vllm.v1.kv_cache_interface import KVCacheConfig
44
else:
45
PretrainedConfig = Any
46
47
QuantizationConfig = Any
48
49
+ KVCacheConfig = Any
50
+
51
logger = init_logger(__name__)
52
53
@@ -568,9 +571,6 @@ class VllmConfig:
571
if not current_platform.support_hybrid_kv_cache():
572
# Hybrid KV cache manager is not supported on non-GPU platforms.
573
self.scheduler_config.disable_hybrid_kv_cache_manager = True
571
- if self.kv_transfer_config is not None:
572
- # Hybrid KV cache manager is not compatible with KV transfer.
573
- self.scheduler_config.disable_hybrid_kv_cache_manager = True
574
if self.kv_events_config is not None:
575
# Hybrid KV cache manager is not compatible with KV events.
576
self.scheduler_config.disable_hybrid_kv_cache_manager = True
vllm/distributed/kv_transfer/kv_connector/factory.py
+15
-3
@@ -6,15 +6,18 @@ from collections.abc import Callable
6
from typing import TYPE_CHECKING, cast
7
8
import vllm.envs as envs
9
+from vllm.config import VllmConfig
10
from vllm.distributed.kv_transfer.kv_connector.base import (
11
KVConnectorBase,
12
KVConnectorBaseType,
13
)
13
-from vllm.distributed.kv_transfer.kv_connector.v1 import KVConnectorRole
14
+from vllm.distributed.kv_transfer.kv_connector.v1 import (
15
+ KVConnectorRole,
16
+ supports_hma,
17
+)
18
from vllm.logger import init_logger
19
20
if TYPE_CHECKING:
17
- from vllm.config import VllmConfig
21
from vllm.config.kv_transfer import KVTransferConfig
22
23
logger = init_logger(__name__)
@@ -38,7 +41,7 @@ class KVConnectorFactory:
41
@classmethod
42
def create_connector(
43
cls,
41
- config: "VllmConfig",
44
+ config: VllmConfig,
45
role: KVConnectorRole,
46
) -> KVConnectorBase:
47
if not envs.VLLM_USE_V1:
@@ -51,6 +54,15 @@ class KVConnectorFactory:
54
if kv_transfer_config is None:
55
raise ValueError("kv_transfer_config must be set to create a connector")
56
connector_cls = cls.get_connector_class(kv_transfer_config)
57
+
58
+ # check if the connector supports HMA
59
+ hma_enabled = not config.scheduler_config.disable_hybrid_kv_cache_manager
60
+ if hma_enabled and not supports_hma(connector_cls):
61
+ raise ValueError(
62
+ f"Connector {connector_cls.__name__} does not support HMA but "
63
+ f"HMA is enabled. Please set `--disable-hybrid-kv-cache-manager`."
64
+ )
65
+
66
logger.info(
67
"Creating v1 connector with name: %s and engine_id: %s",
68
connector_cls.__name__,
vllm/distributed/kv_transfer/kv_connector/v1/__init__.py
+9
-1
@@ -3,9 +3,17 @@
3
from vllm.distributed.kv_transfer.kv_connector.v1.base import (
4
KVConnectorBase_V1,
5
KVConnectorRole,
6
+ SupportsHMA,
7
+ supports_hma,
8
)
9
from vllm.distributed.kv_transfer.kv_connector.v1.decode_bench_connector import ( # noqa E:501
10
DecodeBenchConnector,
11
)
12
11
-__all__ = ["KVConnectorRole", "KVConnectorBase_V1", "DecodeBenchConnector"]
13
+__all__ = [
14
+ "KVConnectorRole",
15
+ "KVConnectorBase_V1",
16
+ "supports_hma",
17
+ "SupportsHMA",
18
+ "DecodeBenchConnector",
19
+]
vllm/distributed/kv_transfer/kv_connector/v1/base.py
+40
-1
@@ -70,6 +70,45 @@ CopyBlocksOp = Callable[
70
logger = init_logger(__name__)
71
72
73
+class SupportsHMA(ABC):
74
+ """
75
+ The class that indicates the corresponding connector supports hybrid memory
76
+ allocator (HMA).
77
+ This is required to use the connector together with hybrid memory allocator.
78
+ """
79
+
80
+ @abstractmethod
81
+ def request_finished_all_groups(
82
+ self,
83
+ request: "Request",
84
+ block_ids: tuple[list[int], ...],
85
+ ) -> tuple[bool, dict[str, Any] | None]:
86
+ """
87
+ Called exactly once when a request has finished for all kv cache groups,
88
+ before its blocks are freed for each group.
89
+
90
+ NOTE(Kuntai): This function is only supported by connectors that support HMA.
91
+
92
+ The connector may assumes responsibility for freeing the blocks
93
+ asynchronously by returning True.
94
+
95
+ Returns:
96
+ True if the request is being saved/sent asynchronously and blocks
97
+ should not be freed until the request_id is returned from
98
+ get_finished().
99
+ Optional KVTransferParams to be included in the request outputs
100
+ returned by the engine.
101
+ """
102
+ raise NotImplementedError
103
+
104
+
105
+def supports_hma(connector: Any) -> bool:
106
+ if isinstance(connector, type):
107
+ return issubclass(connector, SupportsHMA)
108
+ else:
109
+ return isinstance(connector, SupportsHMA)
110
+
111
+
112
class KVConnectorRole(enum.Enum):
113
# Connector running in the scheduler process
114
SCHEDULER = 0
@@ -370,7 +409,7 @@ class KVConnectorBase_V1(ABC):
409
Called exactly once when a request has finished, before its blocks are
410
freed.
411
373
- The connector may assumes responsibility for freeing the the blocks
412
+ The connector may assumes responsibility for freeing the blocks
413
asynchronously by returning True.
414
415
Returns:
vllm/v1/core/sched/scheduler.py
+17
-8
@@ -1,6 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-
3
+import copy
4
import itertools
5
import time
6
from collections import defaultdict
@@ -13,6 +13,7 @@ from vllm.distributed.kv_transfer.kv_connector.factory import KVConnectorFactory
13
from vllm.distributed.kv_transfer.kv_connector.v1 import (
14
KVConnectorBase_V1,
15
KVConnectorRole,
16
+ supports_hma,
17
)
18
from vllm.distributed.kv_transfer.kv_connector.v1.metrics import KVConnectorStats
19
from vllm.logger import init_logger
@@ -86,15 +87,14 @@ class Scheduler(SchedulerInterface):
87
self.connector = None
88
self.connector_prefix_cache_stats: PrefixCacheStats | None = None
89
if self.vllm_config.kv_transfer_config is not None:
89
- assert len(self.kv_cache_config.kv_cache_groups) == 1, (
90
- "Multiple KV cache groups are not currently supported "
91
- "with KV connectors"
92
- )
90
assert not self.is_encoder_decoder, (
91
"Encoder-decoder models are not currently supported with KV connectors"
92
)
93
+
94
+ connector_vllm_config = copy.copy(self.vllm_config)
95
+ connector_vllm_config.kv_cache_config = copy.copy(kv_cache_config)
96
self.connector = KVConnectorFactory.create_connector(
97
- config=self.vllm_config, role=KVConnectorRole.SCHEDULER
97
+ config=connector_vllm_config, role=KVConnectorRole.SCHEDULER
98
)
99
if self.log_stats:
100
self.connector_prefix_cache_stats = PrefixCacheStats()
@@ -1324,8 +1324,17 @@ class Scheduler(SchedulerInterface):
1324
if self.connector is None:
1325
return False, None
1326
1327
- (block_ids,) = self.kv_cache_manager.get_block_ids(request.request_id)
1328
- return self.connector.request_finished(request, block_ids)
1327
+ block_ids = self.kv_cache_manager.get_block_ids(request.request_id)
1328
+
1329
+ if not supports_hma(self.connector):
1330
+ # NOTE(Kuntai): We should deprecate this code path after we enforce
1331
+ # all connectors to support HMA.
1332
+ # Hybrid memory allocator should be already turned off for this
1333
+ # code path, but let's double-check here.
1334
+ assert len(self.kv_cache_config.kv_cache_groups) == 1
1335
+ return self.connector.request_finished(request, block_ids[0])
1336
+ else:
1337
+ return self.connector.request_finished(request, block_ids)
1338
1339
def _update_waiting_for_remote_kv(self, request: Request) -> bool:
1340
"""
vllm/v1/worker/gpu_worker.py
+9
-2
@@ -331,6 +331,15 @@ class Worker(WorkerBase):
331
def initialize_from_config(self, kv_cache_config: KVCacheConfig) -> None:
332
"""Allocate GPU KV cache with the specified kv_cache_config."""
333
334
+ # Init kv cache connector here, because it requires
335
+ # `kv_cache_config`.
336
+ # NOTE(Kuntai): This need to be done before `initialize_kv_cache`,
337
+ # because `initialize_kv_cache` will inject kv cache groups not
338
+ # related to kv cache connector (e.g. kv cache sharing layers).
339
+ connector_vllm_config = copy.copy(self.vllm_config)
340
+ connector_vllm_config.kv_cache_config = copy.copy(kv_cache_config)
341
+ ensure_kv_transfer_initialized(connector_vllm_config)
342
+
343
if self.vllm_config.model_config.enable_sleep_mode:
344
from vllm.device_allocator.cumem import CuMemAllocator
345
@@ -783,5 +792,3 @@ def init_worker_distributed_environment(
792
parallel_config.pipeline_parallel_size,
793
parallel_config.decode_context_parallel_size,
794
)
786
-
787
- ensure_kv_transfer_initialized(vllm_config)