main
py 801 lines 32.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from typing import TYPE_CHECKING, Any, cast
5
6 import torch
7 import torch.nn as nn
8
9 import vllm.envs as envs
10 from vllm.compilation.breakable_cudagraph import eager_break_during_capture
11 from vllm.config import (
12 CacheConfig,
13 get_current_vllm_config,
14 )
15 from vllm.config.vllm import VllmConfig
16 from vllm.forward_context import ForwardContext, get_forward_context
17 from vllm.logger import init_logger
18 from vllm.model_executor.layers.attention.kv_transfer_utils import (
19 maybe_transfer_kv_layer,
20 )
21 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
22 from vllm.model_executor.layers.linear import (
23 UnquantizedLinearMethod,
24 )
25 from vllm.model_executor.layers.quantization import (
26 QuantizationConfig,
27 resolve_quant_method,
28 )
29 from vllm.model_executor.layers.quantization.base_config import QuantizeMethodBase
30 from vllm.model_executor.layers.quantization.input_quant_fp8 import QuantFP8
31 from vllm.model_executor.layers.quantization.kv_cache import BaseKVCacheMethod
32 from vllm.model_executor.layers.quantization.utils.quant_utils import GroupShape
33 from vllm.platforms import current_platform
34 from vllm.utils.torch_utils import (
35 LayerNameType,
36 _encode_layer_name,
37 _resolve_layer_name,
38 direct_register_custom_op,
39 kv_cache_dtype_str_to_dtype,
40 )
41 from vllm.v1.attention.backend import (
42 AttentionBackend,
43 AttentionMetadata,
44 AttentionType,
45 )
46 from vllm.v1.attention.backends.registry import AttentionBackendEnum
47 from vllm.v1.attention.selector import get_attn_backend
48 from vllm.v1.kv_cache_interface import (
49 FullAttentionSpec,
50 KVCacheSpec,
51 SlidingWindowSpec,
52 get_kv_quant_mode,
53 )
54
55 if TYPE_CHECKING:
56 from vllm.model_executor.layers.attention import MLAAttention
57
58 logger = init_logger(__name__)
59
60
61 def validate_kv_sharing_target(
62 current_layer_name, target_layer_name, static_forward_context
63 ):
64 error_msg = (
65 f"Specified KV sharing target layer for {current_layer_name} "
66 f"is not valid: target layer {target_layer_name} "
67 )
68
69 if current_layer_name == target_layer_name:
70 raise ValueError(error_msg + "cannot be the same as the current layer.")
71
72 if target_layer_name not in static_forward_context:
73 from vllm.model_executor.models.utils import extract_layer_index
74
75 # If target layer name is not in the static fwd context, it means either
76 # a) the target layer does not come BEFORE the current layer, or
77 # b) the target layer is not an Attention layer that exists in the model
78 current_layer_idx = extract_layer_index(current_layer_name)
79 target_layer_idx = extract_layer_index(target_layer_name)
80 if current_layer_idx <= target_layer_idx:
81 raise ValueError(error_msg + "must come before the current layer.")
82 else:
83 raise ValueError(error_msg + "is not a valid Attention layer in the model.")
84
85 # Currently KV sharing is only supported between layers of the same type
86 target_layer_attn_type = static_forward_context[target_layer_name].attn_type
87 expected = static_forward_context[current_layer_name].attn_type
88 if target_layer_attn_type != expected:
89 raise ValueError(
90 error_msg + f"must be the same type as the current layer ({expected})."
91 )
92
93
94 def should_load_quant_weights(quant_method: QuantizeMethodBase | None) -> bool:
95 """Returns whether the quantization method should load quantized weights."""
96 return quant_method is not None and not isinstance(
97 quant_method, UnquantizedLinearMethod
98 )
99
100
101 def _largest_kernel_block_within(
102 attn_backend: "type[AttentionBackend]",
103 per_token_bytes: int,
104 page_budget: int,
105 fallback: int,
106 kv_cache_spec: KVCacheSpec | None = None,
107 ) -> int:
108 """Largest supported kernel block size whose page fits in ``page_budget``.
109
110 A padded spec (e.g. skip-quant layer) that pads its page up to a large shared page
111 wastes ``page_budget - block*per_token`` bytes per block. Picking the largest kernel
112 block whose natural page still fits under ``page_budget`` minimizes that waste.
113 ``MultipleOf`` declarations are expanded to the largest aligned block that fits.
114 Falls back to the smallest supported block when nothing fits.
115 """
116 from vllm.v1.attention.backend import MultipleOf
117
118 sizes = attn_backend.get_supported_kernel_block_sizes(kv_cache_spec)
119 max_block_size = page_budget // per_token_bytes
120 candidates = [s for s in sizes if isinstance(s, int)]
121 candidates.extend(
122 max(s.base, max_block_size // s.base * s.base)
123 for s in sizes
124 if isinstance(s, MultipleOf)
125 )
126 if not candidates:
127 return fallback
128 smallest = min(candidates)
129 fitting = [b for b in candidates if b * per_token_bytes <= page_budget]
130 return max(fitting) if fitting else smallest
131
132
133 def set_default_quant_scales(layer: nn.Module, register_buffer: bool = False) -> None:
134 """Sets default quantization scales for the layer."""
135 if register_buffer:
136 layer.register_buffer("_k_scale", torch.tensor(1.0, dtype=torch.float32))
137 layer.register_buffer("_v_scale", torch.tensor(1.0, dtype=torch.float32))
138 layer.register_buffer("_q_scale", torch.tensor(1.0, dtype=torch.float32))
139 layer.register_buffer("_prob_scale", torch.tensor(1.0, dtype=torch.float32))
140 else:
141 layer._k_scale.fill_(1.0)
142 layer._v_scale.fill_(1.0)
143 layer._q_scale.fill_(1.0)
144 layer._prob_scale.fill_(1.0)
145
146 # We also keep q/k/v_scale on host (cpu) memory for attention
147 # backends that require the scales to be on host instead of on device.
148 # e.g. Flashinfer & AITER
149 layer._q_scale_float = 1.0
150 layer._k_scale_float = 1.0
151 layer._v_scale_float = 1.0
152 layer._k_scale_cpu = torch.tensor(1.0, dtype=torch.float32)
153 layer._v_scale_cpu = torch.tensor(1.0, dtype=torch.float32)
154 layer._prob_scale_float = 1.0
155
156
157 def _init_kv_cache_quant(
158 layer: nn.Module,
159 quant_config: QuantizationConfig | None,
160 prefix: str,
161 ) -> None:
162 """Initializes KV cache scaling factors and quantization method.
163
164 This helper function sets up the KV cache quantization attributes that are
165 shared between Attention and MLAAttention layers. It initializes scale
166 tensors for query, key, value, and probability, and configures the
167 quantization method if applicable.
168
169 Args:
170 layer: The attention layer instance to initialize.
171 quant_config: Optional quantization configuration.
172 prefix: Layer name prefix for quantization method lookup.
173
174 """
175 # Note [Register q/k/v/prob scales in state dict]
176 # When calling model.to(device), only parameters/buffers in state dict are
177 # moved. If not registering q/k/v/prob scales in state dict, there would
178 # be an IMA error when a cuda kernel (e.g., quant_fp8) accesses the tensor
179 # on cpu.
180 # Registering in state dict means it interacts with weight loading. One edge
181 # case is when quant_method is None, or quant_method is UnquantizedLinearMethod
182 # (i.e., should_load_quant_weights(quant_method) == False).
183 # In this case, the checkpoint does not have the scales. We need to
184 # initialize the scales to 1.0 and update the scales after weight loading.
185 # This is espectially important when we load dummy weights first (providing
186 # wrong scales) and then load real weights (which misses scales and keeps the
187 # wrong scales from dummy load).
188 set_default_quant_scales(layer, register_buffer=True)
189
190 # The output scale on host memory. This should be the input scale of
191 # the quant op after this attention layer.
192 layer._o_scale_float = None
193
194 quant_method = (
195 resolve_quant_method(quant_config, layer, prefix=prefix)
196 if quant_config
197 else None
198 )
199
200 # See [Note: Register q/k/v/prob scales in state dict]
201 if should_load_quant_weights(quant_method):
202 assert isinstance(quant_method, BaseKVCacheMethod)
203 # TODO (mgoin): kv cache dtype should be specified in the FP8
204 # checkpoint config and become the "auto" behavior
205 if layer.kv_cache_dtype == "fp8_e5m2":
206 # A compressed-tensors checkpoint stores fp8 KV scales only when it
207 # declares a kv_cache_scheme; weight-only ones declare none and must
208 # keep fp8_e5m2, the only fp8 KV dtype usable on Ampere.
209 from vllm.model_executor.layers.quantization.compressed_tensors.compressed_tensors import ( # noqa: E501
210 CompressedTensorsConfig,
211 CompressedTensorsKVCacheMethod,
212 )
213
214 if not isinstance(quant_method, CompressedTensorsKVCacheMethod) or (
215 cast(CompressedTensorsConfig, quant_method.quant_config).kv_cache_scheme
216 is not None
217 ):
218 raise ValueError(
219 "fp8_e5m2 kv-cache is not supported with fp8 checkpoints."
220 )
221 # If quantization is enabled, we make "k_scale" and "v_scale"
222 # parameters so that it can be loaded from the model checkpoint.
223 # The k/v_scale will then be converted back to native float32
224 # values after weight loading.
225 layer.quant_method = quant_method
226 layer.quant_method.create_weights(layer)
227
228
229 class Attention(nn.Module, AttentionLayerBase):
230 """Attention layer.
231
232 This class takes query, key, and value tensors as input. The input tensors
233 can either contain prompt tokens or generation tokens.
234 The class does the following:
235
236 1. Store the input key and value tensors in the KV cache.
237 2. Perform (multi-head/multi-query/grouped-query) attention.
238 3. Return the output tensor.
239 """
240
241 def __init__(
242 self,
243 num_heads: int,
244 head_size: int,
245 scale: float,
246 num_kv_heads: int | None = None,
247 alibi_slopes: list[float] | None = None,
248 use_alibi_sqrt: bool | None = None,
249 cache_config: CacheConfig | None = None,
250 quant_config: QuantizationConfig | None = None,
251 logits_soft_cap: float | None = None,
252 per_layer_sliding_window: int | None = None,
253 prefix: str = "",
254 attn_type: str = AttentionType.DECODER,
255 kv_sharing_target_layer_name: str | None = None,
256 mm_prefix_clamp_sliding_window: bool = False,
257 attn_backend: type[AttentionBackend] | None = None,
258 head_size_v: int | None = None,
259 **extra_impl_args,
260 ) -> None:
261 """The KV cache is stored inside this class and is accessed via
262 `self.kv_cache`.
263 """
264 super().__init__()
265 sliding_window: int | None
266 if per_layer_sliding_window is not None:
267 # per-layer sliding window
268 sliding_window = per_layer_sliding_window
269 elif cache_config is not None:
270 # model-level sliding window
271 sliding_window = cache_config.sliding_window
272 else:
273 sliding_window = None
274
275 vllm_config = get_current_vllm_config()
276 if cache_config is not None:
277 kv_cache_dtype = cache_config.cache_dtype
278 else:
279 kv_cache_dtype = "auto"
280
281 # llm-compressor models declare an FP8 KV-cache scheme in their
282 # checkpoint config. Honor it only when the user did not explicitly
283 # pick a kv_cache_dtype; an explicit choice (e.g. bfloat16) must win.
284 # The "auto" case is normally resolved upstream in
285 # resolve_kv_cache_dtype_string, but we re-apply here defensively in
286 # case anything bypassed that path.
287 kv_cache_scheme = getattr(quant_config, "kv_cache_scheme", None)
288 if kv_cache_scheme is not None and kv_cache_dtype == "auto":
289 kv_cache_dtype = "fp8"
290 if cache_config is not None:
291 cache_config.cache_dtype = "fp8"
292
293 # Check if per-head quant scales are required based on kv_cache_scheme
294 use_per_head_quant_scales = (
295 kv_cache_scheme is not None
296 and kv_cache_scheme.get("strategy") == "attn_head"
297 )
298
299 # Skip quantization for specified layers
300 if cache_config is not None and cache_config.kv_cache_dtype_skip_layers:
301 from vllm.model_executor.models.utils import extract_layer_index
302
303 skip = False
304 # Check attention type
305 if (
306 sliding_window is not None
307 and "sliding_window" in cache_config.kv_cache_dtype_skip_layers
308 ):
309 skip = True
310 # Check layer index
311 layer_idx = extract_layer_index(prefix)
312 if str(layer_idx) in cache_config.kv_cache_dtype_skip_layers:
313 skip = True
314 if skip:
315 kv_cache_dtype = "auto"
316 logger.debug(
317 "Layer %s: kv_cache_dtype=%s, sliding_window=%s",
318 prefix,
319 kv_cache_dtype,
320 sliding_window,
321 )
322
323 self.kv_cache_torch_dtype = kv_cache_dtype_str_to_dtype(
324 kv_cache_dtype, vllm_config.model_config
325 )
326 self.kv_cache_dtype = kv_cache_dtype
327 if num_kv_heads is None:
328 num_kv_heads = num_heads
329 assert num_heads % num_kv_heads == 0, (
330 f"num_heads ({num_heads}) is not divisible by num_kv_heads ({num_kv_heads})"
331 )
332 self.quant_config = quant_config
333 self.layer_name = prefix
334
335 self.num_heads = num_heads
336 self.head_size = head_size
337 self.head_size_v = self.head_size if head_size_v is None else head_size_v
338 self.num_kv_heads = num_kv_heads
339 self.sliding_window = sliding_window
340 self.has_sink = extra_impl_args.get("sinks") is not None
341
342 # NOTE: model_config may be None during certain tests
343 model_config = vllm_config.model_config
344 self.use_mm_prefix = model_config is not None and model_config.is_mm_prefix_lm
345
346 # During model initialization, the default dtype is set as the model
347 # weight and activation dtype.
348 dtype = torch.get_default_dtype()
349 if attn_backend is None:
350 self.attn_backend = get_attn_backend(
351 head_size,
352 dtype,
353 kv_cache_dtype,
354 use_mla=False,
355 has_sink=self.has_sink,
356 use_mm_prefix=self.use_mm_prefix,
357 use_per_head_quant_scales=use_per_head_quant_scales,
358 attn_type=attn_type,
359 has_sliding_window=sliding_window is not None,
360 )
361 else:
362 self.attn_backend = attn_backend
363 backend_supports_alibi_sqrt = self.attn_backend.supports_alibi_sqrt()
364 use_alibi_sqrt = use_alibi_sqrt if use_alibi_sqrt else False
365 if use_alibi_sqrt and not backend_supports_alibi_sqrt:
366 raise ValueError(
367 f"use_alibi_sqrt is not supported by backend "
368 f"{self.attn_backend.get_name()}."
369 )
370 self.use_alibi_sqrt = bool(use_alibi_sqrt)
371 if backend_supports_alibi_sqrt:
372 extra_impl_args["use_alibi_sqrt"] = self.use_alibi_sqrt
373 # prefix caching + batch invariance is currently not supported for
374 # FLASHINFER and TRITON_MLA.
375 if (
376 cache_config is not None
377 and cache_config.enable_prefix_caching
378 and envs.VLLM_BATCH_INVARIANT
379 and (
380 self.attn_backend.get_name() == "FLASHINFER"
381 or self.attn_backend.get_name() == "TRITON_MLA"
382 )
383 ):
384 logger.warning_once(
385 "Disabling prefix caching for FLASHINFER/TRITON_MLA "
386 "with batch invariance, as it is not yet supported.",
387 )
388 cache_config.enable_prefix_caching = False
389
390 if extra_impl_args.get("chunk_lookback", -1) > -1:
391 assert self.attn_backend.get_name() == "TRITON_ATTN", (
392 f"Chunked attention with lookback requires the Triton backend, "
393 f"but got {self.attn_backend.get_name()}."
394 )
395
396 if self.attn_backend.get_name() == "FLEX_ATTENTION":
397 block_m = vllm_config.attention_config.flex_attn_block_m
398 block_n = vllm_config.attention_config.flex_attn_block_n
399
400 if envs.VLLM_BATCH_INVARIANT and cache_config is not None:
401 if block_m is not None and block_m > cache_config.block_size:
402 raise ValueError(
403 f"flex_attn_block_m ({block_m}) must be "
404 f"<= cache block size ({cache_config.block_size}) for "
405 f"batch invariance"
406 )
407 if block_n is not None and block_n > cache_config.block_size:
408 raise ValueError(
409 f"flex_attn_block_n ({block_n}) must be "
410 f"<= cache block size ({cache_config.block_size}) for "
411 f"batch invariance"
412 )
413
414 if block_m is not None:
415 extra_impl_args.setdefault("block_m", block_m)
416 if block_n is not None:
417 extra_impl_args.setdefault("block_n", block_n)
418
419 impl_cls = self.attn_backend.get_impl_cls()
420 self.impl = impl_cls( # type: ignore[assignment] # impl_cls always returns an AttentionImpl subclass
421 num_heads,
422 head_size,
423 scale,
424 num_kv_heads,
425 alibi_slopes,
426 sliding_window,
427 kv_cache_dtype,
428 logits_soft_cap,
429 attn_type,
430 kv_sharing_target_layer_name,
431 **extra_impl_args,
432 )
433 self.backend = AttentionBackendEnum[self.attn_backend.get_name()]
434 self.dtype = dtype
435
436 # For cuda-alike (CUDA and ROCM) and cpu platforms, we control how
437 # torch.compile works by registering the attention as one giant
438 # opaque custom op. For other platforms, we directly call them
439 # and let torch.compile handle them.
440 self.use_direct_call = not current_platform.opaque_attention_op()
441
442 compilation_config = vllm_config.compilation_config
443 if prefix in compilation_config.static_forward_context:
444 raise ValueError(f"Duplicate layer name: {prefix}")
445 compilation_config.static_forward_context[prefix] = self
446 self.attn_type = attn_type
447
448 if kv_sharing_target_layer_name is not None:
449 validate_kv_sharing_target(
450 prefix,
451 kv_sharing_target_layer_name,
452 compilation_config.static_forward_context,
453 )
454 self.kv_sharing_target_layer_name = kv_sharing_target_layer_name
455 # Gemma4: clamp mm_prefix bidirectional ranges by the sliding window
456 # (read by the Triton backend impl). Default False for all other models.
457 self.mm_prefix_clamp_sliding_window = mm_prefix_clamp_sliding_window
458
459 # use a placeholder kv cache tensor during init, which will be replaced
460 # by bind_kv_cache
461 # this variable will not be accessed if use_direct_call is True
462 self.kv_cache = torch.tensor([])
463
464 # Initialize KV cache quantization attributes
465 _init_kv_cache_quant(self, quant_config, prefix)
466
467 # for attn backends supporting query quantization
468 self.query_quant = None
469 if (
470 self.impl.supports_quant_query_input
471 and (
472 self.kv_cache_dtype.startswith("fp8")
473 or self.kv_cache_dtype.startswith("nvfp4")
474 )
475 and not self.kv_cache_dtype.endswith("per_token_head")
476 ):
477 is_per_head = (
478 hasattr(self, "q_scale") and self.q_scale.numel() == self.num_kv_heads
479 )
480 block_size = self.head_size * self.num_heads // self.num_kv_heads
481 self.query_quant = QuantFP8(
482 static=True,
483 group_shape=GroupShape(-1, block_size)
484 if is_per_head
485 else GroupShape.PER_TENSOR,
486 )
487
488 def forward(
489 self,
490 query: torch.Tensor,
491 key: torch.Tensor,
492 value: torch.Tensor,
493 # For some alternate attention backends like MLA the attention output
494 # shape does not match the query shape, so we optionally let the model
495 # definition specify the output tensor shape.
496 output_shape: torch.Size | None = None,
497 output_dtype: torch.dtype | None = None,
498 ) -> torch.Tensor:
499 """The KV cache is stored inside this class and is accessed via
500 `self.kv_cache`.
501
502 Attention metadata (`attn_metadata`) is set using a context manager in
503 the model runner's `execute_model` method. It is accessed via forward
504 context using
505 `vllm.forward_context.get_forward_context().attn_metadata`.
506 """
507 if output_dtype is None:
508 output_dtype = query.dtype
509 if self.query_quant is not None:
510 # quantizing with a simple torch operation enables
511 # torch.compile to fuse this into previous ops
512 # which reduces overheads during decoding.
513 # Otherwise queries are quantized using custom ops
514 # which causes decoding overheads
515 assert self.kv_cache_dtype in {"fp8", "fp8_e4m3"} or (
516 self.kv_cache_dtype.startswith("nvfp4")
517 )
518
519 # check if query quantization is supported
520 if self.impl.supports_quant_query_input:
521 query, _ = self.query_quant(query, self._q_scale)
522
523 if output_shape is None:
524 # Handle both 2D [num_tokens, hidden] and
525 # 3D [num_tokens, heads, head_dim] query
526 num_tokens = query.shape[0]
527 output_shape = torch.Size((num_tokens, self.num_heads * self.head_size_v))
528 output = torch.empty(output_shape, dtype=output_dtype, device=query.device)
529 hidden_size = output_shape[-1]
530 # Reshape the query, key, and value tensors.
531 # NOTE(woosuk): We do this outside the custom op to minimize the
532 # CPU overheads from the non-CUDA-graph regions.
533 query = query.view(-1, self.num_heads, self.head_size)
534 output = output.view(-1, self.num_heads, self.head_size_v)
535 if key is not None:
536 key = key.view(-1, self.num_kv_heads, self.head_size)
537 if value is not None:
538 value = value.view(-1, self.num_kv_heads, self.head_size_v)
539 kv_cache_dummy_dep = None
540 if self.use_direct_call:
541 # Skip this if sharing KV cache with an earlier attention layer.
542 if (
543 not self.attn_backend.forward_includes_kv_cache_update
544 and self.kv_sharing_target_layer_name is None
545 and key is not None
546 and value is not None
547 ):
548 kv_cache_dummy_dep = unified_kv_cache_update(
549 key, value, self.layer_name
550 )
551 unified_attention_with_output(
552 query,
553 key,
554 value,
555 output,
556 self.layer_name,
557 kv_cache_dummy_dep=kv_cache_dummy_dep,
558 )
559 else:
560 # Skip this if sharing KV cache with an earlier attention layer.
561 encoded = _encode_layer_name(self.layer_name)
562 if (
563 not self.attn_backend.forward_includes_kv_cache_update
564 and self.kv_sharing_target_layer_name is None
565 and key is not None
566 and value is not None
567 ):
568 kv_cache_dummy_dep = torch.ops.vllm.unified_kv_cache_update(
569 key, value, encoded
570 )
571 torch.ops.vllm.unified_attention_with_output(
572 query,
573 key,
574 value,
575 output,
576 encoded,
577 kv_cache_dummy_dep=kv_cache_dummy_dep,
578 )
579 return output.view(-1, hidden_size)
580
581 def extra_repr(self) -> str:
582 s = f"head_size={self.impl.head_size}" # type: ignore
583 s += f", num_heads={self.impl.num_heads}" # type: ignore
584 s += f", num_kv_heads={self.impl.num_kv_heads}" # type: ignore
585 s += f", scale={self.impl.scale}" # type: ignore
586 s += f", backend={self.impl.__class__.__name__}"
587 return s
588
589 def process_weights_after_loading(self, act_dtype: torch.dtype):
590 self.impl.process_weights_after_loading(act_dtype)
591
592 # If we should not load quant weights, we initialize the scales to 1.0
593 # as the default value. See [Note: Register q/k/v/prob scales in state dict]
594 # for more details.
595 quant_method = (
596 resolve_quant_method(self.quant_config, self, prefix=self.layer_name)
597 if self.quant_config
598 else None
599 )
600 if not should_load_quant_weights(quant_method):
601 set_default_quant_scales(self, register_buffer=False)
602
603 def get_attn_backend(self) -> type[AttentionBackend]:
604 return self.attn_backend
605
606 def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec | None:
607 # Block size may get updated after model loading, refresh it
608 block_size = vllm_config.cache_config.block_size
609 # Encoder-only attention is prefill-only and keeps no autoregressive KV
610 # cache. In hybrid models (e.g. Qwen3.5 / ColQwen3.5: GatedDeltaNet
611 # linear_attention interleaved with full_attention) the runner iterates
612 # every attention module to build the KV-cache spec, so an ENCODER_ONLY
613 # full_attention layer reaches here; it contributes no KV cache group.
614 if self.attn_type in (AttentionType.ENCODER_ONLY, AttentionType.ENCODER):
615 return None
616 # Should not be called for enc-dec attention.
617 assert self.attn_type == AttentionType.DECODER
618 quant_mode = get_kv_quant_mode(self.kv_cache_dtype)
619 if self.sliding_window is not None:
620 assert not self.attn_backend.is_mla(), (
621 "MLA is not supported for sliding window"
622 )
623 # SW chooses its own block_size, decoupled from the user's
624 # ``--block-size`` (which only constrains primary attention).
625 # When this SW layer is a padded spec (skip-quant: its page is
626 # padded up to ``skip_page_size_padded``), pick the largest kernel
627 # block that still fits the shared page so we waste fewer padding
628 # bytes per block. Otherwise (page_size_padded is None) take the
629 # primary block size when the backend can run it unsplit: if this
630 # page does not divide the primary page, ``unify`` then pads it
631 # (padded pages cannot be split) instead of scaling a small block
632 # to a size coprime with the primary one, which inflates the
633 # scheduler LCM (e.g. a 1024 B/token SWA draft next to a 1152
634 # B/token MLA target: 1728 vs 1536 gives LCM 13824). Backends that
635 # cannot run the primary block start from their smallest block and
636 # ``unify`` scales it up by an integer ratio.
637 shared_page = vllm_config.cache_config.skip_page_size_padded
638 # The backend owns its packing
639 kv_cache_spec = self.attn_backend.customize_spec(
640 SlidingWindowSpec(
641 block_size=1,
642 num_kv_heads=self.num_kv_heads,
643 head_size=self.head_size,
644 head_size_v=self.head_size_v,
645 dtype=self.kv_cache_torch_dtype,
646 kv_quant_mode=quant_mode,
647 sliding_window=self.sliding_window,
648 )
649 )
650 sw_per_token = kv_cache_spec.real_page_size_bytes
651 page_budget = shared_page or sw_per_token * block_size
652 sw_block_size = _largest_kernel_block_within(
653 self.attn_backend,
654 sw_per_token,
655 page_budget,
656 block_size,
657 kv_cache_spec,
658 )
659 return SlidingWindowSpec(
660 block_size=sw_block_size,
661 num_kv_heads=self.num_kv_heads,
662 head_size=self.head_size,
663 head_size_v=self.head_size_v,
664 dtype=self.kv_cache_torch_dtype,
665 kv_quant_mode=quant_mode,
666 sliding_window=self.sliding_window,
667 page_size_padded=shared_page,
668 )
669 else:
670 return FullAttentionSpec(
671 block_size=block_size,
672 num_kv_heads=self.num_kv_heads,
673 head_size=self.head_size,
674 head_size_v=self.head_size_v,
675 dtype=self.kv_cache_torch_dtype,
676 kv_quant_mode=quant_mode,
677 )
678
679
680 def get_attention_context(
681 layer_name: str,
682 ) -> tuple[Any, "Attention | MLAAttention", torch.Tensor, torch.Tensor]:
683 """Extract attention context for a given layer.
684
685 This helper function extracts the attention metadata, attention layer
686 instance, KV cache tensor, and slot mapping for a specific layer.
687
688 Args:
689 layer_name: The name/identifier of the attention layer.
690
691 Returns:
692 A tuple containing:
693 - attn_metadata: Attention metadata for this specific layer, or None if
694 no metadata available
695 - attn_layer: The attention layer instance (Attention or MLAAttention)
696 - kv_cache: The KV cache tensor for current forward pass
697 - slot_mapping: The slot mapping for this specific layer
698
699 Note: attn_metadata may be None, but attn_layer and kv_cache are always
700 extracted from the forward context.
701
702 """
703 forward_context: ForwardContext = get_forward_context()
704 attn_metadata_raw = forward_context.attn_metadata
705 attn_metadata: AttentionMetadata
706 if isinstance(attn_metadata_raw, dict):
707 attn_metadata = attn_metadata_raw[layer_name]
708 elif isinstance(attn_metadata_raw, list):
709 # list[dict[str, AttentionMetadata]]: used in speculative decoding
710 # where [0] is the base-model (non-speculative) metadata dict.
711 attn_metadata = attn_metadata_raw[0][layer_name]
712 else:
713 attn_metadata = attn_metadata_raw
714 attn_layer: Attention | MLAAttention = forward_context.no_compile_layers[layer_name]
715 kv_cache = attn_layer.kv_cache
716 slot_mapping = forward_context.slot_mapping
717 assert isinstance(slot_mapping, dict), (
718 f"Expected slot_mapping to be a dict, got {type(slot_mapping)}. "
719 )
720 layer_slot_mapping = slot_mapping.get(layer_name)
721 return attn_metadata, attn_layer, kv_cache, layer_slot_mapping
722
723
724 def unified_kv_cache_update(
725 key: torch.Tensor,
726 value: torch.Tensor,
727 layer_name: LayerNameType,
728 ) -> torch.Tensor:
729 """Returns a dummy that is passed to unified_attention to signal a side effect and
730 the data dependency between them to ensure torch.compile preserves ordering.
731 """
732 layer_name = _resolve_layer_name(layer_name)
733 _, attn_layer, kv_cache, layer_slot_mapping = get_attention_context(layer_name)
734 if layer_slot_mapping is not None:
735 assert hasattr(attn_layer.impl, "do_kv_cache_update"), (
736 f"{attn_layer.impl.__class__.__name__} does not support kv cache update"
737 )
738 attn_layer.impl.do_kv_cache_update( # type: ignore[attr-defined]
739 attn_layer,
740 key,
741 value,
742 kv_cache,
743 layer_slot_mapping,
744 )
745
746 return key.new_empty(0)
747
748
749 def unified_kv_cache_update_fake(
750 key: torch.Tensor,
751 value: torch.Tensor,
752 layer_name: LayerNameType,
753 ) -> torch.Tensor:
754 return torch.empty(0, device=key.device, dtype=key.dtype)
755
756
757 direct_register_custom_op(
758 op_name="unified_kv_cache_update",
759 op_func=unified_kv_cache_update,
760 fake_impl=unified_kv_cache_update_fake,
761 mutates_args=[],
762 )
763
764
765 @eager_break_during_capture
766 @maybe_transfer_kv_layer
767 def unified_attention_with_output(
768 query: torch.Tensor,
769 key: torch.Tensor,
770 value: torch.Tensor,
771 output: torch.Tensor,
772 layer_name: LayerNameType,
773 output_scale: torch.Tensor | None = None,
774 output_block_scale: torch.Tensor | None = None,
775 kv_cache_dummy_dep: torch.Tensor | None = None,
776 ) -> None:
777 # kv_cache_dummy_dep is not used but accepting it creates a data dependency
778 # that ensures torch.compile preserves ordering between KV cache update and
779 # attention forward.
780 del kv_cache_dummy_dep
781 layer_name = _resolve_layer_name(layer_name)
782 attn_metadata, self, kv_cache, _ = get_attention_context(layer_name)
783
784 self.impl.forward(
785 self,
786 query,
787 key,
788 value,
789 kv_cache,
790 attn_metadata,
791 output=output,
792 output_scale=output_scale,
793 output_block_scale=output_block_scale,
794 )
795
796
797 direct_register_custom_op(
798 op_name="unified_attention_with_output",
799 op_func=unified_attention_with_output,
800 mutates_args=["output", "output_block_scale"],
801 )