main
py 562 lines 19.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import functools
4 from dataclasses import dataclass
5 from typing import TYPE_CHECKING, ClassVar
6
7 if TYPE_CHECKING:
8 from vllm.config.cache import CacheDType
9
10 import torch
11
12 from vllm import _custom_ops as ops
13 from vllm import envs
14 from vllm.config import (
15 VllmConfig,
16 get_current_vllm_config,
17 get_layers_from_vllm_config,
18 )
19 from vllm.logger import init_logger
20 from vllm.model_executor.layers.attention import Attention
21 from vllm.platforms import CpuArchEnum, current_platform
22 from vllm.utils.torch_utils import is_quantized_kv_cache
23 from vllm.v1.attention.backend import (
24 AttentionBackend,
25 AttentionImpl,
26 AttentionLayer,
27 AttentionMetadataBuilder,
28 AttentionType,
29 CommonAttentionMetadata,
30 MultipleOf,
31 )
32 from vllm.v1.attention.backends.utils import (
33 get_num_attention_heads_from_layers,
34 )
35 from vllm.v1.attention.backends.zentorch_sdpa import (
36 should_use_zentorch_sdpa,
37 zentorch_sdpa_attn,
38 )
39 from vllm.v1.kv_cache_interface import (
40 AttentionSpec,
41 CrossAttentionSpec,
42 EncoderOnlyAttentionSpec,
43 KVCacheLayout,
44 )
45
46 logger = init_logger(__name__)
47
48
49 class CPUAttentionBackend(AttentionBackend):
50 forward_includes_kv_cache_update: bool = False
51
52 supported_dtypes: ClassVar[list[torch.dtype]] = [
53 torch.float16,
54 torch.bfloat16,
55 torch.float32,
56 ]
57 supported_kv_cache_dtypes: ClassVar[list["CacheDType"]] = [
58 "auto",
59 "fp8",
60 "fp8_e4m3",
61 "fp8_e5m2",
62 ]
63
64 @staticmethod
65 def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
66 return [MultipleOf(32)]
67
68 @classmethod
69 def get_supported_head_sizes(cls) -> list[int]:
70 return [32, 64, 80, 96, 112, 128, 160, 192, 224, 256, 512]
71
72 @classmethod
73 def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...]:
74 # The CPU backend only reads head-major block interiors.
75 return (KVCacheLayout.LBHNC,)
76
77 @staticmethod
78 def get_name() -> str:
79 return "CPU_ATTN"
80
81 @classmethod
82 def supports_non_causal(cls) -> bool:
83 return True
84
85 @classmethod
86 def supports_sliding_window(cls) -> bool:
87 return True
88
89 @classmethod
90 def supports_attn_type(cls, attn_type: str) -> bool:
91 """CPU attention supports decoder,
92 encoder-only and encoder-decoder attention."""
93 return attn_type in (
94 AttentionType.DECODER,
95 AttentionType.ENCODER,
96 AttentionType.ENCODER_ONLY,
97 AttentionType.ENCODER_DECODER,
98 )
99
100 @staticmethod
101 def get_impl_cls() -> type["CPUAttentionBackendImpl"]:
102 return CPUAttentionBackendImpl
103
104 @staticmethod
105 def get_builder_cls() -> type["CPUAttentionMetadataBuilder"]:
106 return CPUAttentionMetadataBuilder
107
108 @staticmethod
109 def use_cascade_attention(*args, **kwargs) -> bool:
110 return False
111
112
113 @dataclass
114 class CPUAttentionMetadata:
115 num_actual_tokens: int # Number of tokens excluding padding.
116 max_query_len: int
117 query_start_loc: torch.Tensor
118 max_seq_len: int
119 seq_lens: torch.Tensor
120 block_table: torch.Tensor
121 slot_mapping: torch.Tensor
122 scheduler_metadata: torch.Tensor | None
123 causal: bool = True
124 dynamic_causal: torch.Tensor | None = None
125
126 # can be removed after deprecate sdpa
127 use_sdpa_prefill: bool = False
128 num_decode_tokens: int = 0
129 sdpa_attn_masks: list[torch.Tensor | None] | None = None
130 sdpa_start_loc: torch.Tensor | None = None
131
132 encoder_cache: torch.Tensor | None = None
133
134
135 class CPUAttentionMetadataBuilder(AttentionMetadataBuilder[CPUAttentionMetadata]):
136 def __init__(
137 self,
138 kv_cache_spec: AttentionSpec,
139 layer_names: list[str],
140 vllm_config: VllmConfig,
141 device: torch.device,
142 ) -> None:
143 super().__init__(kv_cache_spec, layer_names, vllm_config, device)
144
145 self.kv_cache_spec = kv_cache_spec
146 self.vllm_config = vllm_config
147
148 parallel_config = vllm_config.parallel_config
149 self.num_kv_heads = kv_cache_spec.num_kv_heads
150 # The scheduler metadata built here sizes a scratchpad from the query
151 # head count, so it must come from this group's layers: the model-wide
152 # count is wrong for models that vary it per layer (e.g. Laguna).
153 self.num_heads = get_num_attention_heads_from_layers(
154 vllm_config, layer_names
155 ) or vllm_config.model_config.get_num_attention_heads(parallel_config)
156 self.head_dim = kv_cache_spec.head_size
157 self.dtype = vllm_config.model_config.dtype
158 self.window_size = self._group_sliding_window()
159 self.block_size = kv_cache_spec.block_size
160 self.kv_cache_dtype = vllm_config.cache_config.cache_dtype
161 self.isa = _get_attn_isa(
162 self.dtype,
163 self.block_size,
164 self.head_dim,
165 self.kv_cache_dtype,
166 )
167 self._set_isa_to_layers(layer_names)
168 self.is_cross_attention = isinstance(kv_cache_spec, CrossAttentionSpec)
169 self.is_encoder_only_attention = isinstance(
170 kv_cache_spec, EncoderOnlyAttentionSpec
171 )
172
173 def _set_isa_to_layers(self, layer_names: list[str]) -> None:
174 attn_layers = get_layers_from_vllm_config(
175 self.vllm_config,
176 Attention,
177 layer_names,
178 )
179 for layer in attn_layers.values():
180 layer.isa = self.isa # type: ignore
181
182 def _group_sliding_window(self) -> int:
183 """The window shared by every layer in this group, else -1 (no window).
184
185 Taken from the layers rather than the group spec: one KV cache group can
186 hold both windowed and global layers (e.g. Gemma-3 with the hybrid KV
187 cache manager disabled), and the scheduler metadata built here is shared
188 by the whole group, so it may only assume a window all of them agree on.
189 """
190 layers = get_layers_from_vllm_config(
191 self.vllm_config, Attention, self.layer_names
192 )
193 windows = {
194 layer.impl.sliding_window
195 for layer in layers.values()
196 if isinstance(layer.impl, CPUAttentionBackendImpl)
197 }
198 if len(windows) != 1:
199 return -1
200 window = windows.pop()
201 return -1 if window is None else window
202
203 def build(
204 self,
205 common_prefix_len: int,
206 common_attn_metadata: CommonAttentionMetadata,
207 fast_build: bool = False,
208 ) -> CPUAttentionMetadata:
209 num_reqs = common_attn_metadata.num_reqs
210 num_actual_tokens = common_attn_metadata.num_actual_tokens
211 max_query_len = common_attn_metadata.max_query_len
212 max_seq_len = common_attn_metadata.max_seq_len
213 query_start_loc = common_attn_metadata.query_start_loc
214 seq_lens = common_attn_metadata.seq_lens
215 block_table_tensor = common_attn_metadata.block_table_tensor
216 slot_mapping = common_attn_metadata.slot_mapping
217 is_dynamic_casual = isinstance(common_attn_metadata.causal, torch.Tensor)
218 dynamic_casual = None
219 if is_dynamic_casual:
220 dynamic_casual = common_attn_metadata.causal
221
222 causal = (
223 False
224 if self.is_cross_attention or is_dynamic_casual
225 else common_attn_metadata.causal
226 )
227
228 encoder_cache_tensor = None
229 if self.is_encoder_only_attention:
230 block_nums = (seq_lens + self.block_size - 1) // self.block_size
231 start_block_ids = torch.zeros_like(seq_lens)
232 torch.cumsum(block_nums[:-1], 0, out=start_block_ids[1:])
233 total_block_num: int = block_nums.sum().item()
234 max_block_num = block_nums.max().item()
235 block_offsets = torch.arange(
236 0, max_block_num, dtype=block_table_tensor.dtype
237 )
238 encoder_block_table = start_block_ids[:, None] + block_offsets[None, :]
239 torch.ops._C.compute_slot_mapping_kernel_impl(
240 query_start_loc,
241 common_attn_metadata.positions,
242 encoder_block_table,
243 slot_mapping,
244 self.block_size,
245 )
246 encoder_cache_tensor = torch.zeros(
247 (
248 total_block_num,
249 self.num_kv_heads,
250 self.block_size,
251 2 * self.head_dim,
252 ),
253 dtype=self.dtype,
254 )
255 block_table_tensor = encoder_block_table
256
257 scheduler_metadata = ops.cpu_attn_get_scheduler_metadata(
258 num_reqs=num_reqs,
259 num_heads=self.num_heads,
260 num_kv_heads=self.num_kv_heads,
261 head_dim=self.head_dim,
262 seq_lens=seq_lens,
263 dtype=self.dtype,
264 query_start_loc=query_start_loc,
265 causal=causal,
266 sliding_window_size=self.window_size,
267 isa=self.isa,
268 enable_kv_split=envs.VLLM_CPU_ATTN_SPLIT_KV,
269 dynamic_causal=dynamic_casual,
270 kv_cache_dtype=self.kv_cache_dtype,
271 )
272
273 attn_metadata = CPUAttentionMetadata(
274 num_actual_tokens=num_actual_tokens,
275 max_query_len=max_query_len,
276 query_start_loc=query_start_loc,
277 max_seq_len=max_seq_len,
278 seq_lens=seq_lens,
279 block_table=block_table_tensor,
280 slot_mapping=slot_mapping,
281 scheduler_metadata=scheduler_metadata,
282 causal=causal,
283 encoder_cache=encoder_cache_tensor,
284 dynamic_causal=dynamic_casual,
285 )
286
287 return attn_metadata
288
289
290 class CPUAttentionBackendImpl(AttentionImpl):
291 def __init__(
292 self,
293 num_heads: int,
294 head_size: int,
295 scale: float,
296 num_kv_heads: int,
297 alibi_slopes: list[float] | None,
298 sliding_window: int | None,
299 kv_cache_dtype: str,
300 logits_soft_cap: float | None = None,
301 attn_type: str = AttentionType.DECODER,
302 kv_sharing_target_layer_name: str | None = None,
303 sinks: torch.Tensor | None = None,
304 ) -> None:
305 self.kv_sharing_target_layer_name = kv_sharing_target_layer_name
306 self.num_heads = num_heads
307 self.head_size = head_size
308 self.scale = float(scale)
309 if logits_soft_cap is not None and attn_type in (
310 AttentionType.ENCODER,
311 AttentionType.ENCODER_ONLY,
312 ):
313 logger.warning_once(
314 "CPU_ATTN does not support logits softcap for"
315 " ENCODER and ENCODER_ONLY, outputs may be slightly off"
316 )
317 if logits_soft_cap is None:
318 logits_soft_cap = 0
319 self.logits_soft_cap = logits_soft_cap
320
321 self.num_kv_heads = num_kv_heads
322 if alibi_slopes is not None:
323 alibi_slopes = torch.tensor(alibi_slopes, dtype=torch.float32)
324 self.alibi_slopes = alibi_slopes
325 if sliding_window is None:
326 self.sliding_window = -1
327 else:
328 self.sliding_window = sliding_window
329 self.kv_cache_dtype = kv_cache_dtype
330 self.num_queries_per_kv = self.num_heads // self.num_kv_heads
331
332 self.is_fp8_kv_cache = is_quantized_kv_cache(kv_cache_dtype)
333 self.attn_type = attn_type
334
335 self.sinks = sinks
336 if self.sinks is not None:
337 assert self.sinks.shape[0] == num_heads, (
338 "Sinks must have the same number of heads as the number of "
339 "heads in the layer"
340 )
341
342 vllm_config = get_current_vllm_config()
343 self.use_zentorch_sdpa = should_use_zentorch_sdpa(
344 attn_type,
345 vllm_config.model_config.dtype,
346 )
347
348 def forward(
349 self,
350 layer: AttentionLayer,
351 query: torch.Tensor,
352 key: torch.Tensor,
353 value: torch.Tensor,
354 kv_cache: torch.Tensor,
355 attn_metadata: CPUAttentionMetadata | None,
356 output: torch.Tensor,
357 output_scale: torch.Tensor | None = None,
358 output_block_scale: torch.Tensor | None = None,
359 ) -> torch.Tensor:
360 """Forward pass for CPU attention backend.
361
362 Args:
363 layer: The attention layer, providing the q/k/v quantization scales.
364 query: shape = [num_tokens, num_heads, head_size]
365 key: shape = [num_tokens, num_kv_heads, head_size]
366 value: shape = [num_tokens, num_kv_heads, head_size]
367 kv_cache: shape =
368 [num_blocks, num_kv_heads, block_size, 2 * head_size]
369 attn_metadata: Metadata for attention.
370 output: Tensor that the attention result is written into.
371 output_scale: Scale for fused output quantization; not supported
372 by this backend.
373 output_block_scale: Block scale for fused output quantization;
374 not supported by this backend.
375
376 Returns:
377 shape = [num_tokens, num_heads * head_size]
378
379 """
380 if output_scale is not None or output_block_scale is not None:
381 raise NotImplementedError(
382 "fused output quantization is not yet supported"
383 " for CPUAttentionBackendImpl"
384 )
385
386 # For warming-up
387 if attn_metadata is None:
388 return output
389
390 num_actual_tokens = attn_metadata.num_actual_tokens
391
392 is_encoder_attention = self.attn_type in (
393 AttentionType.ENCODER_ONLY,
394 AttentionType.ENCODER,
395 )
396 if is_encoder_attention:
397 if self.use_zentorch_sdpa:
398 # Encoder attention never reads the KV cache back, so the
399 # zentorch path attends the packed QKV directly instead of
400 # staging it through the scratch encoder cache.
401 zentorch_sdpa_attn(
402 query[:num_actual_tokens],
403 key[:num_actual_tokens],
404 value[:num_actual_tokens],
405 output[:num_actual_tokens],
406 attn_metadata,
407 self.scale,
408 self.sliding_window,
409 self.alibi_slopes,
410 )
411 return output
412 # For encoder attention,
413 kv_cache = attn_metadata.encoder_cache
414
415 # KV cache size are [num_blocks, num_kv_heads, block_size,
416 # 2 * head_size]. Make a view [num_blocks, num_kv_heads,
417 # block_size * 2, head_size]. Then slice KV at dim 2
418 num_blocks, num_kv_heads, block_size, _ = kv_cache.size()
419 kv_cache = kv_cache.view((num_blocks, num_kv_heads, block_size * 2, -1))
420 key_cache, value_cache = kv_cache.chunk(2, dim=2)
421
422 if is_encoder_attention:
423 ops.cpu_attn_reshape_and_cache(
424 key,
425 value,
426 key_cache,
427 value_cache,
428 attn_metadata.slot_mapping,
429 layer.isa, # type: ignore
430 k_scale=layer._k_scale_float,
431 v_scale=layer._v_scale_float,
432 kv_cache_dtype=self.kv_cache_dtype,
433 )
434
435 # The CPU kernel executes attention sinks natively in bf16. If the
436 # sinks tensor is anything other than bf16, cast it to fp32 so it is
437 # executed in full float precision (done lazily here, after weights
438 # are loaded, rather than at __init__ time).
439 if self.sinks is not None and self.sinks.dtype not in [
440 torch.bfloat16,
441 torch.float32,
442 ]:
443 self.sinks = self.sinks.to(torch.float32)
444 ops.cpu_attention_with_kv_cache(
445 query=query[:num_actual_tokens],
446 key_cache=key_cache,
447 value_cache=value_cache,
448 output=output[:num_actual_tokens], # type: ignore
449 query_start_loc=attn_metadata.query_start_loc,
450 seq_lens=attn_metadata.seq_lens,
451 scale=self.scale,
452 causal=attn_metadata.causal,
453 alibi_slopes=self.alibi_slopes, # type: ignore
454 sliding_window=self.sliding_window,
455 block_table=attn_metadata.block_table,
456 softcap=self.logits_soft_cap,
457 scheduler_metadata=attn_metadata.scheduler_metadata,
458 s_aux=self.sinks,
459 dynamic_causal=attn_metadata.dynamic_causal,
460 k_scale=layer._k_scale_float,
461 v_scale=layer._v_scale_float,
462 kv_cache_dtype=self.kv_cache_dtype,
463 )
464
465 return output
466
467 def do_kv_cache_update(
468 self,
469 layer: torch.nn.Module,
470 key: torch.Tensor,
471 value: torch.Tensor,
472 kv_cache: torch.Tensor,
473 slot_mapping: torch.Tensor,
474 ) -> None:
475 if self.attn_type in (AttentionType.ENCODER_ONLY, AttentionType.ENCODER):
476 return
477
478 num_blocks, num_kv_heads, block_size, _ = kv_cache.size()
479 kv_cache = kv_cache.view((num_blocks, num_kv_heads, block_size * 2, -1))
480 key_cache, value_cache = kv_cache.chunk(2, dim=2)
481 ops.cpu_attn_reshape_and_cache(
482 key,
483 value,
484 key_cache,
485 value_cache,
486 slot_mapping,
487 layer.isa, # type: ignore
488 k_scale=layer._k_scale_float,
489 v_scale=layer._v_scale_float,
490 kv_cache_dtype=self.kv_cache_dtype,
491 )
492
493
494 @functools.lru_cache(maxsize=1)
495 def _riscv_supports_rvv() -> bool:
496 """Whether the C++ RVV attention path is usable.
497
498 The kernel in csrc/cpu/cpu_attn_rvv.hpp uses VLEN-agnostic RVVI()
499 macros and supports VLEN=128 and VLEN=256. CMake auto-detects the
500 largest zvl<N>b from /proc/cpuinfo and passes it via -mrvv-vector-bits.
501 The RVV path is compiled whenever __riscv_v_min_vlen is defined, so
502 we check that at least one supported zvl<N>b is advertised.
503 """
504 # The C++ compile-time check is the ground truth: it knows which
505 # VLEN the binary was actually compiled for. The cpuinfo check
506 # below is only a fast-path shortcut.
507 try:
508 import torch
509
510 if torch.ops._C.cpu_attn_has_isa("rvv"):
511 return True
512 except Exception:
513 pass
514
515 # Fallback: check /proc/cpuinfo for zvl128b/zvl256b.
516 try:
517 with open("/proc/cpuinfo") as f:
518 cpuinfo = f.read()
519 except OSError:
520 return False
521 return any(f"zvl{n}b" in cpuinfo for n in (128, 256))
522
523
524 def _get_attn_isa(
525 dtype: torch.dtype,
526 block_size: int,
527 head_size: int | None = None,
528 kv_cache_dtype: str | None = None,
529 ) -> str:
530 fp8_kv = is_quantized_kv_cache(kv_cache_dtype) if kv_cache_dtype else False
531 if head_size is not None and head_size % 32 != 0 and head_size % 16 == 0:
532 if fp8_kv:
533 raise NotImplementedError(
534 "FP8 KV cache requires head_size divisible by 32 on CPU."
535 )
536 return "vec16"
537 supports_amx = torch.cpu._is_amx_tile_supported()
538 arch = current_platform.get_cpu_architecture()
539 supports_arm = arch == CpuArchEnum.ARM
540 supports_vxe = arch == CpuArchEnum.S390X
541 supports_riscv = arch == CpuArchEnum.RISCV
542 supports_vsx = arch == CpuArchEnum.POWERPC
543 supports_avx512 = torch.cpu._is_avx512_supported()
544 if fp8_kv and not supports_amx and not supports_avx512:
545 raise NotImplementedError(
546 "FP8 KV cache on CPU requires x86 with AVX-512 or AMX."
547 )
548 if supports_amx and dtype in (torch.bfloat16,) and block_size % 32 == 0:
549 return "amx"
550 elif supports_arm:
551 return "neon"
552 elif block_size % 32 == 0:
553 if supports_riscv and _riscv_supports_rvv():
554 return "rvv"
555 elif supports_vxe:
556 return "vxe"
557 elif supports_vsx:
558 return "vsx"
559 else:
560 return "vec"
561 else:
562 return "vec16"