main
py 336 lines 10.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import contextlib
5 from types import SimpleNamespace
6 from unittest.mock import Mock
7
8 import pytest
9 import torch
10
11 import vllm.v1.worker.gpu.model_runner as model_runner_module
12 from vllm.model_executor.warmup.jit_warmup import JitWarmupRegistry
13 from vllm.v1.kv_cache_interface import (
14 CircularBufferSpec,
15 FullAttentionSpec,
16 KVCacheConfig,
17 KVCacheGroupSpec,
18 MambaSpec,
19 UniformTypeKVCacheSpecs,
20 )
21 from vllm.v1.worker.gpu.block_table import BlockTables
22 from vllm.v1.worker.gpu.model_runner import ExecuteModelState, GPUModelRunner
23
24
25 def test_non_last_pp_rank_uses_global_batch_for_sample_feedback():
26 runner = GPUModelRunner.__new__(GPUModelRunner)
27 runner.is_last_pp_rank = False
28 local_batch = object()
29 global_batch = SimpleNamespace(idx_mapping=object())
30 runner.pcp_manager = SimpleNamespace(
31 global_batch=global_batch,
32 restore_for_sampling=Mock(),
33 )
34 runner.pp_handler = SimpleNamespace(receive=Mock(return_value=False))
35 runner.postprocess_num_computed_tokens = Mock()
36 runner.model_state = SimpleNamespace(postprocess_state=Mock())
37 runner.kv_connector = SimpleNamespace(post_forward=Mock(return_value=None))
38 runner.eplb = SimpleNamespace(step=Mock())
39 runner.execute_model_state = ExecuteModelState(
40 input_batch=local_batch,
41 attn_metadata=None,
42 slot_mappings_by_layer=None,
43 hidden_states=None,
44 aux_hidden_states=None,
45 dp_sync=None,
46 finished_req_ids=set(),
47 ec_connector_output=None,
48 cudagraph_stats=None,
49 )
50
51 runner.sample_tokens(None)
52
53 runner.pp_handler.receive.assert_called_once_with(global_batch)
54 runner.postprocess_num_computed_tokens.assert_called_once_with(global_batch)
55 runner.model_state.postprocess_state.assert_called_once_with(
56 global_batch.idx_mapping, 0
57 )
58 runner.pcp_manager.restore_for_sampling.assert_not_called()
59
60
61 def test_qsa_circular_group_uses_custom_slot_mapping(monkeypatch):
62 runner = GPUModelRunner.__new__(GPUModelRunner)
63 runner.max_model_len = 262144
64 runner.is_encoder_decoder = False
65 runner.dcp_size = 1
66 runner.dcp_rank = 0
67 runner.cp_interleave = 1
68 runner.cache_config = SimpleNamespace(enable_prefix_caching=True)
69 parallel_config = SimpleNamespace(
70 decode_context_parallel_size=1,
71 cp_kv_cache_interleave_size=1,
72 )
73 runner.parallel_config = parallel_config
74 runner.vllm_config = SimpleNamespace(
75 parallel_config=parallel_config,
76 cache_config=SimpleNamespace(mamba_cache_mode="none"),
77 )
78 runner.jit_warmup_registry = JitWarmupRegistry(runner.vllm_config)
79 runner.model_state = SimpleNamespace(
80 get_additional_cg_support=lambda: (),
81 num_new_sampled_tokens_per_step=1,
82 )
83 runner.speculator = None
84 runner.req_states = []
85 runner.input_buffers = SimpleNamespace(query_start_loc=None)
86 runner.vocab_size = 1
87 runner.max_num_reqs = 1
88 runner.max_num_tokens = 2
89 runner.device = torch.device("cuda")
90
91 raw_spec = CircularBufferSpec(
92 block_size=8,
93 num_kv_heads=1,
94 head_size=128,
95 dtype=torch.bfloat16,
96 )
97 compressed_spec = FullAttentionSpec(
98 block_size=262144,
99 num_kv_heads=1,
100 head_size=128,
101 dtype=torch.bfloat16,
102 )
103 kv_cache_config = KVCacheConfig(
104 num_blocks=1,
105 kv_cache_tensors=[],
106 kv_cache_groups=[
107 KVCacheGroupSpec(
108 layer_names=["raw"],
109 kv_cache_spec=UniformTypeKVCacheSpecs(
110 block_size=8,
111 kv_cache_specs={"raw": raw_spec},
112 ),
113 ),
114 KVCacheGroupSpec(layer_names=["compressed"], kv_cache_spec=compressed_spec),
115 ],
116 )
117
118 class FakeAttnCGSupport:
119 def narrow(self, *args):
120 return self
121
122 attn_cg_support = FakeAttnCGSupport()
123 monkeypatch.setattr(
124 model_runner_module,
125 "init_attn_backend",
126 lambda *args, **kwargs: ([], attn_cg_support, [8, 262144]),
127 )
128 monkeypatch.setattr(
129 model_runner_module,
130 "maybe_create_adaptive_verification_manager",
131 lambda **kwargs: None,
132 )
133
134 captured = {}
135
136 class BlockTablesCaptured(Exception):
137 pass
138
139 def capture_block_tables(**kwargs):
140 captured.update(kwargs)
141 raise BlockTablesCaptured
142
143 monkeypatch.setattr(model_runner_module, "BlockTables", capture_block_tables)
144
145 with pytest.raises(BlockTablesCaptured):
146 runner.initialize_kv_cache(kv_cache_config)
147
148 assert captured["max_num_blocks_per_group"] == [1, 1]
149 assert captured["slot_mapping_enabled"] == [False, True]
150
151
152 @pytest.mark.parametrize(
153 ("mamba_cache_mode", "num_speculative_blocks", "expected"),
154 [
155 pytest.param("align", 0, 65_536, id="align-prefix-cache"),
156 pytest.param("none", 7, 8, id="no-prefix-cache-with-speculation"),
157 ],
158 )
159 def test_initialize_kv_cache_does_not_dcp_shard_mamba_block_table(
160 monkeypatch,
161 mamba_cache_mode: str,
162 num_speculative_blocks: int,
163 expected: int,
164 ):
165 """Mamba/GDN block-table rows index global positions, unlike DCP KV."""
166 max_model_len = 1_048_576
167 attention_block_size = 1_536
168 mamba_block_size = 16
169 dcp_size = 8
170 full_attention_spec = FullAttentionSpec(
171 block_size=attention_block_size,
172 num_kv_heads=1,
173 head_size=1,
174 dtype=torch.bfloat16,
175 )
176 mamba_spec = MambaSpec(
177 shapes=((1,),),
178 dtypes=(torch.bfloat16,),
179 block_size=mamba_block_size,
180 mamba_cache_mode=mamba_cache_mode,
181 num_speculative_blocks=num_speculative_blocks,
182 )
183 kv_cache_config = KVCacheConfig(
184 num_blocks=1,
185 kv_cache_tensors=[],
186 kv_cache_groups=[
187 KVCacheGroupSpec(["attention"], full_attention_spec),
188 KVCacheGroupSpec(["kda"], mamba_spec),
189 ],
190 )
191 parallel_config = SimpleNamespace(
192 decode_context_parallel_size=dcp_size,
193 cp_kv_cache_interleave_size=1,
194 )
195 vllm_config = SimpleNamespace(
196 parallel_config=parallel_config,
197 cache_config=SimpleNamespace(mamba_cache_mode=mamba_cache_mode),
198 )
199 runner = SimpleNamespace(
200 max_model_len=max_model_len,
201 is_encoder_decoder=False,
202 vllm_config=vllm_config,
203 parallel_config=parallel_config,
204 )
205
206 class _CapturedWidths(Exception):
207 pass
208
209 captured: list[int] = []
210
211 def capture_width(max_num_blocks: int, *_args, **_kwargs) -> int:
212 captured.append(max_num_blocks)
213 if len(captured) == 2:
214 raise _CapturedWidths
215 return max_num_blocks
216
217 monkeypatch.setattr(model_runner_module, "get_block_table_width", capture_width)
218
219 with pytest.raises(_CapturedWidths):
220 GPUModelRunner.initialize_kv_cache(runner, kv_cache_config)
221
222 # Attention KV is local to one of eight DCP ranks; KDA state is replicated
223 # and therefore needs one table entry for every global 16-token page.
224 assert captured == [86, expected]
225
226
227 def test_append_block_ids_rejects_write_past_row_capacity():
228 """Reject an oversized staged write before it can corrupt the next row."""
229
230 class _BlockTable:
231 gpu = torch.empty((2, 4), dtype=torch.int32)
232
233 def stage_write(self, *_args):
234 pytest.fail("an oversized write must not be staged")
235
236 block_tables = BlockTables.__new__(BlockTables)
237 block_tables.num_kv_cache_groups = 1
238 block_tables.blocks_per_kv_block = [1]
239 block_tables.block_tables = [_BlockTable()]
240 block_tables.num_blocks = SimpleNamespace(
241 np=torch.tensor([[0, 3]], dtype=torch.int32)
242 )
243
244 with pytest.raises(
245 RuntimeError,
246 match=r"request 1, group 0 exceeds row capacity \(5 > 4\)",
247 ):
248 block_tables.append_block_ids(
249 req_index=1,
250 new_block_ids=([4, 5],),
251 overwrite=False,
252 )
253
254 assert block_tables.num_blocks.np[0, 1] == 3
255
256
257 def _make_capture_runner(captured: bool) -> GPUModelRunner:
258 """Minimal V2 runner for capture_model: fakes everything except the
259 cudagraph_manager's needs_capture decision."""
260 runner = GPUModelRunner.__new__(GPUModelRunner)
261 runner.model_state = SimpleNamespace(supports_mm_inputs=False)
262 runner.cudagraph_manager = SimpleNamespace(
263 needs_capture=lambda: captured,
264 capture=lambda *args, **kwargs: None,
265 )
266 runner.lora_config = None
267 runner.maybe_setup_dummy_loras = lambda _cfg: contextlib.nullcontext()
268 runner.speculator = None
269 runner.adaptive_verification = None
270 runner.model = None
271 runner.input_buffers = None
272 runner.pcp_manager = None
273 runner.intermediate_tensors = None
274 runner.block_tables = None
275 runner.attn_groups = None
276 runner.kv_cache_config = None
277 runner.use_aux_hidden_state_outputs = False
278 runner.kv_connector = model_runner_module.NO_OP_KV_CONNECTOR
279 return runner
280
281
282 def test_capture_model_locks_workspace_after_capture(monkeypatch):
283 """A workspace resize after capture frees the buffer the captured graphs
284 baked in, so capture_model must lock the workspace before returning
285 (https://github.com/vllm-project/vllm/issues/55336)."""
286 runner = _make_capture_runner(captured=True)
287 monkeypatch.setattr(
288 model_runner_module, "freeze_gc_for_cudagraph_capture", contextlib.nullcontext
289 )
290 monkeypatch.setattr(torch.accelerator, "empty_cache", lambda: None)
291 monkeypatch.setattr(
292 torch.accelerator, "get_memory_info", lambda: (1 << 30, 1 << 30)
293 )
294 lock_calls = []
295 monkeypatch.setattr(
296 model_runner_module, "lock_workspace", lambda: lock_calls.append("lock")
297 )
298
299 runner.capture_model()
300
301 assert lock_calls == ["lock"]
302
303
304 def test_capture_model_skips_lock_when_nothing_captured(monkeypatch):
305 """With no graphs to capture (e.g. enforce_eager) there is nothing baked
306 into the workspace, so the early return must not lock it."""
307 runner = _make_capture_runner(captured=False)
308 lock_calls = []
309 monkeypatch.setattr(
310 model_runner_module, "lock_workspace", lambda: lock_calls.append("lock")
311 )
312
313 assert runner.capture_model() == 0
314 assert lock_calls == []
315
316
317 def test_capture_model_profile_only_skips_lock(monkeypatch):
318 """The memory-profiling capture pass runs before kernel warmup and the
319 real capture; locking there would stop the warmup from growing the
320 workspace to its scheduler-realistic size."""
321 runner = _make_capture_runner(captured=True)
322 monkeypatch.setattr(
323 model_runner_module, "freeze_gc_for_cudagraph_capture", contextlib.nullcontext
324 )
325 monkeypatch.setattr(torch.accelerator, "empty_cache", lambda: None)
326 monkeypatch.setattr(
327 torch.accelerator, "get_memory_info", lambda: (1 << 30, 1 << 30)
328 )
329 lock_calls = []
330 monkeypatch.setattr(
331 model_runner_module, "lock_workspace", lambda: lock_calls.append("lock")
332 )
333
334 runner.capture_model(profile_only=True)
335
336 assert lock_calls == []