main
py 842 lines 30.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import functools
5 import json
6
7 import numpy as np
8 import torch
9
10 from vllm.config import MultiModalConfig
11 from vllm.kernels.triton.qkv_padded_fp8_quant import (
12 quantize_fp8_maybe_pad_head_dim,
13 )
14 from vllm.logger import init_logger
15 from vllm.model_executor.custom_op import CustomOp, maybe_get_oot_by_class
16 from vllm.model_executor.layers.quantization.input_quant_fp8 import (
17 QuantFP8,
18 )
19 from vllm.model_executor.layers.quantization.utils.quant_utils import (
20 GroupShape,
21 get_fp8_min_max,
22 )
23 from vllm.model_executor.models.vision import (
24 get_multimodal_config,
25 get_vit_attn_backend,
26 )
27 from vllm.platforms import current_platform
28 from vllm.utils.flashinfer import (
29 is_flashinfer_cudnn_fp8_prefill_attn_supported,
30 )
31 from vllm.utils.math_utils import round_up
32 from vllm.utils.torch_utils import async_tensor_h2d
33 from vllm.v1.attention.backends.fa_utils import get_flash_attn_version
34 from vllm.v1.attention.backends.registry import AttentionBackendEnum
35 from vllm.v1.attention.ops.vit_attn_wrappers import (
36 vit_aiter_fp8_attn_wrapper,
37 vit_flash_attn_wrapper,
38 vit_flashinfer_wrapper,
39 vit_torch_sdpa_wrapper,
40 vit_triton_attn_wrapper,
41 )
42
43 logger = init_logger(__name__)
44
45 _, _FP8_MAX = get_fp8_min_max()
46 _FP8_AMAX_HISTORY_LEN = 16
47
48 # Module-level state for auto-saving dynamic scales. The save is a one-shot
49 # triggered by the first layer whose amax buffer wraps. Path and margin are
50 # captured during layer init (set_current_vllm_config context only lives
51 # across model init, not forward passes).
52 _fp8_scale_save_path: str | None = None
53 _fp8_scale_save_margin: float = MultiModalConfig.mm_encoder_fp8_scale_save_margin
54 _fp8_saved_scale_refs: dict[str, tuple[torch.Tensor, torch.Tensor, torch.Tensor]] = {}
55
56
57 @functools.cache
58 def _load_fp8_scales_file(path: str | None) -> dict[str, dict[str, float]]:
59 """Load per-layer FP8 Q/K/V scales from a JSON file. Results are cached.
60
61 Expected format (keys ``q_scale`` / ``k_scale`` / ``v_scale`` also accepted)::
62
63 {
64 "visual.blocks.0.attn.attn": {"q": 224.0, "k": 198.0, "v": 210.0},
65 "visual.blocks.1.attn.attn": {"q": 218.0, "k": 195.0, "v": 207.0},
66 }
67
68 To produce such a file, run with ``mm_encoder_fp8_scale_save_path`` set.
69 """
70 if path is None:
71 return {}
72
73 with open(path, encoding="utf-8") as f:
74 data = json.load(f)
75
76 # Handle nested "layers" format
77 if "layers" in data and isinstance(data["layers"], dict):
78 data = data["layers"]
79
80 scales: dict[str, dict[str, float]] = {}
81 for layer_name, layer_scales in data.items():
82 if not isinstance(layer_scales, dict):
83 continue
84 q = layer_scales.get("q", layer_scales.get("q_scale"))
85 k = layer_scales.get("k", layer_scales.get("k_scale"))
86 v = layer_scales.get("v", layer_scales.get("v_scale"))
87 if q is not None and k is not None and v is not None:
88 q_f, k_f, v_f = float(q), float(k), float(v)
89 if q_f <= 0 or k_f <= 0 or v_f <= 0:
90 raise ValueError(
91 f"FP8 scales must be positive, got q={q_f}, "
92 f"k={k_f}, v={v_f} for layer '{layer_name}'"
93 )
94 scales[layer_name] = {"q": q_f, "k": k_f, "v": v_f}
95
96 logger.info_once(
97 "Loaded FP8 attention scales from %s (%d layers)", path, len(scales)
98 )
99 return scales
100
101
102 def _maybe_save_fp8_scales(
103 layer_name: str,
104 q_scale: torch.Tensor,
105 k_scale: torch.Tensor,
106 v_scale: torch.Tensor,
107 buffer_wrapped: bool,
108 ) -> None:
109 """Accumulate a layer's scale tensors; on the first amax buffer wrap,
110 dump all accumulated scales to ``mm_encoder_fp8_scale_save_path``.
111
112 No-op unless auto-save is configured. Tensor references are stored on
113 every call (no GPU->CPU sync); ``.item()`` is only called at the single
114 save point to avoid stalling the forward path.
115 """
116 global _fp8_scale_save_path
117 # Fast path: auto-save either disabled or already finished. Path is
118 # captured at layer init and cleared once the save fires.
119 if _fp8_scale_save_path is None:
120 return
121
122 # Stash scale tensor refs (no GPU->CPU sync yet); wait until the amax
123 # history has seen a full cycle before committing scales to disk.
124 _fp8_saved_scale_refs[layer_name] = (q_scale, k_scale, v_scale)
125 if not buffer_wrapped:
126 return
127
128 # Buffer just wrapped for the first time: materialize scales (with
129 # safety margin) and dump to disk. Clearing _fp8_scale_save_path
130 # makes this a one-shot across all layers.
131 path, margin = _fp8_scale_save_path, _fp8_scale_save_margin
132 scales = {
133 name: {
134 "q": q.item() * margin,
135 "k": k.item() * margin,
136 "v": v.item() * margin,
137 }
138 for name, (q, k, v) in _fp8_saved_scale_refs.items()
139 }
140 _fp8_scale_save_path = None
141 _fp8_saved_scale_refs.clear()
142 with open(path, "w", encoding="utf-8") as f:
143 json.dump(scales, f, indent=2)
144 logger.info("Saved FP8 scales (%d layers) to %s", len(scales), path)
145
146
147 # Batch buckets for cuDNN graph caching.
148 # Graphs use batch size and max sequence length as cache key.
149 # This avoids creating a new graph for each unique set of
150 # batch size and max sequence length at runtime.
151 # From the cuDNN team's performance measurements, there
152 # is no significant kernel performance difference between padding
153 # to a smaller batch size/seq length and padding to larger
154 # ones. The bucketing here is solely used to avoid memory
155 # operation overhead, which won't be needed if we have CUDA
156 # graph support in the future.
157 # TODO: Remove buckets after issue #34763
158 # (cuda graph support) is addressed.
159 FLASHINFER_BATCH_BUCKETS = [8, 16, 32, 64]
160 FLASHINFER_MAX_SEQLEN_BUCKETS = [
161 1 * 1024,
162 2 * 1024,
163 4 * 1024,
164 8 * 1024,
165 16 * 1024,
166 32 * 1024,
167 64 * 1024,
168 128 * 1024,
169 ]
170
171 # Workspace buffer for FlashInfer CuDNN backend
172 FLASHINFER_CUDNN_WORKSPACE_SIZE_BYTES = 128 * 1024 * 1024
173 _flashinfer_workspace_buffer: torch.Tensor | None = None
174
175
176 def _get_flashinfer_workspace_buffer() -> torch.Tensor:
177 global _flashinfer_workspace_buffer
178 if _flashinfer_workspace_buffer is None:
179 _flashinfer_workspace_buffer = torch.zeros(
180 FLASHINFER_CUDNN_WORKSPACE_SIZE_BYTES,
181 dtype=torch.uint8,
182 device="cuda",
183 )
184 return _flashinfer_workspace_buffer
185
186
187 def add_padding_to_seqlens(
188 seq: np.ndarray,
189 batch_size: int,
190 padding_value: int,
191 ) -> np.ndarray:
192 batch_size_padded = next(
193 (b for b in FLASHINFER_BATCH_BUCKETS if b >= batch_size),
194 round_up(batch_size, FLASHINFER_BATCH_BUCKETS[0]),
195 )
196 if batch_size_padded == batch_size:
197 return seq
198 return np.concatenate(
199 [
200 seq,
201 np.full((batch_size_padded - batch_size,), padding_value, dtype=seq.dtype),
202 ]
203 )
204
205
206 def bucket_flashinfer_max_seqlen(
207 real_max_seqlen: int,
208 ) -> int:
209 if real_max_seqlen <= 0:
210 return FLASHINFER_MAX_SEQLEN_BUCKETS[0]
211 return next(
212 (s for s in FLASHINFER_MAX_SEQLEN_BUCKETS if s >= real_max_seqlen),
213 round_up(real_max_seqlen, FLASHINFER_MAX_SEQLEN_BUCKETS[-1]),
214 )
215
216
217 # --8<-- [start:mm_encoder_attn]
218 @CustomOp.register("mm_encoder_attn")
219 class MMEncoderAttention(CustomOp):
220 """Multi-headed attention without any cache, used for multimodal encoder."""
221
222 # --8<-- [end:mm_encoder_attn]
223 @classmethod
224 def compute_max_seqlen(
225 cls,
226 attn_backend: AttentionBackendEnum,
227 cu_seqlens: np.ndarray,
228 ) -> int:
229 max_seqlen = 0
230 if (
231 attn_backend
232 in (
233 AttentionBackendEnum.FLASH_ATTN,
234 AttentionBackendEnum.ROCM_AITER_FA,
235 AttentionBackendEnum.TRITON_ATTN,
236 AttentionBackendEnum.FLASHINFER,
237 )
238 and len(cu_seqlens) >= 2
239 ):
240 max_seqlen = int((cu_seqlens[1:] - cu_seqlens[:-1]).max())
241 if attn_backend == AttentionBackendEnum.FLASHINFER:
242 max_seqlen = bucket_flashinfer_max_seqlen(max_seqlen)
243 return max_seqlen
244
245 @classmethod
246 def maybe_compute_seq_lens(
247 cls,
248 attn_backend: AttentionBackendEnum,
249 cu_seqlens: np.ndarray,
250 device: torch.device,
251 ) -> torch.Tensor | None:
252 if (oot_class := maybe_get_oot_by_class(cls)) is not cls:
253 return oot_class.maybe_compute_seq_lens(attn_backend, cu_seqlens, device) # type: ignore[attr-defined]
254
255 if attn_backend != AttentionBackendEnum.FLASHINFER:
256 return None
257
258 sequence_lengths = cu_seqlens[1:] - cu_seqlens[:-1]
259 sequence_lengths = add_padding_to_seqlens(
260 sequence_lengths, len(sequence_lengths), 0
261 )
262 return async_tensor_h2d(sequence_lengths, device=device)
263
264 @classmethod
265 def maybe_recompute_cu_seqlens(
266 cls,
267 attn_backend: AttentionBackendEnum,
268 cu_seqlens: np.ndarray,
269 hidden_size: int,
270 tp_size: int,
271 device: torch.device,
272 fp8_padded_hidden_size: int | None = None,
273 ) -> torch.Tensor:
274 if (oot_class := maybe_get_oot_by_class(cls)) is not cls:
275 return oot_class.maybe_recompute_cu_seqlens( # type: ignore[attr-defined]
276 attn_backend,
277 cu_seqlens,
278 hidden_size,
279 tp_size,
280 device,
281 fp8_padded_hidden_size=fp8_padded_hidden_size,
282 )
283
284 if attn_backend == AttentionBackendEnum.FLASHINFER:
285 batch_size = len(cu_seqlens) - 1
286
287 if fp8_padded_hidden_size is not None:
288 # FP8 path: after quantization Q/K/V are each independent
289 # contiguous tensors with stride H * padded_D per token.
290 # All sections use the same element stride.
291 scale = fp8_padded_hidden_size // tp_size
292 cu_seqlens = cu_seqlens * scale
293 cu_seqlens_padded = add_padding_to_seqlens(
294 cu_seqlens, batch_size, cu_seqlens[-1]
295 )
296 cu_seqlens = np.concatenate([cu_seqlens_padded, cu_seqlens_padded])
297 else:
298 # BF16 path: Q/K/V are non-contiguous views into shared
299 # buffers. V section has 3x stride from interleaved QKV.
300 scale = hidden_size // tp_size
301 cu_seqlens = cu_seqlens * scale
302
303 cu_seqlens_qko = cu_seqlens
304 cu_seqlens_v = cu_seqlens * 3
305
306 cu_seqlens_qko = add_padding_to_seqlens(
307 cu_seqlens_qko, batch_size, cu_seqlens_qko[-1]
308 )
309 cu_seqlens_v = add_padding_to_seqlens(
310 cu_seqlens_v, batch_size, cu_seqlens_v[-1]
311 )
312 cu_seqlens = np.concatenate([cu_seqlens_qko, cu_seqlens_v])
313
314 cu_seqlens = async_tensor_h2d(cu_seqlens, device=device)
315 return cu_seqlens
316
317 def __init__(
318 self,
319 num_heads: int,
320 head_size: int,
321 scale: float | None = None,
322 num_kv_heads: int | None = None,
323 prefix: str = "",
324 ) -> None:
325 """Args:
326 num_heads: number of attention heads per partition.
327 head_size: hidden_size per attention head.
328 scale: scale factor.
329 num_kv_heads: number of kv heads.
330 prefix: This has no effect, it is only here to make it easier to
331 swap between Attention and MultiHeadAttention
332
333 """
334 super().__init__()
335
336 self.num_heads = num_heads
337 self.head_size = head_size
338 self.scale = 1.0 / (head_size**0.5) if scale is None else scale
339 self.num_kv_heads = num_heads if num_kv_heads is None else num_kv_heads
340 self.layer_name = prefix
341 assert self.num_heads % self.num_kv_heads == 0, (
342 f"num_heads ({self.num_heads}) is not "
343 f"divisible by num_kv_heads ({self.num_kv_heads})"
344 )
345 self.num_queries_per_kv = self.num_heads // self.num_kv_heads
346
347 # During model initialization, the default dtype is set as the model
348 # weight and activation dtype.
349 dtype = torch.get_default_dtype()
350 self.dtype = dtype
351
352 # Get device-specific vision attention backend.
353 self.attn_backend = get_vit_attn_backend(
354 head_size=head_size,
355 dtype=dtype,
356 )
357
358 self.is_flash_attn_backend = self.attn_backend in {
359 AttentionBackendEnum.FLASH_ATTN,
360 AttentionBackendEnum.ROCM_AITER_FA,
361 }
362
363 self._fa_version = (
364 get_flash_attn_version(head_size=head_size)
365 if self.is_flash_attn_backend
366 else None
367 )
368
369 if self.attn_backend == AttentionBackendEnum.FLASHINFER:
370 _get_flashinfer_workspace_buffer()
371
372 logger.info_once(f"Using {self.attn_backend} for MMEncoderAttention.")
373
374 self._init_fp8_state()
375
376 def _init_fp8_state(self) -> None:
377 """Initialize FP8 attention state from multimodal config.
378
379 No-op if FP8 is not requested. Raises ``ValueError`` if FP8 is
380 requested but the platform does not support it.
381 """
382 # Populate defaults so backend forward methods can check
383 # ``self.fp8_enabled`` and related state without AttributeError.
384 self.fp8_enabled = False
385 self._fp8_dynamic_scale = False
386 self.fp8_quant: QuantFP8 | None = None
387 self.skip_scale_q = False
388 self.skip_scale_k = False
389 self.skip_scale_v = False
390
391 mm_cfg = get_multimodal_config()
392 if mm_cfg is None or mm_cfg.mm_encoder_attn_dtype != "fp8":
393 return
394
395 if self.attn_backend == AttentionBackendEnum.ROCM_AITER_FA:
396 if not current_platform.is_rocm():
397 raise ValueError("AITER FP8 ViT attention requires ROCm.")
398
399 from vllm.platforms.rocm import on_mi3xx
400
401 if not on_mi3xx():
402 raise ValueError(
403 "AITER FP8 ViT attention requires an MI300-series or "
404 "MI350-series GPU (gfx942 or gfx950)."
405 )
406 try:
407 from aiter import flash_attn_varlen_fp8_pertensor_func # noqa: F401
408 except ImportError as exc:
409 raise ValueError(
410 "mm_encoder_attn_dtype='fp8' with ROCM_AITER_FA requires "
411 "an AITER build that provides "
412 "flash_attn_varlen_fp8_pertensor_func."
413 ) from exc
414 elif self.attn_backend == AttentionBackendEnum.FLASHINFER:
415 if not is_flashinfer_cudnn_fp8_prefill_attn_supported():
416 raise ValueError(
417 "mm_encoder_attn_dtype='fp8' requires the FlashInfer "
418 "cuDNN backend with cuDNN >= 9.17.1 on Blackwell (SM 100) "
419 "or newer. cuDNN's FP8 SDPA path with bf16/fp16 output is "
420 "not available on Hopper (H100/H200) or earlier."
421 )
422 else:
423 raise ValueError(
424 "mm_encoder_attn_dtype='fp8' requires either the "
425 "ROCM_AITER_FA backend on ROCm or the FlashInfer cuDNN "
426 f"backend on CUDA, got {self.attn_backend}."
427 )
428
429 self.fp8_enabled = True
430 self._fp8_dynamic_scale = mm_cfg.mm_encoder_fp8_scale_path is None
431 self.fp8_quant = QuantFP8(static=True, group_shape=GroupShape.PER_TENSOR)
432
433 # Register buffers pre-device-move; values populated in
434 # process_weights_after_loading. Shape (1, 1, 1, 1) is required by cuDNN.
435 for attr in ("_fp8_q_scale", "_fp8_k_scale", "_fp8_v_scale"):
436 self.register_buffer(
437 attr, torch.ones(1, dtype=torch.float32).view(1, 1, 1, 1)
438 )
439 if self._fp8_dynamic_scale:
440 for attr in ("_fp8_q_amax", "_fp8_k_amax", "_fp8_v_amax"):
441 self.register_buffer(
442 attr,
443 torch.zeros(_FP8_AMAX_HISTORY_LEN, dtype=torch.float32),
444 persistent=False,
445 )
446 self._fp8_amax_pos = 0
447
448 # Capture auto-save config now: the VllmConfig context only lives
449 # across model init, not forward passes, so ``_maybe_save_fp8_scales``
450 # reads these globals instead of re-querying ``get_multimodal_config``.
451 if (
452 mm_cfg.mm_encoder_fp8_scale_save_path is not None
453 and self._fp8_dynamic_scale
454 ):
455 global _fp8_scale_save_path, _fp8_scale_save_margin
456 _fp8_scale_save_path = mm_cfg.mm_encoder_fp8_scale_save_path
457 _fp8_scale_save_margin = mm_cfg.mm_encoder_fp8_scale_save_margin
458
459 def process_weights_after_loading(self, act_dtype: torch.dtype) -> None:
460 """Populate FP8 scale buffers after weights are loaded.
461
462 ``act_dtype`` matches the signature used by :class:`Attention` and
463 :class:`MLAAttention` for the loader auto-scan but is unused:
464 FP8 scales are always float32.
465 """
466 if not self.fp8_enabled:
467 return
468
469 mm_cfg = get_multimodal_config()
470 scale_path = mm_cfg.mm_encoder_fp8_scale_path if mm_cfg is not None else None
471 if scale_path is None:
472 logger.info_once(
473 "FP8 attention enabled with dynamic scaling "
474 "(no scale file provided). Scales will adapt from "
475 "observed Q/K/V amax values (history_len=%d).",
476 _FP8_AMAX_HISTORY_LEN,
477 )
478 return
479
480 all_scales = _load_fp8_scales_file(scale_path)
481 layer_scales = all_scales.get(self.layer_name)
482 if layer_scales is None:
483 raise ValueError(
484 "FP8 attention enabled but scales not found for layer "
485 f"'{self.layer_name}' in {scale_path}. "
486 f"Available layers: {list(all_scales.keys())}"
487 )
488
489 for attr, key in (
490 ("_fp8_q_scale", "q"),
491 ("_fp8_k_scale", "k"),
492 ("_fp8_v_scale", "v"),
493 ):
494 getattr(self, attr).fill_(layer_scales[key])
495 self.skip_scale_q = layer_scales["q"] == 1.0
496 self.skip_scale_k = layer_scales["k"] == 1.0
497 self.skip_scale_v = layer_scales["v"] == 1.0
498
499 logger.debug(
500 "FP8 attention enabled for %s: q=%.4f, k=%.4f, v=%.4f",
501 self.layer_name if self.layer_name else "MMEncoderAttention",
502 layer_scales["q"],
503 layer_scales["k"],
504 layer_scales["v"],
505 )
506
507 @classmethod
508 def enabled(cls) -> bool:
509 return True
510
511 def view_qkv_to_4d(
512 self,
513 query: torch.Tensor,
514 key: torch.Tensor,
515 value: torch.Tensor,
516 bsz: int,
517 q_len: int,
518 kv_len: int,
519 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
520 """Reshape query, key, value to 4D tensors:
521 (batch_size, seq_len, num_heads, head_size)
522 """
523 query = query.view(bsz, q_len, self.num_heads, self.head_size)
524 key = key.view(bsz, kv_len, self.num_kv_heads, self.head_size)
525 value = value.view(bsz, kv_len, self.num_kv_heads, self.head_size)
526
527 return query, key, value
528
529 def _forward_sdpa(
530 self,
531 query: torch.Tensor,
532 key: torch.Tensor,
533 value: torch.Tensor,
534 cu_seqlens: torch.Tensor | None = None,
535 ) -> torch.Tensor:
536 """Input shape:
537 (batch_size x seq_len x hidden_size) or
538 (batch_size x seq_len x num_heads x head_size)
539 """
540 bsz, q_len = query.size()[:2]
541 kv_len = key.size(1)
542 is_reshaped = query.dim() != 4
543
544 query, key, value = self.view_qkv_to_4d(query, key, value, bsz, q_len, kv_len)
545
546 output = vit_torch_sdpa_wrapper(
547 q=query,
548 k=key,
549 v=value,
550 scale=self.scale,
551 cu_seqlens=cu_seqlens,
552 enable_gqa=self.num_heads > self.num_kv_heads,
553 )
554 if is_reshaped:
555 output = output.reshape(bsz, q_len, -1)
556 return output
557
558 def _forward_fa(
559 self,
560 query: torch.Tensor,
561 key: torch.Tensor,
562 value: torch.Tensor,
563 cu_seqlens: torch.Tensor | None = None,
564 max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
565 ) -> torch.Tensor:
566 """Input shape:
567 (batch_size x seq_len x hidden_size) or
568 (batch_size x seq_len x num_heads x head_size)
569 """
570 assert (cu_seqlens is not None and max_seqlen is not None) or (
571 cu_seqlens is None and max_seqlen is None
572 ), "cu_seqlens and max_seqlen should be both set or both None."
573
574 bsz, q_len = query.size()[:2]
575 kv_len = key.size(1)
576 is_reshaped = query.dim() != 4
577
578 query, key, value = self.view_qkv_to_4d(query, key, value, bsz, q_len, kv_len)
579
580 output = vit_flash_attn_wrapper(
581 q=query,
582 k=key,
583 v=value,
584 batch_size=bsz,
585 is_rocm_aiter=(self.attn_backend == AttentionBackendEnum.ROCM_AITER_FA),
586 fa_version=self._fa_version,
587 scale=self.scale,
588 cu_seqlens=cu_seqlens,
589 max_seqlen=max_seqlen,
590 )
591 if is_reshaped:
592 output = output.reshape(bsz, q_len, -1)
593 return output
594
595 def _forward_triton(
596 self,
597 query: torch.Tensor,
598 key: torch.Tensor,
599 value: torch.Tensor,
600 cu_seqlens: torch.Tensor | None = None,
601 max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
602 ) -> torch.Tensor:
603 """Input shape:
604 (batch_size x seq_len x hidden_size) or
605 (batch_size x seq_len x num_heads x head_size)
606 """
607 assert (cu_seqlens is not None and max_seqlen is not None) or (
608 cu_seqlens is None and max_seqlen is None
609 ), "cu_seqlens and max_seqlen should be both set or both None."
610
611 bsz, q_len = query.size()[:2]
612 kv_len = key.size(1)
613 is_reshaped = query.dim() != 4
614
615 query, key, value = self.view_qkv_to_4d(query, key, value, bsz, q_len, kv_len)
616
617 output = vit_triton_attn_wrapper(
618 q=query,
619 k=key,
620 v=value,
621 batch_size=bsz,
622 scale=self.scale,
623 cu_seqlens=cu_seqlens,
624 max_seqlen=max_seqlen,
625 )
626 if is_reshaped:
627 output = output.reshape(bsz, q_len, -1)
628 return output
629
630 @torch.no_grad()
631 def _record_amax_and_update_scales(
632 self,
633 query: torch.Tensor,
634 key: torch.Tensor,
635 value: torch.Tensor,
636 ) -> None:
637 """Record Q/K/V amax into circular history and recompute scales.
638
639 All work stays on GPU with no device-to-host sync. The Python-side
640 history position counter is mutated, so this method must NOT be
641 called inside CUDA graph capture/replay. When CUDA graphs are
642 used for the encoder, dynamic scaling should be disabled by
643 providing a static scale file via --mm-encoder-fp8-scale-path.
644 """
645 pos = self._fp8_amax_pos
646 self._fp8_amax_pos = (pos + 1) % _FP8_AMAX_HISTORY_LEN
647
648 for tensor, amax_buf, scale_buf in (
649 (query, self._fp8_q_amax, self._fp8_q_scale),
650 (key, self._fp8_k_amax, self._fp8_k_scale),
651 (value, self._fp8_v_amax, self._fp8_v_scale),
652 ):
653 amax_buf[pos] = tensor.amax()
654 max_amax = amax_buf.max()
655 scale_buf.fill_(
656 torch.clamp(max_amax, min=torch.finfo(torch.float32).tiny) / _FP8_MAX
657 )
658
659 buffer_wrapped = self._fp8_amax_pos == 0 and pos == _FP8_AMAX_HISTORY_LEN - 1
660 _maybe_save_fp8_scales(
661 self.layer_name,
662 self._fp8_q_scale,
663 self._fp8_k_scale,
664 self._fp8_v_scale,
665 buffer_wrapped,
666 )
667
668 def _quantize_qkv_fp8(
669 self,
670 query: torch.Tensor,
671 key: torch.Tensor,
672 value: torch.Tensor,
673 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
674 assert self.fp8_quant is not None
675
676 if self._fp8_dynamic_scale:
677 self._record_amax_and_update_scales(query, key, value)
678
679 query = quantize_fp8_maybe_pad_head_dim(
680 query,
681 self._fp8_q_scale,
682 skip_scale=self.skip_scale_q,
683 fp8_quant=self.fp8_quant,
684 )
685 key = quantize_fp8_maybe_pad_head_dim(
686 key,
687 self._fp8_k_scale,
688 skip_scale=self.skip_scale_k,
689 fp8_quant=self.fp8_quant,
690 )
691 value = quantize_fp8_maybe_pad_head_dim(
692 value,
693 self._fp8_v_scale,
694 skip_scale=self.skip_scale_v,
695 fp8_quant=self.fp8_quant,
696 )
697 return query, key, value
698
699 def _forward_flashinfer(
700 self,
701 query: torch.Tensor,
702 key: torch.Tensor,
703 value: torch.Tensor,
704 cu_seqlens: torch.Tensor | None = None,
705 max_seqlen: torch.Tensor | None = None,
706 sequence_lengths: torch.Tensor
707 | None = None, # Only used for FlashInfer CuDNN backend
708 ) -> torch.Tensor:
709 if self.fp8_enabled:
710 query, key, value = self._quantize_qkv_fp8(query, key, value)
711
712 output = vit_flashinfer_wrapper(
713 q=query,
714 k=key,
715 v=value,
716 scale=self.scale,
717 workspace_buffer=_get_flashinfer_workspace_buffer(),
718 cu_seqlens=cu_seqlens,
719 max_seqlen=max_seqlen,
720 sequence_lengths=sequence_lengths,
721 q_scale=self._fp8_q_scale if self.fp8_enabled else None,
722 k_scale=self._fp8_k_scale if self.fp8_enabled else None,
723 v_scale=self._fp8_v_scale if self.fp8_enabled else None,
724 o_data_type=self.dtype if self.fp8_enabled else None,
725 )
726
727 if self.fp8_enabled and output.shape[-1] != self.head_size:
728 output = output[..., : self.head_size].contiguous()
729
730 return output
731
732 def _forward_aiter_fp8(
733 self,
734 query: torch.Tensor,
735 key: torch.Tensor,
736 value: torch.Tensor,
737 cu_seqlens: torch.Tensor | None = None,
738 max_seqlen: torch.Tensor | None = None,
739 ) -> torch.Tensor:
740 assert (cu_seqlens is not None and max_seqlen is not None) or (
741 cu_seqlens is None and max_seqlen is None
742 ), "cu_seqlens and max_seqlen should be both set or both None."
743
744 bsz, q_len = query.size()[:2]
745 kv_len = key.size(1)
746 is_reshaped = query.dim() != 4
747 query, key, value = self.view_qkv_to_4d(query, key, value, bsz, q_len, kv_len)
748
749 query, key, value = self._quantize_qkv_fp8(query, key, value)
750
751 output = vit_aiter_fp8_attn_wrapper(
752 q=query,
753 k=key,
754 v=value,
755 q_descale=self._fp8_q_scale,
756 k_descale=self._fp8_k_scale,
757 v_descale=self._fp8_v_scale,
758 batch_size=bsz,
759 output_dtype=self.dtype,
760 scale=self.scale,
761 cu_seqlens=cu_seqlens,
762 max_seqlen=max_seqlen,
763 )
764 if output.shape[-1] != self.head_size:
765 output = output[..., : self.head_size].contiguous()
766 if is_reshaped:
767 output = output.reshape(bsz, q_len, -1)
768 return output
769
770 def forward_native(
771 self,
772 query: torch.Tensor,
773 key: torch.Tensor,
774 value: torch.Tensor,
775 cu_seqlens: torch.Tensor | None = None,
776 max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
777 sequence_lengths: torch.Tensor
778 | None = None, # Only used for FlashInfer CuDNN backend
779 ) -> torch.Tensor:
780 return self._forward_sdpa(query, key, value, cu_seqlens)
781
782 def forward_cuda(
783 self,
784 query: torch.Tensor,
785 key: torch.Tensor,
786 value: torch.Tensor,
787 cu_seqlens: torch.Tensor | None = None,
788 max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
789 sequence_lengths: torch.Tensor
790 | None = None, # Only used for FlashInfer CuDNN backend
791 ) -> torch.Tensor:
792 if self.fp8_enabled and self.attn_backend == AttentionBackendEnum.ROCM_AITER_FA:
793 return self._forward_aiter_fp8(query, key, value, cu_seqlens, max_seqlen)
794 elif self.is_flash_attn_backend:
795 return self._forward_fa(query, key, value, cu_seqlens, max_seqlen)
796 elif self.attn_backend == AttentionBackendEnum.TRITON_ATTN:
797 return self._forward_triton(query, key, value, cu_seqlens, max_seqlen)
798 elif self.attn_backend == AttentionBackendEnum.FLASHINFER:
799 return self._forward_flashinfer(
800 query, key, value, cu_seqlens, max_seqlen, sequence_lengths
801 )
802 elif self.attn_backend == AttentionBackendEnum.TORCH_SDPA:
803 return self._forward_sdpa(query, key, value, cu_seqlens)
804 else:
805 raise ValueError(
806 f"Unsupported multi-modal encoder attention backend for CUDA: "
807 f"{self.attn_backend}."
808 )
809
810 def forward_cpu(
811 self,
812 query: torch.Tensor,
813 key: torch.Tensor,
814 value: torch.Tensor,
815 cu_seqlens: torch.Tensor | None = None,
816 max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
817 sequence_lengths: torch.Tensor
818 | None = None, # Only used for FlashInfer CuDNN backend
819 ) -> torch.Tensor:
820 return self._forward_sdpa(query, key, value, cu_seqlens)
821
822 def forward_xpu(
823 self,
824 query: torch.Tensor,
825 key: torch.Tensor,
826 value: torch.Tensor,
827 cu_seqlens: torch.Tensor | None = None,
828 max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
829 sequence_lengths: torch.Tensor
830 | None = None, # Only used for FlashInfer CuDNN backend
831 ) -> torch.Tensor:
832 if self.attn_backend == AttentionBackendEnum.FLASH_ATTN:
833 return self._forward_fa(query, key, value, cu_seqlens, max_seqlen)
834 elif self.attn_backend == AttentionBackendEnum.TRITON_ATTN:
835 return self._forward_triton(query, key, value, cu_seqlens, max_seqlen)
836 elif self.attn_backend == AttentionBackendEnum.TORCH_SDPA:
837 return self._forward_sdpa(query, key, value, cu_seqlens)
838 else:
839 raise ValueError(
840 f"Unsupported multi-modal encoder attention backend for XPU: "
841 f"{self.attn_backend}."
842 )