main
py 112 lines 4.27 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """SWA bounded replay in DeepseekV41ModelState: the batch's replay starts reach
4 the sliding-window builders, and the replayed tokens' slots are padded in the
5 prefix-cacheable groups only."""
6
7 from dataclasses import replace
8 from types import SimpleNamespace
9 from unittest.mock import MagicMock
10
11 import numpy as np
12 import pytest
13 import torch
14
15 from vllm.config import CUDAGraphMode
16 from vllm.models.deepseek_v41.nvidia.model_state import DeepseekV41ModelState
17 from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadataBuilder
18 from vllm.v1.attention.backends.utils import PAD_SLOT_ID
19 from vllm.v1.worker.gpu.input_batch import InputBatch, InputBuffers
20 from vllm.v1.worker.gpu.model_states.default import DefaultModelState
21
22 pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="needs CUDA")
23
24 DEVICE = torch.device("cuda")
25 WINDOW = 4
26 # Group 0 is prefix-cacheable (compressed KV), group 1 is the replayed window.
27 KV_CACHE_CONFIG = SimpleNamespace(
28 kv_cache_groups=[
29 SimpleNamespace(
30 kv_cache_spec=SimpleNamespace(prefix_cacheable=True, prefix_replay_tokens=0)
31 ),
32 SimpleNamespace(
33 kv_cache_spec=SimpleNamespace(
34 prefix_cacheable=False, prefix_replay_tokens=WINDOW
35 )
36 ),
37 ]
38 )
39
40
41 @pytest.fixture
42 def state(monkeypatch):
43 cfg = MagicMock()
44 cfg.model_config.enable_prompt_embeds = False
45 cfg.model_config.uses_mrope = False
46 cfg.model_config.is_multimodal_model = False
47 cfg.scheduler_config.max_num_seqs = 8
48 cfg.scheduler_config.max_num_batched_tokens = 64
49 seen: dict = {}
50
51 def prepare_attn(self, input_batch, cg_mode, block_tables, slot_mappings, *a, **kw):
52 seen["slot_mappings"] = slot_mappings.clone()
53 seen["extra"] = kw["model_specific_attn_metadata"]
54 return {}
55
56 monkeypatch.setattr(DefaultModelState, "prepare_attn", prepare_attn)
57 state = DeepseekV41ModelState(
58 cfg, SimpleNamespace(token_lookback_depth=0), None, DEVICE
59 )
60 state.seen = seen # type: ignore[attr-defined]
61 return state
62
63
64 def _batch(query_lens, seq_lens, idx_mapping, is_prefilling):
65 num_reqs, num_tokens = len(query_lens), sum(query_lens)
66 batch = InputBatch.make_dummy(num_reqs, num_tokens, InputBuffers(8, 64, DEVICE))
67 return replace(
68 batch,
69 query_start_loc=torch.tensor(
70 [0, *np.cumsum(query_lens)], dtype=torch.int32, device=DEVICE
71 ),
72 idx_mapping_np=np.array(idx_mapping, dtype=np.intp),
73 is_prefilling_np=np.array(is_prefilling, dtype=np.bool_),
74 positions=torch.cat(
75 [torch.arange(s - q, s) for q, s in zip(query_lens, seq_lens)]
76 ).to(DEVICE),
77 )
78
79
80 def _prepare(state, batch):
81 slot_mappings = torch.arange(2 * batch.num_tokens, device=DEVICE).view(2, -1)
82 state.prepare_attn(
83 batch, CUDAGraphMode.NONE, (), slot_mappings, [], KV_CACHE_CONFIG
84 )
85 swa_builder = MagicMock(spec=DeepseekSparseSWAMetadataBuilder)
86 extra = state.seen["extra"]
87 replay_start = extra.get_extra_attn_kwargs(swa_builder, batch.num_reqs)[
88 "replay_start"
89 ]
90 assert extra.get_extra_attn_kwargs(MagicMock(), batch.num_reqs) == {}
91 return replay_start.tolist(), state.seen["slot_mappings"]
92
93
94 def test_replayed_tokens_write_only_the_window_group(state):
95 # Request state 3 resumes a hit at 16: positions 16..19 are replayed.
96 state.add_request(3, SimpleNamespace(replay_start=16))
97 state.add_request(1, SimpleNamespace(replay_start=0))
98 batch = _batch([6, 2], [22, 2], idx_mapping=[3, 1], is_prefilling=[True, True])
99 replay_start, slots = _prepare(state, batch)
100 assert replay_start == [16, 0]
101 padded = [PAD_SLOT_ID] * WINDOW + [4, 5, 6, 7]
102 assert slots[0].tolist() == padded # compressed KV keeps its cached rows
103 assert slots[1].tolist() == list(range(8, 16)) # the window is rebuilt
104
105
106 def test_only_prefills_carry_a_replay_start(state):
107 state.add_request(0, SimpleNamespace(replay_start=16))
108 # The same request, decoding: its rows sit above the hit.
109 batch = _batch([1], [40], idx_mapping=[0], is_prefilling=[False])
110 replay_start, slots = _prepare(state, batch)
111 assert replay_start == [0]
112 assert slots.tolist() == [[0], [1]]