main
py 1,900 lines 67.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import gc
5 from contextlib import nullcontext
6 from types import SimpleNamespace
7 from unittest.mock import Mock
8
9 import numpy as np
10 import pytest
11 import torch
12
13 import vllm.v1.worker.gpu_model_runner as gpu_model_runner_module
14 from vllm.config import (
15 AttentionConfig,
16 CacheConfig,
17 ModelConfig,
18 ParallelConfig,
19 SchedulerConfig,
20 VllmConfig,
21 set_current_vllm_config,
22 )
23 from vllm.config.reasoning import ReasoningConfig
24 from vllm.distributed.parallel_state import (
25 init_distributed_environment,
26 initialize_model_parallel,
27 )
28 from vllm.lora.layers import LoRAMappingType
29 from vllm.lora.request import LoRARequest
30 from vllm.model_executor.layers.attention import Attention
31 from vllm.model_executor.layers.mamba.mamba_mixer2 import MambaMixer2
32 from vllm.multimodal.inputs import MultiModalFeatureSpec, PlaceholderRange
33 from vllm.platforms import current_platform
34 from vllm.platforms.interface import Platform
35 from vllm.sampling_params import SamplingParams
36 from vllm.utils.mem_constants import GiB_bytes
37 from vllm.utils.system_utils import update_environment_variables
38 from vllm.utils.torch_utils import set_random_seed
39 from vllm.v1.attention.backend import MultipleOf
40 from vllm.v1.attention.backends.mla.indexer import DeepseekV32IndexerBackend
41 from vllm.v1.attention.backends.mla.rocm_aiter_mla_sparse import (
42 ROCMAiterMLASparseBackend,
43 )
44 from vllm.v1.attention.backends.registry import AttentionBackendEnum
45 from vllm.v1.core.kv_cache_utils import estimate_max_model_len, get_kv_cache_configs
46 from vllm.v1.core.sched.output import CachedRequestData, NewRequestData, SchedulerOutput
47 from vllm.v1.kv_cache_interface import (
48 FullAttentionSpec,
49 KVCacheConfig,
50 KVCacheGroupSpec,
51 KVCacheTensor,
52 )
53 from vllm.v1.outputs import EMPTY_MODEL_RUNNER_OUTPUT
54 from vllm.v1.sample.metadata import SamplingMetadata
55 from vllm.v1.spec_decode.metadata import SpecDecodeMetadata
56 from vllm.v1.worker.block_table import (
57 MultiGroupBlockTable,
58 SlotMappingMode,
59 get_block_table_width,
60 )
61 from vllm.v1.worker.gpu.lora_utils import LoraState
62 from vllm.v1.worker.gpu.mm.encoder_cache import EncoderCache
63 from vllm.v1.worker.gpu.mm.lora import set_active_mm_loras
64 from vllm.v1.worker.gpu_input_batch import InputBatch
65 from vllm.v1.worker.gpu_model_runner import GPUModelRunner
66 from vllm.v1.worker.utils import select_common_block_size
67
68 BLOCK_SIZE = 16
69 NUM_BLOCKS = 10
70 DEVICE_TYPE = current_platform.device_type
71
72
73 @pytest.fixture(autouse=True)
74 def _restore_default_dtype():
75 """Several tests here set the process-wide default dtype to float16 and
76 previously leaked it, corrupting later float-sensitive tests in the same
77 pytest process (torch.randn silently produced fp16)."""
78 old = torch.get_default_dtype()
79 yield
80 torch.set_default_dtype(old)
81
82
83 def initialize_kv_cache(runner: GPUModelRunner):
84 """Only perform necessary steps in GPUModelRunner.initialize_kv_cache()."""
85 attn_spec = FullAttentionSpec(
86 block_size=BLOCK_SIZE,
87 num_kv_heads=runner.model_config.get_num_kv_heads(runner.parallel_config),
88 head_size=runner.model_config.get_head_size(),
89 dtype=runner.kv_cache_dtype,
90 )
91 tensor_size = attn_spec.page_size_bytes * NUM_BLOCKS
92 kv_cache_config = KVCacheConfig(
93 num_blocks=NUM_BLOCKS,
94 kv_cache_tensors=[
95 KVCacheTensor(
96 size=tensor_size,
97 layers=["layer.0"],
98 layer_stride=tensor_size,
99 block_stride=attn_spec.page_size_bytes,
100 ),
101 ],
102 kv_cache_groups=[
103 KVCacheGroupSpec(layer_names=["layer.0"], kv_cache_spec=attn_spec)
104 ],
105 )
106 runner.kv_cache_config = kv_cache_config
107 runner.input_batch = InputBatch(
108 max_num_reqs=runner.max_num_reqs,
109 max_model_len=runner.max_model_len,
110 max_num_batched_tokens=runner.max_num_tokens,
111 device=runner.device,
112 vocab_size=runner.model_config.get_vocab_size(),
113 block_sizes=[kv_cache_config.kv_cache_groups[0].kv_cache_spec.block_size],
114 kernel_block_sizes=[
115 kv_cache_config.kv_cache_groups[0].kv_cache_spec.block_size
116 ],
117 max_num_blocks_per_req=[NUM_BLOCKS],
118 )
119 runner.initialize_attn_backend(kv_cache_config)
120
121
122 def get_vllm_config():
123 model_config = ModelConfig(
124 model="facebook/opt-125m",
125 dtype="float16",
126 seed=42,
127 )
128 scheduler_config = SchedulerConfig(
129 max_num_seqs=10,
130 max_num_batched_tokens=512,
131 max_model_len=512,
132 is_encoder_decoder=model_config.is_encoder_decoder,
133 )
134 cache_config = CacheConfig(
135 block_size=BLOCK_SIZE,
136 gpu_memory_utilization=0.9,
137 cache_dtype="auto",
138 )
139 cache_config.kv_cache_layout = "LBNHC"
140 parallel_config = ParallelConfig()
141 vllm_config = VllmConfig(
142 model_config=model_config,
143 cache_config=cache_config,
144 scheduler_config=scheduler_config,
145 parallel_config=parallel_config,
146 )
147 return vllm_config
148
149
150 @pytest.mark.parametrize("gc_initially_enabled", [True, False])
151 def test_freeze_gc_disables_and_restores_automatic_gc(
152 monkeypatch: pytest.MonkeyPatch,
153 gc_initially_enabled: bool,
154 ):
155 monkeypatch.setenv("VLLM_ENABLE_CUDAGRAPH_GC", "0")
156 original_gc_state = gc.isenabled()
157 try:
158 if gc_initially_enabled:
159 gc.enable()
160 else:
161 gc.disable()
162
163 with (
164 pytest.raises(RuntimeError, match="capture failed"),
165 GPUModelRunner._freeze_gc(),
166 ):
167 assert not gc.isenabled()
168 raise RuntimeError("capture failed")
169
170 assert gc.isenabled() is gc_initially_enabled
171 finally:
172 if original_gc_state:
173 gc.enable()
174 else:
175 gc.disable()
176
177
178 def test_prepare_padding_mask_marks_sequence_parallel_padding():
179 runner = GPUModelRunner.__new__(GPUModelRunner)
180 runner.is_padding = torch.empty(8, dtype=torch.bool)
181
182 mask = runner._prepare_padding_mask(1, 8)
183
184 assert mask.tolist() == [False, True, True, True, True, True, True, True]
185 assert mask.data_ptr() == runner.is_padding.data_ptr()
186
187 mask = runner._prepare_padding_mask(0, 8)
188
189 assert mask.all()
190
191
192 @pytest.fixture
193 def model_runner():
194 vllm_config = get_vllm_config()
195 with set_current_vllm_config(vllm_config):
196 model_config = vllm_config.model_config
197 num_heads = model_config.get_num_kv_heads(vllm_config.parallel_config)
198 head_size = model_config.get_head_size()
199 vllm_config.compilation_config.static_forward_context["layer.0"] = Attention(
200 num_heads, head_size, 0.1
201 )
202 runner = GPUModelRunner(vllm_config, DEVICE_TYPE)
203 initialize_kv_cache(runner)
204 yield runner
205
206
207 model_runner_2 = model_runner
208
209
210 def _schedule_new_request(*req_ids: str) -> SchedulerOutput:
211 new_reqs = []
212 num_scheduled_tokens = {}
213 total_num_scheduled_tokens = 0
214 for req_id in req_ids:
215 new_reqs.append(
216 NewRequestData(
217 req_id=req_id,
218 prompt_token_ids=[1, 2, 3],
219 mm_features=[],
220 sampling_params=SamplingParams(),
221 pooling_params=None,
222 block_ids=([0],),
223 num_computed_tokens=0,
224 lora_request=None,
225 )
226 )
227 num_scheduled_tokens[req_id] = 3
228 total_num_scheduled_tokens += num_scheduled_tokens[req_id]
229
230 return SchedulerOutput(
231 scheduled_new_reqs=new_reqs,
232 scheduled_cached_reqs=CachedRequestData.make_empty(),
233 num_scheduled_tokens=num_scheduled_tokens,
234 total_num_scheduled_tokens=total_num_scheduled_tokens,
235 scheduled_spec_decode_tokens={},
236 scheduled_encoder_inputs={},
237 num_common_prefix_blocks=[],
238 finished_req_ids=set(),
239 free_encoder_mm_hashes=[],
240 )
241
242
243 def _schedule_cached_requests(
244 req_ids: list[str],
245 num_scheduled_tokens: dict[str, int],
246 new_token_ids: list[list[int]],
247 num_computed_tokens: list[int],
248 num_output_tokens: list[int],
249 ) -> SchedulerOutput:
250 return SchedulerOutput(
251 scheduled_new_reqs=[],
252 scheduled_cached_reqs=CachedRequestData(
253 req_ids=req_ids,
254 resumed_req_ids=set(),
255 new_token_ids=new_token_ids,
256 all_token_ids={},
257 new_block_ids=[None] * len(req_ids),
258 num_computed_tokens=num_computed_tokens,
259 num_output_tokens=num_output_tokens,
260 ),
261 num_scheduled_tokens=num_scheduled_tokens,
262 total_num_scheduled_tokens=sum(num_scheduled_tokens.values()),
263 scheduled_spec_decode_tokens={},
264 scheduled_encoder_inputs={},
265 num_common_prefix_blocks=[],
266 finished_req_ids=set(),
267 free_encoder_mm_hashes=[],
268 )
269
270
271 def _is_req_scheduled(model_runner, req_id: str) -> bool:
272 return req_id in model_runner.input_batch.req_id_to_index
273
274
275 def _is_req_added(model_runner, req_id: str) -> bool:
276 return req_id in model_runner.requests
277
278
279 def _is_sampling_metadata_changed(
280 model_runner, sampling_metadata_before: SamplingMetadata
281 ):
282 return model_runner.input_batch.sampling_metadata is not (sampling_metadata_before)
283
284
285 def _is_req_state_block_table_match(model_runner, req_id: str) -> bool:
286 req_index = model_runner.input_batch.req_id_to_index[req_id]
287 block_table = model_runner.input_batch.block_table[0]
288 req_state = model_runner.requests[req_id]
289 if block_table.num_blocks_per_row[req_index] != len(req_state.block_ids[0]):
290 return False
291 num_blocks = block_table.num_blocks_per_row[req_index]
292 return (
293 block_table.block_table.np[req_index, :num_blocks] == req_state.block_ids[0]
294 ).all()
295
296
297 def _make_mock_backend_for_kernel_block_size(
298 supported_sizes: list[int | MultipleOf],
299 ):
300 class _MockBackend:
301 @staticmethod
302 def get_name():
303 return "MOCK"
304
305 @staticmethod
306 def get_supported_kernel_block_sizes():
307 return supported_sizes
308
309 return _MockBackend()
310
311
312 def _make_kv_cache_spec() -> FullAttentionSpec:
313 return FullAttentionSpec(block_size=1, num_kv_heads=1, head_size=1, dtype="float16")
314
315
316 def test_select_common_block_size_prefers_manager_block_size():
317 backend_a = _make_mock_backend_for_kernel_block_size([MultipleOf(32)])
318 backend_b = _make_mock_backend_for_kernel_block_size([64, MultipleOf(16)])
319
320 selected_size = select_common_block_size(128, [backend_a, backend_b])
321 assert selected_size == 128
322
323
324 def test_select_common_block_size_uses_largest_shared_int():
325 backend_a = _make_mock_backend_for_kernel_block_size([128, 64])
326 backend_b = _make_mock_backend_for_kernel_block_size([64, 32])
327
328 selected_size = select_common_block_size(256, [backend_a, backend_b])
329 assert selected_size == 64
330
331
332 def test_select_common_block_size_without_active_backends_uses_manager_size():
333 assert select_common_block_size(256, []) == 256
334
335
336 def test_select_common_block_size_accepts_rocm_sparse_block_size_16(monkeypatch):
337 monkeypatch.setattr(current_platform, "is_rocm", lambda: True)
338
339 selected_size = select_common_block_size(
340 16,
341 [DeepseekV32IndexerBackend, ROCMAiterMLASparseBackend],
342 )
343 assert selected_size == 16
344
345
346 def test_reasoning_config_without_custom_logitsprocs_does_not_need_output_token_ids(
347 dist_init,
348 ):
349 vllm_config = get_vllm_config()
350 assert vllm_config.model_config.logits_processors is None
351 reasoning_config = ReasoningConfig(
352 reasoning_start_str="<think>", reasoning_end_str="</think>"
353 )
354 reasoning_config._reasoning_start_token_ids = [1]
355 reasoning_config._reasoning_end_token_ids = [2]
356 vllm_config.reasoning_config = reasoning_config
357
358 with set_current_vllm_config(vllm_config):
359 model_config = vllm_config.model_config
360 num_heads = model_config.get_num_kv_heads(vllm_config.parallel_config)
361 head_size = model_config.get_head_size()
362 vllm_config.compilation_config.static_forward_context["layer.0"] = Attention(
363 num_heads, head_size, 0.1
364 )
365 runner = GPUModelRunner(vllm_config, torch.device("cpu"))
366
367 assert runner.input_batch.thinking_budget_state_holder is not None
368 assert runner.input_batch.logitsprocs_need_output_token_ids is False
369
370
371 @pytest.mark.skip_global_cleanup
372 @pytest.mark.parametrize(
373 ("world_size", "is_last_rank", "expected_calls"),
374 [(1, True, 0), (2, True, 0), (2, False, 1)],
375 )
376 def test_sample_tokens_receives_pp_sampled_ids_only_on_non_last_rank(
377 monkeypatch: pytest.MonkeyPatch,
378 world_size: int,
379 is_last_rank: bool,
380 expected_calls: int,
381 ):
382 runner = GPUModelRunner.__new__(GPUModelRunner)
383 runner.execute_model_state = None
384 runner.kv_connector_output = None
385 runner.use_async_scheduling = True
386 receive_calls = 0
387
388 def receive_prev_sampled_token_ids():
389 nonlocal receive_calls
390 receive_calls += 1
391
392 runner._pp_receive_prev_sampled_token_ids_to_input_batch = (
393 receive_prev_sampled_token_ids
394 )
395 monkeypatch.setattr(
396 gpu_model_runner_module,
397 "get_pp_group",
398 lambda: SimpleNamespace(world_size=world_size, is_last_rank=is_last_rank),
399 )
400
401 output = GPUModelRunner.sample_tokens(runner, None)
402 assert output in (EMPTY_MODEL_RUNNER_OUTPUT, None)
403 assert receive_calls == expected_calls
404
405
406 @pytest.mark.skip_global_cleanup
407 def test_sample_tokens_skips_pp_group_lookup_without_async_scheduling(
408 monkeypatch: pytest.MonkeyPatch,
409 ):
410 runner = GPUModelRunner.__new__(GPUModelRunner)
411 runner.execute_model_state = None
412 runner.kv_connector_output = None
413 runner.use_async_scheduling = False
414
415 monkeypatch.setattr(
416 gpu_model_runner_module,
417 "get_pp_group",
418 pytest.fail,
419 )
420
421 output = GPUModelRunner.sample_tokens(runner, None)
422 assert output in (EMPTY_MODEL_RUNNER_OUTPUT, None)
423
424
425 def test_select_common_block_size_no_valid_option():
426 backend_a = _make_mock_backend_for_kernel_block_size([64])
427 backend_b = _make_mock_backend_for_kernel_block_size([MultipleOf(16)])
428
429 with pytest.raises(ValueError):
430 select_common_block_size(48, [backend_a, backend_b])
431
432
433 def _mock_backend(supported: list, *, exact: bool = False):
434 """Backend accepting multiples of ``supported``, or only those exact sizes
435 when ``exact`` (as CPU_MLA does)."""
436 from vllm.v1.attention.backend import AttentionBackend
437
438 class _MockBackendCls(AttentionBackend):
439 @staticmethod
440 def get_name() -> str:
441 return "MOCK_EXACT" if exact else "MOCK"
442
443 @staticmethod
444 def get_supported_kernel_block_sizes():
445 return list(supported)
446
447 if exact:
448
449 @classmethod
450 def supports_block_size(cls, block_size: int | None) -> bool:
451 return block_size is None or block_size in supported
452
453 return _MockBackendCls
454
455
456 @pytest.mark.parametrize(
457 "backends,expected",
458 [
459 # A lone backend keeps the default; MultipleOf(8) makes a
460 # minimum-based regression return 8 instead of 16.
461 ([[MultipleOf(8)]], 16),
462 # Sparse-MLA main backend (32 or 64) beside an indexer supporting only
463 # 64: the main backend alone picks 32, which the indexer rejects at
464 # select_common_block_size time.
465 ([[32, 64], [64]], 64),
466 ([[32, 64]], 32),
467 # Neither divides the other, so only the LCM satisfies both.
468 ([[32], [48]], 96),
469 ],
470 )
471 def test_preferred_block_size_satisfies_every_backend(backends, expected):
472 classes = [_mock_backend(s) for s in backends]
473 assert Platform._preferred_block_size_for_backends(classes, 16, None) == expected
474
475
476 def test_preferred_block_size_searches_past_an_exact_size_backend():
477 # Extending greedily picks lcm(16, 32) = 32, which the exact backend
478 # rejects; 96 is accepted by both.
479 classes = [_mock_backend([16, 96], exact=True), _mock_backend([MultipleOf(32)])]
480 assert Platform._preferred_block_size_for_backends(classes, 16, None) == 96
481
482
483 def test_preferred_block_size_rejects_backends_with_no_common_size():
484 classes = [_mock_backend([16], exact=True), _mock_backend([MultipleOf(64)])]
485 with pytest.raises(ValueError, match="share no supported KV cache block size"):
486 Platform._preferred_block_size_for_backends(classes, 16, None)
487
488
489 def test_set_active_mm_loras_builds_tower_and_connector_mappings():
490 model = Mock()
491 model.get_mm_lora_token_counts.side_effect = (
492 lambda *, modality, mm_kwargs, num_mm_embeds: (
493 num_mm_embeds + 1,
494 num_mm_embeds + 11,
495 )
496 )
497 model.get_mm_mapping.return_value = SimpleNamespace(connector=True)
498
499 lora_manager = Mock()
500 lora_manager.supports_tower_connector_lora.return_value = True
501
502 encoder_cache = EncoderCache()
503 encoder_cache.mm_features["req-with-lora"] = [
504 MultiModalFeatureSpec(
505 data=None,
506 modality="image",
507 identifier="img-0",
508 mm_position=PlaceholderRange(offset=0, length=2),
509 ),
510 MultiModalFeatureSpec(
511 data=None,
512 modality="image",
513 identifier="img-1",
514 mm_position=PlaceholderRange(offset=2, length=3),
515 ),
516 ]
517 encoder_cache.mm_features["req-no-lora"] = [
518 MultiModalFeatureSpec(
519 data=None,
520 modality="image",
521 identifier="img-2",
522 mm_position=PlaceholderRange(offset=0, length=1),
523 )
524 ]
525
526 lora_state = LoraState(max_num_reqs=4)
527 lora_request = LoRARequest("vision-lora", 7, "/tmp/vision-lora")
528 lora_state.add_request("req-with-lora", 0, lora_request)
529 lora_state.add_request("req-no-lora", 1, None)
530
531 set_active_mm_loras(
532 model=model,
533 lora_manager=lora_manager,
534 encoder_cache=encoder_cache,
535 req_id_to_index={
536 "req-with-lora": 0,
537 "req-no-lora": 1,
538 },
539 lora_state=lora_state,
540 scheduled_encoder_inputs={
541 "req-with-lora": [1, 0],
542 "req-no-lora": [0],
543 "missing-req": [0],
544 },
545 )
546
547 assert lora_manager.set_active_adapters.call_count == 2
548
549 tower_requests, tower_mapping = lora_manager.set_active_adapters.call_args_list[
550 0
551 ].args
552 assert tower_requests == {lora_request}
553 assert tower_mapping.type is LoRAMappingType.TOWER
554 assert tower_mapping.prompt_mapping == (7, 7, 0)
555 assert tower_mapping.index_mapping == (7, 7, 7, 7, 7, 7, 7, 0, 0)
556
557 connector_requests, connector_mapping = (
558 lora_manager.set_active_adapters.call_args_list[1].args
559 )
560 assert connector_requests == {lora_request}
561 assert connector_mapping.type is LoRAMappingType.CONNECTOR
562 assert connector_mapping.prompt_mapping == (7, 7, 0)
563 assert connector_mapping.index_mapping == ((7,) * 14 + (7,) * 13 + (0,) * 12)
564
565
566 def test_update_states_new_request(model_runner, dist_init):
567 req_id = "req_0"
568
569 # new req
570 scheduler_output = _schedule_new_request(req_id)
571
572 metadata_before = model_runner.input_batch.sampling_metadata
573 model_runner._update_states(scheduler_output)
574 assert _is_sampling_metadata_changed(model_runner, metadata_before)
575 assert _is_req_added(model_runner, req_id)
576 assert _is_req_scheduled(model_runner, req_id)
577 assert _is_req_state_block_table_match(model_runner, req_id)
578
579
580 def test_update_states_request_finished(model_runner, dist_init):
581 req_id = "req_0"
582
583 # new req
584 scheduler_output = _schedule_new_request(req_id)
585
586 model_runner._update_states(scheduler_output)
587 assert _is_req_added(model_runner, req_id)
588 assert _is_req_scheduled(model_runner, req_id)
589
590 # finish req
591 scheduler_output = SchedulerOutput(
592 scheduled_new_reqs=[],
593 scheduled_cached_reqs=CachedRequestData.make_empty(),
594 num_scheduled_tokens={},
595 total_num_scheduled_tokens=0,
596 scheduled_spec_decode_tokens={},
597 scheduled_encoder_inputs={},
598 num_common_prefix_blocks=[],
599 finished_req_ids={req_id},
600 free_encoder_mm_hashes=[],
601 )
602
603 metadata_before = model_runner.input_batch.sampling_metadata
604 model_runner._update_states(scheduler_output)
605 assert _is_sampling_metadata_changed(model_runner, metadata_before)
606 assert not _is_req_added(model_runner, req_id)
607 assert not _is_req_scheduled(model_runner, req_id)
608
609
610 def test_update_states_request_resumed(model_runner, dist_init):
611 req_id = "req_0"
612
613 # new req
614 scheduler_output = _schedule_new_request(req_id)
615
616 model_runner._update_states(scheduler_output)
617 assert _is_req_added(model_runner, req_id)
618 assert _is_req_scheduled(model_runner, req_id)
619
620 # unschedule req
621 scheduler_output = SchedulerOutput(
622 scheduled_new_reqs=[],
623 scheduled_cached_reqs=CachedRequestData.make_empty(),
624 num_scheduled_tokens={},
625 total_num_scheduled_tokens=0,
626 scheduled_spec_decode_tokens={},
627 scheduled_encoder_inputs={},
628 num_common_prefix_blocks=[],
629 finished_req_ids=set(),
630 free_encoder_mm_hashes=[],
631 )
632
633 model_runner._update_states(scheduler_output)
634 assert _is_req_added(model_runner, req_id)
635 assert not _is_req_scheduled(model_runner, req_id)
636
637 # resume req
638 cached_req_data = CachedRequestData(
639 req_ids=[req_id],
640 resumed_req_ids=set(),
641 new_token_ids=[[]],
642 all_token_ids={},
643 new_block_ids=[([0],)],
644 num_computed_tokens=[0],
645 num_output_tokens=[0],
646 )
647
648 scheduler_output = SchedulerOutput(
649 scheduled_new_reqs=[],
650 scheduled_cached_reqs=cached_req_data,
651 num_scheduled_tokens={req_id: 1},
652 total_num_scheduled_tokens=1,
653 scheduled_spec_decode_tokens={},
654 scheduled_encoder_inputs={},
655 num_common_prefix_blocks=[],
656 finished_req_ids=set(),
657 free_encoder_mm_hashes=[],
658 )
659
660 metadata_before = model_runner.input_batch.sampling_metadata
661 model_runner._update_states(scheduler_output)
662 assert _is_sampling_metadata_changed(model_runner, metadata_before)
663 assert _is_req_added(model_runner, req_id)
664 assert _is_req_scheduled(model_runner, req_id)
665 assert _is_req_state_block_table_match(model_runner, req_id)
666
667
668 def test_get_nans_in_logits(model_runner, dist_init, monkeypatch):
669 monkeypatch.setattr(gpu_model_runner_module.envs, "VLLM_RAISE_ON_LOGIT_NANS", False)
670 req_ids = ("req_0", "req_1")
671
672 scheduler_output = _schedule_new_request(*req_ids)
673 model_runner._update_states(scheduler_output)
674
675 logits = torch.tensor(
676 [
677 [1.0, 2.0, 3.0],
678 [3.0, 2.0, 1.0],
679 ],
680 device=DEVICE_TYPE,
681 )
682 result = model_runner._get_nans_in_logits(logits)
683 assert result == {"req_0": 0, "req_1": 0}
684
685 logits = torch.tensor(
686 [
687 [1.0, float("nan"), 3.0],
688 [4.0, float("nan"), float("nan")],
689 ],
690 device=DEVICE_TYPE,
691 )
692 result = model_runner._get_nans_in_logits(logits)
693 assert result == {"req_0": 1, "req_1": 2}
694
695 logits = torch.tensor(
696 [
697 [1.0, 2.0, 3.0],
698 [4.0, float("nan"), float("nan")],
699 ],
700 device=DEVICE_TYPE,
701 )
702 result = model_runner._get_nans_in_logits(logits)
703 assert result == {"req_0": 0, "req_1": 2}
704
705 result = model_runner._get_nans_in_logits(logits=None)
706 assert result == {"req_0": 0, "req_1": 0}
707
708 logits = torch.tensor(
709 [
710 [1.0, float("nan"), 3.0],
711 ],
712 device=DEVICE_TYPE,
713 )
714 result = model_runner._get_nans_in_logits(logits)
715 assert result == {"req_0": 1, "req_1": 0}
716
717 logits = torch.tensor(
718 [
719 [float("nan"), float("nan"), 2.0],
720 [1.0, 2.0, 3.0],
721 [float("nan"), 2.0, 3.0],
722 ],
723 device=DEVICE_TYPE,
724 )
725 result = model_runner._get_nans_in_logits(logits)
726 assert result == {"req_0": 2, "req_1": 0}
727
728
729 def test_update_states_no_changes(model_runner, dist_init):
730 req_id = "req_0"
731
732 # new req
733 scheduler_output = _schedule_new_request(req_id)
734
735 model_runner._update_states(scheduler_output)
736 assert _is_req_added(model_runner, req_id)
737 assert _is_req_scheduled(model_runner, req_id)
738
739 # schedule req
740 scheduler_output = SchedulerOutput(
741 scheduled_new_reqs=[],
742 scheduled_cached_reqs=CachedRequestData.make_empty(),
743 num_scheduled_tokens={req_id: 1},
744 total_num_scheduled_tokens=1,
745 scheduled_spec_decode_tokens={},
746 scheduled_encoder_inputs={},
747 num_common_prefix_blocks=[],
748 finished_req_ids=set(),
749 free_encoder_mm_hashes=[],
750 )
751
752 metadata_before = model_runner.input_batch.sampling_metadata
753 model_runner._update_states(scheduler_output)
754 assert not _is_sampling_metadata_changed(model_runner, metadata_before)
755 assert _is_req_added(model_runner, req_id)
756 assert _is_req_scheduled(model_runner, req_id)
757 assert _is_req_state_block_table_match(model_runner, req_id)
758
759
760 def test_update_states_request_unscheduled(model_runner, dist_init):
761 req_ids = ("req_0", "req_1")
762
763 # new reqs
764 scheduler_output = _schedule_new_request(*req_ids)
765
766 model_runner._update_states(scheduler_output)
767
768 assert _is_req_added(model_runner, req_ids[0])
769 assert _is_req_scheduled(model_runner, req_ids[0])
770
771 assert _is_req_added(model_runner, req_ids[1])
772 assert _is_req_scheduled(model_runner, req_ids[1])
773
774 # unschedule req_1
775 scheduler_output = SchedulerOutput(
776 scheduled_new_reqs=[],
777 scheduled_cached_reqs=CachedRequestData.make_empty(),
778 num_scheduled_tokens={req_ids[0]: 1},
779 total_num_scheduled_tokens=1,
780 scheduled_spec_decode_tokens={},
781 scheduled_encoder_inputs={},
782 num_common_prefix_blocks=[],
783 finished_req_ids=set(),
784 free_encoder_mm_hashes=[],
785 )
786
787 metadata_before = model_runner._update_states(scheduler_output)
788 assert _is_sampling_metadata_changed(model_runner, metadata_before)
789
790 assert _is_req_added(model_runner, req_ids[0])
791 assert _is_req_scheduled(model_runner, req_ids[0])
792
793 assert _is_req_added(model_runner, req_ids[1])
794 assert not _is_req_scheduled(model_runner, req_ids[1])
795
796
797 def test_update_states_pp_non_async_multi_request_keeps_token_buffers_consistent(
798 model_runner, model_runner_2, dist_init, monkeypatch
799 ):
800 req_ids = ["req_0", "req_1"]
801 non_last_runner = model_runner
802 last_runner = model_runner_2
803 non_last_runner.use_async_scheduling = False
804 last_runner.use_async_scheduling = False
805
806 # Both ranks start from the same request set.
807 monkeypatch.setattr(
808 "vllm.v1.worker.gpu_model_runner.get_pp_group",
809 lambda: SimpleNamespace(is_last_rank=False, world_size=2),
810 )
811 non_last_runner._update_states(_schedule_new_request(*req_ids))
812 last_runner._update_states(_schedule_new_request(*req_ids))
813
814 sampled_by_last_rank = {req_ids[0]: 101, req_ids[1]: 201}
815 # Emulate last-rank bookkeeping result from previous step:
816 # sampled tokens already cached in CPU token buffers.
817 for req_id, token_id in sampled_by_last_rank.items():
818 req_index = last_runner.input_batch.req_id_to_index[req_id]
819 start_idx = int(last_runner.input_batch.num_tokens_no_spec[req_index])
820 end_idx = start_idx + 1
821 last_runner.input_batch.token_ids_cpu[req_index, start_idx:end_idx] = [token_id]
822 last_runner.input_batch.is_token_ids[req_index, start_idx:end_idx] = True
823 last_runner.input_batch.num_tokens_no_spec[req_index] = end_idx
824 last_runner.requests[req_id].output_token_ids.append(token_id)
825
826 scheduler_output = _schedule_cached_requests(
827 req_ids=req_ids,
828 num_scheduled_tokens={req_ids[0]: 1, req_ids[1]: 1},
829 new_token_ids=[[101], [201]],
830 num_computed_tokens=[3, 3], # prompt tokens only
831 num_output_tokens=[1, 1],
832 )
833 # non-last rank appends new_token_ids in _update_states.
834 monkeypatch.setattr(
835 "vllm.v1.worker.gpu_model_runner.get_pp_group",
836 lambda: SimpleNamespace(is_last_rank=False, world_size=2),
837 )
838 non_last_runner._update_states(scheduler_output)
839 # last rank should keep its already-bookkept CPU buffers unchanged.
840 monkeypatch.setattr(
841 "vllm.v1.worker.gpu_model_runner.get_pp_group",
842 lambda: SimpleNamespace(is_last_rank=True, world_size=2),
843 )
844 last_runner._update_states(scheduler_output)
845
846 # Verify consistency between PP ranks after _update_states.
847 for req_id in req_ids:
848 non_last_idx = non_last_runner.input_batch.req_id_to_index[req_id]
849 last_idx = last_runner.input_batch.req_id_to_index[req_id]
850 non_last_len = int(non_last_runner.input_batch.num_tokens_no_spec[non_last_idx])
851 last_len = int(last_runner.input_batch.num_tokens_no_spec[last_idx])
852 assert non_last_len == last_len
853 assert (
854 non_last_runner.input_batch.token_ids_cpu[
855 non_last_idx, :non_last_len
856 ].tolist()
857 == last_runner.input_batch.token_ids_cpu[last_idx, :last_len].tolist()
858 )
859
860
861 def test_update_states_pp_async_multi_request_keeps_rank_state_consistent(
862 model_runner, model_runner_2, dist_init, monkeypatch
863 ):
864 req_ids = ["req_0", "req_1"]
865 non_last_runner = model_runner
866 last_runner = model_runner_2
867 non_last_runner.use_async_scheduling = True
868 last_runner.use_async_scheduling = True
869
870 # Both ranks start from the same request set.
871 monkeypatch.setattr(
872 "vllm.v1.worker.gpu_model_runner.get_pp_group",
873 lambda: SimpleNamespace(is_last_rank=False, world_size=2),
874 )
875 non_last_runner._update_states(_schedule_new_request(*req_ids))
876 last_runner._update_states(_schedule_new_request(*req_ids))
877
878 # Simulate async previous-step sampled tokens known on both ranks.
879 # non-last rank may receive them via PP communication; last rank has
880 # them from local sampling/bookkeeping.
881 sampled_by_last_rank = {req_ids[0]: 111, req_ids[1]: 222}
882 for runner in (non_last_runner, last_runner):
883 for req_id, token_id in sampled_by_last_rank.items():
884 req_index = runner.input_batch.req_id_to_index[req_id]
885 start_idx = int(runner.input_batch.num_tokens_no_spec[req_index])
886 end_idx = start_idx + 1
887 runner.input_batch.token_ids_cpu[req_index, start_idx:end_idx] = [token_id]
888 runner.input_batch.is_token_ids[req_index, start_idx:end_idx] = True
889 runner.input_batch.num_tokens_no_spec[req_index] = end_idx
890 runner.requests[req_id].output_token_ids.append(token_id)
891
892 scheduler_output = _schedule_cached_requests(
893 req_ids=req_ids,
894 num_scheduled_tokens={req_ids[0]: 1, req_ids[1]: 1},
895 new_token_ids=[],
896 num_computed_tokens=[4, 4],
897 num_output_tokens=[1, 1],
898 )
899 # non-last rank: async PP branch (new_token_ids empty).
900 monkeypatch.setattr(
901 "vllm.v1.worker.gpu_model_runner.get_pp_group",
902 lambda: SimpleNamespace(is_last_rank=False, world_size=2),
903 )
904 non_last_runner._update_states(scheduler_output)
905 # last rank: keep already-bookkept state aligned with scheduler view.
906 monkeypatch.setattr(
907 "vllm.v1.worker.gpu_model_runner.get_pp_group",
908 lambda: SimpleNamespace(is_last_rank=True, world_size=2),
909 )
910 last_runner._update_states(scheduler_output)
911
912 for req_id in req_ids:
913 non_last_idx = non_last_runner.input_batch.req_id_to_index[req_id]
914 last_idx = last_runner.input_batch.req_id_to_index[req_id]
915 non_last_len = int(non_last_runner.input_batch.num_tokens_no_spec[non_last_idx])
916 last_len = int(last_runner.input_batch.num_tokens_no_spec[last_idx])
917 assert non_last_len == last_len
918 assert (
919 non_last_runner.input_batch.token_ids_cpu[
920 non_last_idx, :non_last_len
921 ].tolist()
922 == last_runner.input_batch.token_ids_cpu[last_idx, :last_len].tolist()
923 )
924
925
926 def test_update_config(model_runner):
927 # Simple update
928 model_runner.update_config({"load_config": {"load_format": "dummy"}})
929 assert model_runner.load_config.load_format == "dummy"
930 # Raise error on non-existing config
931 with pytest.raises(ValueError, match="do_not_exist_config"):
932 model_runner.update_config({"do_not_exist_config": "dummy"})
933
934
935 def test_load_model_weights_inplace(dist_init, model_runner, model_runner_2):
936 # In this test, model_runner loads model + weights in one go, while
937 # model_runner_2 loads dummy weights first then load real weights inplace
938 model_runner.load_model()
939 original_load_format = model_runner_2.load_config.load_format
940 model_runner_2.update_config({"load_config": {"load_format": "dummy"}})
941 model_runner_2.load_model() # Initial model loading with dummy weights
942 assert str(model_runner.get_model().state_dict()) != str(
943 model_runner_2.get_model().state_dict()
944 )
945 model_runner_2.update_config({"load_config": {"load_format": original_load_format}})
946 model_runner_2.reload_weights() # Load real weights inplace
947 assert str(model_runner.get_model().state_dict()) == str(
948 model_runner_2.get_model().state_dict()
949 )
950
951
952 def test_reload_weights_before_load_model(model_runner):
953 with pytest.raises(ValueError):
954 model_runner.reload_weights()
955
956
957 def test_sample_passes_reordered_draft_probs_to_rejection_sampler():
958 runner = object.__new__(GPUModelRunner)
959 runner.use_async_scheduling = False
960 runner.input_batch = SimpleNamespace(
961 sampling_metadata=Mock(spec=SamplingMetadata),
962 update_async_output_token_ids=Mock(),
963 req_ids=["req_a", "req_b", "req_c"],
964 )
965 runner.rejection_sampler = Mock(return_value="sampler_output")
966 runner.sampler = Mock()
967 runner._draft_prob_req_ids = ["req_c", "req_a", "req_b"]
968 runner._draft_probs = torch.arange(3 * 3 * 4, dtype=torch.float32).reshape(3, 3, 4)
969
970 spec_decode_metadata = SpecDecodeMetadata.make_dummy(
971 [[1, 2], [], [3]],
972 device=torch.device("cpu"),
973 )
974 logits = torch.randn(6, 4)
975
976 output = GPUModelRunner._sample(runner, logits, spec_decode_metadata)
977
978 assert output == "sampler_output"
979 passed_draft_probs = runner.rejection_sampler.call_args.args[1]
980 expected_draft_probs = torch.cat(
981 [
982 runner._draft_probs[1, :2],
983 runner._draft_probs[0, :1],
984 ],
985 dim=0,
986 )
987 assert torch.equal(passed_draft_probs, expected_draft_probs)
988
989
990 def test_dummy_sampler_run_warms_all_greedy_rejection_sampler(monkeypatch):
991 runner = object.__new__(GPUModelRunner)
992 runner.device = torch.device("cpu")
993 runner.vllm_config = SimpleNamespace(
994 model_config=SimpleNamespace(multimodal_config=None)
995 )
996 runner.model = SimpleNamespace(compute_logits=Mock(return_value=torch.randn(3, 8)))
997 runner.sampler = Mock(return_value="sampler_output")
998 runner.sampler.logprobs_mode = "processed_logprobs"
999 runner.speculative_config = SimpleNamespace(
1000 rejection_sample_method="standard",
1001 draft_sample_method="greedy",
1002 )
1003 runner.rejection_sampler = Mock()
1004 synchronize = Mock()
1005 monkeypatch.setattr(torch.accelerator, "synchronize", synchronize)
1006
1007 output = GPUModelRunner._dummy_sampler_run(runner, torch.randn(3, 4))
1008
1009 assert output == "sampler_output"
1010 assert runner.rejection_sampler.call_count == 2
1011 mixed_metadata = runner.rejection_sampler.call_args_list[0].args[3]
1012 all_greedy_metadata = runner.rejection_sampler.call_args_list[1].args[3]
1013 assert not mixed_metadata.all_greedy
1014 assert mixed_metadata.temperature is not None
1015 assert all_greedy_metadata.all_greedy
1016 assert not all_greedy_metadata.all_random
1017 assert all_greedy_metadata.temperature is None
1018 synchronize.assert_called_once_with()
1019
1020
1021 def test_invalid_draft_suffixes_remain_rejected_in_metadata():
1022 runner = object.__new__(GPUModelRunner)
1023 runner.device = torch.device("cpu")
1024 runner.arange_np = np.arange(64, dtype=np.int64)
1025 runner._arange_scratch = np.empty(64, dtype=np.int64)
1026 # Placeholder (-1) drafts are kept in input_ids (clamped to 0 only at the
1027 # embedding boundary). For num_draft_tokens=[2, 1, 2] the draft positions
1028 # are [1, 2, 4, 6, 7], so the gather carries the -1s straight into the
1029 # rejection-sampling metadata.
1030 runner.input_ids = SimpleNamespace(
1031 gpu=torch.tensor([99, 10, -1, 99, 12, 99, 13, -1], dtype=torch.int32),
1032 )
1033
1034 metadata = GPUModelRunner._calc_spec_decode_metadata(
1035 runner,
1036 np.array([2, 1, 2], dtype=np.int32),
1037 np.array([3, 5, 8], dtype=np.int32),
1038 )
1039
1040 assert metadata.draft_token_ids.tolist() == [10, -1, 12, 13, -1]
1041
1042
1043 def test_init_kv_cache_with_kv_sharing_invalid_target_layer_order(default_vllm_config):
1044 torch.set_default_dtype(torch.float16)
1045 layer_0 = "model.layers.0.self_attn.attn"
1046 layer_1 = "model.layers.1.self_attn.attn"
1047 error_msg = f"{layer_1} must come before the current layer"
1048 with pytest.raises(ValueError, match=error_msg):
1049 fwd_context = {
1050 # initialization below will fail because target layer is invalid;
1051 # the target layer needs to come before layer 1
1052 layer_0: Attention(
1053 num_heads=8,
1054 head_size=64,
1055 scale=1.0,
1056 prefix=layer_0,
1057 kv_sharing_target_layer_name=layer_1,
1058 ),
1059 layer_1: Attention(
1060 num_heads=8,
1061 head_size=64,
1062 scale=1.0,
1063 prefix=layer_1,
1064 ),
1065 }
1066 # suppress var not used error
1067 assert fwd_context is not None
1068
1069
1070 def test_init_kv_cache_with_kv_sharing_target_layer_not_exist(default_vllm_config):
1071 torch.set_default_dtype(torch.float16)
1072 layer_0 = "model.layers.0.self_attn.attn"
1073 layer_1 = "model.layers.1.self_attn.attn"
1074 invalid_layer = "model.layers.0.cross_attn.attn"
1075 error_msg = f"{invalid_layer} is not a valid Attention layer in the model"
1076 with pytest.raises(ValueError, match=error_msg):
1077 fwd_context = {
1078 layer_0: Attention(
1079 num_heads=8,
1080 head_size=64,
1081 scale=1.0,
1082 prefix=layer_0,
1083 ),
1084 layer_1: Attention(
1085 num_heads=8,
1086 head_size=64,
1087 scale=1.0,
1088 prefix=layer_1,
1089 # invalid layer: cross_attn.atn doesn't exist!
1090 kv_sharing_target_layer_name=invalid_layer,
1091 ),
1092 }
1093 # suppress var not used error
1094 assert fwd_context is not None
1095
1096
1097 def test_init_kv_cache_with_kv_sharing_target_same_as_current(default_vllm_config):
1098 torch.set_default_dtype(torch.float16)
1099 layer_0 = "model.layers.0.self_attn.attn"
1100 layer_1 = "model.layers.1.self_attn.attn"
1101 error_msg = f"{layer_1} cannot be the same as the current layer"
1102 with pytest.raises(ValueError, match=error_msg):
1103 fwd_context = {
1104 # initialization below will fail because target layer is invalid;
1105 # the target layer needs to come before layer 1
1106 layer_0: Attention(
1107 num_heads=8,
1108 head_size=64,
1109 scale=1.0,
1110 prefix=layer_0,
1111 ),
1112 layer_1: Attention(
1113 num_heads=8,
1114 head_size=64,
1115 scale=1.0,
1116 prefix=layer_1,
1117 kv_sharing_target_layer_name=layer_1,
1118 ),
1119 }
1120 # suppress var not used error
1121 assert fwd_context is not None
1122
1123
1124 def test_init_kv_cache_without_kv_sharing(default_vllm_config):
1125 torch.set_default_dtype(torch.float16)
1126 layer_0 = "model.layers.0.self_attn.attn"
1127 layer_1 = "model.layers.1.self_attn.attn"
1128 vllm_config = get_vllm_config()
1129 with set_current_vllm_config(vllm_config):
1130 fwd_context = {
1131 layer_0: Attention(
1132 num_heads=8,
1133 head_size=64,
1134 scale=1.0,
1135 prefix=layer_0,
1136 ),
1137 layer_1: Attention(
1138 num_heads=8,
1139 head_size=64,
1140 scale=1.0,
1141 prefix=layer_1,
1142 ),
1143 }
1144 # suppress var not used error
1145 assert fwd_context is not None
1146 # Set high context length to test max context length estimation
1147 vllm_config.model_config.max_model_len = 3_000_000
1148 vllm_ctx = vllm_config.compilation_config.static_forward_context
1149 runner = GPUModelRunner(vllm_config, DEVICE_TYPE)
1150 kv_cache_spec = runner.get_kv_cache_spec()
1151 assert len(kv_cache_spec) == 2
1152 assert len(runner.shared_kv_cache_layers) == 0
1153
1154 available_memory = 20 * GiB_bytes
1155 # page size for layer 0's kv_cache_spec is 32KB
1156 num_expected_blocks = 327680 # 20GB / 32KB / 2 (num layers)
1157 kv_cache_config = get_kv_cache_configs(
1158 vllm_config, [kv_cache_spec], [available_memory]
1159 )[0]
1160 assert kv_cache_config.num_blocks == num_expected_blocks
1161 assert len(kv_cache_config.kv_cache_tensors) == 1
1162 assert kv_cache_config.kv_cache_tensors[0].size == available_memory
1163
1164 max_context_len = estimate_max_model_len(vllm_config, kv_cache_spec, 5 * GiB_bytes)
1165 # max context len with KV sharing should be 2x as large as without
1166 assert max_context_len == 1310720
1167
1168 # important: override tensor size to prevent large mem alloc during test
1169 # this will only allocate 1 block worth of memory per layer (2 layers * 32kb)
1170 kv_cache_config.num_blocks = 1
1171 for kv_cache_tensor in kv_cache_config.kv_cache_tensors:
1172 page_size = kv_cache_spec[kv_cache_tensor.layers[0]].page_size_bytes
1173 kv_cache_tensor.size = page_size * len(kv_cache_tensor.layers)
1174 # One block per layer: a layer's region is exactly one page.
1175 kv_cache_tensor.layer_stride = page_size
1176 kv_cache_tensor.block_stride = page_size
1177
1178 runner.initialize_kv_cache(kv_cache_config)
1179
1180 layer_0_kv = vllm_ctx[layer_0].kv_cache
1181 layer_1_kv = vllm_ctx[layer_1].kv_cache
1182 # check layer 1 kv cache does NOT share memory with layer 0
1183 assert id(layer_1_kv) != id(layer_0_kv)
1184
1185 # check layer 1 added to kv cache group's layer names
1186 assert len(kv_cache_config.kv_cache_groups) == 1
1187 assert len(kv_cache_config.kv_cache_groups[0].layer_names) == 2
1188 assert kv_cache_config.kv_cache_groups[0].layer_names[0] == layer_0
1189 assert kv_cache_config.kv_cache_groups[0].layer_names[1] == layer_1
1190
1191
1192 def test_init_kv_cache_with_kv_sharing_valid(default_vllm_config):
1193 torch.set_default_dtype(torch.float16)
1194 layer_0 = "model.layers.0.self_attn.attn"
1195 layer_1 = "model.layers.1.self_attn.attn"
1196 vllm_config = get_vllm_config()
1197 with set_current_vllm_config(vllm_config):
1198 fwd_context = {
1199 layer_0: Attention(
1200 num_heads=8,
1201 head_size=64,
1202 scale=1.0,
1203 prefix=layer_0,
1204 ),
1205 layer_1: Attention(
1206 num_heads=8,
1207 head_size=64,
1208 scale=1.0,
1209 prefix=layer_1,
1210 kv_sharing_target_layer_name="model.layers.0.self_attn.attn",
1211 ),
1212 }
1213 # suppress var not used error
1214 assert fwd_context is not None
1215 # Set high context length to test max context length estimation
1216 vllm_config.model_config.max_model_len = 3_000_000
1217 vllm_ctx = vllm_config.compilation_config.static_forward_context
1218 runner = GPUModelRunner(vllm_config, DEVICE_TYPE)
1219 kv_cache_spec = runner.get_kv_cache_spec()
1220 assert len(kv_cache_spec) == 1
1221 assert layer_0 in kv_cache_spec
1222 assert runner.shared_kv_cache_layers[layer_1] == layer_0
1223
1224 available_memory = 20 * GiB_bytes
1225 # page size for layer 0's kv_cache_spec is 32KB
1226 # with KV sharing, we can allocate (available_mem//page_size//1) blocks
1227 # which is twice as many as without KV sharing
1228 num_expected_blocks = 655360 # 20GB / 32KB
1229 kv_cache_config = get_kv_cache_configs(
1230 vllm_config, [kv_cache_spec], [available_memory]
1231 )[0]
1232 assert kv_cache_config.num_blocks == num_expected_blocks
1233 assert len(kv_cache_config.kv_cache_tensors) == 1
1234 # Each layer now has twice the available memory for KV cache
1235 # compared to no KV sharing
1236 assert kv_cache_config.kv_cache_tensors[0].size == available_memory
1237
1238 max_context_len = estimate_max_model_len(vllm_config, kv_cache_spec, 5 * GiB_bytes)
1239 # max context len with KV sharing should be 2x as large as without
1240 assert max_context_len == 2 * 1310720
1241
1242 # important: override tensor size to prevent large mem alloc during test
1243 # this will only allocate 1 block worth of memory (32kb)
1244 kv_cache_config.num_blocks = 1
1245 page_size = kv_cache_spec[layer_0].page_size_bytes
1246 kv_cache_config.kv_cache_tensors[0].size = page_size
1247 kv_cache_config.kv_cache_tensors[0].layer_stride = page_size
1248
1249 runner.initialize_kv_cache(kv_cache_config)
1250 kv_cache_config_after_init = runner.kv_cache_config
1251
1252 layer_0_kv = vllm_ctx[layer_0].kv_cache
1253 layer_1_kv = vllm_ctx[layer_1].kv_cache
1254 # check layer 1 kv cache shares memory with layer 0
1255 assert id(layer_1_kv) == id(layer_0_kv)
1256
1257 # check layer 1 added to kv cache group's layer names
1258 assert len(kv_cache_config_after_init.kv_cache_groups) == 1
1259 assert len(kv_cache_config_after_init.kv_cache_groups[0].layer_names) == 2
1260 assert kv_cache_config_after_init.kv_cache_groups[0].layer_names[0] == layer_0
1261 assert kv_cache_config_after_init.kv_cache_groups[0].layer_names[1] == layer_1
1262
1263
1264 @pytest.mark.skipif(
1265 not current_platform.is_cuda(),
1266 reason="Attention backend FLASHINFER is only supported on CUDA.",
1267 )
1268 def test_hybrid_attention_mamba_tensor_shapes():
1269 """The GPU model runner creates different views into the
1270 KVCacheTensors for the attention and mamba layers
1271 (via _allocate_kv_caches). This test verifies
1272 that the views are compatible: writing a mamba block
1273 will not corrupt an attention block and vice versa
1274 """
1275 set_random_seed(42)
1276
1277 update_environment_variables(
1278 {
1279 "RANK": "0",
1280 "LOCAL_RANK": "0",
1281 "WORLD_SIZE": "1",
1282 "MASTER_ADDR": "localhost",
1283 "MASTER_PORT": "12345",
1284 }
1285 )
1286 from tests.utils import ensure_current_vllm_config
1287
1288 with ensure_current_vllm_config():
1289 init_distributed_environment()
1290 initialize_model_parallel(tensor_model_parallel_size=1)
1291 torch.set_default_dtype(torch.float16)
1292
1293 model_config = ModelConfig(
1294 model="ibm-granite/granite-4.0-tiny-preview",
1295 dtype="float16",
1296 )
1297 scheduler_config = SchedulerConfig(
1298 max_num_seqs=10,
1299 max_num_batched_tokens=512,
1300 max_model_len=512,
1301 is_encoder_decoder=model_config.is_encoder_decoder,
1302 )
1303 cache_config = CacheConfig(
1304 block_size=BLOCK_SIZE,
1305 gpu_memory_utilization=0.9,
1306 cache_dtype="auto",
1307 )
1308 cache_config.kv_cache_layout = "LBNHC"
1309 parallel_config = ParallelConfig()
1310 attention_config = AttentionConfig(backend=AttentionBackendEnum.FLASHINFER)
1311 vllm_config = VllmConfig(
1312 model_config=model_config,
1313 cache_config=cache_config,
1314 scheduler_config=scheduler_config,
1315 parallel_config=parallel_config,
1316 attention_config=attention_config,
1317 )
1318
1319 layer_0 = "model.layers.0.self_attn.attn"
1320 layer_1 = "model.layers.1.self_attn.attn"
1321 layer_2 = "model.layers.2.mixer"
1322 layer_3 = "model.layers.3.mixer"
1323 layer_4 = "model.layers.4.mixer"
1324 layer_5 = "model.layers.5.mixer"
1325
1326 with set_current_vllm_config(vllm_config):
1327 hf_config = vllm_config.model_config.hf_config
1328 fwd_context = {}
1329 for key in [layer_0, layer_1]:
1330 fwd_context[key] = Attention(
1331 num_heads=model_config.get_num_attention_heads(parallel_config),
1332 num_kv_heads=model_config.get_num_kv_heads(parallel_config),
1333 head_size=model_config.get_head_size(),
1334 scale=1.0,
1335 prefix=key,
1336 )
1337 for key in [layer_2, layer_3, layer_4, layer_5]:
1338 fwd_context[key] = MambaMixer2(
1339 hidden_size=hf_config.hidden_size,
1340 ssm_state_size=hf_config.mamba_d_state,
1341 conv_kernel_size=hf_config.mamba_d_conv,
1342 intermediate_size=hf_config.mamba_expand * hf_config.hidden_size,
1343 use_conv_bias=hf_config.mamba_conv_bias,
1344 use_bias=hf_config.mamba_proj_bias,
1345 n_groups=hf_config.mamba_n_groups,
1346 num_heads=hf_config.mamba_n_heads,
1347 head_dim=hf_config.mamba_d_head,
1348 rms_norm_eps=hf_config.rms_norm_eps,
1349 activation=hf_config.hidden_act,
1350 cache_config=cache_config,
1351 model_config=model_config,
1352 prefix=key,
1353 )
1354 # suppress var not used error
1355 assert fwd_context is not None
1356 vllm_ctx = vllm_config.compilation_config.static_forward_context
1357
1358 runner = GPUModelRunner(vllm_config, DEVICE_TYPE)
1359 current_platform.update_block_size_for_backend(vllm_config)
1360 kv_cache_spec = runner.get_kv_cache_spec()
1361
1362 available_memory = 5 * GiB_bytes
1363 kv_cache_config = get_kv_cache_configs(
1364 vllm_config, [kv_cache_spec], [available_memory]
1365 )[0]
1366 runner.initialize_kv_cache(kv_cache_config)
1367
1368 # random partition of blocks
1369 # blocks0 will be assigned to attention layers
1370 # blocks1 will be assigned to mamba layers
1371 num_blocks = kv_cache_config.num_blocks
1372 ind = np.arange(num_blocks)
1373 np.random.shuffle(ind)
1374 blocks0, blocks1 = ind[: (num_blocks // 2)], ind[(num_blocks // 2) :]
1375
1376 attn_shape = vllm_ctx[layer_0].kv_cache.shape
1377 conv_shape = vllm_ctx[layer_2].kv_cache[0].shape
1378 ssm_shape = vllm_ctx[layer_2].kv_cache[1].shape
1379
1380 # assert we are using FlashInfer
1381 assert attn_shape[0] % num_blocks == 0
1382 block_split_ratio = attn_shape[0] // num_blocks
1383
1384 # use small blocks for testing to avoid memory issues
1385 test_block_size = min(2, len(blocks0), len(blocks1))
1386
1387 # use non-overlapping blocks to avoid data contamination
1388 # Split kernel blocks: first half for attention, second half for mamba
1389 mid_point = num_blocks // 2
1390
1391 # attention uses kernel blocks from first half (mapped to logical blocks)
1392 kv_blocks_for_attention = np.array([0, 1])[:test_block_size]
1393
1394 # mamba uses kernel blocks from second half
1395 kv_blocks_for_mamba = np.array([mid_point, mid_point + 1])[:test_block_size]
1396
1397 # create small constant tensors for testing with corrected shapes
1398 # attention: [block_size, ...] starting from dimension 2
1399 attn_constant_shape = attn_shape[2:]
1400 conv_constant_shape = conv_shape[1:]
1401 ssm_constant_shape = ssm_shape[1:]
1402
1403 attn_blocks_constant = torch.full(
1404 (test_block_size, *attn_constant_shape), device=DEVICE_TYPE, fill_value=3.33
1405 )
1406 conv_blocks_constant = torch.full(
1407 (test_block_size, *conv_constant_shape), device=DEVICE_TYPE, fill_value=6.66
1408 )
1409 ssm_blocks_constant = torch.full(
1410 (test_block_size, *ssm_constant_shape), device=DEVICE_TYPE, fill_value=9.99
1411 )
1412
1413 # Fill attention blocks with constants using kv block indices
1414 kernel_blocks_for_attention = kv_blocks_for_attention * block_split_ratio
1415
1416 for layer in [layer_0, layer_1]:
1417 # attention: kv_cache[kernel_block_idx, kv_idx, ...]
1418 for i, kernel_block in enumerate(kernel_blocks_for_attention):
1419 vllm_ctx[layer].kv_cache[kernel_block, :] = attn_blocks_constant[i]
1420
1421 # fill mamba blocks with constants using kernel block indices
1422 for layer in [layer_2, layer_3, layer_4, layer_5]:
1423 # mamba: kv_cache[component][kernel_block_idx, ...]
1424 for i, kv_block in enumerate(kv_blocks_for_mamba):
1425 vllm_ctx[layer].kv_cache[0][kv_block, :] = conv_blocks_constant[i]
1426 vllm_ctx[layer].kv_cache[1][kv_block, :] = ssm_blocks_constant[i]
1427
1428 # verify attention and mamba contents are correct
1429 for layer in [layer_0, layer_1]:
1430 for i, kernel_block in enumerate(kernel_blocks_for_attention):
1431 actual_kv = vllm_ctx[layer].kv_cache[kernel_block, :]
1432 expected = attn_blocks_constant[i]
1433
1434 # Packed layout: (num_kv_heads, block_size, 2*head_size). Every
1435 # head in the block was filled with the same constant.
1436 for head_idx in range(actual_kv.shape[0]):
1437 assert torch.equal(actual_kv[head_idx], expected)
1438
1439 for layer in [layer_2, layer_3, layer_4, layer_5]:
1440 for i, kv_block in enumerate(kv_blocks_for_mamba):
1441 actual_conv = vllm_ctx[layer].kv_cache[0][kv_block, :]
1442 actual_ssm = vllm_ctx[layer].kv_cache[1][kv_block, :]
1443 expected_conv = conv_blocks_constant[i]
1444 expected_ssm = ssm_blocks_constant[i]
1445
1446 assert torch.equal(actual_conv, expected_conv)
1447 assert torch.equal(actual_ssm, expected_ssm)
1448
1449 for layer in [layer_2, layer_3, layer_4, layer_5]:
1450 for i, kv_block in enumerate(kv_blocks_for_mamba):
1451 actual_conv = vllm_ctx[layer].kv_cache[0][kv_block, :]
1452 actual_ssm = vllm_ctx[layer].kv_cache[1][kv_block, :]
1453 expected_conv = conv_blocks_constant[i]
1454 expected_ssm = ssm_blocks_constant[i]
1455 assert torch.equal(actual_conv, expected_conv)
1456 assert torch.equal(actual_ssm, expected_ssm)
1457
1458
1459 def test_input_batch_reinitialized_after_late_interleave_adjustment(monkeypatch):
1460 runner = object.__new__(GPUModelRunner)
1461 runner.vllm_config = SimpleNamespace(reasoning_config=None)
1462 runner.parallel_config = SimpleNamespace(cp_kv_cache_interleave_size=16)
1463 runner.cache_config = SimpleNamespace(use_replayssm=False)
1464 runner.model_config = SimpleNamespace(get_vocab_size=lambda: 32)
1465 runner.max_model_len = 64
1466 runner.max_encoder_len = 0
1467 runner.max_num_reqs = 1
1468 runner.max_num_tokens = 64
1469 runner.num_spec_tokens = 0
1470 runner.device = torch.device("cpu")
1471 runner.is_pooling_model = False
1472 runner._init_block_sizes = [16]
1473 runner._init_kernel_block_sizes = [16]
1474 runner._init_max_num_blocks = [4]
1475 runner._init_slot_mapping_modes = [
1476 gpu_model_runner_module.SlotMappingMode.TOKEN_TO_KV_SLOT
1477 ]
1478 runner.cp_kv_cache_interleave_size = 1
1479 runner.input_batch = SimpleNamespace(
1480 logitsprocs=None,
1481 logitsprocs_need_output_token_ids=False,
1482 )
1483 runner.jit_warmup_registry = Mock()
1484 runner.jit_warmup_registry.activate.return_value = nullcontext()
1485
1486 spec = SimpleNamespace(
1487 block_size=16,
1488 max_num_blocks_per_req=lambda *_: 4,
1489 )
1490 kv_cache_config = SimpleNamespace(
1491 kv_cache_groups=[SimpleNamespace(kv_cache_spec=spec)]
1492 )
1493 input_batch_cls = Mock(return_value=SimpleNamespace())
1494 monkeypatch.setattr(gpu_model_runner_module, "InputBatch", input_batch_cls)
1495 monkeypatch.setattr(
1496 gpu_model_runner_module,
1497 "get_kv_cache_spec_kind",
1498 lambda _: gpu_model_runner_module.KVCacheSpecKind.FULL_ATTENTION,
1499 )
1500
1501 runner.may_reinitialize_input_batch(kv_cache_config, [16])
1502
1503 assert input_batch_cls.call_count == 1
1504 assert input_batch_cls.call_args.kwargs["cp_kv_cache_interleave_size"] == 16
1505
1506
1507 def test_v2_runner_snapshots_late_interleave_adjustment(monkeypatch):
1508 from vllm.v1.worker.gpu import model_runner as v2_model_runner_module
1509
1510 runner = object.__new__(v2_model_runner_module.GPUModelRunner)
1511 runner.parallel_config = SimpleNamespace(cp_kv_cache_interleave_size=16)
1512 runner.cp_interleave = 1
1513
1514 class StopInitialization(Exception):
1515 pass
1516
1517 monkeypatch.setattr(
1518 v2_model_runner_module,
1519 "deepcopy",
1520 Mock(side_effect=StopInitialization),
1521 )
1522
1523 with pytest.raises(StopInitialization):
1524 runner.initialize_kv_cache(SimpleNamespace())
1525
1526 assert runner.cp_interleave == 16
1527
1528
1529 def test_hybrid_block_table_initialization():
1530 """Test hybrid block table with different kernel and kvcache_manager block
1531 sizes."""
1532 from vllm.v1.worker.block_table import BlockTable
1533
1534 # Test configuration: kvcache_manager block size = 32,
1535 # kernel block size = 16
1536 block_size = 32
1537 kernel_block_sizes = [16]
1538 max_num_reqs = 10
1539 max_num_blocks_per_req = 20
1540 max_num_batched_tokens = 512
1541 cp_kv_cache_interleave_size = 8
1542
1543 block_table = BlockTable(
1544 block_size=block_size,
1545 max_num_reqs=max_num_reqs,
1546 max_num_blocks_per_req=max_num_blocks_per_req,
1547 max_num_batched_tokens=max_num_batched_tokens,
1548 pin_memory=False,
1549 device=torch.device(DEVICE_TYPE),
1550 kernel_block_size=kernel_block_sizes[0],
1551 cp_kv_cache_interleave_size=cp_kv_cache_interleave_size,
1552 )
1553
1554 # Verify hybrid block configuration
1555 assert block_table.use_hybrid_blocks is True
1556 assert block_table.block_size == kernel_block_sizes[0]
1557 assert block_table.blocks_per_kv_block == (
1558 block_size // kernel_block_sizes[0]
1559 ) # Changed to use first element
1560
1561 # Test block table conversion logic
1562 # One kvcache_manager block should map to multiple kernel blocks
1563 kvcache_manager_blocks = [0, 1, 2]
1564
1565 # Verify that kvcache_manager blocks can be converted to kernel blocks
1566 # and that block table operations work correctly.
1567 req_index = 0
1568 block_table.append_row(kvcache_manager_blocks, req_index)
1569 # Get expected kernel blocks from the implementation for verification.
1570 expected_kernel_blocks = block_table.map_to_kernel_blocks(
1571 np.array(kvcache_manager_blocks),
1572 block_table.blocks_per_kv_block,
1573 block_table._kernel_block_arange,
1574 )
1575 # Verify block table state
1576 assert block_table.num_blocks_per_row[req_index] == len(expected_kernel_blocks)
1577 assert np.array_equal(
1578 block_table.block_table.np[req_index, : len(expected_kernel_blocks)],
1579 expected_kernel_blocks,
1580 )
1581
1582
1583 def test_get_block_table_width_aligns_to_128_tokens():
1584 assert get_block_table_width(1875, 64) == 1876
1585
1586
1587 def test_get_block_table_width_splits_virtual_blocks():
1588 assert get_block_table_width(235, 256, 64) == 940
1589
1590
1591 def test_mamba_state_table_width_is_not_aligned():
1592 block_tables = MultiGroupBlockTable(
1593 max_num_reqs=1,
1594 max_num_batched_tokens=1,
1595 pin_memory=False,
1596 device=torch.device("cpu"),
1597 block_sizes=[39664],
1598 kernel_block_sizes=[39664],
1599 max_num_blocks=[1],
1600 slot_mapping_modes=[SlotMappingMode.NONE],
1601 )
1602
1603 assert block_tables[0].max_num_blocks_per_req == 1
1604
1605
1606 def test_input_batch_with_kernel_block_sizes():
1607 """Test InputBatch initialization with kernel_block_sizes parameter."""
1608 max_num_reqs = 10
1609 max_model_len = 512
1610 max_num_batched_tokens = 512
1611 device = torch.device(DEVICE_TYPE)
1612 vocab_size = 50272
1613
1614 # Test with different kernel block sizes
1615 block_sizes = [32, 64]
1616 kernel_block_sizes = [16, 32]
1617
1618 input_batch = InputBatch(
1619 max_num_reqs=max_num_reqs,
1620 max_model_len=max_model_len,
1621 max_num_batched_tokens=max_num_batched_tokens,
1622 device=device,
1623 vocab_size=vocab_size,
1624 block_sizes=block_sizes,
1625 kernel_block_sizes=kernel_block_sizes,
1626 max_num_blocks_per_req=[16, 8],
1627 )
1628
1629 # Verify that block tables were created with kernel block sizes
1630 assert len(input_batch.block_table.block_tables) == len(block_sizes)
1631
1632 for i, (kv_size, kernel_size) in enumerate(zip(block_sizes, kernel_block_sizes)):
1633 block_table = input_batch.block_table.block_tables[i]
1634 if kv_size != kernel_size:
1635 assert block_table.use_hybrid_blocks is True
1636 assert block_table.block_size == kernel_size
1637 else:
1638 assert block_table.use_hybrid_blocks is False
1639 assert block_table.block_size == kernel_size
1640
1641
1642 def test_hybrid_cache_integration(default_vllm_config, dist_init):
1643 """Test hybrid cache architecture integration with GPUModelRunner."""
1644 # Create a new model runner with hybrid cache configuration
1645 vllm_config = get_vllm_config()
1646
1647 # Configure hybrid cache with different kvcache_manager block size
1648 vllm_config.cache_config.block_size = 32
1649
1650 model_config = vllm_config.model_config
1651 num_heads = model_config.get_num_kv_heads(vllm_config.parallel_config)
1652 head_size = model_config.get_head_size()
1653 vllm_config.compilation_config.static_forward_context["layer.0"] = Attention(
1654 num_heads, head_size, 0.1
1655 )
1656
1657 runner = GPUModelRunner(vllm_config, DEVICE_TYPE)
1658
1659 # Initialize KV cache with configuration
1660 attn_spec = FullAttentionSpec(
1661 block_size=16, # Use kernel block size directly
1662 num_kv_heads=runner.model_config.get_num_kv_heads(runner.parallel_config),
1663 head_size=runner.model_config.get_head_size(),
1664 dtype=runner.kv_cache_dtype,
1665 )
1666 tensor_size = attn_spec.page_size_bytes * NUM_BLOCKS
1667 kv_cache_config = KVCacheConfig(
1668 num_blocks=NUM_BLOCKS,
1669 kv_cache_tensors=[
1670 KVCacheTensor(
1671 size=tensor_size,
1672 layers=["layer.0"],
1673 layer_stride=tensor_size,
1674 block_stride=attn_spec.page_size_bytes,
1675 ),
1676 ],
1677 kv_cache_groups=[
1678 KVCacheGroupSpec(layer_names=["layer.0"], kv_cache_spec=attn_spec)
1679 ],
1680 )
1681 runner.kv_cache_config = kv_cache_config
1682
1683 # Initialize input batch with kernel block sizes
1684 runner.input_batch = InputBatch(
1685 max_num_reqs=runner.max_num_reqs,
1686 max_model_len=runner.max_model_len,
1687 max_num_batched_tokens=runner.max_num_tokens,
1688 device=runner.device,
1689 vocab_size=runner.model_config.get_vocab_size(),
1690 block_sizes=[kv_cache_config.kv_cache_groups[0].kv_cache_spec.block_size],
1691 kernel_block_sizes=[16],
1692 max_num_blocks_per_req=[NUM_BLOCKS],
1693 ) # Use kernel block size
1694
1695 runner.initialize_attn_backend(kv_cache_config)
1696
1697 # Verify hybrid block table configuration
1698 block_table = runner.input_batch.block_table.block_tables[0]
1699 assert block_table.block_size == (
1700 kv_cache_config.kv_cache_groups[0].kv_cache_spec.block_size
1701 )
1702
1703 # Test request processing with hybrid blocks
1704 req_id = "hybrid_req_0"
1705 scheduler_output = _schedule_new_request(req_id)
1706
1707 # Update states should work with hybrid blocks
1708 runner._update_states(scheduler_output)
1709 assert _is_req_scheduled(runner, req_id)
1710 assert _is_req_state_block_table_match(runner, req_id)
1711
1712
1713 def test_is_uniform_decode() -> None:
1714 # Normal
1715 assert GPUModelRunner._is_uniform_decode(
1716 max_num_scheduled_tokens=1,
1717 uniform_decode_query_len=1,
1718 num_tokens=16,
1719 num_reqs=16,
1720 )
1721 assert not GPUModelRunner._is_uniform_decode(
1722 max_num_scheduled_tokens=2,
1723 uniform_decode_query_len=1,
1724 num_tokens=16,
1725 num_reqs=16,
1726 )
1727 assert not GPUModelRunner._is_uniform_decode(
1728 max_num_scheduled_tokens=1,
1729 uniform_decode_query_len=1,
1730 num_tokens=16,
1731 num_reqs=15,
1732 )
1733 # Spec decoding
1734 assert GPUModelRunner._is_uniform_decode(
1735 max_num_scheduled_tokens=5,
1736 uniform_decode_query_len=5,
1737 num_tokens=30,
1738 num_reqs=6,
1739 )
1740 assert not GPUModelRunner._is_uniform_decode(
1741 max_num_scheduled_tokens=5,
1742 uniform_decode_query_len=4,
1743 num_tokens=30,
1744 num_reqs=6,
1745 )
1746 assert not GPUModelRunner._is_uniform_decode(
1747 max_num_scheduled_tokens=5,
1748 uniform_decode_query_len=5,
1749 num_tokens=30,
1750 num_reqs=7,
1751 )
1752 # Force uniform decode
1753 assert GPUModelRunner._is_uniform_decode(
1754 max_num_scheduled_tokens=1,
1755 uniform_decode_query_len=1,
1756 num_tokens=16,
1757 num_reqs=16,
1758 force_uniform_decode=True,
1759 )
1760 assert GPUModelRunner._is_uniform_decode(
1761 max_num_scheduled_tokens=2,
1762 uniform_decode_query_len=1,
1763 num_tokens=16,
1764 num_reqs=16,
1765 force_uniform_decode=True,
1766 )
1767 assert GPUModelRunner._is_uniform_decode(
1768 max_num_scheduled_tokens=1,
1769 uniform_decode_query_len=1,
1770 num_tokens=16,
1771 num_reqs=15,
1772 force_uniform_decode=True,
1773 )
1774 assert not GPUModelRunner._is_uniform_decode(
1775 max_num_scheduled_tokens=1,
1776 uniform_decode_query_len=1,
1777 num_tokens=16,
1778 num_reqs=16,
1779 force_uniform_decode=False,
1780 )
1781 assert not GPUModelRunner._is_uniform_decode(
1782 max_num_scheduled_tokens=2,
1783 uniform_decode_query_len=1,
1784 num_tokens=16,
1785 num_reqs=16,
1786 force_uniform_decode=False,
1787 )
1788 assert not GPUModelRunner._is_uniform_decode(
1789 max_num_scheduled_tokens=1,
1790 uniform_decode_query_len=1,
1791 num_tokens=16,
1792 num_reqs=15,
1793 force_uniform_decode=False,
1794 )
1795
1796
1797 @pytest.mark.skipif(
1798 not current_platform.is_cuda(),
1799 reason="Attention backend FLASHINFER is only supported on CUDA.",
1800 )
1801 def test_mamba_cache_raises_when_max_num_seqs_exceeds_blocks():
1802 """Test that a ValueError is raised when max_num_seqs exceeds the
1803 available Mamba cache blocks for hybrid models with FULL cudagraphs.
1804
1805 See: https://github.com/vllm-project/vllm/issues/34094
1806 """
1807 set_random_seed(42)
1808
1809 update_environment_variables(
1810 {
1811 "RANK": "0",
1812 "LOCAL_RANK": "0",
1813 "WORLD_SIZE": "1",
1814 "MASTER_ADDR": "localhost",
1815 "MASTER_PORT": "12345",
1816 }
1817 )
1818 from tests.utils import ensure_current_vllm_config
1819
1820 with ensure_current_vllm_config():
1821 init_distributed_environment()
1822 initialize_model_parallel(tensor_model_parallel_size=1)
1823 torch.set_default_dtype(torch.float16)
1824
1825 model_config = ModelConfig(
1826 model="ibm-granite/granite-4.0-tiny-preview",
1827 dtype="float16",
1828 )
1829 scheduler_config = SchedulerConfig(
1830 max_num_seqs=10,
1831 max_num_batched_tokens=512,
1832 max_model_len=512,
1833 is_encoder_decoder=model_config.is_encoder_decoder,
1834 )
1835 cache_config = CacheConfig(
1836 block_size=BLOCK_SIZE,
1837 gpu_memory_utilization=0.9,
1838 cache_dtype="auto",
1839 )
1840 cache_config.kv_cache_layout = "LBNHC"
1841 parallel_config = ParallelConfig()
1842 attention_config = AttentionConfig(backend=AttentionBackendEnum.FLASHINFER)
1843 vllm_config = VllmConfig(
1844 model_config=model_config,
1845 cache_config=cache_config,
1846 scheduler_config=scheduler_config,
1847 parallel_config=parallel_config,
1848 attention_config=attention_config,
1849 )
1850
1851 with set_current_vllm_config(vllm_config):
1852 hf_config = vllm_config.model_config.hf_config
1853 fwd_context = {}
1854 for key in ["model.layers.0.self_attn.attn", "model.layers.1.self_attn.attn"]:
1855 fwd_context[key] = Attention(
1856 num_heads=model_config.get_num_attention_heads(parallel_config),
1857 num_kv_heads=model_config.get_num_kv_heads(parallel_config),
1858 head_size=model_config.get_head_size(),
1859 scale=1.0,
1860 prefix=key,
1861 )
1862 for key in [
1863 "model.layers.2.mixer",
1864 "model.layers.3.mixer",
1865 "model.layers.4.mixer",
1866 "model.layers.5.mixer",
1867 ]:
1868 fwd_context[key] = MambaMixer2(
1869 hidden_size=hf_config.hidden_size,
1870 ssm_state_size=hf_config.mamba_d_state,
1871 conv_kernel_size=hf_config.mamba_d_conv,
1872 intermediate_size=hf_config.mamba_expand * hf_config.hidden_size,
1873 use_conv_bias=hf_config.mamba_conv_bias,
1874 use_bias=hf_config.mamba_proj_bias,
1875 n_groups=hf_config.mamba_n_groups,
1876 num_heads=hf_config.mamba_n_heads,
1877 head_dim=hf_config.mamba_d_head,
1878 rms_norm_eps=hf_config.rms_norm_eps,
1879 activation=hf_config.hidden_act,
1880 cache_config=cache_config,
1881 model_config=model_config,
1882 prefix=key,
1883 )
1884 assert fwd_context is not None
1885
1886 runner = GPUModelRunner(vllm_config, DEVICE_TYPE)
1887 current_platform.update_block_size_for_backend(vllm_config)
1888 kv_cache_spec = runner.get_kv_cache_spec()
1889
1890 available_memory = 5 * GiB_bytes
1891 kv_cache_config = get_kv_cache_configs(
1892 vllm_config, [kv_cache_spec], [available_memory]
1893 )[0]
1894 num_blocks = kv_cache_config.num_blocks
1895
1896 # Force max_num_seqs to exceed num_blocks so the check triggers.
1897 runner.max_num_reqs = num_blocks + 100
1898
1899 with pytest.raises(ValueError, match="max_num_seqs"):
1900 runner.initialize_kv_cache(kv_cache_config)