@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 646d62f636
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 15, 06:00 UTC

[Core] Use tuple for kv cache group block ids (#19175)

Signed-off-by: Nick Hill <nhill@redhat.com>

Nick Hill committed Jun 9, 2025 at 22:01 UTC 646d62f636197d5d809d21090752700e094b8b86
12 files changed +140 -142
tests/v1/core/test_prefix_caching.py
+22 -22
@@ -117,7 +117,7 @@ def test_prefill(hash_algo):
117 blocks = manager.allocate_slots(req0, 55,
118 len(computed_blocks.blocks[0]) * 16,
119 computed_blocks)
120 - assert blocks.get_block_ids() == [[1, 2, 3, 4]]
120 + assert blocks.get_block_ids() == ([1, 2, 3, 4], )
121
122 # Check full block metadata
123 parent_block_hash = None
@@ -141,13 +141,13 @@ def test_prefill(hash_algo):
141 req1 = make_request("1", common_token_ids + unique_token_ids)
142 computed_blocks, num_computed_tokens = manager.get_computed_blocks(req1)
143 assert len(manager.req_to_block_hashes[req1.request_id]) == 3
144 - assert computed_blocks.get_block_ids() == [[1, 2, 3]]
144 + assert computed_blocks.get_block_ids() == ([1, 2, 3], )
145 assert num_computed_tokens == 3 * 16
146 num_new_tokens = 53 - 3 * 16
147 blocks = manager.allocate_slots(req1, num_new_tokens,
148 len(computed_blocks.blocks[0]) * 16,
149 computed_blocks)
150 - assert blocks.get_block_ids() == [[5]]
150 + assert blocks.get_block_ids() == ([5], )
151 for block in computed_blocks.blocks[0]:
152 assert block.ref_cnt == 2
153
@@ -175,13 +175,13 @@ def test_prefill(hash_algo):
175 req2 = make_request("2", common_token_ids + unique_token_ids)
176 computed_blocks, num_computed_tokens = manager.get_computed_blocks(req2)
177 assert len(manager.req_to_block_hashes[req2.request_id]) == 3
178 - assert computed_blocks.get_block_ids() == [[1, 2, 3]]
178 + assert computed_blocks.get_block_ids() == ([1, 2, 3], )
179 assert num_computed_tokens == 3 * 16
180 num_new_tokens = 53 - 3 * 16
181 blocks = manager.allocate_slots(req2, num_new_tokens,
182 len(computed_blocks.blocks[0]) * 16,
183 computed_blocks)
184 - assert blocks.get_block_ids() == [[6]]
184 + assert blocks.get_block_ids() == ([6], )
185
186 # Although we only have 6 free blocks, we have 8 blocks in
187 # the free block queue due to lazy removal.
@@ -205,7 +205,7 @@ def test_prefill(hash_algo):
205 len(computed_blocks.blocks[0]) * 16,
206 computed_blocks)
207 # This block ID order also checks the eviction order.
208 - assert blocks.get_block_ids() == [[7, 8, 9, 10, 4, 5, 6, 3, 2, 1]]
208 + assert blocks.get_block_ids() == ([7, 8, 9, 10, 4, 5, 6, 3, 2, 1], )
209 assert manager.block_pool.free_block_queue.num_free_blocks == 0
210 assert manager.block_pool.free_block_queue.free_list_head is None
211 assert manager.block_pool.free_block_queue.free_list_tail is None
@@ -236,8 +236,8 @@ def test_prefill_hybrid_model():
236 blocks = manager.allocate_slots(req0, 55,
237 len(computed_blocks.blocks[0]) * 16,
238 computed_blocks)
239 - assert blocks.get_block_ids() == [[1, 2, 3, 4], [5, 6, 7, 8],
240 - [9, 10, 11, 12]]
239 + assert blocks.get_block_ids() == ([1, 2, 3, 4], [5, 6, 7,
240 + 8], [9, 10, 11, 12])
241
242 # Check full block metadata
243 parent_block_hash = None
@@ -263,14 +263,14 @@ def test_prefill_hybrid_model():
263 req1 = make_request("1", common_token_ids + unique_token_ids)
264 computed_blocks, num_computed_tokens = manager.get_computed_blocks(req1)
265 assert len(manager.req_to_block_hashes[req1.request_id]) == 3
266 - assert computed_blocks.get_block_ids() == [[1, 2, 3], [0, 6, 7],
267 - [0, 10, 11]]
266 + assert computed_blocks.get_block_ids() == ([1, 2, 3], [0, 6,
267 + 7], [0, 10, 11])
268 assert num_computed_tokens == 3 * 16
269 num_new_tokens = 53 - 3 * 16
270 blocks = manager.allocate_slots(req1, num_new_tokens,
271 len(computed_blocks.blocks[0]) * 16,
272 computed_blocks)
273 - assert blocks.get_block_ids() == [[13], [14], [15]]
273 + assert blocks.get_block_ids() == ([13], [14], [15])
274 for block_per_group in computed_blocks.blocks:
275 for block in block_per_group:
276 if block != manager.block_pool.null_block:
@@ -374,7 +374,7 @@ def test_prefill_plp():
374 blocks = manager.allocate_slots(req0, 55,
375 len(computed_blocks.blocks[0]) * 16,
376 computed_blocks)
377 - assert blocks.get_block_ids() == [[1, 2, 3, 4]]
377 + assert blocks.get_block_ids() == ([1, 2, 3, 4], )
378 req0_block_hashes = [b.block_hash for b in blocks.blocks[0]]
379
380 # Check full block metadata
@@ -400,13 +400,13 @@ def test_prefill_plp():
400 req1 = make_request("1", common_token_ids + unique_token_ids)
401 computed_blocks, num_computed_tokens = manager.get_computed_blocks(req1)
402 assert len(manager.req_to_block_hashes[req1.request_id]) == 3
403 - assert computed_blocks.get_block_ids() == [[1, 2, 3]]
403 + assert computed_blocks.get_block_ids() == ([1, 2, 3], )
404 assert num_computed_tokens == 3 * 16
405 num_new_tokens = 53 - 3 * 16
406 blocks = manager.allocate_slots(req1, num_new_tokens,
407 len(computed_blocks.blocks[0]) * 16,
408 computed_blocks)
409 - assert blocks.get_block_ids() == [[5]]
409 + assert blocks.get_block_ids() == ([5], )
410 for block in computed_blocks.blocks[0]:
411 assert block.ref_cnt == 2
412
@@ -444,7 +444,7 @@ def test_prefill_plp():
444 block_ids = blocks.get_block_ids()
445 # Duplicate cached blocks have different ids but same hashes vs request #0
446 assert [b.block_hash for b in blocks.blocks[0]] == req0_block_hashes
447 - assert block_ids != [[1, 2, 3, 4]]
447 + assert block_ids != ([1, 2, 3, 4], )
448
449 # Request #2 block hashes are valid since request #0 hashes are.
450 # Check block reference counts.
@@ -474,7 +474,7 @@ def test_decode():
474 blocks = manager.allocate_slots(req0, 55,
475 len(computed_blocks.blocks[0]) * 16,
476 computed_blocks)
477 - assert blocks.get_block_ids() == [[1, 2, 3, 4]]
477 + assert blocks.get_block_ids() == ([1, 2, 3, 4], )
478
479 # Append slots without allocating a new block.
480 req0.num_computed_tokens = 55
@@ -546,12 +546,12 @@ def test_evict():
546 # Touch the first 2 blocks.
547 req2 = make_request("2", list(range(2 * 16 + 3)))
548 computed_blocks, num_computed_tokens = manager.get_computed_blocks(req2)
549 - assert computed_blocks.get_block_ids() == [[1, 2]]
549 + assert computed_blocks.get_block_ids() == ([1, 2], )
550 assert num_computed_tokens == 2 * 16
551 blocks = manager.allocate_slots(req2, 3,
552 len(computed_blocks.blocks[0]) * 16,
553 computed_blocks)
554 - assert blocks.get_block_ids() == [[10]]
554 + assert blocks.get_block_ids() == ([10], )
555 assert manager.block_pool.free_block_queue.num_free_blocks == 7
556
557
@@ -865,7 +865,7 @@ def test_mm_prefix_caching():
865 blocks = manager.allocate_slots(req0, 59,
866 len(computed_blocks.blocks[0]) * 16,
867 computed_blocks)
868 - assert blocks.get_block_ids() == [[1, 2, 3, 4]]
868 + assert blocks.get_block_ids() == ([1, 2, 3, 4], )
869 req0.num_computed_tokens = 59
870
871 # Append slots without allocating a new block.
@@ -926,7 +926,7 @@ def test_cache_key_salting():
926 blocks = manager.allocate_slots(req0, 59,
927 len(computed_blocks.blocks[0]) * 16,
928 computed_blocks)
929 - assert blocks.get_block_ids() == [[1, 2, 3, 4]]
929 + assert blocks.get_block_ids() == ([1, 2, 3, 4], )
930 req0.num_computed_tokens = 59
931
932 # Append slots without allocating a new block.
@@ -1042,7 +1042,7 @@ def test_reset_prefix_cache():
1042 all_token_ids = full_block_token_ids + unique_token_ids
1043 req0 = make_request("0", all_token_ids)
1044 blocks = manager.allocate_slots(req0, 55)
1045 - assert blocks.get_block_ids() == [[1, 2, 3, 4]]
1045 + assert blocks.get_block_ids() == ([1, 2, 3, 4], )
1046
1047 unique_token_ids = [4] * 7
1048 all_token_ids = full_block_token_ids + unique_token_ids
@@ -1053,7 +1053,7 @@ def test_reset_prefix_cache():
1053 blocks = manager.allocate_slots(req1, 7,
1054 len(computed_blocks.blocks[0]) * 16,
1055 computed_blocks)
1056 - assert blocks.get_block_ids() == [[5]]
1056 + assert blocks.get_block_ids() == ([5], )
1057
1058 # Failed to reset prefix cache because some blocks are not freed yet.
1059 assert not manager.reset_prefix_cache()
tests/v1/tpu/worker/test_tpu_model_runner.py
+4 -4
@@ -71,7 +71,7 @@ def _schedule_new_request(*req_ids: str) -> SchedulerOutput:
71 mm_hashes=[],
72 mm_positions=[],
73 sampling_params=SamplingParams(),
74 - block_ids=[[0]], # block_ids should be list[list[int]]
74 + block_ids=([0], ), # block_ids should be tuple[list[int]]
75 num_computed_tokens=0,
76 lora_request=None,
77 ))
@@ -116,10 +116,10 @@ def _is_req_state_block_table_match(model_runner, req_id: str) -> bool:
116 # This is safe since we currently only use single KV cache groups
117 block_table = multi_group_block_table[0]
118
119 - # req_state.block_ids is now list[list[int]] for MultiGroupBlockTable
119 + # req_state.block_ids is now tuple[list[int], ...] for MultiGroupBlockTable
120 # Extract the first group's block IDs
121 if isinstance(req_state.block_ids[0], list):
122 - # New format: list[list[int]] - extract first group
122 + # New format: tuple[list[int], ...] - extract first group
123 req_block_ids = req_state.block_ids[0]
124 else:
125 # Legacy format: list[int] - use directly
@@ -210,7 +210,7 @@ def test_update_states_request_resumed(model_runner):
210 req_id=req_id,
211 resumed_from_preemption=False,
212 new_token_ids=[],
213 - new_block_ids=[[]],
213 + new_block_ids=([], ),
214 num_computed_tokens=0,
215 )
216
tests/v1/worker/test_gpu_input_batch.py
+1 -1
@@ -203,7 +203,7 @@ def _construct_cached_request_state(req_id_suffix: int):
203 sampling_params=_create_sampling_params(),
204 mm_inputs=[],
205 mm_positions=[],
206 - block_ids=[[]],
206 + block_ids=([], ),
207 generator=None,
208 num_computed_tokens=len(output_token_ids),
209 output_token_ids=output_token_ids,
tests/v1/worker/test_gpu_model_runner.py
+2 -2
@@ -123,7 +123,7 @@ def _schedule_new_request(*req_ids: str) -> SchedulerOutput:
123 mm_hashes=[],
124 mm_positions=[],
125 sampling_params=SamplingParams(),
126 - block_ids=[[0]],
126 + block_ids=([0], ),
127 num_computed_tokens=0,
128 lora_request=None,
129 ))
@@ -251,7 +251,7 @@ def test_update_states_request_resumed(model_runner):
251 req_id=req_id,
252 resumed_from_preemption=False,
253 new_token_ids=[],
254 - new_block_ids=[[]],
254 + new_block_ids=([], ),
255 num_computed_tokens=0,
256 )
257
vllm/v1/core/block_pool.py
+4 -4
@@ -89,8 +89,8 @@ class BlockPool:
89 BlockHashWithGroupId(block_hash, group_id))
90 if not cached_blocks_one_group:
91 return None
92 - first_block_id = next(iter(cached_blocks_one_group))
93 - cached_blocks.append(cached_blocks_one_group[first_block_id])
92 + first_block = next(iter(cached_blocks_one_group.values()))
93 + cached_blocks.append(first_block)
94 return cached_blocks
95
96 def cache_full_blocks(
@@ -260,7 +260,7 @@ class BlockPool:
260 return True
261 return False
262
263 - def touch(self, blocks: list[list[KVCacheBlock]]) -> None:
263 + def touch(self, blocks: tuple[list[KVCacheBlock], ...]) -> None:
264 """Touch a block increases its reference count by 1, and may remove
265 the block from the free queue. This is used when a block is hit by
266 another request with the same prefix.
@@ -299,7 +299,7 @@ class BlockPool:
299 bool: True if the prefix cache is successfully reset,
300 False otherwise.
301 """
302 - num_used_blocks = (self.num_gpu_blocks - self.get_num_free_blocks())
302 + num_used_blocks = self.num_gpu_blocks - self.get_num_free_blocks()
303 if num_used_blocks != 1: # The null block is always marked as used
304 logger.warning(
305 "Failed to reset prefix cache because some "
vllm/v1/core/kv_cache_coordinator.py
+53 -48
@@ -5,8 +5,7 @@ from typing import Callable, Optional
5 from vllm.v1.core.block_pool import BlockPool
6 from vllm.v1.core.kv_cache_utils import BlockHash, KVCacheBlock
7 from vllm.v1.core.single_type_kv_cache_manager import (
8 - FullAttentionManager, SingleTypeKVCacheManager,
9 - get_manager_for_kv_cache_spec)
8 + FullAttentionManager, get_manager_for_kv_cache_spec)
9 from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig
10 from vllm.v1.request import Request
11
@@ -30,25 +29,21 @@ class KVCacheCoordinator(ABC):
29
30 self.block_pool = BlockPool(kv_cache_config.num_blocks, enable_caching,
31 enable_kv_cache_events)
33 - self.single_type_managers: list[SingleTypeKVCacheManager] = []
32
33 # Needs special handling for find_longest_cache_hit if eagle is enabled
34 self.use_eagle = use_eagle
37 -
38 - for i in range(len(self.kv_cache_config.kv_cache_groups)):
39 - kv_cache_spec = self.kv_cache_config.kv_cache_groups[
40 - i].kv_cache_spec
41 - self.single_type_managers.append(
42 - get_manager_for_kv_cache_spec(
43 - kv_cache_spec=kv_cache_spec,
44 - block_pool=self.block_pool,
45 - kv_cache_group_id=i,
46 - caching_hash_fn=caching_hash_fn,
47 - ))
35 + self.single_type_managers = tuple(
36 + get_manager_for_kv_cache_spec(
37 + kv_cache_spec=kv_cache_group.kv_cache_spec,
38 + block_pool=self.block_pool,
39 + kv_cache_group_id=i,
40 + caching_hash_fn=caching_hash_fn,
41 + ) for i, kv_cache_group in enumerate(
42 + self.kv_cache_config.kv_cache_groups))
43
44 def get_num_blocks_to_allocate(
45 self, request_id: str, num_tokens: int,
51 - new_computed_blocks: list[list[KVCacheBlock]]) -> int:
46 + new_computed_blocks: tuple[list[KVCacheBlock], ...]) -> int:
47 """
48 Get the number of blocks needed to be allocated for the request.
49
@@ -70,7 +65,7 @@ class KVCacheCoordinator(ABC):
65
66 def save_new_computed_blocks(
67 self, request_id: str,
73 - new_computed_blocks: list[list[KVCacheBlock]]) -> None:
68 + new_computed_blocks: tuple[list[KVCacheBlock], ...]) -> None:
69 """
70 Add the new computed blocks to the request.
71
@@ -84,7 +79,7 @@ class KVCacheCoordinator(ABC):
79 new_computed_blocks[i])
80
81 def allocate_new_blocks(self, request_id: str,
87 - num_tokens: int) -> list[list[KVCacheBlock]]:
82 + num_tokens: int) -> tuple[list[KVCacheBlock], ...]:
83 """
84 Allocate new blocks for the request to give it at least `num_tokens`
85 token slots.
@@ -97,11 +92,9 @@ class KVCacheCoordinator(ABC):
92 Returns:
93 The new allocated blocks.
94 """
100 - new_blocks = []
101 - for manager in self.single_type_managers:
102 - new_blocks.append(
103 - manager.allocate_new_blocks(request_id, num_tokens))
104 - return new_blocks
95 + return tuple(
96 + manager.allocate_new_blocks(request_id, num_tokens)
97 + for manager in self.single_type_managers)
98
99 def cache_blocks(self, request: Request, block_hashes: list[BlockHash],
100 num_computed_tokens: int) -> None:
@@ -159,19 +152,20 @@ class KVCacheCoordinator(ABC):
152 for manager in self.single_type_managers:
153 manager.remove_skipped_blocks(request_id, num_computed_tokens)
154
162 - def get_blocks(self, request_id: str) -> list[list[KVCacheBlock]]:
155 + def get_blocks(self, request_id: str) -> tuple[list[KVCacheBlock], ...]:
156 """
157 Get the blocks for the request.
158 """
166 - return [
159 + return tuple(
160 manager.req_to_blocks.get(request_id) or []
168 - for manager in self.single_type_managers
169 - ]
161 + for manager in self.single_type_managers)
162
163 @abstractmethod
164 def find_longest_cache_hit(
173 - self, block_hashes: list[BlockHash],
174 - max_cache_hit_length: int) -> tuple[list[list[KVCacheBlock]], int]:
165 + self,
166 + block_hashes: list[BlockHash],
167 + max_cache_hit_length: int,
168 + ) -> tuple[tuple[list[KVCacheBlock], ...], int]:
169 pass
170
171
@@ -195,8 +189,10 @@ class UnitaryKVCacheCoordinator(KVCacheCoordinator):
189 "UnitaryKVCacheCoordinator assumes only one kv cache group")
190
191 def find_longest_cache_hit(
198 - self, block_hashes: list[BlockHash],
199 - max_cache_hit_length: int) -> tuple[list[list[KVCacheBlock]], int]:
192 + self,
193 + block_hashes: list[BlockHash],
194 + max_cache_hit_length: int,
195 + ) -> tuple[tuple[list[KVCacheBlock], ...], int]:
196 hit_blocks = self.single_type_managers[0].find_longest_cache_hit(
197 block_hashes=block_hashes,
198 max_length=max_cache_hit_length,
@@ -275,11 +271,24 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
271 "KVCacheCoordinator assumes the block_size of full attention "
272 "layers is divisible by other layers now.")
273
274 + if max(self.full_attention_group_ids) < min(self.other_group_ids):
275 + self.full_attn_first = True
276 + elif max(self.other_group_ids) < min(self.full_attention_group_ids):
277 + self.full_attn_first = False
278 + else:
279 + raise ValueError(
280 + "HybridKVCacheCoordinator assumes the full "
281 + "attention group ids and other attention group ids "
282 + "do not interleave, either full attention group ids "
283 + "are before other attention group ids or vice versa."
284 + "This is for simplifying merging hit_blocks_full_attn and "
285 + "hit_blocks_other_attn to hit_blocks.")
286 +
287 def find_longest_cache_hit(
288 self,
289 block_hashes: list[BlockHash],
290 max_cache_hit_length: int,
282 - ) -> tuple[list[list[KVCacheBlock]], int]:
291 + ) -> tuple[tuple[list[KVCacheBlock], ...], int]:
292 """
293 Find the longest cache hit for the request.
294
@@ -318,27 +327,25 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
327 ))
328 hit_length = len(hit_blocks_other_attn[0]) * self.other_block_size
329
321 - # NOTE: the prefix cache hit length must be a multiply of block_size as
330 + # NOTE: the prefix cache hit length must be a multiple of block_size as
331 # we don't support partial block cache hit yet. The cache hit length
323 - # of other attention is ensured to be a multiply of the block size of
332 + # of other attention is ensured to be a multiple of the block size of
333 # full attention layers in current implementation, because hit_length is
325 - # a multiply of other attention's block size, and other attention's
326 - # block size is a multiply of full attention's block size (verified in
334 + # a multiple of other attention's block size, and other attention's
335 + # block size is a multiple of full attention's block size (verified in
336 # `verify_and_split_kv_cache_groups`).
337 assert hit_length % self.full_attention_block_size == 0
338
339 # Truncate the full attention cache hit to the length of the
340 # cache hit of the other attention.
332 - for i in range(len(hit_blocks_full_attn)):
333 - del hit_blocks_full_attn[i][hit_length //
334 - self.full_attention_block_size:]
341 + for group_hit_blocks in hit_blocks_full_attn:
342 + del group_hit_blocks[hit_length // self.full_attention_block_size:]
343
344 # Merge the hit blocks of full attention and other attention.
337 - hit_blocks = hit_blocks_other_attn
338 - for group_id, blocks in enumerate(hit_blocks_full_attn):
339 - # NOTE: there is only one full attention group in most cases. So
340 - # the time complexity of insert is fine.
341 - hit_blocks.insert(group_id, blocks)
345 + if self.full_attn_first:
346 + hit_blocks = hit_blocks_full_attn + hit_blocks_other_attn
347 + else:
348 + hit_blocks = hit_blocks_other_attn + hit_blocks_full_attn
349 return hit_blocks, hit_length
350
351
@@ -351,8 +358,6 @@ def get_kv_cache_coordinator(
358 use_eagle, enable_caching,
359 caching_hash_fn,
360 enable_kv_cache_events)
354 - else:
355 - return HybridKVCacheCoordinator(kv_cache_config, max_model_len,
356 - use_eagle, enable_caching,
357 - caching_hash_fn,
358 - enable_kv_cache_events)
361 + return HybridKVCacheCoordinator(kv_cache_config, max_model_len, use_eagle,
362 + enable_caching, caching_hash_fn,
363 + enable_kv_cache_events)
vllm/v1/core/kv_cache_manager.py
+18 -20
@@ -21,11 +21,11 @@ logger = init_logger(__name__)
21 @dataclass
22 class KVCacheBlocks:
23 """
24 - The allocation result of KVCacheManager, work as the interface between
25 - Scheduler and KVCacheManager, to hide KVCacheManager's internal data
24 + The allocation result of KVCacheManager, work as the interface between
25 + Scheduler and KVCacheManager, to hide KVCacheManager's internal data
26 structure from the Scheduler.
27 """
28 - blocks: list[list[KVCacheBlock]]
28 + blocks: tuple[list[KVCacheBlock], ...]
29 """
30 blocks[i][j] refers to the i-th kv_cache_group and the j-th block of tokens.
31 We don't use block of tokens as the outer dimension because it assumes all
@@ -37,21 +37,19 @@ class KVCacheBlocks:
37 def __add__(self, other: "KVCacheBlocks") -> "KVCacheBlocks":
38 """Adds two KVCacheBlocks instances."""
39 return KVCacheBlocks(
40 - [blk1 + blk2 for blk1, blk2 in zip(self.blocks, other.blocks)])
40 + tuple(blk1 + blk2
41 + for blk1, blk2 in zip(self.blocks, other.blocks)))
42
42 - def get_block_ids(self) -> list[list[int]]:
43 + def get_block_ids(self) -> tuple[list[int], ...]:
44 """
45 Converts the KVCacheBlocks instance to block_ids.
46
47 Returns:
47 - list[list[int]]: A two-level list where
48 - * the outer list corresponds to KV cache groups
48 + tuple[list[int], ...]: A tuple of lists where
49 + * the outer tuple corresponds to KV cache groups
50 * each inner list contains the block_ids of the blocks in that group
51 """
51 - block_ids = []
52 - for group in self.blocks:
53 - block_ids.append([blk.block_id for blk in group])
54 - return block_ids
52 + return tuple([blk.block_id for blk in group] for group in self.blocks)
53
54 def get_unhashed_block_ids(self) -> list[int]:
55 """Get block_ids of unhashed blocks from KVCacheBlocks instance."""
@@ -63,7 +61,7 @@ class KVCacheBlocks:
61
62 def new_empty(self) -> "KVCacheBlocks":
63 """Creates a new KVCacheBlocks instance with no blocks."""
66 - return KVCacheBlocks([[] for _ in range(len(self.blocks))])
64 + return KVCacheBlocks(tuple([] for _ in range(len(self.blocks))))
65
66
67 class KVCacheManager:
@@ -232,9 +230,8 @@ class KVCacheManager:
230 if new_computed_blocks is not None:
231 new_computed_block_list = new_computed_blocks.blocks
232 else:
235 - new_computed_block_list = [
236 - [] for _ in range(len(self.kv_cache_config.kv_cache_groups))
237 - ]
233 + new_computed_block_list = tuple(
234 + [] for _ in range(len(self.kv_cache_config.kv_cache_groups)))
235
236 # Free the blocks that are skipped during the attention computation
237 # (e.g., tokens outside the sliding window).
@@ -267,7 +264,7 @@ class KVCacheManager:
264 if self.enable_caching:
265 self.block_pool.touch(new_computed_block_list)
266 else:
270 - assert all(not blocks for blocks in new_computed_block_list), (
267 + assert not any(new_computed_block_list), (
268 "Computed blocks should be empty when "
269 "prefix caching is disabled")
270
@@ -378,17 +375,18 @@ class KVCacheManager:
375 """
376 return self.block_pool.take_events()
377
381 - def get_block_ids(self, request_id: str) -> list[list[int]]:
378 + def get_block_ids(self, request_id: str) -> tuple[list[int], ...]:
379 """Get the block ids of a request."""
380 return KVCacheBlocks(
381 self.coordinator.get_blocks(request_id)).get_block_ids()
382
386 - def cache_blocks(self, request: Request, block_hashes: list[BlockHash],
387 - num_computed_tokens: int) -> None:
383 + def cache_blocks(self, request: Request, num_computed_tokens: int) -> None:
384 """Cache the blocks for the request."""
385 + block_hashes = self.req_to_block_hashes[request.request_id]
386 self.coordinator.cache_blocks(request, block_hashes,
387 num_computed_tokens)
388
389 def create_empty_block_list(self) -> KVCacheBlocks:
390 """Creates a new KVCacheBlocks instance with no blocks."""
394 - return KVCacheBlocks([[] for _ in range(self.num_kv_cache_groups)])
391 + return KVCacheBlocks(tuple([]
392 + for _ in range(self.num_kv_cache_groups)))
vllm/v1/core/sched/output.py
+4 -4
@@ -27,7 +27,7 @@ class NewRequestData:
27 mm_hashes: list[str]
28 mm_positions: list[PlaceholderRange]
29 sampling_params: SamplingParams
30 - block_ids: list[list[int]]
30 + block_ids: tuple[list[int], ...]
31 num_computed_tokens: int
32 lora_request: Optional[LoRARequest]
33
@@ -35,7 +35,7 @@ class NewRequestData:
35 def from_request(
36 cls,
37 request: Request,
38 - block_ids: list[list[int]],
38 + block_ids: tuple[list[int], ...],
39 ) -> NewRequestData:
40 return cls(
41 req_id=request.request_id,
@@ -86,7 +86,7 @@ class CachedRequestData:
86 # request's block IDs instead of appending to the existing block IDs.
87 resumed_from_preemption: bool
88 new_token_ids: list[int]
89 - new_block_ids: list[list[int]]
89 + new_block_ids: tuple[list[int], ...]
90 num_computed_tokens: int
91
92 @classmethod
@@ -95,7 +95,7 @@ class CachedRequestData:
95 request: Request,
96 resumed_from_preemption: bool,
97 new_token_ids: list[int],
98 - new_block_ids: list[list[int]],
98 + new_block_ids: tuple[list[int], ...],
99 ) -> CachedRequestData:
100 return cls(
101 req_id=request.request_id,
vllm/v1/core/sched/scheduler.py
+4 -8
@@ -180,7 +180,7 @@ class Scheduler(SchedulerInterface):
180 # uses structured decoding.
181 structured_output_request_ids: dict[str, int] = {}
182
183 - req_to_new_block_ids: dict[str, list[list[int]]] = {}
183 + req_to_new_block_ids: dict[str, tuple[list[int], ...]] = {}
184 num_scheduled_tokens: dict[str, int] = {}
185 token_budget = self.max_num_scheduled_tokens
186 # Encoder-related.
@@ -471,7 +471,7 @@ class Scheduler(SchedulerInterface):
471 token_budget -= num_new_tokens
472 request.status = RequestStatus.RUNNING
473 request.num_computed_tokens = num_computed_tokens
474 - # Count the number of prifix cached tokens.
474 + # Count the number of prefix cached tokens.
475 if request.num_cached_tokens < 0:
476 request.num_cached_tokens = num_computed_tokens
477 # Encoder-related.
@@ -588,7 +588,7 @@ class Scheduler(SchedulerInterface):
588 request: Request,
589 num_scheduled_tokens: int,
590 num_scheduled_spec_tokens: int,
591 - new_block_ids: list[list[int]],
591 + new_block_ids: tuple[list[int], ...],
592 resumed_from_preemption: bool,
593 ) -> CachedRequestData:
594 # OPTIMIZATION: Cache the CachedRequestData objects to avoid creating
@@ -1015,11 +1015,7 @@ class Scheduler(SchedulerInterface):
1015 num_computed_tokens = min(num_computed_tokens, request.num_tokens)
1016 if num_computed_tokens == request.num_tokens:
1017 num_computed_tokens -= 1
1018 - self.kv_cache_manager.cache_blocks(
1019 - request,
1020 - self.kv_cache_manager.req_to_block_hashes[request.request_id],
1021 - num_computed_tokens,
1022 - )
1018 + self.kv_cache_manager.cache_blocks(request, num_computed_tokens)
1019
1020 # Update the request state for scheduling.
1021 request.num_computed_tokens = num_computed_tokens
vllm/v1/core/single_type_kv_cache_manager.py
+24 -26
@@ -197,7 +197,7 @@ class SingleTypeKVCacheManager(ABC):
197 block_pool: BlockPool,
198 kv_cache_spec: KVCacheSpec,
199 use_eagle: bool,
200 - ) -> list[list[KVCacheBlock]]:
200 + ) -> tuple[list[KVCacheBlock], ...]:
201 """
202 Get the longest cache hit prefix of the blocks that is not longer than
203 `max_length`. The prefix should be a common prefix hit for all the
@@ -222,7 +222,7 @@ class SingleTypeKVCacheManager(ABC):
222 element is a list of cached blocks for the i-th kv cache group
223 in `kv_cache_group_ids`.
224 For example, sliding window manager should return a list like
225 - [[NULL, NULL, KVCacheBlock(7), KVCacheBlock(8)]] for block size 4
225 + ([NULL, NULL, KVCacheBlock(7), KVCacheBlock(8)]) for block size 4
226 and sliding window 8 and len(kv_cache_group_ids) = 1.
227 """
228
@@ -254,27 +254,25 @@ class FullAttentionManager(SingleTypeKVCacheManager):
254 block_pool: BlockPool,
255 kv_cache_spec: KVCacheSpec,
256 use_eagle: bool,
257 - ) -> list[list[KVCacheBlock]]:
257 + ) -> tuple[list[KVCacheBlock], ...]:
258 assert isinstance(kv_cache_spec, FullAttentionSpec), (
259 "FullAttentionManager can only be used for full attention groups")
260 - computed_blocks: list[list[KVCacheBlock]] = [
261 - [] for _ in range(len(kv_cache_group_ids))
262 - ]
260 + computed_blocks: tuple[list[KVCacheBlock], ...] = tuple(
261 + [] for _ in range(len(kv_cache_group_ids)))
262 max_num_blocks = max_length // kv_cache_spec.block_size
264 - for i in range(max_num_blocks):
265 - block_hash = block_hashes[i]
263 + for i, block_hash in zip(range(max_num_blocks), block_hashes):
264 # block_hashes is a chain of block hashes. If a block hash is not
265 # in the cached_block_hash_to_id, the following block hashes are
266 # not computed yet for sure.
267 if cached_block := block_pool.get_cached_block(
268 block_hash, kv_cache_group_ids):
271 - for j in range(len(kv_cache_group_ids)):
272 - computed_blocks[j].append(cached_block[j])
269 + for computed, cached in zip(computed_blocks, cached_block):
270 + computed.append(cached)
271 else:
272 break
275 - if use_eagle and len(computed_blocks[0]) > 0:
276 - for j in range(len(kv_cache_group_ids)):
277 - computed_blocks[j].pop()
273 + if use_eagle and computed_blocks[0]:
274 + for computed in computed_blocks:
275 + computed.pop()
276 return computed_blocks
277
278 def remove_skipped_blocks(self, request_id: str,
@@ -311,7 +309,7 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
309 block_pool: BlockPool,
310 kv_cache_spec: KVCacheSpec,
311 use_eagle: bool,
314 - ) -> list[list[KVCacheBlock]]:
312 + ) -> tuple[list[KVCacheBlock], ...]:
313 assert isinstance(kv_cache_spec, SlidingWindowSpec), (
314 "SlidingWindowManager can only be used for sliding window groups")
315
@@ -332,23 +330,23 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
330 # sliding_window_contiguous_blocks),
331 # which is good for low cache hit rate scenarios.
332 max_num_blocks = max_length // kv_cache_spec.block_size
335 - computed_blocks = [[block_pool.null_block] * max_num_blocks
336 - for _ in range(len(kv_cache_group_ids))]
333 + computed_blocks = tuple([block_pool.null_block] * max_num_blocks
334 + for _ in range(len(kv_cache_group_ids)))
335 num_contiguous_blocks = 0
336 match_found = False
337 # Search from right to left and early stop when a match is found.
338 for i in range(max_num_blocks - 1, -1, -1):
339 if cached_block := block_pool.get_cached_block(
340 block_hashes[i], kv_cache_group_ids):
343 - for j in range(len(kv_cache_group_ids)):
344 - computed_blocks[j][i] = cached_block[j]
341 + for computed, cached in zip(computed_blocks, cached_block):
342 + computed[i] = cached
343 num_contiguous_blocks += 1
346 - if (num_contiguous_blocks >= sliding_window_contiguous_blocks):
344 + if num_contiguous_blocks >= sliding_window_contiguous_blocks:
345 # Trim the trailing blocks.
346 # E.g., [NULL, NULL, 8, 3, NULL, 9] -> [NULL, NULL, 8, 3]
347 # when sliding_window_contiguous_blocks=2.
350 - for j in range(len(kv_cache_group_ids)):
351 - del computed_blocks[j][i + num_contiguous_blocks:]
348 + for computed in computed_blocks:
349 + del computed[i + num_contiguous_blocks:]
350 match_found = True
351 break
352 else:
@@ -356,11 +354,11 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
354 if not match_found:
355 # The first `num_contiguous_blocks` is a cache hit even if
356 # `num_contiguous_blocks < sliding_window_contiguous_blocks`.
359 - for j in range(len(kv_cache_group_ids)):
360 - del computed_blocks[j][num_contiguous_blocks:]
361 - if use_eagle and len(computed_blocks[0]) > 0:
362 - for j in range(len(kv_cache_group_ids)):
363 - computed_blocks[j].pop()
357 + for computed in computed_blocks:
358 + del computed[num_contiguous_blocks:]
359 + if use_eagle and computed_blocks[0]:
360 + for computed in computed_blocks:
361 + computed.pop()
362 return computed_blocks
363
364 def remove_skipped_blocks(self, request_id: str,
vllm/v1/worker/block_table.py
+3 -2
@@ -112,11 +112,12 @@ class MultiGroupBlockTable:
112 for block_size in block_sizes
113 ]
114
115 - def append_row(self, block_ids: list[list[int]], row_idx: int) -> None:
115 + def append_row(self, block_ids: tuple[list[int], ...],
116 + row_idx: int) -> None:
117 for i, block_table in enumerate(self.block_tables):
118 block_table.append_row(block_ids[i], row_idx)
119
119 - def add_row(self, block_ids: list[list[int]], row_idx: int) -> None:
120 + def add_row(self, block_ids: tuple[list[int], ...], row_idx: int) -> None:
121 for i, block_table in enumerate(self.block_tables):
122 block_table.add_row(block_ids[i], row_idx)
123
vllm/v1/worker/gpu_input_batch.py
+1 -1
@@ -30,7 +30,7 @@ class CachedRequestState:
30 sampling_params: SamplingParams
31 generator: Optional[torch.Generator]
32
33 - block_ids: list[list[int]]
33 + block_ids: tuple[list[int], ...]
34 num_computed_tokens: int
35 output_token_ids: list[int]
36