@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 9607d5eb44
Fork 0

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

[Hybrid Allocator] Support full attention with different hidden size (#25101)

Signed-off-by: Chen Zhang <zhangch99@outlook.com>

Chen Zhang committed Sep 19, 2025 at 23:43 UTC 9607d5eb449711b349d4c2bee0a9c94afcc7ed14
6 files changed +325 -93
tests/v1/core/test_kv_cache_utils.py
+103 -15
@@ -18,12 +18,14 @@ from vllm.v1.core.kv_cache_manager import KVCacheManager
18 from vllm.v1.core.kv_cache_utils import (
19 BlockHash, FreeKVCacheBlockQueue, KVCacheBlock, PrefixCachingMetrics,
20 estimate_max_model_len, generate_block_hash_extra_keys,
21 - get_kv_cache_configs, get_max_concurrency_for_kv_cache_config,
22 - get_request_block_hasher, hash_block_tokens, init_none_hash,
23 - is_kv_cache_type_uniform, make_block_hash_with_group_id)
21 + generate_scheduler_kv_cache_config, get_kv_cache_configs,
22 + get_max_concurrency_for_kv_cache_config, get_request_block_hasher,
23 + hash_block_tokens, init_none_hash, is_kv_cache_spec_uniform,
24 + make_block_hash_with_group_id)
25 from vllm.v1.kv_cache_interface import (FullAttentionSpec, KVCacheConfig,
26 KVCacheGroupSpec, KVCacheSpec,
26 - KVCacheTensor, SlidingWindowSpec)
27 + KVCacheTensor, SlidingWindowSpec,
28 + UniformTypeKVCacheSpecs)
29 from vllm.v1.metrics.stats import PrefixCacheStats
30 from vllm.v1.request import Request
31
@@ -927,36 +929,36 @@ def test_merge_kv_cache_spec():
929 assert merged_layer_spec.sliding_window == 1
930
931
930 -def test_is_kv_cache_type_uniform():
932 +def test_is_kv_cache_spec_uniform():
933 kv_cache_spec = {
934 "layer_1": new_kv_cache_spec(num_kv_heads=32),
935 "layer_2": new_kv_cache_spec(num_kv_heads=32),
936 }
935 - assert is_kv_cache_type_uniform(kv_cache_spec)
937 + assert is_kv_cache_spec_uniform(kv_cache_spec)
938
939 kv_cache_spec = {
940 "layer_1": new_kv_cache_spec(num_kv_heads=32),
941 "layer_2": new_kv_cache_spec(num_kv_heads=32, sliding_window=1),
942 }
941 - assert is_kv_cache_type_uniform(kv_cache_spec)
943 + assert is_kv_cache_spec_uniform(kv_cache_spec)
944
945 kv_cache_spec = {
946 "layer_1": new_kv_cache_spec(num_kv_heads=32),
947 "layer_2": new_sliding_window_spec(num_kv_heads=32, sliding_window=1),
948 }
947 - assert not is_kv_cache_type_uniform(kv_cache_spec)
949 + assert not is_kv_cache_spec_uniform(kv_cache_spec)
950
951 kv_cache_spec = {
952 "layer_1": new_sliding_window_spec(num_kv_heads=32, sliding_window=1),
953 "layer_2": new_sliding_window_spec(num_kv_heads=32, sliding_window=1),
954 }
953 - assert is_kv_cache_type_uniform(kv_cache_spec)
955 + assert is_kv_cache_spec_uniform(kv_cache_spec)
956
957 kv_cache_spec = {
958 "layer_1": new_sliding_window_spec(num_kv_heads=32, sliding_window=1),
959 "layer_2": new_sliding_window_spec(num_kv_heads=32, sliding_window=2),
960 }
959 - assert not is_kv_cache_type_uniform(kv_cache_spec)
961 + assert not is_kv_cache_spec_uniform(kv_cache_spec)
962
963
964 @pytest.mark.parametrize(
@@ -1286,14 +1288,28 @@ def test_get_kv_cache_config_one_worker():
1288 ],
1289 )
1290
1289 - # different hidden size, unimplemented
1291 + # different hidden size
1292 kv_cache_specs_hybrid = {
1293 'layer_1': new_kv_cache_spec(head_size=128),
1292 - 'layer_2': new_kv_cache_spec(),
1294 + 'layer_2': new_kv_cache_spec(head_size=64),
1295 }
1294 - with pytest.raises(NotImplementedError):
1295 - get_kv_cache_configs(vllm_config, [kv_cache_specs_hybrid],
1296 - [mem_per_block_per_layer * 2 * 32])[0]
1296 + kv_cache_config_hybrid = get_kv_cache_configs(
1297 + vllm_config, [kv_cache_specs_hybrid],
1298 + [mem_per_block_per_layer * 3 * 32])[0]
1299 + assert kv_cache_config_hybrid == KVCacheConfig(
1300 + num_blocks=32,
1301 + kv_cache_tensors=[
1302 + KVCacheTensor(size=mem_per_block_per_layer * 32 * 2,
1303 + shared_by=["layer_1"]),
1304 + KVCacheTensor(size=mem_per_block_per_layer * 32,
1305 + shared_by=["layer_2"]),
1306 + ],
1307 + kv_cache_groups=[
1308 + KVCacheGroupSpec(["layer_1", "layer_2"],
1309 + UniformTypeKVCacheSpecs(
1310 + block_size=16,
1311 + kv_cache_specs=kv_cache_specs_hybrid))
1312 + ])
1313
1314 # Test num_gpu_blocks_override
1315 vllm_config.cache_config.num_gpu_blocks_override = 16
@@ -1324,3 +1340,75 @@ def test_get_kv_cache_configs_attention_free():
1340 kv_cache_groups=[],
1341 )
1342 ]
1343 +
1344 +
1345 +def test_generate_uniform_type_kv_cache_specs():
1346 + # All layers are full attention, can be merged
1347 + kv_cache_specs = {
1348 + 'layer_1': new_kv_cache_spec(),
1349 + 'layer_2': new_kv_cache_spec(head_size=128),
1350 + }
1351 + uniform_spec = UniformTypeKVCacheSpecs.from_specs(kv_cache_specs)
1352 + assert uniform_spec == UniformTypeKVCacheSpecs(
1353 + block_size=16, kv_cache_specs=kv_cache_specs)
1354 +
1355 + # Full attention + sliding window, cannot be merged
1356 + kv_cache_specs = {
1357 + 'layer_1': new_kv_cache_spec(),
1358 + 'layer_2': new_sliding_window_spec(sliding_window=1),
1359 + }
1360 + uniform_spec = UniformTypeKVCacheSpecs.from_specs(kv_cache_specs)
1361 + assert uniform_spec is None
1362 +
1363 + # different order of full attention + sliding window, cannot be merged
1364 + kv_cache_specs = {
1365 + 'layer_1': new_sliding_window_spec(sliding_window=1),
1366 + 'layer_2': new_kv_cache_spec(),
1367 + }
1368 + uniform_spec = UniformTypeKVCacheSpecs.from_specs(kv_cache_specs)
1369 + assert uniform_spec is None
1370 +
1371 + # Same-size sliding window, can be merged
1372 + kv_cache_specs = {
1373 + 'layer_1': new_sliding_window_spec(sliding_window=1),
1374 + 'layer_2': new_sliding_window_spec(sliding_window=1, head_size=128),
1375 + }
1376 + uniform_spec = UniformTypeKVCacheSpecs.from_specs(kv_cache_specs)
1377 + assert uniform_spec == UniformTypeKVCacheSpecs(
1378 + block_size=16, kv_cache_specs=kv_cache_specs)
1379 +
1380 + # different block sizes, cannot be merged
1381 + kv_cache_specs = {
1382 + 'layer_1': new_kv_cache_spec(block_size=16),
1383 + 'layer_2': new_kv_cache_spec(block_size=32),
1384 + }
1385 + uniform_spec = UniformTypeKVCacheSpecs.from_specs(kv_cache_specs)
1386 + assert uniform_spec is None
1387 +
1388 +
1389 +def test_generate_scheduler_kv_cache_config():
1390 + kv_cache_specs = {
1391 + 'layer_1': new_kv_cache_spec(),
1392 + 'layer_2': new_kv_cache_spec(head_size=128),
1393 + }
1394 + kv_cache_configs = [
1395 + KVCacheConfig(
1396 + num_blocks=10,
1397 + kv_cache_tensors=[],
1398 + kv_cache_groups=[
1399 + KVCacheGroupSpec(['layer_1', 'layer_2'],
1400 + UniformTypeKVCacheSpecs(
1401 + block_size=16,
1402 + kv_cache_specs=kv_cache_specs)),
1403 + ],
1404 + )
1405 + ]
1406 + scheduler_kv_cache_config = generate_scheduler_kv_cache_config(
1407 + kv_cache_configs)
1408 + assert scheduler_kv_cache_config == KVCacheConfig(
1409 + num_blocks=10,
1410 + kv_cache_tensors=[],
1411 + kv_cache_groups=[
1412 + KVCacheGroupSpec(['layer_1', 'layer_2'], new_kv_cache_spec())
1413 + ],
1414 + )
vllm/v1/core/kv_cache_utils.py
+108 -38
@@ -2,6 +2,7 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """KV-Cache Utilities."""
4
5 +import copy
6 import os
7 from collections import defaultdict, deque
8 from collections.abc import Iterable, Sequence
@@ -15,7 +16,8 @@ from vllm.utils import GiB_bytes, cdiv, sha256_cbor
16 from vllm.v1.kv_cache_interface import (ChunkedLocalAttentionSpec,
17 FullAttentionSpec, KVCacheConfig,
18 KVCacheGroupSpec, KVCacheSpec,
18 - KVCacheTensor, SlidingWindowSpec)
19 + KVCacheTensor, SlidingWindowSpec,
20 + UniformTypeKVCacheSpecs)
21 from vllm.v1.metrics.stats import PrefixCacheStats
22 from vllm.v1.request import Request
23
@@ -750,7 +752,7 @@ def create_kv_cache_group_specs(
752 return kv_cache_groups
753
754
753 -def is_kv_cache_type_uniform(kv_cache_spec: dict[str, KVCacheSpec]) -> bool:
755 +def is_kv_cache_spec_uniform(kv_cache_spec: dict[str, KVCacheSpec]) -> bool:
756 """
757 Whether all layers in the given KVCacheSpec have the same KV cache spec.
758 Note that we regard FullAttentionSpec with and without sliding window as
@@ -793,6 +795,21 @@ def get_max_concurrency_for_kv_cache_config(
795 return max_concurrency
796
797
798 +def may_override_num_blocks(vllm_config: VllmConfig, num_blocks: int) -> int:
799 + """
800 + Override the number of kv cache blocks if `num_gpu_blocks_override` is set.
801 + """
802 + if vllm_config.cache_config.num_gpu_blocks_override is not None:
803 + num_gpu_blocks_override = \
804 + vllm_config.cache_config.num_gpu_blocks_override
805 + logger.info(
806 + "Overriding num_gpu_blocks=%d with "
807 + "num_gpu_blocks_override=%d", num_blocks, num_gpu_blocks_override)
808 + num_blocks = num_gpu_blocks_override
809 +
810 + return num_blocks
811 +
812 +
813 def get_num_blocks(vllm_config: VllmConfig, num_layers: int,
814 available_memory: int, page_size: int) -> int:
815 """
@@ -806,13 +823,7 @@ def get_num_blocks(vllm_config: VllmConfig, num_layers: int,
823 """
824 num_blocks = int(available_memory // page_size // num_layers)
825 num_blocks = max(num_blocks, 0)
809 - if vllm_config.cache_config.num_gpu_blocks_override is not None:
810 - num_gpu_blocks_override = \
811 - vllm_config.cache_config.num_gpu_blocks_override
812 - logger.info(
813 - "Overriding num_gpu_blocks=%d with "
814 - "num_gpu_blocks_override=%d", num_blocks, num_gpu_blocks_override)
815 - num_blocks = num_gpu_blocks_override
826 + num_blocks = may_override_num_blocks(vllm_config, num_blocks)
827 return num_blocks
828
829
@@ -825,11 +836,11 @@ def get_uniform_page_size(kv_cache_spec: dict[str, KVCacheSpec]) -> int:
836 return page_sizes.pop()
837
838
828 -def _get_kv_cache_groups_uniform_type(
839 +def _get_kv_cache_groups_uniform_spec(
840 kv_cache_specs: dict[str, KVCacheSpec]) -> list[KVCacheGroupSpec]:
841 """
831 - Generates the KV cache configuration for a model with one type of KV cache.
832 - Divide the available memory equally among all layers.
842 + Generates the KV cache configuration for a model with the same KV cache
843 + spec for all layers.
844
845 Args:
846 kv_cache_specs: The kv cache spec of each attention layer in the model
@@ -842,6 +853,22 @@ def _get_kv_cache_groups_uniform_type(
853 [list(kv_cache_specs.keys())])
854
855
856 +def _get_kv_cache_groups_uniform_type(
857 + spec: UniformTypeKVCacheSpecs) -> list[KVCacheGroupSpec]:
858 + """
859 + Generates the KV cache configuration for a model with one type of KV cache
860 + but different hidden sizes. All layers are merged into one group.
861 +
862 + Args:
863 + spec: The UniformTypeKVCacheSpecs of the model
864 +
865 + Returns:
866 + The generated KVCacheGroupSpecs
867 + """
868 +
869 + return [KVCacheGroupSpec(list(spec.kv_cache_specs.keys()), spec)]
870 +
871 +
872 def is_kv_cache_page_size_uniform(
873 kv_cache_spec: dict[str, KVCacheSpec]) -> bool:
874 """
@@ -1000,28 +1027,45 @@ def get_kv_cache_config_from_groups(vllm_config: VllmConfig,
1027 )
1028
1029 # Determine how model runners should initialize the KV cache tensors.
1003 - # We will have group_size memory pools, each is shared by one layer from
1004 - # each group. As layers of different groups have different block table,
1005 - # they will use different parts of the shared Tensor.
1006 - # The memory layout for 3 groups (full.0, full.1), (sw.0, sw.2),
1007 - # (sw.1, padding) will be: (group_size = 2)
1008 - # full.0, sw.0, sw.1: share a Tensor with size=available_memory//2
1009 - # full.1, sw.2: share another Tensor with size=available_memory//2
1010 - group_size = max(len(group.layer_names) for group in kv_cache_groups)
1011 -
1012 - page_size = get_uniform_page_size(kv_cache_specs)
1013 - assert group_size > 0, "group_size must be greater than 0"
1014 - num_blocks = get_num_blocks(vllm_config, group_size, available_memory,
1015 - page_size)
1016 - per_memory_pool_size = page_size * num_blocks
1017 - kv_cache_tensors = []
1018 - for i in range(group_size):
1019 - shared_by = []
1020 - for j in range(len(kv_cache_groups)):
1021 - if i < len(kv_cache_groups[j].layer_names):
1022 - shared_by.append(kv_cache_groups[j].layer_names[i])
1023 - kv_cache_tensors.append(
1024 - KVCacheTensor(size=per_memory_pool_size, shared_by=shared_by))
1030 + if len(kv_cache_groups) == 1 and \
1031 + isinstance(kv_cache_groups[0].kv_cache_spec, UniformTypeKVCacheSpecs):
1032 + # Special case: all layers have the same type of KV cache but with
1033 + # different hidden size. Allocate different amount of memory for each
1034 + # layer based on its hidden size.
1035 + num_blocks = available_memory // kv_cache_groups[
1036 + 0].kv_cache_spec.page_size_bytes
1037 + num_blocks = may_override_num_blocks(vllm_config, num_blocks)
1038 + per_layer_specs = kv_cache_groups[0].kv_cache_spec.kv_cache_specs
1039 + kv_cache_tensors = [
1040 + KVCacheTensor(size=per_layer_specs[layer_name].page_size_bytes *
1041 + num_blocks,
1042 + shared_by=[layer_name])
1043 + for layer_name in kv_cache_groups[0].layer_names
1044 + ]
1045 + else:
1046 + # General case:
1047 + # We will have group_size memory pools, each is shared by one layer from
1048 + # each group. As layers of different groups have different block table,
1049 + # they will use different parts of the shared Tensor.
1050 + # The memory layout for 3 groups (full.0, full.1), (sw.0, sw.2),
1051 + # (sw.1, padding) will be: (group_size = 2)
1052 + # full.0, sw.0, sw.1: share a Tensor with size=available_memory//2
1053 + # full.1, sw.2: share another Tensor with size=available_memory//2
1054 + group_size = max(len(group.layer_names) for group in kv_cache_groups)
1055 +
1056 + page_size = get_uniform_page_size(kv_cache_specs)
1057 + assert group_size > 0, "group_size must be greater than 0"
1058 + num_blocks = get_num_blocks(vllm_config, group_size, available_memory,
1059 + page_size)
1060 + kv_cache_tensors = []
1061 + for i in range(group_size):
1062 + shared_by = []
1063 + for j in range(len(kv_cache_groups)):
1064 + if i < len(kv_cache_groups[j].layer_names):
1065 + shared_by.append(kv_cache_groups[j].layer_names[i])
1066 + kv_cache_tensors.append(
1067 + KVCacheTensor(size=page_size * num_blocks,
1068 + shared_by=shared_by))
1069
1070 kv_cache_config = KVCacheConfig(
1071 num_blocks=num_blocks,
@@ -1059,7 +1103,7 @@ def unify_hybrid_kv_cache_specs(kv_cache_spec: dict[str, KVCacheSpec]):
1103 kv_cache_spec: The kv cache spec of each attention layer in the model
1104 """
1105
1062 - if is_kv_cache_type_uniform(kv_cache_spec):
1106 + if is_kv_cache_spec_uniform(kv_cache_spec):
1107 return
1108
1109 logger.warning(
@@ -1097,7 +1141,7 @@ def unify_hybrid_kv_cache_specs(kv_cache_spec: dict[str, KVCacheSpec]):
1141 attention_chunk_size=spec.attention_chunk_size,
1142 )
1143
1100 - if not is_kv_cache_type_uniform(kv_cache_spec):
1144 + if not is_kv_cache_spec_uniform(kv_cache_spec):
1145 raise ValueError("Hybrid KV cache manager is disabled but failed to "
1146 "convert the KV cache specs to one unified type.")
1147
@@ -1122,11 +1166,16 @@ def get_kv_cache_groups(
1166 # This returns an empty list to allow for the KVCacheManager to handle
1167 # attention free models.
1168 return []
1125 - elif is_kv_cache_type_uniform(kv_cache_spec):
1169 + elif is_kv_cache_spec_uniform(kv_cache_spec):
1170 # KV cache of all layers are the same, which is true for
1171 # most models. Allocate the same amount of memory for
1172 # each layer.
1129 - return _get_kv_cache_groups_uniform_type(kv_cache_spec)
1173 + return _get_kv_cache_groups_uniform_spec(kv_cache_spec)
1174 + elif uniform_spec := UniformTypeKVCacheSpecs.from_specs(kv_cache_spec):
1175 + # All layers need the same number of token slots (e.g., all layers are
1176 + # full attention, or all layers are sliding window attention with the
1177 + # same window size). Put all layers into one group.
1178 + return _get_kv_cache_groups_uniform_type(uniform_spec)
1179 elif is_kv_cache_page_size_uniform(kv_cache_spec):
1180 # Model contains multiple attention types, but KV cache of all layers
1181 # have the same physical memory per block per layer. Split the layers
@@ -1137,6 +1186,27 @@ def get_kv_cache_groups(
1186 raise NotImplementedError
1187
1188
1189 +def generate_scheduler_kv_cache_config(
1190 + kv_cache_configs: list[KVCacheConfig]) -> KVCacheConfig:
1191 + """
1192 + Generate the KV cache configuration for the scheduler.
1193 + """
1194 + assert all([
1195 + cfg.num_blocks == kv_cache_configs[0].num_blocks
1196 + for cfg in kv_cache_configs
1197 + ])
1198 + # All workers have the same kv_cache_config except layer names, so use
1199 + # an arbitrary one to initialize the scheduler.
1200 + cfg = copy.deepcopy(kv_cache_configs[0])
1201 + for group in cfg.kv_cache_groups:
1202 + if isinstance(group.kv_cache_spec, UniformTypeKVCacheSpecs):
1203 + # All layers in the UniformTypeKVCacheSpecs have the same type,
1204 + # so use an arbitrary one to initialize the scheduler.
1205 + group.kv_cache_spec = next(
1206 + iter(group.kv_cache_spec.kv_cache_specs.values()))
1207 + return cfg
1208 +
1209 +
1210 def get_kv_cache_configs(vllm_config: VllmConfig,
1211 kv_cache_specs: list[dict[str, KVCacheSpec]],
1212 available_memory: list[int]) -> list[KVCacheConfig]:
vllm/v1/engine/core.py
+6 -10
@@ -29,7 +29,9 @@ from vllm.transformers_utils.config import (
29 maybe_register_config_serialize_by_value)
30 from vllm.utils import (decorate_logs, get_hash_fn_by_name, make_zmq_socket,
31 resolve_obj_by_qualname, set_process_title)
32 -from vllm.v1.core.kv_cache_utils import (BlockHash, get_kv_cache_configs,
32 +from vllm.v1.core.kv_cache_utils import (BlockHash,
33 + generate_scheduler_kv_cache_config,
34 + get_kv_cache_configs,
35 get_request_block_hasher,
36 init_none_hash)
37 from vllm.v1.core.sched.interface import SchedulerInterface
@@ -196,16 +198,10 @@ class EngineCore:
198
199 kv_cache_configs = get_kv_cache_configs(vllm_config, kv_cache_specs,
200 available_gpu_memory)
199 -
200 - # All workers have the same kv_cache_config except layer names, so use
201 - # an arbitrary one to initialize the scheduler.
202 - assert all([
203 - cfg.num_blocks == kv_cache_configs[0].num_blocks
204 - for cfg in kv_cache_configs
205 - ])
206 - num_gpu_blocks = kv_cache_configs[0].num_blocks
201 + scheduler_kv_cache_config = generate_scheduler_kv_cache_config(
202 + kv_cache_configs)
203 + num_gpu_blocks = scheduler_kv_cache_config.num_blocks
204 num_cpu_blocks = 0
208 - scheduler_kv_cache_config = kv_cache_configs[0]
205
206 # Initialize kv cache and warmup the execution
207 self.model_executor.initialize_from_config(kv_cache_configs)
vllm/v1/kv_cache_interface.py
+70
@@ -234,6 +234,76 @@ class CrossAttentionSpec(AttentionSpec):
234 return cdiv(max_encoder_len, self.block_size) * self.page_size_bytes
235
236
237 +@dataclass(frozen=True)
238 +class UniformTypeKVCacheSpecs(KVCacheSpec):
239 + """
240 + A KV cache spec for multiple layers with the same type of attention. Here,
241 + same types means always need the same number of token slots. For example,
242 + sliding window attentions with different window sizes are not the same type
243 + and should not be merged into one UniformTypeKVCacheSpecs.
244 + """
245 + kv_cache_specs: dict[str, KVCacheSpec]
246 +
247 + @property
248 + def page_size_bytes(self) -> int:
249 + return sum(spec.page_size_bytes
250 + for spec in self.kv_cache_specs.values())
251 +
252 + def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
253 + max_num_pages = max(
254 + cdiv(spec.max_memory_usage_bytes(vllm_config),
255 + spec.page_size_bytes)
256 + for spec in self.kv_cache_specs.values())
257 + return max_num_pages * self.page_size_bytes
258 +
259 + @classmethod
260 + def is_uniform_type(cls, kv_cache_specs: dict[str, KVCacheSpec]) -> bool:
261 + """
262 + Whether all layers have the same type of KV cache spec.
263 + """
264 + block_sizes = set(spec.block_size for spec in kv_cache_specs.values())
265 + if len(block_sizes) > 1:
266 + # Different block sizes, not uniform.
267 + return False
268 + one_spec = next(iter(kv_cache_specs.values()))
269 + if isinstance(one_spec, (FullAttentionSpec, CrossAttentionSpec)):
270 + return all(
271 + isinstance(spec, type(one_spec))
272 + for spec in kv_cache_specs.values())
273 + elif isinstance(one_spec, SlidingWindowSpec):
274 + return all(
275 + isinstance(spec, SlidingWindowSpec)
276 + and spec.sliding_window == one_spec.sliding_window
277 + for spec in kv_cache_specs.values())
278 + elif isinstance(one_spec, ChunkedLocalAttentionSpec):
279 + return all(
280 + isinstance(spec, ChunkedLocalAttentionSpec)
281 + and spec.attention_chunk_size == one_spec.attention_chunk_size
282 + for spec in kv_cache_specs.values())
283 + elif isinstance(one_spec, MambaSpec):
284 + return all(
285 + isinstance(spec, MambaSpec) and spec.num_speculative_blocks ==
286 + one_spec.num_speculative_blocks
287 + for spec in kv_cache_specs.values())
288 + else:
289 + # NOTE(Chen): Please add new branches for new KV cache spec types.
290 + raise NotImplementedError(
291 + f"Unsupported KV cache spec type: {type(one_spec)}")
292 +
293 + @classmethod
294 + def from_specs(cls, kv_cache_specs: dict[str,
295 + KVCacheSpec]) -> Optional[Self]:
296 + """
297 + Return a SameTypeKVCacheSpecs object if all layers have the same type
298 + of KV cache spec. Return None if not.
299 + """
300 + if cls.is_uniform_type(kv_cache_specs):
301 + block_size = next(iter(kv_cache_specs.values())).block_size
302 + return cls(block_size=block_size, kv_cache_specs=kv_cache_specs)
303 + else:
304 + return None
305 +
306 +
307 @dataclass
308 class KVCacheTensor:
309 """
vllm/v1/worker/gpu_model_runner.py
+36 -29
@@ -8,7 +8,7 @@ from collections import defaultdict
8 from collections.abc import Iterator
9 from contextlib import contextmanager
10 from copy import deepcopy
11 -from typing import TYPE_CHECKING, Any, Optional, Union, cast
11 +from typing import TYPE_CHECKING, Any, NamedTuple, Optional, Union, cast
12
13 import numpy as np
14 import torch
@@ -74,7 +74,8 @@ from vllm.v1.kv_cache_interface import (AttentionSpec,
74 EncoderOnlyAttentionSpec,
75 FullAttentionSpec, KVCacheConfig,
76 KVCacheGroupSpec, KVCacheSpec,
77 - MambaSpec, SlidingWindowSpec)
77 + MambaSpec, SlidingWindowSpec,
78 + UniformTypeKVCacheSpecs)
79 # yapf: enable
80 from vllm.v1.outputs import (EMPTY_MODEL_RUNNER_OUTPUT, AsyncModelRunnerOutput,
81 DraftTokenIds, LogprobsLists, LogprobsTensors,
@@ -1187,7 +1188,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1188 common_prefix_len = self._compute_cascade_attn_prefix_len(
1189 num_scheduled_tokens,
1190 num_common_prefix_blocks,
1190 - kv_cache_group_spec.kv_cache_spec,
1191 + attn_group.kv_cache_spec,
1192 builder,
1193 )
1194
@@ -3453,12 +3454,16 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3454 assert len(self.attn_groups) == 0, \
3455 "Attention backends are already initialized"
3456
3456 - def get_attn_backends_for_layers(
3457 - layer_names: list[str]
3458 - ) -> dict[type[AttentionBackend], list[str]]:
3459 - layers = get_layers_from_vllm_config(self.vllm_config,
3460 - AttentionLayerBase,
3461 - layer_names)
3457 + class AttentionGroupKey(NamedTuple):
3458 + attn_backend: type[AttentionBackend]
3459 + kv_cache_spec: KVCacheSpec
3460 +
3461 + def get_attn_backends_for_group(
3462 + kv_cache_group_spec: KVCacheGroupSpec,
3463 + ) -> dict[AttentionGroupKey, list[str]]:
3464 + layers = get_layers_from_vllm_config(
3465 + self.vllm_config, AttentionLayerBase,
3466 + kv_cache_group_spec.layer_names)
3467 attn_backends = {}
3468 attn_backend_layers = defaultdict(list)
3469 # Dedupe based on full class name; this is a bit safer than
@@ -3466,7 +3471,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3471 # attention backend subclasses (e.g. ChunkedLocalAttention) unless
3472 # they are cached correctly, there will be different objects per
3473 # layer.
3469 - for layer_name in layer_names:
3474 + for layer_name in kv_cache_group_spec.layer_names:
3475 attn_backend = layers[layer_name].get_attn_backend()
3476
3477 if layer_name in self.kv_sharing_fast_prefill_eligible_layers:
@@ -3475,8 +3480,14 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3480 attn_backend,
3481 )
3482
3478 - key = attn_backend.full_cls_name()
3479 - attn_backends[key] = attn_backend
3483 + full_cls_name = attn_backend.full_cls_name()
3484 + layer_kv_cache_spec = kv_cache_group_spec.kv_cache_spec
3485 + if isinstance(layer_kv_cache_spec, UniformTypeKVCacheSpecs):
3486 + layer_kv_cache_spec = layer_kv_cache_spec.kv_cache_specs[
3487 + layer_name]
3488 + key = (full_cls_name, layer_kv_cache_spec)
3489 + attn_backends[key] = AttentionGroupKey(attn_backend,
3490 + layer_kv_cache_spec)
3491 attn_backend_layers[key].append(layer_name)
3492 return {
3493 attn_backends[k]: v
@@ -3484,11 +3495,11 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3495 }
3496
3497 def create_attn_groups(
3487 - attn_backends_map: dict[AttentionBackend, list[str]],
3488 - kv_cache_spec: KVCacheSpec,
3498 + attn_backends_map: dict[AttentionGroupKey, list[str]],
3499 ) -> list[AttentionGroup]:
3500 attn_groups: list[AttentionGroup] = []
3491 - for attn_backend, layer_names in attn_backends_map.items():
3501 + for (attn_backend,
3502 + kv_cache_spec), layer_names in attn_backends_map.items():
3503 attn_metadata_builders = []
3504 attn_metadata_builders.append(attn_backend.get_builder_cls()(
3505 kv_cache_spec,
@@ -3506,16 +3517,13 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3517 ))
3518 attn_group = AttentionGroup(attn_backend,
3519 attn_metadata_builders,
3509 - layer_names)
3520 + layer_names, kv_cache_spec)
3521 attn_groups.append(attn_group)
3522 return attn_groups
3523
3524 for kv_cache_group_spec in kv_cache_config.kv_cache_groups:
3514 - kv_cache_spec = kv_cache_group_spec.kv_cache_spec
3515 - attn_backends = get_attn_backends_for_layers(
3516 - kv_cache_group_spec.layer_names)
3517 - self.attn_groups.append(
3518 - create_attn_groups(attn_backends, kv_cache_spec))
3525 + attn_backends = get_attn_backends_for_group(kv_cache_group_spec)
3526 + self.attn_groups.append(create_attn_groups(attn_backends))
3527
3528 # Calculate reorder batch threshold (if needed)
3529 self.calculate_reorder_batch_threshold()
@@ -3680,14 +3688,11 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3688 def _attn_group_iterator(self) -> Iterator[AttentionGroup]:
3689 return itertools.chain.from_iterable(self.attn_groups)
3690
3683 - def _kv_cache_spec_attn_group_iterator(
3684 - self) -> Iterator[tuple[KVCacheSpec, AttentionGroup]]:
3691 + def _kv_cache_spec_attn_group_iterator(self) -> Iterator[AttentionGroup]:
3692 if not self.kv_cache_config.kv_cache_groups:
3693 return
3687 - for kv_cache_spec_id, attn_groups in enumerate(self.attn_groups):
3688 - for attn_group in attn_groups:
3689 - yield self.kv_cache_config.kv_cache_groups[
3690 - kv_cache_spec_id].kv_cache_spec, attn_group
3694 + for attn_groups in self.attn_groups:
3695 + yield from attn_groups
3696
3697 def _reshape_kv_cache_tensors(
3698 self,
@@ -3707,7 +3712,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3712 """
3713 kv_caches: dict[str, torch.Tensor] = {}
3714 has_attn, has_mamba = False, False
3710 - for kv_cache_spec, group in self._kv_cache_spec_attn_group_iterator():
3715 + for group in self._kv_cache_spec_attn_group_iterator():
3716 + kv_cache_spec = group.kv_cache_spec
3717 attn_backend = group.backend
3718 for layer_name in group.layer_names:
3719 if layer_name in self.runner_only_attn_layers:
@@ -3787,7 +3793,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3793 kv_caches: The KV cache buffer of each layer.
3794 """
3795
3790 - for kv_cache_spec, group in self._kv_cache_spec_attn_group_iterator():
3796 + for group in self._kv_cache_spec_attn_group_iterator():
3797 + kv_cache_spec = group.kv_cache_spec
3798 for layer_name in group.layer_names:
3799 kv_cache = kv_caches[layer_name]
3800 if (isinstance(kv_cache_spec, AttentionSpec)
vllm/v1/worker/utils.py
+2 -1
@@ -15,7 +15,7 @@ from vllm.multimodal.registry import MultiModalRegistry
15 from vllm.platforms import current_platform
16 from vllm.v1.attention.backends.utils import AttentionMetadataBuilder
17 from vllm.v1.core.encoder_cache_manager import compute_mm_encoder_budget
18 -from vllm.v1.kv_cache_interface import KVCacheGroupSpec
18 +from vllm.v1.kv_cache_interface import KVCacheGroupSpec, KVCacheSpec
19
20 if TYPE_CHECKING:
21 from vllm.attention.layer import Attention
@@ -132,6 +132,7 @@ class AttentionGroup:
132 backend: type[AttentionBackend]
133 metadata_builders: list[AttentionMetadataBuilder]
134 layer_names: list[str]
135 + kv_cache_spec: KVCacheSpec
136
137 def get_metadata_builder(self,
138 ubatch_id: Optional[int] = None