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

Revert "[v1] Support multiple KV cache groups in GPU model runner (#17945) (#18459)

Signed-off-by: Mark McLoughlin <markmc@redhat.com>

Mark McLoughlin committed May 21, 2025 at 18:25 UTC bb0a3112130a077e557b36632de3f1b5836b4c40
15 files changed +214 -481
tests/v1/core/test_kv_cache_utils.py
+3 -68
@@ -19,8 +19,7 @@ from vllm.v1.core.kv_cache_utils import (FreeKVCacheBlockQueue, KVCacheBlock,
19 hash_request_tokens,
20 unify_kv_cache_configs)
21 from vllm.v1.kv_cache_interface import (FullAttentionSpec, KVCacheConfig,
22 - KVCacheGroupSpec, KVCacheTensor,
23 - SlidingWindowSpec)
22 + KVCacheGroupSpec, KVCacheTensor)
23 from vllm.v1.metrics.stats import PrefixCacheStats
24 from vllm.v1.request import Request
25
@@ -55,14 +54,12 @@ def new_kv_cache_spec(block_size=16,
54 num_kv_heads=2,
55 head_size=64,
56 dtype=torch.float32,
58 - use_mla=False,
59 - sliding_window=None):
57 + use_mla=False):
58 return FullAttentionSpec(block_size=block_size,
59 num_kv_heads=num_kv_heads,
60 head_size=head_size,
61 dtype=dtype,
64 - use_mla=use_mla,
65 - sliding_window=sliding_window)
62 + use_mla=use_mla)
63
64
65 def test_none_hash(monkeypatch):
@@ -495,68 +492,6 @@ def test_unify_kv_cache_configs():
492 unify_kv_cache_configs(diff_kv_cache_config)
493
494
498 -def test_merge_kv_cache_spec():
499 - same_layer_specs = [
500 - new_kv_cache_spec(num_kv_heads=32),
501 - new_kv_cache_spec(num_kv_heads=32),
502 - ]
503 - merged_layer_spec = same_layer_specs[0].merge(same_layer_specs)
504 - assert merged_layer_spec.block_size == 16
505 - assert merged_layer_spec.num_kv_heads == 32
506 - assert merged_layer_spec.head_size == 64
507 - assert merged_layer_spec.dtype == torch.float32
508 - assert merged_layer_spec.sliding_window is None
509 -
510 - different_layer_specs = [
511 - new_kv_cache_spec(num_kv_heads=32),
512 - new_kv_cache_spec(num_kv_heads=16),
513 - ]
514 - with pytest.raises(AssertionError):
515 - different_layer_specs[0].merge(different_layer_specs)
516 -
517 - full_spec = new_kv_cache_spec(num_kv_heads=32)
518 - different_type_layer_specs = [
519 - full_spec,
520 - SlidingWindowSpec(
521 - block_size=full_spec.block_size,
522 - num_kv_heads=full_spec.num_kv_heads,
523 - head_size=full_spec.head_size,
524 - dtype=full_spec.dtype,
525 - use_mla=full_spec.use_mla,
526 - sliding_window=1,
527 - ),
528 - ]
529 - with pytest.raises(AssertionError):
530 - different_type_layer_specs[0].merge(different_type_layer_specs)
531 - with pytest.raises(AssertionError):
532 - different_type_layer_specs[1].merge(different_type_layer_specs)
533 -
534 - different_sliding_window_layer_specs = [
535 - new_kv_cache_spec(num_kv_heads=32),
536 - new_kv_cache_spec(num_kv_heads=32, sliding_window=1),
537 - new_kv_cache_spec(num_kv_heads=32, sliding_window=2),
538 - ]
539 - with pytest.raises(ValueError):
540 - different_sliding_window_layer_specs[0].merge(
541 - different_sliding_window_layer_specs)
542 -
543 - same_sliding_window_layer_specs = [
544 - new_kv_cache_spec(num_kv_heads=32, sliding_window=1),
545 - new_kv_cache_spec(num_kv_heads=32, sliding_window=1),
546 - ]
547 - merged_layer_spec = same_sliding_window_layer_specs[0].merge(
548 - same_sliding_window_layer_specs)
549 - assert merged_layer_spec.sliding_window == 1
550 -
551 - same_sliding_window_layer_spec_with_none = [
552 - new_kv_cache_spec(num_kv_heads=32, sliding_window=1),
553 - new_kv_cache_spec(num_kv_heads=32, sliding_window=None),
554 - ]
555 - merged_layer_spec = same_sliding_window_layer_spec_with_none[0].merge(
556 - same_sliding_window_layer_spec_with_none)
557 - assert merged_layer_spec.sliding_window == 1
558 -
559 -
495 @pytest.mark.parametrize(
496 ("model_id", "max_model_len", "want_estimated_max_len"), [
497 ("Qwen/Qwen1.5-7B", 16385, 16384),
tests/v1/core/test_prefix_caching.py
+18 -18
@@ -84,7 +84,7 @@ def test_prefill(hash_algo):
84 blocks = manager.allocate_slots(req0, 55,
85 len(computed_blocks.blocks) * 16,
86 computed_blocks)
87 - assert blocks.get_block_ids() == [[1, 2, 3, 4]]
87 + assert blocks.get_block_ids() == [1, 2, 3, 4]
88
89 # Check full block metadata
90 parent_block_hash = None
@@ -107,13 +107,13 @@ def test_prefill(hash_algo):
107 req1 = make_request("1", common_token_ids + unique_token_ids)
108 computed_blocks, num_computed_tokens = manager.get_computed_blocks(req1)
109 assert len(manager.req_to_block_hashes[req1.request_id]) == 3
110 - assert computed_blocks.get_block_ids() == [[1, 2, 3]]
110 + assert computed_blocks.get_block_ids() == [1, 2, 3]
111 assert num_computed_tokens == 3 * 16
112 num_new_tokens = 53 - 3 * 16
113 blocks = manager.allocate_slots(req1, num_new_tokens,
114 len(computed_blocks.blocks) * 16,
115 computed_blocks)
116 - assert blocks.get_block_ids() == [[5]]
116 + assert blocks.get_block_ids() == [5]
117 for block in computed_blocks.blocks:
118 assert block.ref_cnt == 2
119
@@ -141,13 +141,13 @@ def test_prefill(hash_algo):
141 req2 = make_request("2", common_token_ids + unique_token_ids)
142 computed_blocks, num_computed_tokens = manager.get_computed_blocks(req2)
143 assert len(manager.req_to_block_hashes[req2.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(req2, num_new_tokens,
148 len(computed_blocks.blocks) * 16,
149 computed_blocks)
150 - assert blocks.get_block_ids() == [[6]]
150 + assert blocks.get_block_ids() == [6]
151
152 # Although we only have 6 free blocks, we have 8 blocks in
153 # the free block queue due to lazy removal.
@@ -171,7 +171,7 @@ def test_prefill(hash_algo):
171 len(computed_blocks.blocks) * 16,
172 computed_blocks)
173 # This block ID order also checks the eviction order.
174 - assert blocks.get_block_ids() == [[7, 8, 9, 10, 4, 5, 6, 3, 2, 1]]
174 + assert blocks.get_block_ids() == [7, 8, 9, 10, 4, 5, 6, 3, 2, 1]
175 assert manager.block_pool.free_block_queue.num_free_blocks == 0
176 assert manager.block_pool.free_block_queue.free_list_head is None
177 assert manager.block_pool.free_block_queue.free_list_tail is None
@@ -208,7 +208,7 @@ def test_prefill_plp():
208 blocks = manager.allocate_slots(req0, 55,
209 len(computed_blocks.blocks) * 16,
210 computed_blocks)
211 - assert blocks.get_block_ids() == [[1, 2, 3, 4]]
211 + assert blocks.get_block_ids() == [1, 2, 3, 4]
212 req0_block_hashes = [b.block_hash for b in blocks.blocks]
213
214 # Check full block metadata
@@ -233,13 +233,13 @@ def test_prefill_plp():
233 req1 = make_request("1", common_token_ids + unique_token_ids)
234 computed_blocks, num_computed_tokens = manager.get_computed_blocks(req1)
235 assert len(manager.req_to_block_hashes[req1.request_id]) == 3
236 - assert computed_blocks.get_block_ids() == [[1, 2, 3]]
236 + assert computed_blocks.get_block_ids() == [1, 2, 3]
237 assert num_computed_tokens == 3 * 16
238 num_new_tokens = 53 - 3 * 16
239 blocks = manager.allocate_slots(req1, num_new_tokens,
240 len(computed_blocks.blocks) * 16,
241 computed_blocks)
242 - assert blocks.get_block_ids() == [[5]]
242 + assert blocks.get_block_ids() == [5]
243 for block in computed_blocks.blocks:
244 assert block.ref_cnt == 2
245
@@ -277,11 +277,11 @@ def test_prefill_plp():
277 block_ids = blocks.get_block_ids()
278 # Duplicate cached blocks have different ids but same hashes vs request #0
279 assert [b.block_hash for b in blocks.blocks] == req0_block_hashes
280 - assert block_ids != [[1, 2, 3, 4]]
280 + assert block_ids != [1, 2, 3, 4]
281
282 # Request #2 block hashes are valid since request #0 hashes are.
283 # Check block reference counts.
284 - for block_id in block_ids[0]:
284 + for block_id in block_ids:
285 assert manager.block_pool.blocks[block_id].ref_cnt == 1
286
287 manager.free(req2)
@@ -307,7 +307,7 @@ def test_decode():
307 blocks = manager.allocate_slots(req0, 55,
308 len(computed_blocks.blocks) * 16,
309 computed_blocks)
310 - assert blocks.get_block_ids() == [[1, 2, 3, 4]]
310 + assert blocks.get_block_ids() == [1, 2, 3, 4]
311
312 # Append slots without allocating a new block.
313 req0.num_computed_tokens = 55
@@ -379,12 +379,12 @@ def test_evict():
379 # Touch the first 2 blocks.
380 req2 = make_request("2", list(range(2 * 16 + 3)))
381 computed_blocks, num_computed_tokens = manager.get_computed_blocks(req2)
382 - assert computed_blocks.get_block_ids() == [[1, 2]]
382 + assert computed_blocks.get_block_ids() == [1, 2]
383 assert num_computed_tokens == 2 * 16
384 blocks = manager.allocate_slots(req2, 3,
385 len(computed_blocks.blocks) * 16,
386 computed_blocks)
387 - assert blocks.get_block_ids() == [[10]]
387 + assert blocks.get_block_ids() == [10]
388 assert manager.block_pool.free_block_queue.num_free_blocks == 7
389
390
@@ -625,7 +625,7 @@ def test_mm_prefix_caching():
625 blocks = manager.allocate_slots(req0, 59,
626 len(computed_blocks.blocks) * 16,
627 computed_blocks)
628 - assert blocks.get_block_ids() == [[1, 2, 3, 4]]
628 + assert blocks.get_block_ids() == [1, 2, 3, 4]
629 req0.num_computed_tokens = 59
630
631 # Append slots without allocating a new block.
@@ -686,7 +686,7 @@ def test_cache_key_salting():
686 blocks = manager.allocate_slots(req0, 59,
687 len(computed_blocks.blocks) * 16,
688 computed_blocks)
689 - assert blocks.get_block_ids() == [[1, 2, 3, 4]]
689 + assert blocks.get_block_ids() == [1, 2, 3, 4]
690 req0.num_computed_tokens = 59
691
692 # Append slots without allocating a new block.
@@ -797,7 +797,7 @@ def test_reset_prefix_cache():
797 all_token_ids = full_block_token_ids + unique_token_ids
798 req0 = make_request("0", all_token_ids)
799 blocks = manager.allocate_slots(req0, 55)
800 - assert blocks.get_block_ids() == [[1, 2, 3, 4]]
800 + assert blocks.get_block_ids() == [1, 2, 3, 4]
801
802 unique_token_ids = [4] * 7
803 all_token_ids = full_block_token_ids + unique_token_ids
@@ -808,7 +808,7 @@ def test_reset_prefix_cache():
808 blocks = manager.allocate_slots(req1, 7,
809 len(computed_blocks.blocks) * 16,
810 computed_blocks)
811 - assert blocks.get_block_ids() == [[5]]
811 + assert blocks.get_block_ids() == [5]
812
813 # Failed to reset prefix cache because some blocks are not freed yet.
814 assert not manager.reset_prefix_cache()
tests/v1/worker/test_gpu_input_batch.py
+6 -33
@@ -9,11 +9,9 @@ import torch
9
10 from vllm.sampling_params import SamplingParams
11 from vllm.utils import is_pin_memory_available, make_tensor_with_pad
12 -from vllm.v1.kv_cache_interface import (FullAttentionSpec, KVCacheConfig,
13 - KVCacheGroupSpec, KVCacheTensor)
12 from vllm.v1.sample.metadata import SamplingMetadata
15 -from vllm.v1.worker.block_table import BlockTable, MultiGroupBlockTable
16 -from vllm.v1.worker.gpu_input_batch import CachedRequestState, InputBatch
13 +from vllm.v1.worker.gpu_input_batch import (BlockTable, CachedRequestState,
14 + InputBatch)
15
16 VOCAB_SIZE = 1024
17 NUM_OUTPUT_TOKENS = 20
@@ -24,27 +22,6 @@ CUDA_DEVICES = [
22 MAX_NUM_PROMPT_TOKENS = 64
23
24
27 -def get_kv_cache_config() -> KVCacheConfig:
28 - return KVCacheConfig(
29 - num_blocks=10,
30 - tensors={
31 - "layer.0": KVCacheTensor(size=1024),
32 - },
33 - kv_cache_groups=[
34 - KVCacheGroupSpec(
35 - layer_names=["layer.0"],
36 - kv_cache_spec=FullAttentionSpec(
37 - block_size=1,
38 - num_kv_heads=1,
39 - head_size=16,
40 - dtype=torch.float16,
41 - use_mla=False,
42 - ),
43 - ),
44 - ],
45 - )
46 -
47 -
25 def _compare_objs(obj1, obj2):
26 attrs = inspect.getmembers(obj1, lambda a: not (inspect.isroutine(a)))
27 attr_names = set([
@@ -64,10 +41,6 @@ def _compare_objs(obj1, obj2):
41 elif isinstance(a, np.ndarray):
42 if np.allclose(a, b):
43 is_same = True
67 - elif isinstance(a, MultiGroupBlockTable):
68 - for a_i, b_i in zip(a.block_tables, b.block_tables):
69 - _compare_objs(a_i, b_i)
70 - is_same = True
44 elif isinstance(a, (BlockTable, SamplingMetadata)):
45 _compare_objs(a, b)
46 is_same = True # if we make it here must be same
@@ -225,7 +198,7 @@ def _construct_cached_request_state(req_id_suffix: int):
198 sampling_params=_create_sampling_params(),
199 mm_inputs=[],
200 mm_positions=[],
228 - block_ids=[[]],
201 + block_ids=[],
202 generator=None,
203 num_computed_tokens=len(output_token_ids),
204 output_token_ids=output_token_ids,
@@ -247,11 +220,11 @@ def test_sampling_metadata_in_input_batch(device: str, batch_size: int):
220 input_batch: InputBatch = InputBatch(
221 max_num_reqs=batch_size,
222 max_model_len=1024,
223 + max_num_blocks_per_req=10,
224 max_num_batched_tokens=1024,
225 device=torch.device(device),
226 pin_memory=is_pin_memory_available(),
227 vocab_size=1024,
254 - kv_cache_config=get_kv_cache_config(),
228 )
229 reqs: list[CachedRequestState] = []
230 req_id_reqs = {}
@@ -337,20 +310,20 @@ def test_swap_states_in_input_batch(device: str, batch_size: int,
310 input_batch: InputBatch = InputBatch(
311 max_num_reqs=batch_size,
312 max_model_len=1024,
313 + max_num_blocks_per_req=10,
314 max_num_batched_tokens=1024,
315 device=torch.device(device),
316 pin_memory=is_pin_memory_available(),
317 vocab_size=1024,
344 - kv_cache_config=get_kv_cache_config(),
318 )
319 ref_input_batch: InputBatch = InputBatch(
320 max_num_reqs=batch_size,
321 max_model_len=1024,
322 + max_num_blocks_per_req=10,
323 max_num_batched_tokens=1024,
324 device=torch.device(device),
325 pin_memory=is_pin_memory_available(),
326 vocab_size=1024,
353 - kv_cache_config=get_kv_cache_config(),
327 )
328
329 reqs: list[CachedRequestState] = []
tests/v1/worker/test_gpu_model_runner.py
+16 -41
@@ -1,16 +1,15 @@
1 # SPDX-License-Identifier: Apache-2.0
2 +import weakref
3
4 import pytest
5 +import torch
6
5 -from vllm.config import (CacheConfig, ModelConfig, ParallelConfig,
6 - SchedulerConfig, VllmConfig)
7 +from vllm.config import CacheConfig, ModelConfig, SchedulerConfig, VllmConfig
8 from vllm.sampling_params import SamplingParams
9 from vllm.v1.core.sched.output import (CachedRequestData, NewRequestData,
10 SchedulerOutput)
10 -from vllm.v1.kv_cache_interface import (FullAttentionSpec, KVCacheConfig,
11 - KVCacheGroupSpec, KVCacheTensor)
11 +from vllm.v1.kv_cache_interface import FullAttentionSpec
12 from vllm.v1.sample.metadata import SamplingMetadata
13 -from vllm.v1.worker.gpu_input_batch import InputBatch
13 from vllm.v1.worker.gpu_model_runner import GPUModelRunner
14
15
@@ -18,34 +17,13 @@ def initialize_kv_cache(runner: GPUModelRunner):
17 """
18 Only perform necessary steps in GPUModelRunner.initialize_kv_cache()
19 """
21 - kv_cache_config = KVCacheConfig(
22 - num_blocks=10,
23 - tensors={
24 - "layer.0": KVCacheTensor(size=1024),
25 - },
26 - kv_cache_groups=[
27 - KVCacheGroupSpec(
28 - layer_names=["layer.0"],
29 - kv_cache_spec=FullAttentionSpec(
30 - block_size=16,
31 - num_kv_heads=runner.model_config.get_num_kv_heads(
32 - runner.parallel_config),
33 - head_size=runner.model_config.get_head_size(),
34 - dtype=runner.kv_cache_dtype,
35 - use_mla=False,
36 - ))
37 - ])
38 - runner.kv_cache_config = kv_cache_config
39 - runner.input_batch = InputBatch(
40 - max_num_reqs=runner.max_num_reqs,
41 - max_model_len=runner.max_model_len,
42 - max_num_batched_tokens=runner.max_num_tokens,
43 - device=runner.device,
44 - pin_memory=runner.pin_memory,
45 - vocab_size=runner.model_config.get_vocab_size(),
46 - kv_cache_config=kv_cache_config,
47 - )
48 - runner.initialize_attn_backend(kv_cache_config)
20 + kv_cache_spec = FullAttentionSpec(block_size=16,
21 + num_kv_heads=1,
22 + head_size=64,
23 + dtype=torch.float16,
24 + use_mla=False)
25 + runner.attn_metadata_builder = runner.attn_backend.get_builder_cls()(
26 + weakref.proxy(runner), kv_cache_spec, runner.input_batch.block_table)
27
28
29 @pytest.fixture
@@ -70,12 +48,10 @@ def model_runner():
48 swap_space=0,
49 cache_dtype="auto",
50 )
73 - parallel_config = ParallelConfig()
51 vllm_config = VllmConfig(
52 model_config=model_config,
53 cache_config=cache_config,
54 scheduler_config=scheduler_config,
78 - parallel_config=parallel_config,
55 )
56
57 device = "cuda"
@@ -97,7 +73,7 @@ def _schedule_new_request(*req_ids: str) -> SchedulerOutput:
73 mm_hashes=[],
74 mm_positions=[],
75 sampling_params=SamplingParams(),
100 - block_ids=[[0]],
76 + block_ids=[0],
77 num_computed_tokens=0,
78 lora_request=None,
79 ))
@@ -135,14 +111,13 @@ def _is_sampling_metadata_changed(model_runner,
111
112 def _is_req_state_block_table_match(model_runner, req_id: str) -> bool:
113 req_index = model_runner.input_batch.req_id_to_index[req_id]
138 - block_table = model_runner.input_batch.block_table[0]
114 + block_table = model_runner.input_batch.block_table
115 req_state = model_runner.requests[req_id]
140 - if block_table.num_blocks_per_row[req_index] != len(
141 - req_state.block_ids[0]):
116 + if block_table.num_blocks_per_row[req_index] != len(req_state.block_ids):
117 return False
118 num_blocks = block_table.num_blocks_per_row[req_index]
119 return (block_table.block_table_np[req_index, :num_blocks] ==
145 - req_state.block_ids[0]).all()
120 + req_state.block_ids).all()
121
122
123 def test_update_states_new_request(model_runner):
@@ -225,7 +200,7 @@ def test_update_states_request_resumed(model_runner):
200 req_id=req_id,
201 resumed_from_preemption=False,
202 new_token_ids=[],
228 - new_block_ids=[[]],
203 + new_block_ids=[],
204 num_computed_tokens=0,
205 )
206
vllm/distributed/kv_transfer/kv_connector/v1/shared_storage_connector.py
+3 -3
@@ -288,7 +288,7 @@ class SharedStorageConnector(KVConnectorBase_V1):
288 for new_req in scheduler_output.scheduled_new_reqs:
289 if new_req.req_id in self._requests_need_load:
290 meta.add_request(token_ids=new_req.prompt_token_ids,
291 - block_ids=new_req.block_ids[0],
291 + block_ids=new_req.block_ids,
292 block_size=self._block_size,
293 is_store=False)
294 total_need_load += 1
@@ -299,7 +299,7 @@ class SharedStorageConnector(KVConnectorBase_V1):
299 # the original prompt tokens.
300 if not self._found_match_for_request(new_req):
301 meta.add_request(token_ids=new_req.prompt_token_ids,
302 - block_ids=new_req.block_ids[0],
302 + block_ids=new_req.block_ids,
303 block_size=self._block_size,
304 is_store=True)
305
@@ -319,7 +319,7 @@ class SharedStorageConnector(KVConnectorBase_V1):
319
320 # NOTE(rob): For resumed req, new_block_ids is all
321 # of the block_ids for the request.
322 - block_ids = cached_req.new_block_ids[0]
322 + block_ids = cached_req.new_block_ids
323
324 meta.add_request(token_ids=token_ids,
325 block_ids=block_ids,
vllm/v1/attention/backends/mla/rocm_aiter_mla.py
+2 -2
@@ -67,13 +67,13 @@ class AiterMLAMetadataBuilder(MLACommonMetadataBuilder[AiterMLAMetadata]):
67 max_model_len = self.runner.model_config.max_model_len
68 assert max_model_len == 32768,\
69 "AITER MLA requires max_model_len=32768"
70 - assert self.kv_cache_spec.block_size == 1, "AITER MLA" \
70 + assert self.runner.block_size == 1, "AITER MLA" \
71 "only supports block size 1."
72
73 def _get_paged_kv_tensors(
74 self, block_table: torch.Tensor,
75 seq_lens: torch.Tensor) -> tuple[torch.Tensor, ...]:
76 - page_size = self.kv_cache_spec.block_size
76 + page_size = self.runner.block_size
77 block_table_bounds = (seq_lens + page_size - 1) // page_size
78
79 mask = (torch.arange(block_table.size(1),
vllm/v1/core/kv_cache_manager.py
+13 -21
@@ -32,16 +32,9 @@ class KVCacheBlocks:
32 """Creates a new KVCacheBlocks instance with no blocks."""
33 return cls([])
34
35 - def get_block_ids(self) -> list[list[int]]:
36 - """
37 - Converts the KVCacheBlocks instance to block_ids.
38 -
39 - Returns:
40 - list[list[int]]: A two-level list where
41 - * the outer list corresponds to KV cache groups (only 1 group now)
42 - * each inner list contains the block_ids of the blocks in that group
43 - """
44 - return [[block.block_id for block in self.blocks]]
35 + def get_block_ids(self) -> list[int]:
36 + """Converts the KVCacheBlocks instance to a list of block IDs."""
37 + return [block.block_id for block in self.blocks]
38
39 def get_unhashed_block_ids(self) -> list[int]:
40 """Get block_ids of unhashed blocks from KVCacheBlocks instance."""
@@ -307,9 +300,9 @@ class KVCacheManager:
300 self,
301 request: Request,
302 num_running_requests: int,
310 - ) -> list[int]:
303 + ) -> int:
304 """Calculate the number of common prefix blocks shared by all requests
312 - in the RUNNING state for each kv cache group.
305 + in the RUNNING state.
306
307 The function determines this by selecting any request and iterating
308 through its blocks. A block is considered a common prefix block if its
@@ -339,14 +332,11 @@ class KVCacheManager:
332 requests in the current step.
333
334 Returns:
342 - list[int]: The number of common prefix blocks for each kv cache
343 - group.
335 + int: The number of common prefix blocks.
336 """
337 assert request.status == RequestStatus.RUNNING
346 - return [
347 - self.single_type_manager.get_num_common_prefix_blocks(
348 - request.request_id, num_running_requests)
349 - ]
338 + return self.single_type_manager.get_num_common_prefix_blocks(
339 + request.request_id, num_running_requests)
340
341 def free_block_hashes(self, request: Request) -> None:
342 """Discard the block hashes for the request.
@@ -364,8 +354,10 @@ class KVCacheManager:
354 """
355 return self.block_pool.take_events()
356
367 - def get_block_ids(self, request_id: str) -> list[list[int]]:
357 + def get_block_ids(self, request_id: str) -> list[int]:
358 """Get the block ids of a request."""
359 assert request_id in self.single_type_manager.req_to_blocks
370 - return KVCacheBlocks(self.single_type_manager.req_to_blocks[request_id]
371 - ).get_block_ids()
360 + return [
361 + block.block_id
362 + for block in self.single_type_manager.req_to_blocks[request_id]
363 + ]
vllm/v1/core/kv_cache_utils.py
+7 -6
@@ -577,12 +577,14 @@ def create_kv_cache_group_specs(
577 """
578 kv_cache_groups = []
579 for layer_names_one_group in grouped_layer_names:
580 - layer_specs = [
581 - kv_cache_spec[layer_name] for layer_name in layer_names_one_group
582 - ]
583 - merged_layer_spec = layer_specs[0].merge(layer_specs)
580 + layer_spec = kv_cache_spec[layer_names_one_group[0]]
581 + assert all(
582 + kv_cache_spec[layer_name] == layer_spec
583 + for layer_name in layer_names_one_group[1:]), (
584 + "All layers in the same KV cache group must share the same "
585 + "KVCacheSpec.")
586 kv_cache_groups.append(
585 - KVCacheGroupSpec(layer_names_one_group, merged_layer_spec))
587 + KVCacheGroupSpec(layer_names_one_group, layer_spec))
588 return kv_cache_groups
589
590
@@ -681,7 +683,6 @@ def unify_hybrid_kv_cache_specs(kv_cache_spec: dict[str, KVCacheSpec]):
683 head_size=spec.head_size,
684 dtype=spec.dtype,
685 use_mla=spec.use_mla,
684 - sliding_window=spec.sliding_window,
686 )
687
688
vllm/v1/core/sched/output.py
+6 -6
@@ -26,7 +26,7 @@ class NewRequestData:
26 mm_hashes: list[str]
27 mm_positions: list[PlaceholderRange]
28 sampling_params: SamplingParams
29 - block_ids: list[list[int]]
29 + block_ids: list[int]
30 num_computed_tokens: int
31 lora_request: Optional[LoRARequest]
32
@@ -34,7 +34,7 @@ class NewRequestData:
34 def from_request(
35 cls,
36 request: Request,
37 - block_ids: list[list[int]],
37 + block_ids: list[int],
38 ) -> NewRequestData:
39 return cls(
40 req_id=request.request_id,
@@ -85,7 +85,7 @@ class CachedRequestData:
85 # request's block IDs instead of appending to the existing block IDs.
86 resumed_from_preemption: bool
87 new_token_ids: list[int]
88 - new_block_ids: list[list[int]]
88 + new_block_ids: list[int]
89 num_computed_tokens: int
90
91 @classmethod
@@ -94,7 +94,7 @@ class CachedRequestData:
94 request: Request,
95 resumed_from_preemption: bool,
96 new_token_ids: list[int],
97 - new_block_ids: list[list[int]],
97 + new_block_ids: list[int],
98 ) -> CachedRequestData:
99 return cls(
100 req_id=request.request_id,
@@ -131,9 +131,9 @@ class SchedulerOutput:
131 # E.g., if a request has [0, 1], it could mean the vision encoder needs
132 # to process that the request's 0-th and 1-th images in the current step.
133 scheduled_encoder_inputs: dict[str, list[int]]
134 - # Number of common prefix blocks for all requests in each KV cache group.
134 + # Number of common prefix blocks for all requests.
135 # This can be used for cascade attention.
136 - num_common_prefix_blocks: list[int]
136 + num_common_prefix_blocks: int
137
138 # Request IDs that are finished in between the previous and the current
139 # steps. This is used to notify the workers about the finished requests
vllm/v1/core/sched/scheduler.py
+6 -10
@@ -173,7 +173,7 @@ class Scheduler(SchedulerInterface):
173 # uses structured decoding.
174 structured_output_request_ids: dict[str, int] = {}
175
176 - req_to_new_block_ids: dict[str, list[list[int]]] = {}
176 + req_to_new_block_ids: dict[str, list[int]] = {}
177 num_scheduled_tokens: dict[str, int] = {}
178 token_budget = self.max_num_scheduled_tokens
179 # Encoder-related.
@@ -484,8 +484,7 @@ class Scheduler(SchedulerInterface):
484
485 # Get the longest common prefix among all requests in the running queue.
486 # This can be potentially used for cascade attention.
487 - num_common_prefix_blocks = [0] * len(
488 - self.kv_cache_config.kv_cache_groups)
487 + num_common_prefix_blocks = 0
488 if self.running:
489 any_request = self.running[0]
490 num_common_prefix_blocks = (
@@ -572,7 +571,7 @@ class Scheduler(SchedulerInterface):
571 request: Request,
572 num_scheduled_tokens: int,
573 num_scheduled_spec_tokens: int,
575 - new_block_ids: list[list[int]],
574 + new_block_ids: list[int],
575 resumed_from_preemption: bool,
576 ) -> CachedRequestData:
577 # OPTIMIZATION: Cache the CachedRequestData objects to avoid creating
@@ -947,9 +946,7 @@ class Scheduler(SchedulerInterface):
946 """
947 if self.connector is None:
948 return False, None
950 - assert len(self.kv_cache_config.kv_cache_groups
951 - ) == 1, "KV connector only supports one KV cache group now"
952 - block_ids = self.kv_cache_manager.get_block_ids(request.request_id)[0]
949 + block_ids = self.kv_cache_manager.get_block_ids(request.request_id)
950 return self.connector.request_finished(request, block_ids)
951
952 def _update_waiting_for_remote_kv(self, request: Request) -> bool:
@@ -966,10 +963,9 @@ class Scheduler(SchedulerInterface):
963 """
964 if request.request_id not in self.finished_recving_kv_req_ids:
965 return False
969 - assert len(self.kv_cache_config.kv_cache_groups
970 - ) == 1, "KV connector only supports one KV cache group now"
966 +
967 # Now that the blocks are ready, actually cache them.
972 - block_ids = self.kv_cache_manager.get_block_ids(request.request_id)[0]
968 + block_ids = self.kv_cache_manager.get_block_ids(request.request_id)
969 num_computed_tokens = len(block_ids) * self.block_size
970 if num_computed_tokens == request.num_tokens:
971 num_computed_tokens -= 1
vllm/v1/kv_cache_interface.py
-42
@@ -1,11 +1,8 @@
1 # SPDX-License-Identifier: Apache-2.0
2
3 -import copy
3 from dataclasses import dataclass
5 -from typing import Optional
4
5 import torch
8 -from typing_extensions import Self
6
7 from vllm.config import VllmConfig
8 from vllm.logger import init_logger
@@ -56,16 +53,6 @@ class KVCacheSpec:
53 """
54 raise NotImplementedError
55
59 - @classmethod
60 - def merge(cls, specs: list[Self]) -> Self:
61 - """
62 - Merge a list of KVCacheSpec objects into a single KVCacheSpec object.
63 - """
64 - assert all(spec.type_id == specs[0].type_id for spec in specs[1:]), (
65 - "All layers in the same KV cache group must share the same "
66 - "type_id.")
67 - return copy.deepcopy(specs[0])
68 -
56
57 @dataclass
58 class AttentionSpec(KVCacheSpec):
@@ -84,16 +71,6 @@ class AttentionSpec(KVCacheSpec):
71
72 @dataclass
73 class FullAttentionSpec(AttentionSpec):
87 - sliding_window: Optional[int] = None
88 - """
89 - When hybrid allocator is disabled and the model contains both full
90 - attention layers and sliding window attention layers, sliding
91 - window attention are regarded as full attention in KV cache manager
92 - (blocks are allocated for all tokens), while computed as sliding window
93 - attention in model runner.
94 - In this case, we use FullAttentionSpec and record the sliding window size.
95 - Default to None for not using sliding window attention.
96 - """
74
75 @property
76 def type_id(self) -> str:
@@ -103,25 +80,6 @@ class FullAttentionSpec(AttentionSpec):
80 max_model_len = vllm_config.model_config.max_model_len
81 return cdiv(max_model_len, self.block_size) * self.page_size_bytes
82
106 - @classmethod
107 - def merge(cls, specs: list[Self]) -> Self:
108 - """
109 - Merge a list of FullAttentionSpec objects into a single
110 - FullAttentionSpec object.
111 - """
112 - merged_spec = super().merge(specs)
113 - sliding_window = set(spec.sliding_window for spec in specs
114 - if spec.sliding_window is not None)
115 - if len(sliding_window) == 0:
116 - merged_spec.sliding_window = None
117 - elif len(sliding_window) == 1:
118 - merged_spec.sliding_window = sliding_window.pop()
119 - else:
120 - raise ValueError(
121 - "All sliding window layers in the same KV cache group "
122 - "must have the same window size.")
123 - return merged_spec
124 -
83
84 @dataclass
85 class SlidingWindowSpec(AttentionSpec):
vllm/v1/worker/block_table.py
-47
@@ -4,8 +4,6 @@ import numpy as np
4 import torch
5
6 from vllm.logger import init_logger
7 -from vllm.utils import cdiv
8 -from vllm.v1.kv_cache_interface import KVCacheConfig
7
8 logger = init_logger(__name__)
9
@@ -98,48 +96,3 @@ class BlockTable:
96 def get_numpy_array(self) -> np.ndarray:
97 """Returns the numpy array of the block table."""
98 return self.block_table_np
101 -
102 -
103 -class MultiGroupBlockTable:
104 - """The BlockTables for each KV cache group."""
105 -
106 - def __init__(self, max_num_reqs: int, max_model_len: int,
107 - max_num_batched_tokens: int, pin_memory: bool,
108 - device: torch.device, kv_cache_config: KVCacheConfig) -> None:
109 - max_num_blocks_per_req = [
110 - cdiv(max_model_len, g.kv_cache_spec.block_size)
111 - for g in kv_cache_config.kv_cache_groups
112 - ]
113 - self.block_tables = [
114 - BlockTable(max_num_reqs, max_num_blocks_per_req[i],
115 - max_num_batched_tokens, pin_memory, device)
116 - for i in range(len(kv_cache_config.kv_cache_groups))
117 - ]
118 -
119 - def append_row(self, block_ids: list[list[int]], row_idx: int) -> None:
120 - for i, block_table in enumerate(self.block_tables):
121 - block_table.append_row(block_ids[i], row_idx)
122 -
123 - def add_row(self, block_ids: list[list[int]], row_idx: int) -> None:
124 - for i, block_table in enumerate(self.block_tables):
125 - block_table.add_row(block_ids[i], row_idx)
126 -
127 - def move_row(self, src: int, tgt: int) -> None:
128 - for block_table in self.block_tables:
129 - block_table.move_row(src, tgt)
130 -
131 - def swap_row(self, src: int, tgt: int) -> None:
132 - for block_table in self.block_tables:
133 - block_table.swap_row(src, tgt)
134 -
135 - def commit(self, num_reqs: int) -> None:
136 - for block_table in self.block_tables:
137 - block_table.commit(num_reqs)
138 -
139 - def clear(self) -> None:
140 - for block_table in self.block_tables:
141 - block_table.clear()
142 -
143 - def __getitem__(self, idx: int) -> "BlockTable":
144 - """Returns the BlockTable for the i-th KV cache group."""
145 - return self.block_tables[idx]
vllm/v1/worker/gpu_input_batch.py
+6 -7
@@ -11,11 +11,10 @@ from vllm.lora.request import LoRARequest
11 from vllm.multimodal.inputs import MultiModalKwargs, PlaceholderRange
12 from vllm.sampling_params import SamplingParams, SamplingType
13 from vllm.utils import swap_dict_values
14 -from vllm.v1.kv_cache_interface import KVCacheConfig
14 from vllm.v1.outputs import LogprobsTensors
15 from vllm.v1.sample.metadata import SamplingMetadata
16 from vllm.v1.utils import copy_slice
18 -from vllm.v1.worker.block_table import MultiGroupBlockTable
17 +from vllm.v1.worker.block_table import BlockTable
18
19 _SAMPLING_EPS = 1e-5
20
@@ -30,7 +29,7 @@ class CachedRequestState:
29 sampling_params: SamplingParams
30 generator: Optional[torch.Generator]
31
33 - block_ids: list[list[int]]
32 + block_ids: list[int]
33 num_computed_tokens: int
34 output_token_ids: list[int]
35
@@ -59,14 +58,15 @@ class InputBatch:
58 self,
59 max_num_reqs: int,
60 max_model_len: int,
61 + max_num_blocks_per_req: int,
62 max_num_batched_tokens: int,
63 device: torch.device,
64 pin_memory: bool,
65 vocab_size: int,
66 - kv_cache_config: KVCacheConfig,
66 ):
67 self.max_num_reqs = max_num_reqs
68 self.max_model_len = max_model_len
69 + self.max_num_blocks_per_req = max_num_blocks_per_req
70 self.max_num_batched_tokens = max_num_batched_tokens
71 self.device = device
72 self.pin_memory = pin_memory
@@ -99,13 +99,12 @@ class InputBatch:
99 self.num_computed_tokens_cpu_tensor.numpy()
100
101 # Block table.
102 - self.block_table = MultiGroupBlockTable(
102 + self.block_table = BlockTable(
103 max_num_reqs=max_num_reqs,
104 - max_model_len=max_model_len,
104 + max_num_blocks_per_req=max_num_blocks_per_req,
105 max_num_batched_tokens=max_num_batched_tokens,
106 pin_memory=pin_memory,
107 device=device,
108 - kv_cache_config=kv_cache_config,
108 )
109
110 # Sampling-related.
vllm/v1/worker/gpu_model_runner.py
+112 -158
@@ -12,8 +12,6 @@ import torch.distributed
12 import torch.nn as nn
13
14 from vllm.attention import AttentionType, get_attn_backend
15 -from vllm.attention.backends.abstract import (AttentionBackend,
16 - AttentionMetadataBuilder)
15 from vllm.attention.layer import Attention
16 from vllm.attention.utils.fa_utils import get_flash_attn_version
17 from vllm.config import (CompilationLevel, VllmConfig,
@@ -34,8 +32,8 @@ from vllm.multimodal.utils import group_mm_inputs_by_modality
32 from vllm.sampling_params import SamplingType
33 from vllm.sequence import IntermediateTensors
34 from vllm.utils import (STR_DTYPE_TO_TORCH_DTYPE, DeviceMemoryProfiler,
37 - GiB_bytes, LazyLoader, cdiv, check_use_alibi,
38 - is_pin_memory_available)
35 + GiB_bytes, LayerBlockType, LazyLoader, cdiv,
36 + check_use_alibi, is_pin_memory_available)
37 from vllm.v1.attention.backends.flash_attn import FlashAttentionMetadata
38 from vllm.v1.attention.backends.utils import CommonAttentionMetadata
39 from vllm.v1.core.encoder_cache_manager import compute_encoder_budget
@@ -53,7 +51,6 @@ from vllm.v1.spec_decode.metadata import SpecDecodeMetadata
51 from vllm.v1.spec_decode.ngram_proposer import NgramProposer
52 from vllm.v1.spec_decode.utils import is_spec_decode_supported
53 from vllm.v1.utils import bind_kv_cache
56 -from vllm.v1.worker.block_table import BlockTable
54 from vllm.v1.worker.gpu_input_batch import CachedRequestState, InputBatch
55 from vllm.v1.worker.lora_model_runner_mixin import LoRAModelRunnerMixin
56
@@ -105,17 +102,59 @@ class GPUModelRunner(LoRAModelRunnerMixin):
102 self.kv_cache_dtype = STR_DTYPE_TO_TORCH_DTYPE[
103 cache_config.cache_dtype]
104
105 + # NOTE(woosuk): sliding_window is None for models with interleaved
106 + # attention. Use interleaved_sliding_window instead.
107 + self.sliding_window = model_config.get_sliding_window()
108 + self.interleaved_sliding_window = getattr(
109 + model_config.hf_text_config, "interleaved_sliding_window", None)
110 + self.window_size = (self.sliding_window
111 + or self.interleaved_sliding_window)
112 +
113 self.is_multimodal_model = model_config.is_multimodal_model
114 + self.block_size = cache_config.block_size
115 self.max_model_len = model_config.max_model_len
116 + self.max_num_blocks_per_req = cdiv(self.max_model_len, self.block_size)
117 self.max_num_tokens = scheduler_config.max_num_batched_tokens
118 self.max_num_reqs = scheduler_config.max_num_seqs
119
120 # Model-related.
121 + self.num_attn_layers = model_config.get_num_layers_by_block_type(
122 + parallel_config, LayerBlockType.attention)
123 self.num_query_heads = model_config.get_num_attention_heads(
124 parallel_config)
125 + self.num_kv_heads = model_config.get_num_kv_heads(parallel_config)
126 + self.head_size = model_config.get_head_size()
127 self.hidden_size = model_config.get_hidden_size()
128 self.attention_chunk_size = model_config.attention_chunk_size
129
130 + self.attn_backend = get_attn_backend(
131 + self.head_size,
132 + self.dtype,
133 + self.kv_cache_dtype,
134 + self.block_size,
135 + self.model_config.is_attention_free,
136 + use_mla=self.model_config.use_mla,
137 + )
138 + if self.attn_backend is None:
139 + error_msg = (
140 + f"Error with get_att_backend: {self.head_size=}, "
141 + f"{self.dtype=}, {self.kv_cache_dtype=}, {self.block_size=}, "
142 + f"{self.model_config.is_attention_free=}, "
143 + f"{self.model_config.use_mla=}")
144 + logger.error(error_msg)
145 + raise NotImplementedError(
146 + "Non-Attention backend is not supported by V1 GPUModelRunner.")
147 +
148 + if self.vllm_config.compilation_config.full_cuda_graph:
149 + attn_backend_name = self.attn_backend.__name__
150 + flash_attn_version = get_flash_attn_version()
151 + if attn_backend_name != "FlashAttentionBackend" or \
152 + flash_attn_version != 3:
153 + raise ValueError(
154 + f"full_cuda_graph is only supported with "
155 + f"FA3. Current attention backend is {attn_backend_name}, "
156 + f"FlashAttention version is {flash_attn_version}.")
157 +
158 self.cascade_attn_enabled = not self.model_config.disable_cascade_attn
159
160 # Multi-modal data support
@@ -137,10 +176,8 @@ class GPUModelRunner(LoRAModelRunnerMixin):
176 # self.model: nn.Module # Set after load_model
177 # Initialize in initialize_kv_cache
178 self.kv_caches: list[torch.Tensor] = []
140 - self.attn_metadata_builders: list[AttentionMetadataBuilder] = []
141 - self.attn_backends: list[type[AttentionBackend]] = []
179 # self.kv_cache_config: KVCacheConfig
143 - # self.input_batch: InputBatch # Persistent batch.
180 + # self.attn_metadata_builder: type[AttentionMetadataBuilder]
181
182 # req_id -> (input_id -> encoder_output)
183 self.encoder_cache: dict[str, dict[int, torch.Tensor]] = {}
@@ -169,6 +206,16 @@ class GPUModelRunner(LoRAModelRunnerMixin):
206
207 # Request states.
208 self.requests: dict[str, CachedRequestState] = {}
209 + # Persistent batch.
210 + self.input_batch = InputBatch(
211 + max_num_reqs=self.max_num_reqs,
212 + max_model_len=self.max_model_len,
213 + max_num_blocks_per_req=self.max_num_blocks_per_req,
214 + max_num_batched_tokens=self.max_num_tokens,
215 + device=self.device,
216 + pin_memory=self.pin_memory,
217 + vocab_size=model_config.get_vocab_size(),
218 + )
219
220 self.use_cuda_graph = (self.vllm_config.compilation_config.level
221 == CompilationLevel.PIECEWISE
@@ -263,31 +310,6 @@ class GPUModelRunner(LoRAModelRunnerMixin):
310 pin_memory=self.pin_memory)
311 self.seq_lens_np = self.seq_lens_cpu.numpy()
312
266 - def _may_reorder_batch(self, scheduler_output: "SchedulerOutput") -> bool:
267 - """
268 - Update the order of requests in the batch based on the attention
269 - backend's needs. For example, some attention backends (namely MLA) may
270 - want to separate requests based on if the attention computation will be
271 - compute-bound or memory-bound.
272 -
273 - Args:
274 - scheduler_output: The scheduler output.
275 -
276 - Returns:
277 - True if the batch was reordered, False otherwise.
278 - """
279 - batch_reordered = self.attn_metadata_builders[0].reorder_batch(
280 - self.input_batch, scheduler_output)
281 -
282 - # For models with multiple KV cache groups, the groups should agree on
283 - # the same order of requests. We ensure this by only allowing the first
284 - # group to reorder the batch and asserting that all other groups do not
285 - # reorder the batch.
286 - for i in range(1, len(self.kv_cache_config.kv_cache_groups)):
287 - assert not self.attn_metadata_builders[i].reorder_batch(
288 - self.input_batch, scheduler_output)
289 - return batch_reordered
290 -
313 def _update_states(self, scheduler_output: "SchedulerOutput") -> None:
314 """Update the cached states and the persistent batch with the scheduler
315 output.
@@ -424,8 +446,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
446 # Update the block IDs.
447 if not req_data.resumed_from_preemption:
448 # Append the new blocks to the existing block IDs.
427 - for i in range(len(self.kv_cache_config.kv_cache_groups)):
428 - req_state.block_ids[i].extend(req_data.new_block_ids[i])
449 + req_state.block_ids.extend(req_data.new_block_ids)
450 else:
451 # The request is resumed from preemption.
452 # Replace the existing block IDs with the new ones.
@@ -483,7 +504,11 @@ class GPUModelRunner(LoRAModelRunnerMixin):
504 if removed_req_indices:
505 self.input_batch.condense(removed_req_indices)
506
486 - batch_reordered = self._may_reorder_batch(scheduler_output)
507 + # Some attention backends (namely MLA) may want to separate requests
508 + # based on if the attention computation will be compute-bound or
509 + # memory-bound. This gives them a hook to do that.
510 + batch_reordered = self.attn_metadata_builder.reorder_batch(
511 + self.input_batch, scheduler_output)
512
513 if batch_changed or batch_reordered:
514 self.input_batch.refresh_sampling_metadata()
@@ -551,29 +576,21 @@ class GPUModelRunner(LoRAModelRunnerMixin):
576 torch.from_numpy(token_indices),
577 out=self.input_ids_cpu[:total_num_scheduled_tokens])
578
554 - # Calculate the slot mapping for each KV cache group.
555 - for kv_cache_group_id, kv_cache_group_spec in enumerate(
556 - self.kv_cache_config.kv_cache_groups):
557 - block_size = kv_cache_group_spec.kv_cache_spec.block_size
558 - block_table: BlockTable = self.input_batch.block_table[
559 - kv_cache_group_id]
560 - # E.g., [0, 1, 0, 1, 2, 3, 4, 0, 1, 2]
561 - # -> [0, 0, K, K, K + 1, K + 1, K + 2, 2 * K, 2 * K, 2 * K + 1]
562 - # where K is the max_num_blocks_per_req and the block size is 2.
563 - # NOTE(woosuk): We can't simply use `token_indices // block_size`
564 - # here because M (max_model_len) is not necessarily divisible by
565 - # block_size.
566 - block_table_indices = (
567 - req_indices * block_table.max_num_blocks_per_req +
568 - positions_np // block_size)
569 - block_table_cpu = block_table.get_cpu_tensor()
570 - block_numbers = block_table_cpu.flatten(
571 - )[block_table_indices].numpy()
572 - block_offsets = positions_np % block_size
573 - np.add(
574 - block_numbers * block_size,
575 - block_offsets,
576 - out=block_table.slot_mapping_np[:total_num_scheduled_tokens])
579 + # Calculate the slot mapping.
580 + # E.g., [0, 1, 0, 1, 2, 3, 4, 0, 1, 2]
581 + # -> [0, 0, K, K, K + 1, K + 1, K + 2, 2 * K, 2 * K, 2 * K + 1]
582 + # where K is the max_num_blocks_per_req and the block size is 2.
583 + # NOTE(woosuk): We can't simply use `token_indices // block_size` here
584 + # because M (max_model_len) is not necessarily divisible by block_size.
585 + block_table_indices = (req_indices * self.max_num_blocks_per_req +
586 + positions_np // self.block_size)
587 + block_table_cpu = self.input_batch.block_table.get_cpu_tensor()
588 + block_numbers = block_table_cpu.flatten()[block_table_indices].numpy()
589 + block_offsets = positions_np % self.block_size
590 + np.add(block_numbers * self.block_size,
591 + block_offsets,
592 + out=self.input_batch.block_table.
593 + slot_mapping_np[:total_num_scheduled_tokens])
594
595 # Prepare the attention metadata.
596 self.query_start_loc_np[0] = 0
@@ -615,6 +632,10 @@ class GPUModelRunner(LoRAModelRunnerMixin):
632 attn_metadata: dict[str, FlashAttentionMetadata] = {}
633 # Prepare the attention metadata for each KV cache group and make layers
634 # in the same group share the same metadata.
635 + # NOTE(Chen): there is exactly one KV cache group that contains all
636 + # attetnion layers in the model for now, so the current logic for
637 + # getting attn_metadata is not related to kv_cache_group information.
638 + # Will extend this part to support multiple KV cache groups later.
639 for kv_cache_group_id, kv_cache_group_spec in enumerate(
640 self.kv_cache_config.kv_cache_groups):
641
@@ -623,19 +644,15 @@ class GPUModelRunner(LoRAModelRunnerMixin):
644 if self.cascade_attn_enabled:
645 common_prefix_len = self._compute_cascade_attn_prefix_len(
646 num_scheduled_tokens,
626 - scheduler_output.
627 - num_common_prefix_blocks[kv_cache_group_id],
628 - kv_cache_group_spec.kv_cache_spec,
629 - self.attn_metadata_builders[kv_cache_group_id],
647 + scheduler_output.num_common_prefix_blocks,
648 )
649
632 - attn_metadata_i = (
633 - self.attn_metadata_builders[kv_cache_group_id].build(
634 - num_reqs=num_reqs,
635 - num_actual_tokens=total_num_scheduled_tokens,
636 - max_query_len=max_num_scheduled_tokens,
637 - common_prefix_len=common_prefix_len,
638 - common_attn_metadata=common_attn_metadata))
650 + attn_metadata_i = self.attn_metadata_builder.build(
651 + num_reqs=num_reqs,
652 + num_actual_tokens=total_num_scheduled_tokens,
653 + max_query_len=max_num_scheduled_tokens,
654 + common_prefix_len=common_prefix_len,
655 + common_attn_metadata=common_attn_metadata)
656 for layer_name in kv_cache_group_spec.layer_names:
657 attn_metadata[layer_name] = attn_metadata_i
658
@@ -673,8 +690,6 @@ class GPUModelRunner(LoRAModelRunnerMixin):
690 self,
691 num_scheduled_tokens: np.ndarray,
692 num_common_prefix_blocks: int,
676 - kv_cache_spec: KVCacheSpec,
677 - attn_metadata_builder: AttentionMetadataBuilder,
693 ) -> int:
694 """Compute the length of the common prefix for cascade attention.
695
@@ -693,7 +708,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
708 Returns:
709 int: Length of common prefix in tokens.
710 """
696 - common_prefix_len = num_common_prefix_blocks * kv_cache_spec.block_size
711 + common_prefix_len = num_common_prefix_blocks * self.block_size
712 if common_prefix_len == 0:
713 # Common case.
714 return 0
@@ -742,19 +757,15 @@ class GPUModelRunner(LoRAModelRunnerMixin):
757 common_prefix_len,
758 self.input_batch.num_computed_tokens_cpu[:num_reqs].min())
759 # common_prefix_len should be a multiple of the block size.
745 - common_prefix_len = (common_prefix_len // kv_cache_spec.block_size *
746 - kv_cache_spec.block_size)
747 - use_sliding_window = (isinstance(kv_cache_spec, SlidingWindowSpec) or
748 - (isinstance(kv_cache_spec, FullAttentionSpec)
749 - and kv_cache_spec.sliding_window is not None))
750 - assert isinstance(kv_cache_spec, AttentionSpec)
751 - use_cascade = attn_metadata_builder.use_cascade_attention(
760 + common_prefix_len = (common_prefix_len // self.block_size *
761 + self.block_size)
762 + use_cascade = self.attn_metadata_builder.use_cascade_attention(
763 common_prefix_len=common_prefix_len,
764 query_lens=num_scheduled_tokens,
765 num_query_heads=self.num_query_heads,
755 - num_kv_heads=kv_cache_spec.num_kv_heads,
766 + num_kv_heads=self.num_kv_heads,
767 use_alibi=self.use_alibi,
757 - use_sliding_window=use_sliding_window,
768 + use_sliding_window=self.window_size is not None,
769 num_sms=self.num_sms,
770 )
771 return common_prefix_len if use_cascade else 0
@@ -1640,7 +1651,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1651 dtype=np.int32)
1652
1653 if skip_attn:
1643 - attn_metadata: Optional[dict[str, FlashAttentionMetadata]] = None
1654 + attn_metadata = None
1655 else:
1656 query_start_loc = self.query_start_loc[:num_reqs + 1]
1657 seq_lens = self.seq_lens[:num_reqs]
@@ -1648,19 +1659,13 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1659 common_attn_metadata = CommonAttentionMetadata(
1660 query_start_loc=query_start_loc, seq_lens=seq_lens)
1661
1651 - attn_metadata = {}
1652 - for kv_cache_group_id, kv_cache_group_spec in enumerate(
1653 - self.kv_cache_config.kv_cache_groups):
1654 - attn_metadata_i = (
1655 - self.attn_metadata_builders[kv_cache_group_id].build(
1656 - num_reqs=num_tokens,
1657 - num_actual_tokens=num_tokens,
1658 - max_query_len=num_tokens,
1659 - common_prefix_len=0,
1660 - common_attn_metadata=common_attn_metadata,
1661 - ))
1662 - for layer_name in kv_cache_group_spec.layer_names:
1663 - attn_metadata[layer_name] = attn_metadata_i
1662 + attn_metadata = self.attn_metadata_builder.build(
1663 + num_reqs=num_tokens,
1664 + num_actual_tokens=num_tokens,
1665 + max_query_len=num_tokens,
1666 + common_prefix_len=0,
1667 + common_attn_metadata=common_attn_metadata,
1668 + )
1669
1670 with self.maybe_dummy_run_with_lora(self.lora_config,
1671 num_scheduled_tokens):
@@ -1890,56 +1895,6 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1895 logger.info("Graph capturing finished in %.0f secs, took %.2f GiB",
1896 elapsed_time, cuda_graph_size / (1 << 30))
1897
1893 - def initialize_attn_backend(self, kv_cache_config: KVCacheConfig) -> None:
1894 - """
1895 - Initialize the attention backends and attention metadata builders.
1896 - """
1897 - assert len(self.attn_backends) == 0 and len(
1898 - self.attn_metadata_builders
1899 - ) == 0, "Attention backends are already initialized"
1900 - for i, kv_cache_group_spec in enumerate(
1901 - kv_cache_config.kv_cache_groups):
1902 - kv_cache_spec = kv_cache_group_spec.kv_cache_spec
1903 - if not isinstance(kv_cache_spec, AttentionSpec):
1904 - raise NotImplementedError(
1905 - "Only AttentionSpec is supported for now.")
1906 - attn_backend_i = get_attn_backend(
1907 - kv_cache_spec.head_size,
1908 - self.dtype,
1909 - kv_cache_spec.dtype,
1910 - kv_cache_spec.block_size,
1911 - self.model_config.is_attention_free,
1912 - use_mla=kv_cache_spec.use_mla,
1913 - )
1914 - if attn_backend_i is None:
1915 - error_msg = (
1916 - f"Error with get_attn_backend: {kv_cache_spec.head_size=}, "
1917 - f"{self.dtype=}, {kv_cache_spec.dtype=}, "
1918 - f"{kv_cache_spec.block_size=}, "
1919 - f"{self.model_config.is_attention_free=}, "
1920 - f"{kv_cache_spec.use_mla=}")
1921 - logger.error(error_msg)
1922 - raise NotImplementedError(
1923 - "Non-Attention backend is not supported by V1 "
1924 - "GPUModelRunner.")
1925 -
1926 - if self.vllm_config.compilation_config.full_cuda_graph:
1927 - attn_backend_name = attn_backend_i.__name__
1928 - flash_attn_version = get_flash_attn_version()
1929 - if attn_backend_name != "FlashAttentionBackend" or \
1930 - flash_attn_version != 3:
1931 - raise ValueError(
1932 - f"full_cuda_graph is only supported with "
1933 - f"FA3. Current attention backend is "
1934 - f"{attn_backend_name}, FlashAttention version is "
1935 - f"{flash_attn_version}.")
1936 -
1937 - block_table_i = self.input_batch.block_table[i]
1938 - attn_metadata_builder_i = attn_backend_i.get_builder_cls()(
1939 - weakref.proxy(self), kv_cache_spec, block_table_i)
1940 - self.attn_backends.append(attn_backend_i)
1941 - self.attn_metadata_builders.append(attn_metadata_builder_i)
1942 -
1898 def initialize_kv_cache(self, kv_cache_config: KVCacheConfig) -> None:
1899 """
1900 Initialize KV cache based on `kv_cache_config`.
@@ -1947,21 +1902,15 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1902 kv_cache_config: Configuration for the KV cache, including the KV
1903 cache size of each layer
1904 """
1905 + if len(kv_cache_config.kv_cache_groups) > 1:
1906 + raise NotImplementedError(
1907 + "Hybrid models with more than one KV cache type are not "
1908 + "supported yet.")
1909 self.kv_cache_config = kv_cache_config
1951 - self.input_batch = InputBatch(
1952 - max_num_reqs=self.max_num_reqs,
1953 - max_model_len=self.max_model_len,
1954 - max_num_batched_tokens=self.max_num_tokens,
1955 - device=self.device,
1956 - pin_memory=self.pin_memory,
1957 - vocab_size=self.model_config.get_vocab_size(),
1958 - kv_cache_config=kv_cache_config,
1959 - )
1960 - self.initialize_attn_backend(kv_cache_config)
1910
1911 kv_caches: dict[str, torch.Tensor] = {}
1912
1964 - for i, kv_cache_group in enumerate(kv_cache_config.kv_cache_groups):
1913 + for kv_cache_group in kv_cache_config.kv_cache_groups:
1914 kv_cache_spec = kv_cache_group.kv_cache_spec
1915 for layer_name in kv_cache_group.layer_names:
1916 tensor_config = kv_cache_config.tensors[layer_name]
@@ -1976,7 +1925,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1925 # the min of all `num_blocks`. Verify it here.
1926 assert num_blocks >= kv_cache_config.num_blocks
1927 if isinstance(kv_cache_spec, AttentionSpec):
1979 - kv_cache_shape = self.attn_backends[i].get_kv_cache_shape(
1928 + kv_cache_shape = self.attn_backend.get_kv_cache_shape(
1929 num_blocks, kv_cache_spec.block_size,
1930 kv_cache_spec.num_kv_heads, kv_cache_spec.head_size)
1931 dtype = kv_cache_spec.dtype
@@ -1996,6 +1945,11 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1945 if has_kv_transfer_group():
1946 get_kv_transfer_group().register_kv_caches(kv_caches)
1947
1948 + self.attn_metadata_builder = self.attn_backend.get_builder_cls()(
1949 + weakref.proxy(self),
1950 + kv_cache_config.kv_cache_groups[0].kv_cache_spec,
1951 + self.input_batch.block_table)
1952 +
1953 def get_kv_cache_spec(self) -> dict[str, KVCacheSpec]:
1954 """
1955 Generates the KVCacheSpec by parsing the kv cache format from each
vllm/v1/worker/tpu_model_runner.py
+16 -19
@@ -171,10 +171,19 @@ class TPUModelRunner(LoRAModelRunnerMixin):
171 self.kv_caches: list[torch.Tensor] = []
172 # req_id -> (input_id -> encoder_output)
173 self.encoder_cache: dict[str, dict[int, torch.Tensor]] = {}
174 - # self.input_batch: InputBatch # Persistent batch.
174
175 # Request states.
176 self.requests: dict[str, CachedRequestState] = {}
177 + # Persistent batch.
178 + self.input_batch = InputBatch(
179 + max_num_reqs=self.max_num_reqs,
180 + max_model_len=self.max_model_len,
181 + max_num_blocks_per_req=self.max_num_blocks_per_req,
182 + max_num_batched_tokens=self.max_num_tokens,
183 + device=self.device,
184 + pin_memory=self.pin_memory,
185 + vocab_size=self.vocab_size,
186 + )
187
188 # Cached torch/numpy tensor
189 # The pytorch tensor and numpy array share the same buffer.
@@ -190,7 +199,7 @@ class TPUModelRunner(LoRAModelRunnerMixin):
199
200 self.block_table_cpu = torch.zeros(
201 (self.max_num_reqs, self.max_num_blocks_per_req),
193 - dtype=torch.int32,
202 + dtype=self.input_batch.block_table.get_cpu_tensor().dtype,
203 device="cpu")
204
205 self.query_start_loc_cpu = torch.zeros(self.max_num_tokens + 1,
@@ -515,12 +524,12 @@ class TPUModelRunner(LoRAModelRunnerMixin):
524 # NOTE(woosuk): We use torch.index_select instead of np.take here
525 # because torch.index_select is much faster than np.take for large
526 # tensors.
518 - block_table_cpu = self.input_batch.block_table[0].get_cpu_tensor()
527 + block_table_cpu = self.input_batch.block_table.get_cpu_tensor()
528 block_numbers = block_table_cpu.flatten()[block_table_indices].numpy()
529 block_offsets = positions_np % self.block_size
530 np.add(block_numbers * self.block_size,
531 block_offsets,
523 - out=self.input_batch.block_table[0].
532 + out=self.input_batch.block_table.
533 slot_mapping_np[:total_num_scheduled_tokens])
534
535 # Prepare the attention metadata.
@@ -545,15 +554,15 @@ class TPUModelRunner(LoRAModelRunnerMixin):
554 self.position_ids = self.positions_cpu[:
555 padded_total_num_scheduled_tokens].to(
556 self.device)
548 - self.input_batch.block_table[0].slot_mapping_cpu[
557 + self.input_batch.block_table.slot_mapping_cpu[
558 total_num_scheduled_tokens:] = _PAD_SLOT_ID
559 slot_mapping = (
551 - self.input_batch.block_table[0].
560 + self.input_batch.block_table.
561 slot_mapping_cpu[:padded_total_num_scheduled_tokens].to(
562 self.device))
563 block_tables = self.block_table_cpu[:self.max_num_reqs]
564 block_tables[:num_reqs, :self.max_num_blocks_per_req] = (
556 - self.input_batch.block_table[0].get_cpu_tensor()[:num_reqs])
565 + self.input_batch.block_table.get_cpu_tensor()[:num_reqs])
566 block_tables = block_tables.to(self.device)
567 query_start_loc = self.query_start_loc_cpu[:self.max_num_reqs + 1].to(
568 self.device)
@@ -1254,18 +1263,6 @@ class TPUModelRunner(LoRAModelRunnerMixin):
1263 "Hybrid models with more than one KV cache type are not "
1264 "supported yet.")
1265
1257 - self.input_batch = InputBatch(
1258 - max_num_reqs=self.max_num_reqs,
1259 - max_model_len=self.max_model_len,
1260 - max_num_batched_tokens=self.max_num_tokens,
1261 - device=self.device,
1262 - pin_memory=self.pin_memory,
1263 - vocab_size=self.model_config.get_vocab_size(),
1264 - kv_cache_config=kv_cache_config,
1265 - )
1266 - assert self.block_table_cpu.dtype == self.input_batch.block_table[
1267 - 0].get_cpu_tensor().dtype
1268 -
1266 kv_caches: dict[str, torch.Tensor] = {}
1267
1268 for kv_cache_group in kv_cache_config.kv_cache_groups: