main
py 504 lines 16.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 # ruff: noqa: E501
4 """Shared vision attention stack for Dots dense / MoE ViT encoders.
5
6 Exports every attention backend (eager, eager_v2, sdpa, flash_attention_2,
7 flash_attention_3), RoPE helpers, and block wiring utilities.
8 """
9
10 from __future__ import annotations
11
12 import math
13 from dataclasses import dataclass
14 from typing import Any
15
16 import torch
17 import torch.nn.functional as F
18 from torch import nn
19
20 from vllm.logger import init_logger
21 from vllm.vllm_flash_attn import flash_attn_varlen_func, is_fa_version_supported
22
23 logger = init_logger(__name__)
24
25 _flash_attn_available = is_fa_version_supported(2)
26 _flash_attn3_available = is_fa_version_supported(3)
27 _flash_attn3_varlen_func = flash_attn_varlen_func
28 _flash_attn3_use_positional_args = False
29
30
31 # ---------------------------------------------------------------------------
32 # RoPE
33 # ---------------------------------------------------------------------------
34
35
36 def rotate_half(x: torch.Tensor) -> torch.Tensor:
37 x1 = x[..., : x.shape[-1] // 2]
38 x2 = x[..., x.shape[-1] // 2 :]
39 return torch.cat((-x2, x1), dim=-1)
40
41
42 VisionRotaryPositionEmbedding = tuple[torch.Tensor, torch.Tensor]
43
44
45 def prepare_rotary_pos_emb_vision(
46 freqs: torch.Tensor,
47 ) -> VisionRotaryPositionEmbedding:
48 """Materialize vision RoPE cos/sin once for every encoder layer."""
49 cos = freqs.cos().unsqueeze(1).repeat(1, 1, 2).unsqueeze(0).float()
50 sin = freqs.sin().unsqueeze(1).repeat(1, 1, 2).unsqueeze(0).float()
51 return cos, sin
52
53
54 def apply_rotary_pos_emb_vision(
55 tensor: torch.Tensor,
56 rotary_pos_emb: torch.Tensor | VisionRotaryPositionEmbedding,
57 ) -> torch.Tensor:
58 orig_dtype = tensor.dtype
59 tensor = tensor.float()
60 if isinstance(rotary_pos_emb, torch.Tensor):
61 cos, sin = prepare_rotary_pos_emb_vision(rotary_pos_emb)
62 else:
63 cos, sin = rotary_pos_emb
64 output = (tensor * cos) + (rotate_half(tensor) * sin)
65 return output.to(orig_dtype)
66
67
68 class VisionRotaryEmbedding(nn.Module):
69 """2D vision RoPE frequency table.
70
71 When ``cache_seq_len`` is set, precomputes and reuses frequencies up to that
72 length (MoE default). Otherwise frequencies are computed on each forward
73 (dense default).
74 """
75
76 def __init__(
77 self,
78 dim: int,
79 theta: float = 10000.0,
80 cache_seq_len: int | None = None,
81 ) -> None:
82 super().__init__()
83 inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
84 self.register_buffer("inv_freq", inv_freq, persistent=False)
85 self._cache_seq_len = cache_seq_len
86 self.freqs_cache: torch.Tensor
87 if cache_seq_len is not None:
88 self.register_buffer(
89 "freqs_cache",
90 self._compute_freqs(cache_seq_len),
91 persistent=False,
92 )
93
94 def _compute_freqs(self, seqlen: int) -> torch.Tensor:
95 seq = torch.arange(
96 seqlen, device=self.inv_freq.device, dtype=self.inv_freq.dtype
97 )
98 return torch.outer(seq, self.inv_freq)
99
100 def forward(self, seqlen: int) -> torch.Tensor:
101 if self._cache_seq_len is None:
102 return self._compute_freqs(seqlen)
103 if seqlen > self.freqs_cache.shape[0]:
104 self.freqs_cache = self._compute_freqs(seqlen)
105 return self.freqs_cache[:seqlen]
106
107
108 # ---------------------------------------------------------------------------
109 # Attention modules
110 # ---------------------------------------------------------------------------
111
112
113 class VisionRMSNorm(nn.Module):
114 """Dots ViT RMSNorm with an fp32 reduction and input-dtype output."""
115
116 def __init__(self, dim: int, eps: float = 1e-6):
117 super().__init__()
118 self.weight = nn.Parameter(torch.ones(dim))
119 self.eps = eps
120
121 def forward(self, x: torch.Tensor) -> torch.Tensor:
122 output = self._norm(x.float()).type_as(x)
123 return output * self.weight
124
125 def extra_repr(self) -> str:
126 return f"{tuple(self.weight.shape)}, eps={self.eps}"
127
128 def _norm(self, x: torch.Tensor) -> torch.Tensor:
129 return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
130
131
132 @dataclass(frozen=True)
133 class VisionAttentionParams:
134 dim: int
135 num_heads: int
136 bias: bool = True
137 is_causal: bool = False
138 rms_norm_eps: float = 1e-5
139 use_qk_norm: bool = False
140
141
142 def vision_attention_params_from_config(config: Any) -> VisionAttentionParams:
143 return VisionAttentionParams(
144 dim=config.embed_dim,
145 num_heads=config.num_attention_heads,
146 bias=getattr(config, "use_bias", True),
147 is_causal=config.is_causal,
148 rms_norm_eps=config.rms_norm_eps,
149 use_qk_norm=getattr(config, "use_qk_norm", False),
150 )
151
152
153 class _VisionAttentionBase(nn.Module):
154 """QKV projection, optional Q/K norm, and output projection."""
155
156 def __init__(self, params: VisionAttentionParams) -> None:
157 super().__init__()
158 self.num_heads = params.num_heads
159 self.head_dim = params.dim // params.num_heads
160 self.is_causal = params.is_causal
161 self.use_qk_norm = params.use_qk_norm
162 self.qkv = nn.Linear(params.dim, params.dim * 3, bias=params.bias)
163 self.proj = nn.Linear(params.dim, params.dim, bias=params.bias)
164 if self.use_qk_norm:
165 self.q_norm = VisionRMSNorm(self.head_dim, eps=params.rms_norm_eps)
166 self.k_norm = VisionRMSNorm(self.head_dim, eps=params.rms_norm_eps)
167
168 def _qkv_with_rope(
169 self,
170 hidden_states: torch.Tensor,
171 rotary_pos_emb: torch.Tensor | VisionRotaryPositionEmbedding | None,
172 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
173 seq_length = hidden_states.shape[0]
174 q, k, v = (
175 self.qkv(hidden_states)
176 .reshape(seq_length, 3, self.num_heads, -1)
177 .permute(1, 0, 2, 3)
178 .unbind(0)
179 )
180 if self.use_qk_norm:
181 q = self.q_norm(q)
182 k = self.k_norm(k)
183 if rotary_pos_emb is None:
184 raise ValueError("Vision rotary position embedding is required")
185 q = apply_rotary_pos_emb_vision(q.unsqueeze(0), rotary_pos_emb).squeeze(0)
186 k = apply_rotary_pos_emb_vision(k.unsqueeze(0), rotary_pos_emb).squeeze(0)
187 return q, k, v
188
189
190 class VisionAttention(_VisionAttentionBase):
191 """Eager attention with a dense block-diagonal mask (cu_seqlens boundaries)."""
192
193 def forward(
194 self,
195 hidden_states: torch.Tensor,
196 cu_seqlens: torch.Tensor,
197 max_seqlen: int,
198 rotary_pos_emb: torch.Tensor | VisionRotaryPositionEmbedding | None = None,
199 seqlens: list[int] | None = None,
200 ) -> torch.Tensor:
201 seq_length = hidden_states.shape[0]
202 q, k, v = self._qkv_with_rope(hidden_states, rotary_pos_emb)
203
204 attention_mask = torch.full(
205 [1, seq_length, seq_length],
206 torch.finfo(q.dtype).min,
207 device=q.device,
208 dtype=q.dtype,
209 )
210 for i in range(1, len(cu_seqlens)):
211 attention_mask[
212 ...,
213 cu_seqlens[i - 1] : cu_seqlens[i],
214 cu_seqlens[i - 1] : cu_seqlens[i],
215 ] = 0
216
217 q = q.transpose(0, 1)
218 k = k.transpose(0, 1)
219 v = v.transpose(0, 1)
220 attn_weights = torch.matmul(q, k.transpose(1, 2)) / math.sqrt(self.head_dim)
221 attn_weights = attn_weights + attention_mask
222 attn_weights = F.softmax(attn_weights, dim=-1, dtype=torch.float32).to(q.dtype)
223 attn_output = torch.matmul(attn_weights, v)
224 attn_output = attn_output.transpose(0, 1).reshape(seq_length, -1)
225 return self.proj(attn_output)
226
227
228 class VisionAttentionV2(_VisionAttentionBase):
229 """Eager attention per varlen segment (lower peak memory than :class:`VisionAttention`)."""
230
231 def forward(
232 self,
233 hidden_states: torch.Tensor,
234 cu_seqlens: torch.Tensor,
235 max_seqlen: int,
236 rotary_pos_emb: torch.Tensor | VisionRotaryPositionEmbedding | None = None,
237 seqlens: list[int] | None = None,
238 ) -> torch.Tensor:
239 seq_length = hidden_states.shape[0]
240 q, k, v = self._qkv_with_rope(hidden_states, rotary_pos_emb)
241
242 if seqlens is None:
243 seqlens = torch.diff(cu_seqlens).tolist()
244
245 outputs = []
246 for q_i, k_i, v_i in zip(
247 torch.split(q, seqlens, 0),
248 torch.split(k, seqlens, 0),
249 torch.split(v, seqlens, 0),
250 ):
251 q_i = q_i.transpose(0, 1)
252 k_i = k_i.transpose(0, 1)
253 v_i = v_i.transpose(0, 1)
254 out = torch.matmul(q_i, k_i.transpose(1, 2)) / math.sqrt(self.head_dim)
255 out = F.softmax(out, dim=-1, dtype=torch.float32).to(q.dtype)
256 out = torch.matmul(out, v_i).transpose(0, 1)
257 outputs.append(out)
258
259 attn_output = torch.concat(outputs, dim=0).reshape(seq_length, -1)
260 return self.proj(attn_output)
261
262
263 # MoE checkpoints historically named this implementation ``VisionEagerAttention``.
264 VisionEagerAttention = VisionAttentionV2
265
266
267 class VisionFlashAttention2(_VisionAttentionBase):
268 def forward(
269 self,
270 hidden_states: torch.Tensor,
271 cu_seqlens: torch.Tensor,
272 max_seqlen: int,
273 rotary_pos_emb: torch.Tensor | VisionRotaryPositionEmbedding | None = None,
274 seqlens: list[int] | None = None,
275 ) -> torch.Tensor:
276 seq_length = hidden_states.shape[0]
277 q, k, v = self._qkv_with_rope(hidden_states, rotary_pos_emb)
278 attn_output = flash_attn_varlen_func(
279 q,
280 k,
281 v,
282 cu_seqlens_q=cu_seqlens,
283 cu_seqlens_k=cu_seqlens,
284 max_seqlen_q=max_seqlen,
285 max_seqlen_k=max_seqlen,
286 causal=self.is_causal,
287 fa_version=2,
288 ).reshape(seq_length, -1)
289 return self.proj(attn_output)
290
291
292 class VisionFlashAttention3(_VisionAttentionBase):
293 def forward(
294 self,
295 hidden_states: torch.Tensor,
296 cu_seqlens: torch.Tensor,
297 max_seqlen: int,
298 rotary_pos_emb: torch.Tensor | VisionRotaryPositionEmbedding | None = None,
299 seqlens: list[int] | None = None,
300 ) -> torch.Tensor:
301 seq_length = hidden_states.shape[0]
302 q, k, v = self._qkv_with_rope(hidden_states, rotary_pos_emb)
303 if _flash_attn3_use_positional_args:
304 attn_output = _flash_attn3_varlen_func(
305 q,
306 k,
307 v,
308 cu_seqlens,
309 cu_seqlens,
310 max_seqlen,
311 max_seqlen,
312 causal=self.is_causal,
313 )
314 else:
315 result = _flash_attn3_varlen_func(
316 q,
317 k,
318 v,
319 cu_seqlens_q=cu_seqlens,
320 cu_seqlens_k=cu_seqlens,
321 max_seqlen_q=max_seqlen,
322 max_seqlen_k=max_seqlen,
323 causal=self.is_causal,
324 fa_version=3,
325 )
326 attn_output = result[0] if isinstance(result, tuple) else result
327 attn_output = attn_output.reshape(seq_length, -1)
328 return self.proj(attn_output)
329
330
331 class VisionSdpaAttention(_VisionAttentionBase):
332 def forward(
333 self,
334 hidden_states: torch.Tensor,
335 cu_seqlens: torch.Tensor,
336 max_seqlen: int,
337 rotary_pos_emb: torch.Tensor | VisionRotaryPositionEmbedding | None = None,
338 seqlens: list[int] | None = None,
339 ) -> torch.Tensor:
340 seq_length = hidden_states.shape[0]
341 q, k, v = self._qkv_with_rope(hidden_states, rotary_pos_emb)
342
343 attention_mask = torch.zeros(
344 [1, seq_length, seq_length], device=q.device, dtype=torch.bool
345 )
346 for i in range(1, len(cu_seqlens)):
347 attention_mask[
348 ...,
349 cu_seqlens[i - 1] : cu_seqlens[i],
350 cu_seqlens[i - 1] : cu_seqlens[i],
351 ] = True
352
353 q = q.transpose(0, 1).unsqueeze(0)
354 k = k.transpose(0, 1).unsqueeze(0)
355 v = v.transpose(0, 1).unsqueeze(0)
356
357 if attention_mask.stride(-1) != 1:
358 attention_mask = torch.empty_like(
359 attention_mask, memory_format=torch.contiguous_format
360 ).copy_(attention_mask)
361
362 from torch.nn.attention import SDPBackend, sdpa_kernel
363
364 with sdpa_kernel(SDPBackend.EFFICIENT_ATTENTION):
365 attn_output = F.scaled_dot_product_attention(
366 q, k, v, attention_mask, dropout_p=0.0
367 )
368
369 attn_output = attn_output.squeeze(0).transpose(0, 1).reshape(seq_length, -1)
370 return self.proj(attn_output)
371
372
373 DOTS_VISION_ATTENTION_CLASSES: dict[str, type[nn.Module]] = {
374 "eager": VisionAttention,
375 "eager_v2": VisionAttentionV2,
376 "flash_attention_2": VisionFlashAttention2,
377 "flash_attention_3": VisionFlashAttention3,
378 "sdpa": VisionSdpaAttention,
379 }
380
381
382 def resolve_attn_implementation(
383 attn_implementation: str,
384 *,
385 eager_fallback: str = "eager",
386 ) -> str:
387 """Apply FA3 → FA2 → eager fallback when backends are missing.
388
389 ``eager_fallback`` selects which eager backend is used when flash-attn is
390 unavailable. MoE historically used per-segment eager (``eager_v2``); dense
391 uses full-mask ``eager``.
392 """
393 if attn_implementation == "flash_attention_3" and not _flash_attn3_available:
394 logger.warning_once(
395 "flash attention 3 not available! fallback to flash attention 2 implementation"
396 )
397 attn_implementation = "flash_attention_2"
398 if attn_implementation == "flash_attention_2" and not _flash_attn_available:
399 logger.warning_once(
400 "flash attention 2 not available! fallback to %s implementation",
401 eager_fallback,
402 )
403 attn_implementation = eager_fallback
404 return attn_implementation
405
406
407 def build_vision_attention(
408 attn_implementation: str,
409 config: Any,
410 *,
411 dim: int | None = None,
412 num_heads: int | None = None,
413 bias: bool | None = None,
414 eager_fallback: str = "eager",
415 ) -> nn.Module:
416 """Instantiate a vision attention module from ``attn_implementation`` and config."""
417 attn_implementation = resolve_attn_implementation(
418 attn_implementation, eager_fallback=eager_fallback
419 )
420 if attn_implementation not in DOTS_VISION_ATTENTION_CLASSES:
421 raise ValueError(
422 f"Unknown attn_implementation {attn_implementation!r}; "
423 f"expected one of {sorted(DOTS_VISION_ATTENTION_CLASSES)}"
424 )
425 if dim is None:
426 params = vision_attention_params_from_config(config)
427 else:
428 params = VisionAttentionParams(
429 dim=dim,
430 num_heads=num_heads
431 if num_heads is not None
432 else config.num_attention_heads,
433 bias=bias if bias is not None else getattr(config, "use_bias", True),
434 is_causal=config.is_causal,
435 rms_norm_eps=config.rms_norm_eps,
436 use_qk_norm=getattr(config, "use_qk_norm", False),
437 )
438 return DOTS_VISION_ATTENTION_CLASSES[attn_implementation](params)
439
440
441 def attn_uses_seqlens(attn_implementation: str) -> bool:
442 return attn_implementation == "eager_v2"
443
444
445 def prepare_seqlens_for_attention(
446 attn_implementation: str,
447 cu_seqlens: torch.Tensor,
448 *,
449 eager_fallback: str = "eager",
450 ) -> list[int] | None:
451 """Return per-segment lengths when the resolved backend needs ``seqlens``."""
452 resolved = resolve_attn_implementation(
453 attn_implementation, eager_fallback=eager_fallback
454 )
455 if attn_uses_seqlens(resolved):
456 return torch.diff(cu_seqlens).tolist()
457 return None
458
459
460 def apply_vision_attention_residual(
461 attn: nn.Module,
462 norm: nn.Module,
463 hidden_states: torch.Tensor,
464 cu_seqlens: torch.Tensor,
465 max_seqlen: int,
466 rotary_pos_emb: torch.Tensor | VisionRotaryPositionEmbedding,
467 *,
468 seqlens: list[int] | None = None,
469 uses_seqlens: bool = False,
470 ) -> torch.Tensor:
471 """Pre-norm residual attention used by dense / MoE vision blocks."""
472 attn_kwargs: dict[str, Any] = {"rotary_pos_emb": rotary_pos_emb}
473 if uses_seqlens:
474 attn_kwargs["seqlens"] = seqlens
475 return hidden_states + attn(
476 norm(hidden_states),
477 cu_seqlens,
478 max_seqlen,
479 **attn_kwargs,
480 )
481
482
483 __all__ = [
484 "DOTS_VISION_ATTENTION_CLASSES",
485 "VisionAttention",
486 "VisionAttentionParams",
487 "VisionAttentionV2",
488 "VisionEagerAttention",
489 "VisionFlashAttention2",
490 "VisionFlashAttention3",
491 "VisionRotaryEmbedding",
492 "VisionRotaryPositionEmbedding",
493 "VisionRMSNorm",
494 "VisionSdpaAttention",
495 "apply_rotary_pos_emb_vision",
496 "apply_vision_attention_residual",
497 "attn_uses_seqlens",
498 "build_vision_attention",
499 "prepare_rotary_pos_emb_vision",
500 "prepare_seqlens_for_attention",
501 "resolve_attn_implementation",
502 "rotate_half",
503 "vision_attention_params_from_config",
504 ]