Files
enginex-ascend-910-vllm/vllm_ascend/worker/v2/attn_utils.py
Sun Ruoxi 7f8a1b1f7a init v0.23.0
Signed-off-by: Sun Ruoxi <sunruoxi@4paradigm.com>
2026-08-27 15:11:51 +08:00

505 lines
22 KiB
Python

# Adapt from https://github.com/vllm-project/vllm/blob/main/vllm/v1/worker/gpu/attn_utils.py
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This file is a part of the vllm-ascend project.
#
from collections.abc import Sequence
from typing import Any
import numpy as np
import torch
from vllm.config import VllmConfig, get_current_vllm_config, get_layers_from_vllm_config
from vllm.model_executor.layers.attention import Attention
from vllm.model_executor.layers.attention.mla_attention import MLAAttention
from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
from vllm.v1.attention.backend import AttentionBackend
from vllm.v1.kv_cache_interface import (
AttentionSpec,
EncoderOnlyAttentionSpec,
KVCacheConfig,
KVCacheSpec,
MLAAttentionSpec,
UniformTypeKVCacheSpecs,
)
from vllm.v1.worker.gpu.model_states.interface import ModelSpecificAttnMetadata
from vllm.v1.worker.utils import AttentionGroup
from vllm_ascend.attention.attention_mask import AttentionMaskBuilder
from vllm_ascend.attention.attention_v1 import AscendAttentionState
from vllm_ascend.attention.utils import AscendCommonAttentionMetadata, AscendPrefillContextParallelMetadata
from vllm_ascend.core.kv_cache_interface import AscendMLAAttentionSpec
from vllm_ascend.quantization.utils import enable_fa_quant
from vllm_ascend.utils import calc_split_factor
_ATTENTION_MASK_BUILDER = None
def get_kv_cache_spec(vllm_config: VllmConfig) -> dict[str, KVCacheSpec]:
"""Build Ascend-specific KV cache specs for v2 worker patching."""
kv_cache_spec: dict[str, KVCacheSpec] = {}
layer_type = AttentionLayerBase
attn_layers = get_layers_from_vllm_config(vllm_config, layer_type)
for layer_name, attn_module in attn_layers.items():
if getattr(attn_module, "kv_sharing_target_layer_name", None):
continue
if isinstance(attn_module, Attention):
if spec := attn_module.get_kv_cache_spec(vllm_config):
kv_cache_spec[layer_name] = spec
continue
if isinstance(attn_module, MLAAttention):
spec = attn_module.get_kv_cache_spec(vllm_config)
if spec is None:
continue
if getattr(attn_module.impl, "fa_quant_layer", False):
head_size = attn_module.head_size + attn_module.qk_rope_head_dim
dtype, cache_dtype_str = attn_module.impl.dtype, None
else:
head_size = spec.head_size
dtype = spec.dtype
cache_dtype_str = spec.cache_dtype_str
kv_cache_spec[layer_name] = AscendMLAAttentionSpec(
block_size=spec.block_size,
num_kv_heads=spec.num_kv_heads,
head_size=head_size,
dtype=dtype,
cache_dtype_str=cache_dtype_str,
)
return kv_cache_spec
def get_attn_mask_builder(device: torch.device):
"""Get attention mask builder which only have one instance."""
global _ATTENTION_MASK_BUILDER
if _ATTENTION_MASK_BUILDER is None:
_ATTENTION_MASK_BUILDER = AttentionMaskBuilder(device)
return _ATTENTION_MASK_BUILDER
def build_attn_metadata(
*,
attn_groups: list[list[AttentionGroup]],
num_reqs: int,
num_tokens: int,
query_start_loc_gpu: torch.Tensor,
query_start_loc_cpu: torch.Tensor,
max_query_len: int,
seq_lens: torch.Tensor,
max_seq_len: int,
block_tables: Sequence[torch.Tensor],
slot_mappings: torch.Tensor,
kv_cache_config: KVCacheConfig,
dcp_local_seq_lens: torch.Tensor | None = None,
# extra attributes for ascend npus.
seq_lens_np: np.ndarray | None = None,
num_computed_tokens_cpu: torch.Tensor | None = None,
positions: torch.Tensor | None = None,
attn_state: Any | None = None,
graph_pad_size: int = -1,
num_input_tokens: int = 0,
prefill_context_parallel_metadata: AscendPrefillContextParallelMetadata | None = None,
model_specific_attn_metadata: ModelSpecificAttnMetadata | None = None,
for_cudagraph_capture: bool = False,
causal: bool = True,
) -> dict[str, Any]:
"""Build attention metadata for Ascend NPUs."""
# TODO(Ronald1995): optimize AscendCommonAttentionMetadata.
# seq_lens_np is used for ascend npus, it maybe None in spec_decode case,
# we fill it with max_seq_len in case `attn_metadata_builder.build` raise
# an error.
if seq_lens_np is None:
seq_lens_np = np.full(num_reqs, max_seq_len, dtype=np.int32)
seq_lens_cpu = torch.from_numpy(seq_lens_np)[:num_reqs]
attn_metadata: dict[str, Any] = {}
kv_cache_groups = kv_cache_config.kv_cache_groups
for i, kv_cache_spec in enumerate(kv_cache_groups):
block_table = block_tables[i]
slot_mapping = slot_mappings[i]
common_attn_metadata_extra_kwargs = (
model_specific_attn_metadata.get_extra_common_attn_kwargs(i, num_reqs)
if model_specific_attn_metadata is not None
else {}
)
common_attn_metadata = AscendCommonAttentionMetadata(
query_start_loc=query_start_loc_gpu,
query_start_loc_cpu=query_start_loc_cpu,
seq_lens_cpu=seq_lens_cpu,
seq_lens_cpu_upper_bound=seq_lens_cpu,
seq_lens=seq_lens[:num_reqs],
num_reqs=num_reqs,
num_actual_tokens=num_tokens,
max_query_len=max_query_len,
block_table_tensor=block_table,
slot_mapping=slot_mapping,
positions=positions,
attn_state=attn_state,
graph_pad_size=graph_pad_size,
num_input_tokens=num_input_tokens,
prefill_context_parallel_metadata=prefill_context_parallel_metadata,
max_seq_len=max_seq_len,
causal=causal,
**common_attn_metadata_extra_kwargs,
)
for attn_group in attn_groups[i]:
attn_metadata_builder = attn_group.get_metadata_builder(0)
if for_cudagraph_capture:
metadata = attn_metadata_builder.build_for_cudagraph_capture(common_attn_metadata)
else:
attn_metadata_extra_kwargs = (
model_specific_attn_metadata.get_extra_attn_kwargs(
attn_metadata_builder,
num_reqs,
)
if model_specific_attn_metadata is not None
else {}
)
metadata = attn_metadata_builder.build(
common_prefix_len=0,
common_attn_metadata=common_attn_metadata,
**attn_metadata_extra_kwargs,
)
for layer_name in attn_group.layer_names:
attn_metadata[layer_name] = metadata
return attn_metadata
def build_attn_state(
vllm_config: VllmConfig,
seq_lens_np: np.ndarray,
num_reqs,
num_scheduled_tokens,
num_valid_tokens,
):
"""Build attention state for npu's attention backend."""
if vllm_config.model_config.runner_type == "pooling":
if isinstance(
vllm_config.kv_cache_config.kv_cache_groups[0].kv_cache_spec,
EncoderOnlyAttentionSpec,
):
attn_state = AscendAttentionState.PrefillNoCache
else:
attn_state = AscendAttentionState.PrefillCacheHit
elif np.array_equal(seq_lens_np[:num_reqs], num_scheduled_tokens):
attn_state = AscendAttentionState.PrefillNoCache
# We assume it is the decode stage, where prefill occurs
# but only one token is not hit in cache.
elif np.all(num_scheduled_tokens == 1):
attn_state = AscendAttentionState.DecodeOnly
if vllm_config.speculative_config and vllm_config.speculative_config.method == "mtp":
# SpecDecoding now supports seq_len=1 and seq_len=2
# In Prefilling Decoding Disaggregation scenario, SpecDecoding
# need to supports seq_len=1
attn_state = AscendAttentionState.SpecDecoding
# Speculative decoding.
elif np.all(num_valid_tokens == 1):
if vllm_config.speculative_config and vllm_config.speculative_config.method == "mtp":
attn_state = AscendAttentionState.SpecDecoding
else:
attn_state = AscendAttentionState.ChunkedPrefill
# splitfuse
elif vllm_config.scheduler_config.enable_chunked_prefill:
attn_state = AscendAttentionState.ChunkedPrefill
else:
attn_state = AscendAttentionState.PrefillCacheHit
return attn_state
def _get_layer_kv_cache_specs(kv_cache_config: KVCacheConfig) -> dict[str, KVCacheSpec]:
layer_kv_cache_spec: dict[str, KVCacheSpec] = {}
for group_kv_cache_spec in kv_cache_config.kv_cache_groups:
group_spec = group_kv_cache_spec.kv_cache_spec
for layer_name in group_kv_cache_spec.layer_names:
if isinstance(group_spec, UniformTypeKVCacheSpecs):
layer_kv_cache_spec[layer_name] = group_spec.kv_cache_specs[layer_name]
else:
layer_kv_cache_spec[layer_name] = group_spec
return layer_kv_cache_spec
def _get_attention_kv_cache_dims(layer_name: str, kv_cache_spec: AttentionSpec) -> tuple[int, int]:
if isinstance(kv_cache_spec, AscendMLAAttentionSpec):
attn_layers = get_layers_from_vllm_config(get_current_vllm_config(), AttentionLayerBase, [layer_name])
attn_layer = attn_layers[layer_name]
if not isinstance(attn_layer, MLAAttention):
raise TypeError(f"Expected AscendMLAAttention layer for {layer_name}, got {type(attn_layer).__name__}.")
return attn_layer.kv_lora_rank, attn_layer.qk_rope_head_dim
head_size_v = kv_cache_spec.head_size_v if hasattr(kv_cache_spec, "head_size_v") else kv_cache_spec.head_size
return kv_cache_spec.head_size, head_size_v
def _align_memory(tensor: torch.Tensor, alignment: int) -> torch.Tensor:
data_ptr = tensor.data_ptr()
aligned_addr = (data_ptr + alignment - 1) // alignment * alignment
offset = (aligned_addr - data_ptr) // tensor.element_size()
return tensor[int(offset) :]
def _allocate_kv_cache(
kv_cache_config: KVCacheConfig,
shared_layers: dict[str, str],
device: torch.device,
) -> dict[str, tuple[torch.Tensor, torch.Tensor]]:
"""
Initialize the KV cache buffer with the correct size. The buffer needs to be
reshaped to the desired shape before being used by the models.
NOTE: To support prefill disaggregation, we need to split kvcache tensor
into k_cache and v_cache, and the addr of both are aligned by 2M.
Args:
kv_cache_config: The KV cache config
device: The device
Returns:
dict[str, tuple[torch.Tensor, torch.Tensor]]: A map between layer names
to their corresponding memory buffer for K cache and V cache
"""
vllm_config = get_current_vllm_config()
# init kv cache tensors
kv_cache_raw_tensors: dict[str, tuple[torch.Tensor, torch.Tensor]] = {}
# prefill disaggregation need the addr of cache tensor be aligned with 2M
alignment = 2 * 1024 * 1024
layer_kv_cache_spec = _get_layer_kv_cache_specs(kv_cache_config)
for kv_cache_tensor in kv_cache_config.kv_cache_tensors:
if len(kv_cache_tensor.shared_by) == 0:
continue
# NOTE: We need to init k_cache tensor (nope cache tensor in mla) and
# v_cache tensor (rope cache tensor in mla) separately to support
# prefill disaggregation, as it only supports the 0-dim of kv_cache is
# `num_blocks`.
# For deepseek mla, we need to spilt cache tensor accrodding to the nope
# head dim and rope head dim.
example_layer_name = kv_cache_tensor.shared_by[0]
example_kv_cache_spec = layer_kv_cache_spec[example_layer_name]
assert isinstance(example_kv_cache_spec, AttentionSpec)
k_dim, v_dim = _get_attention_kv_cache_dims(example_layer_name, example_kv_cache_spec)
assert k_dim > 0 and v_dim > 0
kv_head_dim_list = [k_dim, v_dim]
if enable_fa_quant(vllm_config):
k_tensor_split_factor, v_tensor_split_factor = vllm_config.quant_config.get_kv_quant_split_factor(
example_layer_name, kv_head_dim_list
)
else:
k_tensor_split_factor, v_tensor_split_factor = calc_split_factor(kv_head_dim_list)
k_tensor_size = int(kv_cache_tensor.size // k_tensor_split_factor)
v_tensor_size = int(kv_cache_tensor.size // v_tensor_split_factor)
if vllm_config.kv_transfer_config is None:
k_tensor = torch.zeros(k_tensor_size, dtype=torch.int8, device=device)
v_tensor = torch.zeros(v_tensor_size, dtype=torch.int8, device=device)
else:
k_tensor = torch.zeros(k_tensor_size + alignment, dtype=torch.int8, device=device)
v_tensor = torch.zeros(v_tensor_size + alignment, dtype=torch.int8, device=device)
k_tensor = _align_memory(k_tensor, alignment)[:k_tensor_size]
v_tensor = _align_memory(v_tensor, alignment)[:v_tensor_size]
for layer_name in kv_cache_tensor.shared_by:
kv_cache_raw_tensors[layer_name] = (k_tensor, v_tensor)
layer_names = set()
for group in kv_cache_config.kv_cache_groups:
for layer_name in group.layer_names:
layer_names.add(layer_name)
assert layer_names == (kv_cache_raw_tensors.keys() | shared_layers.keys()), (
"Some layers are not correctly initialized"
)
return kv_cache_raw_tensors
def _reshape_kv_cache(
kv_cache_config: KVCacheConfig,
kv_cache_raw_tensors: dict[str, tuple[torch.Tensor, torch.Tensor]],
attn_backends: dict[str, AttentionBackend],
cache_dtype: str,
kernel_block_sizes: list[int] | None = None,
shared_kv_cache_layers: dict[str, str] | None = None,
) -> dict[str, tuple[torch.Tensor, torch.Tensor]]:
"""
Reshape the KV cache tensors to the desired shape and dtype.
Args:
kv_cache_config: The KV cache config
kv_cache_raw_tensors: The KV cache buffer of each layer, with correct
size but uninitialized shape
Returns:
dict[str, tuple[torch.Tensor, torch.Tensor]]: A map between layer names
to their corresponding memory buffer for KV cache
"""
vllm_config = get_current_vllm_config()
kv_caches: dict[str, tuple[torch.Tensor, torch.Tensor]] = {}
kernel_block_sizes = kernel_block_sizes or []
for kv_cache_group_id, kv_cache_group_spec in enumerate(kv_cache_config.kv_cache_groups):
for layer_name in kv_cache_group_spec.layer_names:
if shared_kv_cache_layers and layer_name in shared_kv_cache_layers:
continue
kv_cache_spec = kv_cache_group_spec.kv_cache_spec
if isinstance(kv_cache_spec, UniformTypeKVCacheSpecs):
kv_cache_spec = kv_cache_spec.kv_cache_specs[layer_name]
assert isinstance(kv_cache_spec, AttentionSpec)
if isinstance(kv_cache_spec, AttentionSpec):
raw_k_tensor, raw_v_tensor = kv_cache_raw_tensors[layer_name]
assert raw_k_tensor is not None
assert raw_v_tensor is not None
sum_page_size_bytes = raw_k_tensor.numel() + raw_v_tensor.numel()
assert sum_page_size_bytes % kv_cache_spec.page_size_bytes == 0
num_blocks = sum_page_size_bytes // kv_cache_spec.page_size_bytes
# `num_blocks` is the number of blocks the model runner can use.
# `kv_cache_config.num_blocks` is the number of blocks that
# KVCacheManager may allocate.
# Since different GPUs may have different number of layers and
# different memory capacities, `num_blocks` can be different on
# different GPUs, and `kv_cache_config.num_blocks` is set to
# the min of all `num_blocks`. Verify it here.
assert num_blocks >= kv_cache_config.num_blocks
attn_backend = attn_backends[layer_name]
if kv_cache_group_id < len(kernel_block_sizes):
kernel_block_size = kernel_block_sizes[kv_cache_group_id]
num_blocks *= kv_cache_spec.block_size // kernel_block_size
else:
kernel_block_size = kv_cache_spec.block_size
if kv_cache_spec.storage_block_size != kv_cache_spec.block_size:
shape_block_size = kv_cache_spec.storage_block_size
else:
shape_block_size = kernel_block_size
kv_cache_shape = attn_backend.get_kv_cache_shape(
num_blocks,
shape_block_size,
kv_cache_spec.num_kv_heads,
kv_cache_spec.head_size,
cache_dtype,
)
if not isinstance(kv_cache_spec, AscendMLAAttentionSpec):
k_shape = kv_cache_shape[1:]
if hasattr(kv_cache_spec, "head_size_v"):
v_shape = (*kv_cache_shape[1:-1], kv_cache_spec.head_size_v)
else:
v_shape = k_shape
else:
# k_cache: nope_cache v_cache: rope_cache
mla_num_blocks, mla_block_size, num_kv_heads, _ = kv_cache_shape
k_dim, v_dim = _get_attention_kv_cache_dims(layer_name, kv_cache_spec)
k_shape = (mla_num_blocks, mla_block_size, num_kv_heads, k_dim)
v_shape = (mla_num_blocks, mla_block_size, num_kv_heads, v_dim)
k_cache_dtype = v_cache_dtype = kv_cache_spec.dtype
if enable_fa_quant(vllm_config):
k_cache_dtype, v_cache_dtype = vllm_config.quant_config.get_kv_quant_dtype(
layer_name, kv_cache_spec.dtype, vllm_config.model_config
)
k_cache = raw_k_tensor.view(k_cache_dtype).view(k_shape)
v_cache = raw_v_tensor.view(v_cache_dtype).view(v_shape)
kv_caches[layer_name] = (k_cache, v_cache)
else:
raise ValueError("Unknown KV cache spec type.")
if shared_kv_cache_layers:
for layer_name, target_layer_name in shared_kv_cache_layers.items():
kv_caches[layer_name] = kv_caches[target_layer_name]
return kv_caches
def _reshape_kv_cache_v2(
attn_groups: Sequence[AttentionGroup],
kv_cache_raw_tensors: dict[str, tuple[torch.Tensor, torch.Tensor]],
cache_dtype: str,
kernel_block_sizes: list[int],
shared_kv_cache_layers: dict[str, str],
kv_cache_config: "KVCacheConfig | None" = None,
) -> dict[str, tuple[torch.Tensor, torch.Tensor]]:
vllm_config = get_current_vllm_config()
is_kv_consumer = (
vllm_config.kv_transfer_config.is_kv_consumer if vllm_config.kv_transfer_config is not None else False
)
kv_caches: dict[str, tuple[torch.Tensor, torch.Tensor]] = {}
for group in attn_groups:
if group.kv_cache_group_id >= len(kernel_block_sizes):
continue
kv_cache_spec = group.kv_cache_spec
if kv_cache_spec.storage_block_size != kv_cache_spec.block_size:
kernel_block_size = kv_cache_spec.storage_block_size
else:
kernel_block_size = kernel_block_sizes[group.kv_cache_group_id]
for layer_name in group.layer_names:
if layer_name in shared_kv_cache_layers:
continue
assert isinstance(kv_cache_spec, AttentionSpec)
raw_k_tensor, raw_v_tensor = kv_cache_raw_tensors[layer_name]
assert raw_k_tensor is not None
assert raw_v_tensor is not None
sum_page_size_bytes = raw_k_tensor.numel() + raw_v_tensor.numel()
assert sum_page_size_bytes % kv_cache_spec.page_size_bytes == 0
num_blocks = sum_page_size_bytes // kv_cache_spec.page_size_bytes
num_blocks_per_kv_block = kv_cache_spec.block_size // kernel_block_size
kernel_num_blocks = num_blocks * num_blocks_per_kv_block
kv_cache_shape = group.backend.get_kv_cache_shape(
kernel_num_blocks,
kernel_block_size,
kv_cache_spec.num_kv_heads,
kv_cache_spec.head_size,
cache_dtype,
)
if not isinstance(kv_cache_spec, (AscendMLAAttentionSpec, MLAAttentionSpec)):
k_shape = kv_cache_shape[1:]
if hasattr(kv_cache_spec, "head_size_v"):
v_shape = (*kv_cache_shape[1:-1], kv_cache_spec.head_size_v)
else:
v_shape = k_shape
else:
mla_num_blocks, mla_block_size, num_kv_heads, _ = kv_cache_shape
k_dim, v_dim = _get_attention_kv_cache_dims(layer_name, kv_cache_spec)
k_shape = (mla_num_blocks, mla_block_size, num_kv_heads, k_dim)
v_shape = (mla_num_blocks, mla_block_size, num_kv_heads, v_dim)
k_cache_dtype = v_cache_dtype = kv_cache_spec.dtype
if is_kv_consumer and enable_fa_quant(vllm_config):
k_cache_dtype, v_cache_dtype = vllm_config.quant_config.get_kv_quant_dtype(
layer_name, kv_cache_spec.dtype, vllm_config.model_config
)
k_cache = raw_k_tensor.view(k_cache_dtype).view(k_shape)
v_cache = raw_v_tensor.view(v_cache_dtype).view(v_shape)
kv_caches[layer_name] = (k_cache, v_cache)
for layer_name, target_layer_name in shared_kv_cache_layers.items():
kv_caches[layer_name] = kv_caches[target_layer_name]
return kv_caches