@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 755fa8b657
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, 23:45 UTC

[KVCache] Make KVCacheSpec hashable (#21791)

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

Chen Zhang committed Jul 29, 2025 at 04:58 UTC 755fa8b657e3666cc93b08a6d2b9a50d0f46c37e
5 files changed +100 -88
tests/v1/core/test_kv_cache_utils.py
+33 -1
@@ -17,7 +17,7 @@ from vllm.v1.core.kv_cache_utils import (
17 estimate_max_model_len, generate_block_hash_extra_keys,
18 get_kv_cache_config, get_max_concurrency_for_kv_cache_config,
19 hash_block_tokens, hash_request_tokens, init_none_hash,
20 - unify_kv_cache_configs)
20 + is_kv_cache_type_uniform, unify_kv_cache_configs)
21 from vllm.v1.kv_cache_interface import (FullAttentionSpec, KVCacheConfig,
22 KVCacheGroupSpec, KVCacheTensor,
23 SlidingWindowSpec)
@@ -685,6 +685,38 @@ def test_merge_kv_cache_spec():
685 assert merged_layer_spec.sliding_window == 1
686
687
688 +def test_is_kv_cache_type_uniform():
689 + kv_cache_spec = {
690 + "layer_1": new_kv_cache_spec(num_kv_heads=32),
691 + "layer_2": new_kv_cache_spec(num_kv_heads=32),
692 + }
693 + assert is_kv_cache_type_uniform(kv_cache_spec)
694 +
695 + kv_cache_spec = {
696 + "layer_1": new_kv_cache_spec(num_kv_heads=32),
697 + "layer_2": new_kv_cache_spec(num_kv_heads=32, sliding_window=1),
698 + }
699 + assert is_kv_cache_type_uniform(kv_cache_spec)
700 +
701 + kv_cache_spec = {
702 + "layer_1": new_kv_cache_spec(num_kv_heads=32),
703 + "layer_2": new_sliding_window_spec(num_kv_heads=32, sliding_window=1),
704 + }
705 + assert not is_kv_cache_type_uniform(kv_cache_spec)
706 +
707 + kv_cache_spec = {
708 + "layer_1": new_sliding_window_spec(num_kv_heads=32, sliding_window=1),
709 + "layer_2": new_sliding_window_spec(num_kv_heads=32, sliding_window=1),
710 + }
711 + assert is_kv_cache_type_uniform(kv_cache_spec)
712 +
713 + kv_cache_spec = {
714 + "layer_1": new_sliding_window_spec(num_kv_heads=32, sliding_window=1),
715 + "layer_2": new_sliding_window_spec(num_kv_heads=32, sliding_window=2),
716 + }
717 + assert not is_kv_cache_type_uniform(kv_cache_spec)
718 +
719 +
720 @pytest.mark.parametrize(
721 ("model_id", "max_model_len", "want_estimated_max_len"), [
722 ("Qwen/Qwen1.5-7B", 16385, 16384),
tests/v1/e2e/test_correctness_sliding_window.py
+6 -2
@@ -30,7 +30,9 @@ model_config = {
30 ])
31 @pytest.mark.parametrize("batch_size", [5])
32 @pytest.mark.parametrize("seed", [1])
33 -def test_sliding_window_retrieval(monkeypatch, model, batch_size, seed):
33 +@pytest.mark.parametrize("disable_hybrid_kv_cache_manager", [True, False])
34 +def test_sliding_window_retrieval(monkeypatch, model, batch_size, seed,
35 + disable_hybrid_kv_cache_manager):
36 """
37 The test does a bunch of assignments "x1 = 10\nx2 = 33\n..." and then
38 asks for value of one of them (which is outside the sliding window).
@@ -42,7 +44,9 @@ def test_sliding_window_retrieval(monkeypatch, model, batch_size, seed):
44
45 test_config = model_config[model]
46
45 - llm = LLM(model=model)
47 + llm = LLM(
48 + model=model,
49 + disable_hybrid_kv_cache_manager=disable_hybrid_kv_cache_manager)
50 sampling_params = SamplingParams(temperature=0.0, max_tokens=100)
51
52 prompts, answer, indices = prep_prompts(batch_size,
vllm/v1/core/kv_cache_coordinator.py
+14 -17
@@ -7,7 +7,8 @@ from vllm.v1.core.block_pool import BlockPool
7 from vllm.v1.core.kv_cache_utils import BlockHash, KVCacheBlock
8 from vllm.v1.core.single_type_kv_cache_manager import (
9 FullAttentionManager, get_manager_for_kv_cache_spec)
10 -from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig
10 +from vllm.v1.kv_cache_interface import (FullAttentionSpec, KVCacheConfig,
11 + KVCacheSpec)
12 from vllm.v1.request import Request
13
14
@@ -258,44 +259,40 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
259 one of them is full attention. Then, split the kv cache groups into full
260 attention groups and other groups.
261 """
261 - full_attention_type_id: Optional[str] = None
262 - other_type_id: Optional[str] = None
262 + full_attention_spec: Optional[FullAttentionSpec] = None
263 + other_spec: Optional[KVCacheSpec] = None
264 self.full_attention_group_ids: list[int] = []
265 self.other_group_ids: list[int] = []
266 for i, g in enumerate(self.kv_cache_config.kv_cache_groups):
267 if isinstance(g.kv_cache_spec, FullAttentionSpec):
267 - if full_attention_type_id is None:
268 - full_attention_type_id = g.kv_cache_spec.type_id
268 + if full_attention_spec is None:
269 + full_attention_spec = g.kv_cache_spec
270 else:
270 - assert full_attention_type_id == g.kv_cache_spec.type_id, (
271 + assert full_attention_spec == g.kv_cache_spec, (
272 "HybridKVCacheCoordinator assumes exactly one type of "
273 "full attention groups now.")
274 self.full_attention_group_ids.append(i)
275 else:
275 - if other_type_id is None:
276 - other_type_id = g.kv_cache_spec.type_id
276 + if other_spec is None:
277 + other_spec = g.kv_cache_spec
278 else:
278 - assert other_type_id == g.kv_cache_spec.type_id, (
279 + assert other_spec == g.kv_cache_spec, (
280 "HybridKVCacheCoordinator assumes "
281 "exactly one other type of groups now.")
282 self.other_group_ids.append(i)
283
283 - assert full_attention_type_id is not None, (
284 + assert full_attention_spec is not None, (
285 "HybridKVCacheCoordinator assumes exactly one type of full "
286 "attention groups now.")
286 - assert other_type_id is not None, (
287 + assert other_spec is not None, (
288 "HybridKVCacheCoordinator assumes exactly one type of other "
289 "groups now.")
290
291 self.full_attention_manager_cls = FullAttentionManager
292 self.other_attention_cls = self.single_type_managers[
293 self.other_group_ids[0]].__class__
293 -
294 - self.full_attention_spec = self.kv_cache_config.kv_cache_groups[
295 - self.full_attention_group_ids[0]].kv_cache_spec
296 - self.other_spec = self.kv_cache_config.kv_cache_groups[
297 - self.other_group_ids[0]].kv_cache_spec
298 -
294 + self.full_attention_spec = full_attention_spec
295 + self.other_spec = other_spec
296 self.full_attention_block_size = self.full_attention_spec.block_size
297 self.other_block_size = self.other_spec.block_size
298
vllm/v1/core/kv_cache_utils.py
+18 -17
@@ -5,7 +5,7 @@
5 import os
6 from collections import defaultdict, deque
7 from collections.abc import Iterable, Sequence
8 -from dataclasses import dataclass
8 +from dataclasses import astuple, dataclass
9 from typing import Any, Callable, NamedTuple, Optional
10
11 from vllm.config import VllmConfig
@@ -727,7 +727,9 @@ def create_kv_cache_group_specs(
727
728 def is_kv_cache_type_uniform(kv_cache_spec: dict[str, KVCacheSpec]) -> bool:
729 """
730 - Whether all layers in the given KVCacheSpec have the same type of KV cache.
730 + Whether all layers in the given KVCacheSpec have the same KV cache spec.
731 + Note that we regard FullAttentionSpec with and without sliding window as
732 + the same type.
733
734 Args:
735 kv_cache_spec: The kv cache spec of each attention layer in the model
@@ -736,8 +738,12 @@ def is_kv_cache_type_uniform(kv_cache_spec: dict[str, KVCacheSpec]) -> bool:
738 True if all layers have the same type, False otherwise.
739 """
740
739 - layer_keys = set(layer.type_id for layer in kv_cache_spec.values())
740 - return len(layer_keys) == 1
741 + try:
742 + kv_cache_spec_values = list(kv_cache_spec.values())
743 + _ = kv_cache_spec_values[0].merge(kv_cache_spec_values)
744 + except AssertionError:
745 + return False
746 + return True
747
748
749 def get_max_concurrency_for_kv_cache_config(
@@ -928,12 +934,12 @@ def _get_kv_cache_config_uniform_page_size(
934 Returns:
935 The generated KVCacheConfig
936 """
931 - # Group all layers by type_id.
937 + # Group all layers by kv_cache_spec.
938 # E.g., 2 full attention layers and 3 sliding window attention layers,
939 # -> (full.0, full.1), (sw.0, sw.1, sw.2).
934 - same_type_layers: dict[str, list[str]] = defaultdict(list)
940 + same_type_layers: dict[KVCacheSpec, list[str]] = defaultdict(list)
941 for layer_name, layer_spec in kv_cache_spec.items():
936 - same_type_layers[layer_spec.type_id].append(layer_name)
942 + same_type_layers[layer_spec].append(layer_name)
943
944 # Split each group into smaller groups, to make the number of layers in each
945 # group identical. Add padding to the last group of each type if necessary.
@@ -1017,12 +1023,7 @@ def unify_hybrid_kv_cache_specs(kv_cache_spec: dict[str, KVCacheSpec]):
1023 kv_cache_spec: The kv cache spec of each attention layer in the model
1024 """
1025
1020 - def is_hybrid(kv_cache_spec: dict[str, KVCacheSpec]) -> bool:
1021 - type_ids = set(layer_spec.type_id
1022 - for layer_spec in kv_cache_spec.values())
1023 - return len(type_ids) > 1
1024 -
1025 - if not is_hybrid(kv_cache_spec):
1026 + if is_kv_cache_type_uniform(kv_cache_spec):
1027 return
1028
1029 logger.warning(
@@ -1060,7 +1061,7 @@ def unify_hybrid_kv_cache_specs(kv_cache_spec: dict[str, KVCacheSpec]):
1061 attention_chunk_size=spec.attention_chunk_size,
1062 )
1063
1063 - if is_hybrid(kv_cache_spec):
1064 + if not is_kv_cache_type_uniform(kv_cache_spec):
1065 raise ValueError("Hybrid KV cache manager is disabled but failed to "
1066 "convert the KV cache specs to one unified type.")
1067
@@ -1119,11 +1120,11 @@ def unify_kv_cache_configs(kv_cache_configs: list[KVCacheConfig]):
1120 in-place modified to make them consistent.
1121 """
1122
1122 - # Sort the kv cache groups by the type_id of their KV cache spec.
1123 + # Sort the kv cache groups by their KV cache spec.
1124 # This can avoid the inconsistency caused by the order of groups.
1125 for kv_cache_config in kv_cache_configs:
1125 - kv_cache_config.kv_cache_groups.sort(
1126 - key=lambda x: x.kv_cache_spec.type_id)
1126 + kv_cache_config.kv_cache_groups.sort(key=lambda x: (type(
1127 + x.kv_cache_spec).__name__, astuple(x.kv_cache_spec)))
1128
1129 # Verify that the groups of each rank are the same.
1130 for kv_cache_config in kv_cache_configs[1:]:
vllm/v1/kv_cache_interface.py
+29 -51
@@ -2,7 +2,7 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import copy
5 -from dataclasses import dataclass
5 +from dataclasses import dataclass, fields
6 from math import prod
7 from typing import Optional
8
@@ -16,7 +16,7 @@ from vllm.utils import cdiv, get_dtype_size
16 logger = init_logger(__name__)
17
18
19 -@dataclass
19 +@dataclass(frozen=True)
20 class KVCacheSpec:
21 """
22 A base class for specifying the KV cache format of one layer.
@@ -25,20 +25,6 @@ class KVCacheSpec:
25 # number of tokens in a block
26 block_size: int
27
28 - @property
29 - def type_id(self) -> str:
30 - """
31 - The type identifier of this KV cache.
32 - Return different strings for layers with different KV cache type (e.g.,
33 - different number of tokens like full attention vs sliding window
34 - attention, different KV cache size per token like layers with different
35 - number of heads)
36 -
37 - Returns:
38 - The type identifier of this KV cache.
39 - """
40 - raise NotImplementedError
41 -
28 @property
29 def page_size_bytes(self) -> int:
30 """
@@ -63,13 +49,12 @@ class KVCacheSpec:
49 """
50 Merge a list of KVCacheSpec objects into a single KVCacheSpec object.
51 """
66 - assert all(spec.type_id == specs[0].type_id for spec in specs[1:]), (
67 - "All layers in the same KV cache group must share the same "
68 - "type_id.")
52 + assert all(spec == specs[0] for spec in specs[1:]), (
53 + "All layers in the same KV cache group must be the same.")
54 return copy.deepcopy(specs[0])
55
56
72 -@dataclass
57 +@dataclass(frozen=True)
58 class AttentionSpec(KVCacheSpec):
59 num_kv_heads: int
60 head_size: int
@@ -84,7 +69,7 @@ class AttentionSpec(KVCacheSpec):
69 * get_dtype_size(self.dtype)
70
71
87 -@dataclass
72 +@dataclass(frozen=True)
73 class FullAttentionSpec(AttentionSpec):
74 sliding_window: Optional[int] = None
75 attention_chunk_size: Optional[int] = None
@@ -98,10 +83,6 @@ class FullAttentionSpec(AttentionSpec):
83 Default to None for not using sliding window attention.
84 """
85
101 - @property
102 - def type_id(self) -> str:
103 - return f"full_attention_{self.block_size}_{self.page_size_bytes}"
104 -
86 def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
87 max_model_len = vllm_config.model_config.max_model_len
88 return cdiv(max_model_len, self.block_size) * self.page_size_bytes
@@ -123,15 +104,28 @@ class FullAttentionSpec(AttentionSpec):
104 Merge a list of FullAttentionSpec objects into a single
105 FullAttentionSpec object.
106 """
126 - merged_spec = super().merge(specs)
107 + assert all(isinstance(spec, FullAttentionSpec) for spec in specs), (
108 + "All attention layers in the same KV cache group must be "
109 + "FullAttentionSpec.")
110 +
111 sliding_window = set(spec.sliding_window for spec in specs
112 if spec.sliding_window is not None)
113 attention_chunk_size = set(spec.attention_chunk_size for spec in specs
114 if spec.attention_chunk_size is not None)
131 -
132 - merged_spec.sliding_window = cls.merge_window_sizes(sliding_window)
133 - merged_spec.attention_chunk_size = (
134 - cls.merge_window_sizes(attention_chunk_size))
115 + merged_spec = cls(
116 + block_size=specs[0].block_size,
117 + num_kv_heads=specs[0].num_kv_heads,
118 + head_size=specs[0].head_size,
119 + dtype=specs[0].dtype,
120 + use_mla=specs[0].use_mla,
121 + sliding_window=cls.merge_window_sizes(sliding_window),
122 + attention_chunk_size=cls.merge_window_sizes(attention_chunk_size),
123 + )
124 + for spec in specs:
125 + for f in fields(AttentionSpec):
126 + assert getattr(spec, f.name) == getattr(merged_spec, f.name), (
127 + "All attention layers in the same KV cache group must have "
128 + "the same attention spec.")
129 assert (
130 (merged_spec.sliding_window is not None) +
131 (merged_spec.attention_chunk_size is not None) <= 1
@@ -140,16 +134,10 @@ class FullAttentionSpec(AttentionSpec):
134 return merged_spec
135
136
143 -@dataclass
137 +@dataclass(frozen=True)
138 class ChunkedLocalAttentionSpec(AttentionSpec):
139 attention_chunk_size: int
140
147 - @property
148 - def type_id(self) -> str:
149 - return (
150 - f"local_attention_{self.attention_chunk_size}_{self.block_size}_{self.page_size_bytes}"
151 - ) # noqa
152 -
141 def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
142 max_model_len = vllm_config.model_config.max_model_len
143 max_num_batched_tokens = (
@@ -165,17 +153,13 @@ class ChunkedLocalAttentionSpec(AttentionSpec):
153 return cdiv(num_tokens, self.block_size) * self.page_size_bytes
154
155
168 -@dataclass
156 +@dataclass(frozen=True)
157 class SlidingWindowSpec(AttentionSpec):
158 sliding_window: int
159
160 def __post_init__(self):
161 assert not self.use_mla, "MLA is not supported for sliding window"
162
175 - @property
176 - def type_id(self) -> str:
177 - return f"sliding_window_{self.sliding_window}_{self.block_size}_{self.page_size_bytes}" # noqa
178 -
163 def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
164 max_model_len = vllm_config.model_config.max_model_len
165 max_num_batched_tokens = (
@@ -195,23 +179,17 @@ class SlidingWindowSpec(AttentionSpec):
179 return (cdiv(num_tokens, self.block_size) + 1) * self.page_size_bytes
180
181
198 -@dataclass
182 +@dataclass(frozen=True)
183 class MambaSpec(KVCacheSpec):
184 shapes: tuple[tuple[int, ...], ...]
185 dtype: torch.dtype
186 page_size_padded: Optional[int] = None
187 mamba_type: str = "mamba2"
188
205 - def __post_init__(self):
206 - self.num_elements = sum(prod(shape) for shape in self.shapes)
207 -
208 - @property
209 - def type_id(self) -> str:
210 - return f"mamba_{self.shapes}_{self.dtype}_{self.mamba_type}"
211 -
189 @property
190 def page_size_bytes(self) -> int:
214 - page_size = self.num_elements * get_dtype_size(self.dtype)
191 + num_elements = sum(prod(shape) for shape in self.shapes)
192 + page_size = num_elements * get_dtype_size(self.dtype)
193 if self.page_size_padded is not None:
194 assert self.page_size_padded >= page_size
195 return self.page_size_padded