main
py 239 lines 8.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import functools
4 from copy import copy
5
6 import numpy as np
7 import torch
8
9 from vllm.config import CacheConfig, VllmConfig
10 from vllm.logger import init_logger
11 from vllm.model_executor.layers.attention import Attention
12 from vllm.utils.math_utils import cdiv
13 from vllm.v1.attention.backend import (
14 AttentionBackend,
15 AttentionMetadata,
16 AttentionType,
17 CommonAttentionMetadata,
18 subclass_attention_backend_with_overrides,
19 )
20 from vllm.v1.attention.selector import get_attn_backend
21 from vllm.v1.kv_cache_interface import (
22 CrossAttentionSpec,
23 KVCacheSpec,
24 get_kv_quant_mode,
25 )
26
27 logger = init_logger(__name__)
28
29
30 def _get_cross_slot_mapping(
31 encoder_seq_lens: np.ndarray,
32 block_table_tensor: torch.Tensor,
33 kv_cache_spec: CrossAttentionSpec,
34 device: torch.device,
35 ) -> torch.Tensor:
36 """Get cross-attention slot mappings."""
37 block_size = kv_cache_spec.block_size
38 slot_mappings = []
39
40 # Find indices with non-zero encoder sequence lengths
41 # The majority of parallel requests will be running the
42 # decoder, so this list should be relatively small.
43 active_indices = np.nonzero(encoder_seq_lens)[0]
44
45 for req_index in active_indices:
46 encoder_seq_len = encoder_seq_lens[req_index].item()
47
48 # Calculate the number of blocks needed for this request
49 num_blocks_needed = cdiv(encoder_seq_len, block_size)
50
51 # Get the block IDs for this request from the tensor
52 req_block_ids = block_table_tensor[req_index]
53
54 # Get only the blocks we need (first num_blocks_needed blocks)
55 needed_block_ids = req_block_ids[:num_blocks_needed]
56
57 # All needed blocks are allocated
58 i_values = torch.arange(encoder_seq_len, dtype=torch.int64, device=device)
59 block_indices = i_values // block_size
60 block_offsets = i_values % block_size
61 block_numbers = needed_block_ids[block_indices]
62 slot_mapping = block_numbers * block_size + block_offsets
63
64 slot_mappings.append(slot_mapping)
65
66 if slot_mappings:
67 return torch.cat(slot_mappings)
68 else:
69 return torch.empty(0, dtype=torch.int64, device=device)
70
71
72 @functools.lru_cache
73 def create_cross_attention_backend(
74 underlying_attn_backend: type[AttentionBackend],
75 ) -> type[AttentionBackend]:
76 prefix = "CrossAttention_"
77 underlying_builder = underlying_attn_backend.get_builder_cls()
78 underlying_impl = underlying_attn_backend.get_impl_cls()
79
80 class CrossAttentionBuilder(underlying_builder): # type: ignore
81 def build(
82 self,
83 common_prefix_len: int,
84 common_attn_metadata: CommonAttentionMetadata,
85 fast_build: bool = False,
86 ) -> AttentionMetadata:
87 new_metadata = copy(common_attn_metadata)
88 new_metadata.causal = False
89 assert new_metadata.encoder_seq_lens_cpu is not None
90 max_encoder_len = int(new_metadata.encoder_seq_lens_cpu.max())
91 new_metadata.max_seq_len = max_encoder_len
92 # Any computed tokens indicates decode step>1 (no chunked prefill).
93 # The upper bound is exact for this `> 0` test - prefill rows have
94 # num_computed == 0 and decode rows have num_computed > 0.
95 query_lens_cpu = (
96 common_attn_metadata.query_start_loc_cpu[1:]
97 - common_attn_metadata.query_start_loc_cpu[:-1]
98 )
99 assert common_attn_metadata.seq_lens_cpu_upper_bound is not None
100 num_computed_tokens_cpu = (
101 common_attn_metadata.seq_lens_cpu_upper_bound - query_lens_cpu
102 )
103 num_cache_decodes = (num_computed_tokens_cpu > 0).sum().item()
104 if num_cache_decodes > 0:
105 # CrossAttn KV cache has already been populated on first decoder step,
106 # skip slot_mapping calculation for requests that do not need
107 # reshape_and_cache.
108 num_tokens = num_computed_tokens_cpu.numpy()
109 new_metadata.encoder_seq_lens_cpu = np.where(
110 num_tokens > 0, 0, new_metadata.encoder_seq_lens_cpu
111 )
112
113 # seq_lens is provided by model runner: initial encoder input length is
114 # needed here to know how many tokens to attend to from the cached
115 # cross-attention KV cache.
116 new_metadata.seq_lens = common_attn_metadata.encoder_seq_lens
117 new_metadata.seq_lens_cpu_upper_bound = torch.from_numpy(
118 common_attn_metadata.encoder_seq_lens_cpu
119 )
120
121 # NOTE (NickLucche) use `new_metadata` instead of `common_*` (initial) here
122 slot_mapping = _get_cross_slot_mapping(
123 new_metadata.encoder_seq_lens_cpu,
124 new_metadata.block_table_tensor,
125 self.kv_cache_spec,
126 self.device,
127 )
128 attn_metadata = super().build(common_prefix_len, new_metadata, fast_build)
129 attn_metadata.slot_mapping = slot_mapping # type: ignore[attr-defined]
130 return attn_metadata
131
132 # NOTE(Lucas): we need a custom impl so we can use the slot-mapping computed by
133 # `CrossAttentionBuilder` instead of the one computed by `BlockTable`
134 # (gpu_model_runner)
135 class CrossAttentionImpl(underlying_impl): # type: ignore[valid-type,misc]
136 def forward(
137 self,
138 layer: torch.nn.Module,
139 query: torch.Tensor,
140 key: torch.Tensor,
141 value: torch.Tensor,
142 kv_cache: torch.Tensor,
143 attn_metadata: AttentionMetadata,
144 output: torch.Tensor,
145 output_scale: torch.Tensor | None = None,
146 output_block_scale: torch.Tensor | None = None,
147 ) -> torch.Tensor:
148 if (
149 not underlying_attn_backend.forward_includes_kv_cache_update
150 and attn_metadata is not None
151 and layer.kv_sharing_target_layer_name is None
152 and key is not None
153 and value is not None
154 ):
155 self.do_kv_cache_update( # type: ignore[attr-defined]
156 layer,
157 key,
158 value,
159 kv_cache,
160 attn_metadata.slot_mapping, # type: ignore[attr-defined]
161 )
162
163 return super().forward(
164 layer,
165 query,
166 key,
167 value,
168 kv_cache,
169 attn_metadata,
170 output,
171 output_scale,
172 output_block_scale,
173 )
174
175 attn_backend = subclass_attention_backend_with_overrides(
176 name_prefix=prefix,
177 attention_backend_cls=underlying_attn_backend,
178 overrides={
179 "get_builder_cls": lambda: CrossAttentionBuilder,
180 "get_impl_cls": lambda: CrossAttentionImpl,
181 "forward_includes_kv_cache_update": True,
182 },
183 )
184
185 return attn_backend
186
187
188 class CrossAttention(Attention):
189 """Cross-attention for encoder-decoder models.
190 Handles attention between decoder queries and encoder keys/values.
191 """
192
193 def __init__(
194 self,
195 num_heads: int,
196 head_size: int,
197 scale: float,
198 cache_config: CacheConfig | None = None,
199 attn_type: str | None = None,
200 **kwargs,
201 ):
202 dtype = torch.get_default_dtype()
203
204 if cache_config is not None:
205 kv_cache_dtype = cache_config.cache_dtype
206 else:
207 kv_cache_dtype = "auto"
208
209 if attn_type is not None:
210 assert attn_type == AttentionType.ENCODER_DECODER, (
211 "CrossAttention only supports AttentionType.ENCODER_DECODER"
212 )
213
214 underlying_attn_backend = get_attn_backend(
215 head_size,
216 dtype,
217 kv_cache_dtype,
218 attn_type=AttentionType.ENCODER_DECODER,
219 )
220 attn_backend = create_cross_attention_backend(underlying_attn_backend)
221
222 super().__init__(
223 num_heads=num_heads,
224 head_size=head_size,
225 scale=scale,
226 cache_config=cache_config,
227 attn_backend=attn_backend,
228 attn_type=AttentionType.ENCODER_DECODER,
229 **kwargs,
230 )
231
232 def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
233 return CrossAttentionSpec(
234 block_size=vllm_config.cache_config.block_size,
235 num_kv_heads=self.num_kv_heads,
236 head_size=self.head_size,
237 dtype=self.kv_cache_torch_dtype,
238 kv_quant_mode=get_kv_quant_mode(self.kv_cache_dtype),
239 )