main
py 380 lines 13.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
5 import torch
6
7 from vllm.config import CacheConfig, ModelConfig, get_current_vllm_config
8 from vllm.distributed import get_tensor_model_parallel_world_size
9 from vllm.forward_context import ForwardContext, get_forward_context
10 from vllm.model_executor.custom_op import PluggableLayer
11 from vllm.model_executor.layers.linear import (
12 ColumnParallelLinear,
13 MergedColumnParallelLinear,
14 RowParallelLinear,
15 )
16 from vllm.model_executor.layers.mamba.abstract import MambaBase
17 from vllm.model_executor.layers.mamba.mamba_utils import (
18 MambaStateDtypeCalculator,
19 MambaStateShapeCalculator,
20 is_conv_state_dim_first,
21 )
22 from vllm.model_executor.layers.mamba.ops.causal_conv1d import (
23 causal_conv1d_fn,
24 causal_conv1d_update,
25 )
26 from vllm.model_executor.layers.quantization import QuantizationConfig
27 from vllm.platforms import current_platform
28 from vllm.utils.torch_utils import direct_register_custom_op
29 from vllm.v1.attention.backend import AttentionMetadata
30 from vllm.v1.attention.backends.registry import MambaAttentionBackendEnum
31 from vllm.v1.attention.backends.short_conv_attn import ShortConvAttentionMetadata
32
33
34 # --8<-- [start:short_conv]
35 @PluggableLayer.register("short_conv")
36 class ShortConv(MambaBase, PluggableLayer):
37 # --8<-- [end:short_conv]
38
39 def __init__(
40 self,
41 config,
42 dim: int,
43 layer_idx: int,
44 model_config: ModelConfig | None = None,
45 cache_config: CacheConfig | None = None,
46 quant_config: QuantizationConfig | None = None,
47 prefix: str = "",
48 ):
49 super().__init__()
50 self.config = config
51 self.layer_idx = layer_idx
52 self.conv_dim = dim
53 self.L_cache = config.conv_L_cache
54 self.bias = config.conv_bias
55
56 self.conv = ColumnParallelLinear(
57 input_size=self.L_cache,
58 output_size=dim,
59 bias=self.bias,
60 prefix=f"{prefix}.conv1d",
61 )
62 # unsqueeze to fit conv1d weights shape into the linear weights shape.
63 # Can't do this in `weight_loader` since it already exists in
64 # `ColumnParallelLinear` and `set_weight_attrs`
65 # doesn't allow to override it
66 self.conv.weight.data = self.conv.weight.data.unsqueeze(1)
67
68 self.in_proj = MergedColumnParallelLinear(
69 input_size=dim,
70 output_sizes=[dim] * 3,
71 bias=self.bias,
72 quant_config=quant_config,
73 prefix=f"{prefix}.in_proj",
74 )
75 self.out_proj = RowParallelLinear(
76 input_size=dim,
77 output_size=dim,
78 bias=self.bias,
79 quant_config=quant_config,
80 prefix=f"{prefix}.out_proj",
81 )
82
83 vllm_config = get_current_vllm_config()
84 self.num_spec = vllm_config.num_speculative_tokens
85 compilation_config = vllm_config.compilation_config
86 if prefix in compilation_config.static_forward_context:
87 raise ValueError(f"Duplicate layer name: {prefix}")
88 compilation_config.static_forward_context[prefix] = self
89 self.kv_cache = (torch.tensor([]),)
90
91 self.model_config = model_config
92 self.cache_config = cache_config
93 self.prefix = prefix
94
95 def forward_native(
96 self,
97 hidden_states: torch.Tensor,
98 output: torch.Tensor,
99 ):
100 # Reference torch causal conv1d; runs on all CPU platforms. AMX kernels
101 # for causal conv can be plugged in here later.
102 from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
103 causal_conv1d_fn_cpu as causal_conv1d_torch,
104 )
105 from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
106 causal_conv1d_update_cpu,
107 causal_conv1d_update_torch,
108 )
109 from vllm.platforms import CpuArchEnum, current_platform
110
111 forward_context = get_forward_context()
112 attn_metadata_raw = forward_context.attn_metadata
113 attn_metadata: AttentionMetadata | None = None
114 if attn_metadata_raw is not None:
115 assert isinstance(attn_metadata_raw, dict)
116 attn_metadata = attn_metadata_raw.get(self.prefix)
117 assert attn_metadata is None or isinstance(
118 attn_metadata, ShortConvAttentionMetadata
119 )
120
121 BCx, _ = self.in_proj(hidden_states)
122 B, C, x = BCx.chunk(3, dim=-1)
123
124 # (dim, kernel_size) — same reshape as forward_cuda
125 conv_weights = self.conv.weight.view(
126 self.conv.weight.size(0), self.conv.weight.size(2)
127 )
128
129 if attn_metadata is None:
130 # Profile run — output value doesn't matter
131 Bx = (B * x).contiguous()
132 output_tensor, _ = self.out_proj(C * Bx)
133 output[: hidden_states.shape[0]] = output_tensor
134 return
135
136 conv_state = (
137 self.kv_cache[0]
138 if is_conv_state_dim_first()
139 else self.kv_cache[0].transpose(-1, -2)
140 ) # (num_blocks, dim, state_len)
141
142 num_prefills = attn_metadata.num_prefills
143 num_decodes = attn_metadata.num_decode_tokens
144 num_prefill_tokens = attn_metadata.num_prefill_tokens
145 has_prefill = num_prefills > 0
146 has_decode = num_decodes > 0
147 num_actual_tokens = num_decodes + num_prefill_tokens
148
149 B_d, B_p = torch.split(
150 B[:num_actual_tokens], [num_decodes, num_prefill_tokens], dim=0
151 )
152 C_d, C_p = torch.split(
153 C[:num_actual_tokens], [num_decodes, num_prefill_tokens], dim=0
154 )
155 x_d, x_p = torch.split(
156 x[:num_actual_tokens], [num_decodes, num_prefill_tokens], dim=0
157 )
158
159 conv_output_list = []
160
161 if has_prefill:
162 assert attn_metadata.state_indices_tensor_p is not None
163 Bx_p = (B_p * x_p).transpose(0, 1) # (dim, num_prefill_tokens)
164 out_p = causal_conv1d_torch(
165 Bx_p,
166 conv_weights,
167 self.conv.bias,
168 conv_state,
169 attn_metadata.query_start_loc_p,
170 attn_metadata.state_indices_tensor_p.flatten(),
171 attn_metadata.has_initial_states_p,
172 activation=None,
173 ).transpose(0, 1)[:num_prefill_tokens] # (num_prefill_tokens, dim)
174 conv_output_list.append(C_p * out_p)
175
176 if has_decode:
177 assert attn_metadata.state_indices_tensor_d is not None
178 state_indices_d = attn_metadata.state_indices_tensor_d.flatten()
179 Bx_d = B_d * x_d # (num_decodes, dim)
180 if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
181 conv_state_view = conv_state[state_indices_d].contiguous()
182 out_d = causal_conv1d_update_torch(
183 Bx_d.unsqueeze(-1),
184 conv_state_view,
185 conv_weights,
186 self.conv.bias,
187 activation=None,
188 ).squeeze(-1)
189 conv_state[state_indices_d] = conv_state_view
190 else:
191 out_d = causal_conv1d_update_cpu(
192 Bx_d,
193 conv_state,
194 conv_weights,
195 self.conv.bias,
196 activation=None,
197 conv_state_indices=state_indices_d,
198 )
199 conv_output_list.insert(0, C_d * out_d)
200
201 hidden_states_out = torch.vstack(conv_output_list)
202 output[:num_actual_tokens], _ = self.out_proj(hidden_states_out)
203
204 def forward(
205 self,
206 hidden_states: torch.Tensor,
207 output: torch.Tensor,
208 ):
209 torch.ops.vllm.short_conv(
210 hidden_states,
211 output,
212 self.prefix,
213 )
214
215 def forward_cuda(
216 self,
217 hidden_states: torch.Tensor,
218 output: torch.Tensor,
219 ):
220 forward_context = get_forward_context()
221 # ShortConvAttentionMetadata contains metadata necessary for the
222 # short_conv triton kernels to operate in continuous batching and in
223 # chunked prefill modes; they are computed at top-level model forward
224 # since they stay the same and reused for all mamba layers in the same
225 # iteration.
226 attn_metadata_raw = forward_context.attn_metadata
227 attn_metadata: AttentionMetadata | None = None
228 if attn_metadata_raw is not None:
229 assert isinstance(attn_metadata_raw, dict)
230 attn_metadata = attn_metadata_raw.get(self.prefix)
231 if attn_metadata is not None:
232 assert isinstance(attn_metadata, ShortConvAttentionMetadata)
233 conv_state = (
234 self.kv_cache[0]
235 if is_conv_state_dim_first()
236 else self.kv_cache[0].transpose(-1, -2)
237 )
238 state_indices_tensor_p = attn_metadata.state_indices_tensor_p
239 state_indices_tensor_d = attn_metadata.state_indices_tensor_d
240 has_initial_states_p = attn_metadata.has_initial_states_p
241 query_start_loc_p = attn_metadata.query_start_loc_p
242 num_accepted_tokens = attn_metadata.num_accepted_tokens
243 query_start_loc_d = attn_metadata.query_start_loc_d
244
245 BCx, _ = self.in_proj(hidden_states)
246
247 B, C, x = BCx.chunk(3, dim=-1)
248
249 conv_weights = self.conv.weight.view(
250 self.conv.weight.size(0), self.conv.weight.size(2)
251 )
252
253 if attn_metadata is None:
254 # V1 profile run
255 Bx = (B * x).contiguous()
256 hidden_states = C * Bx
257 contextualized_states, _ = self.out_proj(hidden_states)
258 return contextualized_states
259
260 num_prefills = attn_metadata.num_prefills # request count
261 num_decodes = attn_metadata.num_decode_tokens # token count (=request)
262 num_prefill_tokens = attn_metadata.num_prefill_tokens # token count
263 has_prefill = num_prefills > 0
264 has_decode = num_decodes > 0
265 num_actual_tokens = num_decodes + num_prefill_tokens
266
267 # NOTE: V1 puts decode before prefill
268 # Separate prefill and decode by splitting varlen input
269 # Split along token dimension
270 B_d, B_p = torch.split(
271 B[:num_actual_tokens],
272 [num_decodes, num_prefill_tokens],
273 dim=0,
274 )
275 C_d, C_p = torch.split(
276 C[:num_actual_tokens],
277 [num_decodes, num_prefill_tokens],
278 dim=0,
279 )
280 x_d, x_p = torch.split(
281 x[:num_actual_tokens],
282 [num_decodes, num_prefill_tokens],
283 dim=0,
284 )
285 conv_output_list = []
286
287 if has_prefill:
288 Bx_p = (B_p * x_p).transpose(0, 1)
289 Bx = causal_conv1d_fn(
290 Bx_p,
291 conv_weights,
292 self.conv.bias,
293 activation=None,
294 conv_states=conv_state,
295 has_initial_state=has_initial_states_p,
296 cache_indices=state_indices_tensor_p,
297 metadata=attn_metadata,
298 query_start_loc=query_start_loc_p,
299 ).transpose(0, 1)[:num_prefill_tokens]
300
301 y = C_p * Bx
302 conv_output_list.append(y)
303
304 if has_decode:
305 Bx_d = (B_d * x_d).contiguous()
306 if num_accepted_tokens is not None:
307 # Speculative decode: the verify step feeds >1 query token per
308 # decode request, so use the spec-aware conv update path
309 # (mirrors mamba_mixer2). state_indices_tensor_d is
310 # (num_decodes, 1 + num_spec_tokens) here.
311 assert state_indices_tensor_d is not None
312 Bx = causal_conv1d_update(
313 Bx_d,
314 conv_state,
315 conv_weights,
316 self.conv.bias,
317 activation=None,
318 conv_state_indices=state_indices_tensor_d,
319 num_accepted_tokens=num_accepted_tokens,
320 query_start_loc=query_start_loc_d,
321 max_query_len=state_indices_tensor_d.size(-1),
322 )
323 else:
324 # Non-spec decode
325 Bx = causal_conv1d_update(
326 Bx_d,
327 conv_state,
328 conv_weights,
329 self.conv.bias,
330 activation=None,
331 conv_state_indices=state_indices_tensor_d,
332 )
333 y = C_d * Bx
334 conv_output_list.insert(0, y)
335
336 # Merge prefill and decode outputs before passing to gated MLP
337 hidden_states = torch.vstack(conv_output_list)
338
339 # Final linear projection
340 output[:num_actual_tokens], _ = self.out_proj(hidden_states)
341
342 def get_state_dtype(self) -> tuple[torch.dtype, ...]:
343 assert self.model_config is not None
344 assert self.cache_config is not None
345 return MambaStateDtypeCalculator.short_conv_state_dtype(
346 self.model_config.dtype,
347 self.cache_config.mamba_cache_dtype,
348 )
349
350 def get_state_shape(self) -> tuple[tuple[int, ...]]:
351 return MambaStateShapeCalculator.short_conv_state_shape(
352 tp_world_size=get_tensor_model_parallel_world_size(),
353 intermediate_size=self.conv_dim,
354 conv_kernel=self.L_cache,
355 num_spec=self.num_spec,
356 )
357
358 @property
359 def mamba_type(self) -> MambaAttentionBackendEnum:
360 return MambaAttentionBackendEnum.SHORT_CONV
361
362
363 def short_conv(
364 hidden_states: torch.Tensor,
365 output: torch.Tensor,
366 layer_name: str,
367 ) -> None:
368 forward_context: ForwardContext = get_forward_context()
369 self = forward_context.no_compile_layers[layer_name]
370 if not current_platform.is_cpu():
371 self.forward_cuda(hidden_states=hidden_states, output=output)
372 else:
373 self.forward_native(hidden_states=hidden_states, output=output)
374
375
376 direct_register_custom_op(
377 op_name="short_conv",
378 op_func=short_conv,
379 mutates_args=["output"],
380 )